Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions _msbuild.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ class ResourceFile(CSourceFile):
CFunction('fd_supports_vt100'),
CFunction('date_as_str'),
CFunction('datetime_as_str'),
CFunction('reg_rename_key'),
source='src/_native',
RootNamespace='_native',
)
Expand Down
1 change: 1 addition & 0 deletions _msbuild_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
CFunction('fd_supports_vt100'),
CFunction('date_as_str'),
CFunction('datetime_as_str'),
CFunction('reg_rename_key'),
source='src/_native',
),
)
24 changes: 24 additions & 0 deletions src/_native/misc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,28 @@ PyObject *datetime_as_str(PyObject *, PyObject *, PyObject *) {
return PyUnicode_FromWideChar(buffer, cch - 1);
}

PyObject *reg_rename_key(PyObject *, PyObject *args, PyObject *kwargs) {
static const char * keywords[] = {"handle", "name1", "name2", NULL};
PyObject *handle_obj;
wchar_t *name1 = NULL;
wchar_t *name2 = NULL;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OO&O&:reg_rename_key", keywords,
&handle_obj, as_utf16, &name1, as_utf16, &name2)) {
return NULL;
}
PyObject *r = NULL;
HKEY h;
if (PyLong_AsNativeBytes(handle_obj, &h, sizeof(h), -1) >= 0) {
int err = (int)RegRenameKey(h, name1, name2);
if (!err) {
r = Py_GetConstant(Py_CONSTANT_NONE);
} else {
PyErr_SetFromWindowsErr(err);
}
}
PyMem_Free(name1);
PyMem_Free(name2);
return r;
}

}
69 changes: 66 additions & 3 deletions src/manage/pep514utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@
winreg.SetValueEx(key, k, None, v_kind, v)


def _is_tag_managed(company_key, tag_name):
def _is_tag_managed(company_key, tag_name, *, creating=False):
try:
tag = winreg.OpenKey(company_key, tag_name)
except FileNotFoundError:
Expand All @@ -147,7 +147,70 @@
return True
except FileNotFoundError:
pass
return False
if not creating:
return False

# gh-11: Clean up invalid entries from other installers
# It's highly likely that our old MSI installer wouldn't properly remove
# its registry keys on uninstall, so we'll check for the InstallPath
# subkey and if it's missing, back up the key and then use it ourselves.
try:
with _reg_open(tag, "InstallPath") as subkey:
if subkey:
# if InstallPath refers to a directory that exists,
# leave it alone.
p = winreg.QueryValueEx(subkey, None)[0]
if p and Path(p).exists():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if p and Path(p).exists():
if p and Path(p).is_dir():

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather be vague here, play it safe.

return False
except FileNotFoundError:
pass
except OSError:

Check warning on line 167 in src/manage/pep514utils.py

View check run for this annotation

Codecov / codecov/patch

src/manage/pep514utils.py#L164-L167

Added lines #L164 - L167 were not covered by tests
# If we couldn't access it for some reason, leave it alone.
return False

Check warning on line 169 in src/manage/pep514utils.py

View check run for this annotation

Codecov / codecov/patch

src/manage/pep514utils.py#L169

Added line #L169 was not covered by tests

# Existing key is almost certainly not valid, so let's rename it,
# warn the user, and then continue.
LOGGER.debug("Key %s appears invalid, so moving it and taking it for this "
"new install", tag_name)
try:
from _native import reg_rename_key
except ImportError:
LOGGER.debug("Failed to import reg_rename_key", exc_info=True)
return False

Check warning on line 179 in src/manage/pep514utils.py

View check run for this annotation

Codecov / codecov/patch

src/manage/pep514utils.py#L177-L179

Added lines #L177 - L179 were not covered by tests

parent_name, _, orig_name = tag_name.replace("/", "\\").rpartition("\\")
with _reg_open(company_key, parent_name, writable=True) as tag:
if not tag:
# Key is no longer there, so we can use it
return True

Check warning on line 185 in src/manage/pep514utils.py

View check run for this annotation

Codecov / codecov/patch

src/manage/pep514utils.py#L185

Added line #L185 was not covered by tests
for i in range(1000):
try:
new_name = f"{orig_name}.{i}"
# Raises standard PermissionError (5) if new_name exists
reg_rename_key(tag.handle, orig_name, new_name)
LOGGER.warn("An existing registry key for %s was renamed to %s "
"because it appeared to be invalid. If this is "
"correct, the registry key can be safely deleted. "
"To avoid this in future, ensure that the "
"InstallPath key refers to a valid path.",
tag_name, new_name)
break
except FileNotFoundError:
LOGGER.debug("Original key disappeared, so we will claim it")
return True

Check warning on line 200 in src/manage/pep514utils.py

View check run for this annotation

Codecov / codecov/patch

src/manage/pep514utils.py#L199-L200

Added lines #L199 - L200 were not covered by tests
except PermissionError:
LOGGER.debug("Failed to rename %s to %s", orig_name, new_name,
exc_info=True)
# Continue, hopefully the next new_name is available
except OSError:
LOGGER.debug("Unexpected error while renaming %s to %s",

Check warning on line 206 in src/manage/pep514utils.py

View check run for this annotation

Codecov / codecov/patch

src/manage/pep514utils.py#L205-L206

Added lines #L205 - L206 were not covered by tests
orig_name, new_name, exc_info=True)
raise

Check warning on line 208 in src/manage/pep514utils.py

View check run for this annotation

Codecov / codecov/patch

src/manage/pep514utils.py#L208

Added line #L208 was not covered by tests
else:
LOGGER.warn("Attempted to clean up invalid registry key %s but "

Check warning on line 210 in src/manage/pep514utils.py

View check run for this annotation

Codecov / codecov/patch

src/manage/pep514utils.py#L210

Added line #L210 was not covered by tests
"failed after too many attempts.", tag_name);
return False

Check warning on line 212 in src/manage/pep514utils.py

View check run for this annotation

Codecov / codecov/patch

src/manage/pep514utils.py#L212

Added line #L212 was not covered by tests
return True


def _split_root(root_name):
Expand All @@ -166,7 +229,7 @@
def update_registry(root_name, install, data):
hive, name = _split_root(root_name)
with winreg.CreateKey(hive, name) as root:
if _is_tag_managed(root, data["Key"]):
if _is_tag_managed(root, data["Key"], creating=True):
with winreg.CreateKey(root, data["Key"]) as tag:
LOGGER.debug("Creating/updating %s\\%s", root_name, data["Key"])
winreg.SetValueEx(tag, "ManagedByPyManager", None, winreg.REG_DWORD, 1)
Expand Down
54 changes: 54 additions & 0 deletions tests/test_pep514utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import pytest
import winreg

from manage import pep514utils

REG_TEST_ROOT = r"Software\Python\PyManagerTesting"

@pytest.fixture(scope='function')
def registry():
try:
with winreg.CreateKey(winreg.HKEY_CURRENT_USER, REG_TEST_ROOT) as key:
yield key
finally:
pep514utils._reg_rmtree(winreg.HKEY_CURRENT_USER, REG_TEST_ROOT)


def init_reg(registry, **keys):
for k, v in keys.items():
if isinstance(v, dict):
with winreg.CreateKey(registry, k) as subkey:
init_reg(subkey, **v)
elif isinstance(v, str):
winreg.SetValueEx(registry, k, None, winreg.REG_SZ, v)
elif isinstance(v, (bytes, bytearray)):
winreg.SetValueEx(registry, k, None, winreg.REG_BINARY, v)

Check warning on line 25 in tests/test_pep514utils.py

View check run for this annotation

Codecov / codecov/patch

tests/test_pep514utils.py#L25

Added line #L25 was not covered by tests
elif isinstance(v, int):
if v.bit_count() < 32:
winreg.SetValueEx(registry, k, None, winreg.REG_DWORD, v)
else:
winreg.SetValueEx(registry, k, None, winreg.REG_QWORD, v)

Check warning on line 30 in tests/test_pep514utils.py

View check run for this annotation

Codecov / codecov/patch

tests/test_pep514utils.py#L30

Added line #L30 was not covered by tests
else:
raise TypeError("unsupported type in registry")

Check warning on line 32 in tests/test_pep514utils.py

View check run for this annotation

Codecov / codecov/patch

tests/test_pep514utils.py#L32

Added line #L32 was not covered by tests


def test_is_tag_managed(registry, tmp_path):
init_reg(registry, Company={
"1.0": {"InstallPath": {"": str(tmp_path)}},
"2.0": {"InstallPath": {"": str(tmp_path)}, "ManagedByPyManager": 0},
"2.1": {"InstallPath": {"": str(tmp_path)}, "ManagedByPyManager": 1},
"3.0": {"InstallPath": {"": str(tmp_path / "missing")}},
"3.0.0": {"": "Just in the way here"},
"3.0.1": {"": "Also in the way here"},
})

assert not pep514utils._is_tag_managed(registry, r"Company\1.0")
assert not pep514utils._is_tag_managed(registry, r"Company\2.0")
assert pep514utils._is_tag_managed(registry, r"Company\2.1")

assert not pep514utils._is_tag_managed(registry, r"Company\3.0")
with pytest.raises(FileNotFoundError):
winreg.OpenKey(registry, r"Company\3.0.2")
assert pep514utils._is_tag_managed(registry, r"Company\3.0", creating=True)
with winreg.OpenKey(registry, r"Company\3.0.2"):
pass