Skip to content

Commit 9817099

Browse files
authored
Fixes #11 Detect and overwrite corrupt PEP 514 registry entries (#21)
pep514utils now attempts to move existing registry entries if they appear invalid. This should reduce the likelihood of bad MSI uninstalls causing repeated warnings for the rest of time, at a small risk of corrupting (intentionally useless) entries.
1 parent f76599b commit 9817099

File tree

5 files changed

+146
-3
lines changed

5 files changed

+146
-3
lines changed

_msbuild.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ class ResourceFile(CSourceFile):
8383
CFunction('fd_supports_vt100'),
8484
CFunction('date_as_str'),
8585
CFunction('datetime_as_str'),
86+
CFunction('reg_rename_key'),
8687
source='src/_native',
8788
RootNamespace='_native',
8889
)

_msbuild_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
CFunction('fd_supports_vt100'),
5050
CFunction('date_as_str'),
5151
CFunction('datetime_as_str'),
52+
CFunction('reg_rename_key'),
5253
source='src/_native',
5354
),
5455
)

src/_native/misc.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,28 @@ PyObject *datetime_as_str(PyObject *, PyObject *, PyObject *) {
9595
return PyUnicode_FromWideChar(buffer, cch - 1);
9696
}
9797

98+
PyObject *reg_rename_key(PyObject *, PyObject *args, PyObject *kwargs) {
99+
static const char * keywords[] = {"handle", "name1", "name2", NULL};
100+
PyObject *handle_obj;
101+
wchar_t *name1 = NULL;
102+
wchar_t *name2 = NULL;
103+
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OO&O&:reg_rename_key", keywords,
104+
&handle_obj, as_utf16, &name1, as_utf16, &name2)) {
105+
return NULL;
106+
}
107+
PyObject *r = NULL;
108+
HKEY h;
109+
if (PyLong_AsNativeBytes(handle_obj, &h, sizeof(h), -1) >= 0) {
110+
int err = (int)RegRenameKey(h, name1, name2);
111+
if (!err) {
112+
r = Py_GetConstant(Py_CONSTANT_NONE);
113+
} else {
114+
PyErr_SetFromWindowsErr(err);
115+
}
116+
}
117+
PyMem_Free(name1);
118+
PyMem_Free(name2);
119+
return r;
120+
}
121+
98122
}

src/manage/pep514utils.py

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def _update_reg_values(key, data, install, exclude=set()):
136136
winreg.SetValueEx(key, k, None, v_kind, v)
137137

138138

139-
def _is_tag_managed(company_key, tag_name):
139+
def _is_tag_managed(company_key, tag_name, *, creating=False):
140140
try:
141141
tag = winreg.OpenKey(company_key, tag_name)
142142
except FileNotFoundError:
@@ -147,7 +147,70 @@ def _is_tag_managed(company_key, tag_name):
147147
return True
148148
except FileNotFoundError:
149149
pass
150-
return False
150+
if not creating:
151+
return False
152+
153+
# gh-11: Clean up invalid entries from other installers
154+
# It's highly likely that our old MSI installer wouldn't properly remove
155+
# its registry keys on uninstall, so we'll check for the InstallPath
156+
# subkey and if it's missing, back up the key and then use it ourselves.
157+
try:
158+
with _reg_open(tag, "InstallPath") as subkey:
159+
if subkey:
160+
# if InstallPath refers to a directory that exists,
161+
# leave it alone.
162+
p = winreg.QueryValueEx(subkey, None)[0]
163+
if p and Path(p).exists():
164+
return False
165+
except FileNotFoundError:
166+
pass
167+
except OSError:
168+
# If we couldn't access it for some reason, leave it alone.
169+
return False
170+
171+
# Existing key is almost certainly not valid, so let's rename it,
172+
# warn the user, and then continue.
173+
LOGGER.debug("Key %s appears invalid, so moving it and taking it for this "
174+
"new install", tag_name)
175+
try:
176+
from _native import reg_rename_key
177+
except ImportError:
178+
LOGGER.debug("Failed to import reg_rename_key", exc_info=True)
179+
return False
180+
181+
parent_name, _, orig_name = tag_name.replace("/", "\\").rpartition("\\")
182+
with _reg_open(company_key, parent_name, writable=True) as tag:
183+
if not tag:
184+
# Key is no longer there, so we can use it
185+
return True
186+
for i in range(1000):
187+
try:
188+
new_name = f"{orig_name}.{i}"
189+
# Raises standard PermissionError (5) if new_name exists
190+
reg_rename_key(tag.handle, orig_name, new_name)
191+
LOGGER.warn("An existing registry key for %s was renamed to %s "
192+
"because it appeared to be invalid. If this is "
193+
"correct, the registry key can be safely deleted. "
194+
"To avoid this in future, ensure that the "
195+
"InstallPath key refers to a valid path.",
196+
tag_name, new_name)
197+
break
198+
except FileNotFoundError:
199+
LOGGER.debug("Original key disappeared, so we will claim it")
200+
return True
201+
except PermissionError:
202+
LOGGER.debug("Failed to rename %s to %s", orig_name, new_name,
203+
exc_info=True)
204+
# Continue, hopefully the next new_name is available
205+
except OSError:
206+
LOGGER.debug("Unexpected error while renaming %s to %s",
207+
orig_name, new_name, exc_info=True)
208+
raise
209+
else:
210+
LOGGER.warn("Attempted to clean up invalid registry key %s but "
211+
"failed after too many attempts.", tag_name);
212+
return False
213+
return True
151214

152215

153216
def _split_root(root_name):
@@ -166,7 +229,7 @@ def _split_root(root_name):
166229
def update_registry(root_name, install, data):
167230
hive, name = _split_root(root_name)
168231
with winreg.CreateKey(hive, name) as root:
169-
if _is_tag_managed(root, data["Key"]):
232+
if _is_tag_managed(root, data["Key"], creating=True):
170233
with winreg.CreateKey(root, data["Key"]) as tag:
171234
LOGGER.debug("Creating/updating %s\\%s", root_name, data["Key"])
172235
winreg.SetValueEx(tag, "ManagedByPyManager", None, winreg.REG_DWORD, 1)

tests/test_pep514utils.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import pytest
2+
import winreg
3+
4+
from manage import pep514utils
5+
6+
REG_TEST_ROOT = r"Software\Python\PyManagerTesting"
7+
8+
@pytest.fixture(scope='function')
9+
def registry():
10+
try:
11+
with winreg.CreateKey(winreg.HKEY_CURRENT_USER, REG_TEST_ROOT) as key:
12+
yield key
13+
finally:
14+
pep514utils._reg_rmtree(winreg.HKEY_CURRENT_USER, REG_TEST_ROOT)
15+
16+
17+
def init_reg(registry, **keys):
18+
for k, v in keys.items():
19+
if isinstance(v, dict):
20+
with winreg.CreateKey(registry, k) as subkey:
21+
init_reg(subkey, **v)
22+
elif isinstance(v, str):
23+
winreg.SetValueEx(registry, k, None, winreg.REG_SZ, v)
24+
elif isinstance(v, (bytes, bytearray)):
25+
winreg.SetValueEx(registry, k, None, winreg.REG_BINARY, v)
26+
elif isinstance(v, int):
27+
if v.bit_count() < 32:
28+
winreg.SetValueEx(registry, k, None, winreg.REG_DWORD, v)
29+
else:
30+
winreg.SetValueEx(registry, k, None, winreg.REG_QWORD, v)
31+
else:
32+
raise TypeError("unsupported type in registry")
33+
34+
35+
def test_is_tag_managed(registry, tmp_path):
36+
init_reg(registry, Company={
37+
"1.0": {"InstallPath": {"": str(tmp_path)}},
38+
"2.0": {"InstallPath": {"": str(tmp_path)}, "ManagedByPyManager": 0},
39+
"2.1": {"InstallPath": {"": str(tmp_path)}, "ManagedByPyManager": 1},
40+
"3.0": {"InstallPath": {"": str(tmp_path / "missing")}},
41+
"3.0.0": {"": "Just in the way here"},
42+
"3.0.1": {"": "Also in the way here"},
43+
})
44+
45+
assert not pep514utils._is_tag_managed(registry, r"Company\1.0")
46+
assert not pep514utils._is_tag_managed(registry, r"Company\2.0")
47+
assert pep514utils._is_tag_managed(registry, r"Company\2.1")
48+
49+
assert not pep514utils._is_tag_managed(registry, r"Company\3.0")
50+
with pytest.raises(FileNotFoundError):
51+
winreg.OpenKey(registry, r"Company\3.0.2")
52+
assert pep514utils._is_tag_managed(registry, r"Company\3.0", creating=True)
53+
with winreg.OpenKey(registry, r"Company\3.0.2"):
54+
pass

0 commit comments

Comments
 (0)