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
105 changes: 80 additions & 25 deletions src/manage/uninstall_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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

Expand Down
8 changes: 8 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
19 changes: 19 additions & 0 deletions tests/test_uninstall_command.py
Original file line number Diff line number Diff line change
@@ -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())