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()]

Check warning on line 13 in src/manage/uninstall_command.py

View check run for this annotation

Codecov / codecov/patch

src/manage/uninstall_command.py#L13

Added line #L13 was not covered by tests
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

Check warning on line 25 in src/manage/uninstall_command.py

View check run for this annotation

Codecov / codecov/patch

src/manage/uninstall_command.py#L25

Added line #L25 was not covered by tests
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)

Check warning on line 32 in src/manage/uninstall_command.py

View check run for this annotation

Codecov / codecov/patch

src/manage/uninstall_command.py#L30-L32

Added lines #L30 - L32 were not covered by tests
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 "

Check warning on line 57 in src/manage/uninstall_command.py

View check run for this annotation

Codecov / codecov/patch

src/manage/uninstall_command.py#L57

Added line #L57 was not covered by tests
"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",

Check warning on line 64 in src/manage/uninstall_command.py

View check run for this annotation

Codecov / codecov/patch

src/manage/uninstall_command.py#L63-L64

Added lines #L63 - L64 were not covered by tests
exc_info=True)

if not global_dir.is_dir():
return

Check warning on line 68 in src/manage/uninstall_command.py

View check run for this annotation

Codecov / codecov/patch

src/manage/uninstall_command.py#L68

Added line #L68 was not covered by tests
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 @@
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

Check warning on line 91 in src/manage/uninstall_command.py

View check run for this annotation

Codecov / codecov/patch

src/manage/uninstall_command.py#L90-L91

Added lines #L90 - L91 were not covered by tests
for i in installed:
LOGGER.info("Purging %s from %s", i["display-name"], i["prefix"])
try:
rmtree(

Check warning on line 95 in src/manage/uninstall_command.py

View check run for this annotation

Codecov / codecov/patch

src/manage/uninstall_command.py#L93-L95

Added lines #L93 - L95 were not covered by tests
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.",

Check warning on line 101 in src/manage/uninstall_command.py

View check run for this annotation

Codecov / codecov/patch

src/manage/uninstall_command.py#L100-L101

Added lines #L100 - L101 were not covered by tests
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"))

Check warning on line 105 in src/manage/uninstall_command.py

View check run for this annotation

Codecov / codecov/patch

src/manage/uninstall_command.py#L103-L105

Added lines #L103 - L105 were not covered by tests
# Purge global commands directory
_do_purge_global_dir(cmd.global_dir, warn_msg.format("global commands"))
LOGGER.info("Purging all shortcuts")

Check warning on line 108 in src/manage/uninstall_command.py

View check run for this annotation

Codecov / codecov/patch

src/manage/uninstall_command.py#L107-L108

Added lines #L107 - L108 were not covered by tests
for _, cleanup in SHORTCUT_HANDLERS.values():
cleanup(cmd, [])

Check warning on line 110 in src/manage/uninstall_command.py

View check run for this annotation

Codecov / codecov/patch

src/manage/uninstall_command.py#L110

Added line #L110 was not covered by tests
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 @@
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]

Check warning on line 210 in tests/conftest.py

View check run for this annotation

Codecov / codecov/patch

tests/conftest.py#L209-L210

Added lines #L209 - L210 were not covered by tests

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())