diff --git a/src/manage/uninstall_command.py b/src/manage/uninstall_command.py index def69f9..85795ac 100644 --- a/src/manage/uninstall_command.py +++ b/src/manage/uninstall_command.py @@ -3,20 +3,75 @@ from .installs import get_matching_install_tags from .install_command import SHORTCUT_HANDLERS, update_all_shortcuts from .logging import LOGGER -from .pathutils import PurePath +from .pathutils import Path, PurePath from .tagutils import tag_or_range def _iterdir(p, only_files=False): try: if only_files: - return [f for f in p.iterdir() if p.is_file()] - return list(p.iterdir()) + return [f for f in Path(p).iterdir() if f.is_file()] + return list(Path(p).iterdir()) except FileNotFoundError: LOGGER.debug("Skipping %s because it does not exist", p) return [] +def _do_purge_global_dir(global_dir, warn_msg, *, hive=None, subkey="Environment"): + import os + import winreg + + if hive is None: + hive = winreg.HKEY_CURRENT_USER + try: + with winreg.OpenKeyEx(hive, subkey) as key: + path, kind = winreg.QueryValueEx(key, "Path") + if kind not in (winreg.REG_SZ, winreg.REG_EXPAND_SZ): + raise ValueError("Value kind is not a string") + except (OSError, ValueError): + LOGGER.debug("Not removing global commands directory from PATH", exc_info=True) + else: + LOGGER.debug("Current PATH contains %s", path) + paths = path.split(";") + newpaths = [] + for p in paths: + # We should expand entries here, but we only want to remove those + # that we added ourselves (during firstrun), and we never use + # environment variables. So even if the kind is REG_EXPAND_SZ, we + # don't need to expand to find our own entry. + #ep = os.path.expandvars(p) if kind == winreg.REG_EXPAND_SZ else p + ep = p + if PurePath(ep).match(global_dir): + LOGGER.debug("Removing from PATH: %s", p) + else: + newpaths.append(p) + if len(newpaths) < len(paths): + newpath = ";".join(newpaths) + with winreg.CreateKeyEx(hive, subkey, access=winreg.KEY_READ|winreg.KEY_WRITE) as key: + path2, kind2 = winreg.QueryValueEx(key, "Path") + if path2 == path and kind2 == kind: + LOGGER.info("Removing global commands directory from PATH") + LOGGER.debug("New PATH contains %s", newpath) + winreg.SetValueEx(key, "Path", 0, kind, newpath) + else: + LOGGER.debug("Not removing global commands directory from PATH " + "because the registry changed while processing.") + + try: + from _native import broadcast_settings_change + broadcast_settings_change() + except (ImportError, OSError): + LOGGER.debug("Did not broadcast settings change notification", + exc_info=True) + + if not global_dir.is_dir(): + return + LOGGER.info("Purging global commands from %s", global_dir) + for f in _iterdir(global_dir): + LOGGER.debug("Purging %s", f) + rmtree(f, after_5s_warning=warn_msg) + + def execute(cmd): LOGGER.debug("BEGIN uninstall_command.execute: %r", cmd.args) @@ -31,28 +86,28 @@ def execute(cmd): cmd.tags = [] if cmd.purge: - if cmd.ask_yn("Uninstall all runtimes?"): - for i in installed: - LOGGER.info("Purging %s from %s", i["display-name"], i["prefix"]) - try: - rmtree( - i["prefix"], - after_5s_warning=warn_msg.format(i["display-name"]), - remove_ext_first=("exe", "dll", "json") - ) - except FilesInUseError: - LOGGER.warn("Unable to purge %s because it is still in use.", - i["display-name"]) - continue - LOGGER.info("Purging saved downloads from %s", cmd.download_dir) - rmtree(cmd.download_dir, after_5s_warning=warn_msg.format("cached downloads")) - LOGGER.info("Purging global commands from %s", cmd.global_dir) - for f in _iterdir(cmd.global_dir): - LOGGER.debug("Purging %s", f) - rmtree(f, after_5s_warning=warn_msg.format("global commands")) - LOGGER.info("Purging all shortcuts") - for _, cleanup in SHORTCUT_HANDLERS.values(): - cleanup(cmd, []) + if not cmd.ask_yn("Uninstall all runtimes?"): + LOGGER.debug("END uninstall_command.execute") + return + for i in installed: + LOGGER.info("Purging %s from %s", i["display-name"], i["prefix"]) + try: + rmtree( + i["prefix"], + after_5s_warning=warn_msg.format(i["display-name"]), + remove_ext_first=("exe", "dll", "json") + ) + except FilesInUseError: + LOGGER.warn("Unable to purge %s because it is still in use.", + i["display-name"]) + continue + LOGGER.info("Purging saved downloads from %s", cmd.download_dir) + rmtree(cmd.download_dir, after_5s_warning=warn_msg.format("cached downloads")) + # Purge global commands directory + _do_purge_global_dir(cmd.global_dir, warn_msg.format("global commands")) + LOGGER.info("Purging all shortcuts") + for _, cleanup in SHORTCUT_HANDLERS.values(): + cleanup(cmd, []) LOGGER.debug("END uninstall_command.execute") return diff --git a/tests/conftest.py b/tests/conftest.py index 321aeaf..d475ad5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -205,6 +205,14 @@ def setup(self, _subkey=None, **keys): else: raise TypeError("unsupported type in registry") + def getvalue(self, subkey, valuename): + with winreg.OpenKeyEx(self.key, subkey) as key: + return winreg.QueryValueEx(key, valuename)[0] + + def getvalueandkind(self, subkey, valuename): + with winreg.OpenKeyEx(self.key, subkey) as key: + return winreg.QueryValueEx(key, valuename) + @pytest.fixture(scope='function') def registry(): diff --git a/tests/test_uninstall_command.py b/tests/test_uninstall_command.py new file mode 100644 index 0000000..6edb26c --- /dev/null +++ b/tests/test_uninstall_command.py @@ -0,0 +1,19 @@ +import os +import pytest +import winreg + +from pathlib import Path + +from manage import uninstall_command as UC + + +def test_purge_global_dir(monkeypatch, registry, tmp_path): + registry.setup(Path=rf"C:\A;{tmp_path}\X;{tmp_path};C:\B;%PTH%;C:\%D%\E") + (tmp_path / "test.txt").write_bytes(b"") + (tmp_path / "test2.txt").write_bytes(b"") + + monkeypatch.setitem(os.environ, "PTH", str(tmp_path)) + UC._do_purge_global_dir(tmp_path, "SLOW WARNING", hive=registry.hive, subkey=registry.root) + assert registry.getvalueandkind("", "Path") == ( + rf"C:\A;{tmp_path}\X;C:\B;%PTH%;C:\%D%\E", winreg.REG_SZ) + assert not list(tmp_path.iterdir())