Skip to content

Commit 985f2c3

Browse files
committed
Fixes #11 Detect and overwrite corrupt PEP 514 registry entries
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 5f38dab commit 985f2c3

File tree

5 files changed

+145
-3
lines changed

5 files changed

+145
-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: 65 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,69 @@ 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+
except OSError:
205+
LOGGER.debug("Unexpected error while renaming %s to %s",
206+
orig_name, new_name, exc_info=True)
207+
raise
208+
else:
209+
LOGGER.warn("Attempted to clean up invalid registry key %s but "
210+
"failed after too many attempts.", tag_name);
211+
return False
212+
return True
151213

152214

153215
def _split_root(root_name):
@@ -166,7 +228,7 @@ def _split_root(root_name):
166228
def update_registry(root_name, install, data):
167229
hive, name = _split_root(root_name)
168230
with winreg.CreateKey(hive, name) as root:
169-
if _is_tag_managed(root, data["Key"]):
231+
if _is_tag_managed(root, data["Key"], creating=True):
170232
with winreg.CreateKey(root, data["Key"]) as tag:
171233
LOGGER.debug("Creating/updating %s\\%s", root_name, data["Key"])
172234
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)