Skip to content

Commit f44e097

Browse files
authored
Makes uninstall --purge also remove PATH entry. (#112)
Fixes #95
1 parent a2ccd85 commit f44e097

File tree

3 files changed

+107
-25
lines changed

3 files changed

+107
-25
lines changed

src/manage/uninstall_command.py

Lines changed: 80 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,75 @@
33
from .installs import get_matching_install_tags
44
from .install_command import SHORTCUT_HANDLERS, update_all_shortcuts
55
from .logging import LOGGER
6-
from .pathutils import PurePath
6+
from .pathutils import Path, PurePath
77
from .tagutils import tag_or_range
88

99

1010
def _iterdir(p, only_files=False):
1111
try:
1212
if only_files:
13-
return [f for f in p.iterdir() if p.is_file()]
14-
return list(p.iterdir())
13+
return [f for f in Path(p).iterdir() if f.is_file()]
14+
return list(Path(p).iterdir())
1515
except FileNotFoundError:
1616
LOGGER.debug("Skipping %s because it does not exist", p)
1717
return []
1818

1919

20+
def _do_purge_global_dir(global_dir, warn_msg, *, hive=None, subkey="Environment"):
21+
import os
22+
import winreg
23+
24+
if hive is None:
25+
hive = winreg.HKEY_CURRENT_USER
26+
try:
27+
with winreg.OpenKeyEx(hive, subkey) as key:
28+
path, kind = winreg.QueryValueEx(key, "Path")
29+
if kind not in (winreg.REG_SZ, winreg.REG_EXPAND_SZ):
30+
raise ValueError("Value kind is not a string")
31+
except (OSError, ValueError):
32+
LOGGER.debug("Not removing global commands directory from PATH", exc_info=True)
33+
else:
34+
LOGGER.debug("Current PATH contains %s", path)
35+
paths = path.split(";")
36+
newpaths = []
37+
for p in paths:
38+
# We should expand entries here, but we only want to remove those
39+
# that we added ourselves (during firstrun), and we never use
40+
# environment variables. So even if the kind is REG_EXPAND_SZ, we
41+
# don't need to expand to find our own entry.
42+
#ep = os.path.expandvars(p) if kind == winreg.REG_EXPAND_SZ else p
43+
ep = p
44+
if PurePath(ep).match(global_dir):
45+
LOGGER.debug("Removing from PATH: %s", p)
46+
else:
47+
newpaths.append(p)
48+
if len(newpaths) < len(paths):
49+
newpath = ";".join(newpaths)
50+
with winreg.CreateKeyEx(hive, subkey, access=winreg.KEY_READ|winreg.KEY_WRITE) as key:
51+
path2, kind2 = winreg.QueryValueEx(key, "Path")
52+
if path2 == path and kind2 == kind:
53+
LOGGER.info("Removing global commands directory from PATH")
54+
LOGGER.debug("New PATH contains %s", newpath)
55+
winreg.SetValueEx(key, "Path", 0, kind, newpath)
56+
else:
57+
LOGGER.debug("Not removing global commands directory from PATH "
58+
"because the registry changed while processing.")
59+
60+
try:
61+
from _native import broadcast_settings_change
62+
broadcast_settings_change()
63+
except (ImportError, OSError):
64+
LOGGER.debug("Did not broadcast settings change notification",
65+
exc_info=True)
66+
67+
if not global_dir.is_dir():
68+
return
69+
LOGGER.info("Purging global commands from %s", global_dir)
70+
for f in _iterdir(global_dir):
71+
LOGGER.debug("Purging %s", f)
72+
rmtree(f, after_5s_warning=warn_msg)
73+
74+
2075
def execute(cmd):
2176
LOGGER.debug("BEGIN uninstall_command.execute: %r", cmd.args)
2277

@@ -31,28 +86,28 @@ def execute(cmd):
3186
cmd.tags = []
3287

3388
if cmd.purge:
34-
if cmd.ask_yn("Uninstall all runtimes?"):
35-
for i in installed:
36-
LOGGER.info("Purging %s from %s", i["display-name"], i["prefix"])
37-
try:
38-
rmtree(
39-
i["prefix"],
40-
after_5s_warning=warn_msg.format(i["display-name"]),
41-
remove_ext_first=("exe", "dll", "json")
42-
)
43-
except FilesInUseError:
44-
LOGGER.warn("Unable to purge %s because it is still in use.",
45-
i["display-name"])
46-
continue
47-
LOGGER.info("Purging saved downloads from %s", cmd.download_dir)
48-
rmtree(cmd.download_dir, after_5s_warning=warn_msg.format("cached downloads"))
49-
LOGGER.info("Purging global commands from %s", cmd.global_dir)
50-
for f in _iterdir(cmd.global_dir):
51-
LOGGER.debug("Purging %s", f)
52-
rmtree(f, after_5s_warning=warn_msg.format("global commands"))
53-
LOGGER.info("Purging all shortcuts")
54-
for _, cleanup in SHORTCUT_HANDLERS.values():
55-
cleanup(cmd, [])
89+
if not cmd.ask_yn("Uninstall all runtimes?"):
90+
LOGGER.debug("END uninstall_command.execute")
91+
return
92+
for i in installed:
93+
LOGGER.info("Purging %s from %s", i["display-name"], i["prefix"])
94+
try:
95+
rmtree(
96+
i["prefix"],
97+
after_5s_warning=warn_msg.format(i["display-name"]),
98+
remove_ext_first=("exe", "dll", "json")
99+
)
100+
except FilesInUseError:
101+
LOGGER.warn("Unable to purge %s because it is still in use.",
102+
i["display-name"])
103+
continue
104+
LOGGER.info("Purging saved downloads from %s", cmd.download_dir)
105+
rmtree(cmd.download_dir, after_5s_warning=warn_msg.format("cached downloads"))
106+
# Purge global commands directory
107+
_do_purge_global_dir(cmd.global_dir, warn_msg.format("global commands"))
108+
LOGGER.info("Purging all shortcuts")
109+
for _, cleanup in SHORTCUT_HANDLERS.values():
110+
cleanup(cmd, [])
56111
LOGGER.debug("END uninstall_command.execute")
57112
return
58113

tests/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,14 @@ def setup(self, _subkey=None, **keys):
205205
else:
206206
raise TypeError("unsupported type in registry")
207207

208+
def getvalue(self, subkey, valuename):
209+
with winreg.OpenKeyEx(self.key, subkey) as key:
210+
return winreg.QueryValueEx(key, valuename)[0]
211+
212+
def getvalueandkind(self, subkey, valuename):
213+
with winreg.OpenKeyEx(self.key, subkey) as key:
214+
return winreg.QueryValueEx(key, valuename)
215+
208216

209217
@pytest.fixture(scope='function')
210218
def registry():

tests/test_uninstall_command.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import os
2+
import pytest
3+
import winreg
4+
5+
from pathlib import Path
6+
7+
from manage import uninstall_command as UC
8+
9+
10+
def test_purge_global_dir(monkeypatch, registry, tmp_path):
11+
registry.setup(Path=rf"C:\A;{tmp_path}\X;{tmp_path};C:\B;%PTH%;C:\%D%\E")
12+
(tmp_path / "test.txt").write_bytes(b"")
13+
(tmp_path / "test2.txt").write_bytes(b"")
14+
15+
monkeypatch.setitem(os.environ, "PTH", str(tmp_path))
16+
UC._do_purge_global_dir(tmp_path, "SLOW WARNING", hive=registry.hive, subkey=registry.root)
17+
assert registry.getvalueandkind("", "Path") == (
18+
rf"C:\A;{tmp_path}\X;C:\B;%PTH%;C:\%D%\E", winreg.REG_SZ)
19+
assert not list(tmp_path.iterdir())

0 commit comments

Comments
 (0)