Skip to content

Commit 624ffb6

Browse files
committed
Rework alias linking to ensure links are only created within bin directory.
Fixes #258
1 parent a11b7d3 commit 624ffb6

File tree

2 files changed

+53
-48
lines changed

2 files changed

+53
-48
lines changed

src/manage/aliasutils.py

Lines changed: 50 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -129,17 +129,58 @@ def _create_alias(
129129
return
130130

131131
existing_bytes = b''
132-
try:
133-
with open(p, 'rb') as f:
134-
existing_bytes = f.read(len(launcher_bytes) + 1)
135-
except FileNotFoundError:
136-
pass
137-
except OSError:
138-
LOGGER.debug("Failed to read existing alias launcher.")
132+
if getattr(cmd, "force", False):
133+
# Only expect InstallCommand to have .force
134+
unlink(p)
135+
else:
136+
try:
137+
with open(p, 'rb') as f:
138+
existing_bytes = f.read(len(launcher_bytes) + 1)
139+
except FileNotFoundError:
140+
pass
141+
except OSError:
142+
LOGGER.debug("Failed to read existing alias launcher.")
139143

140144
launcher_remap = cmd.scratch.setdefault("aliasutils.create_alias.launcher_remap", {})
141-
if not allow_link or not _link:
142-
# If links are disallowed, always replace the target with a copy.
145+
146+
if existing_bytes != launcher_bytes and allow_link and _link:
147+
# Try to find an existing launcher we can hard-link
148+
launcher2 = launcher_remap.get(launcher.name)
149+
if not launcher2:
150+
# None known, so search existing files
151+
try:
152+
LOGGER.debug("Searching %s for suitable launcher to link", cmd.global_dir)
153+
for p2 in cmd.global_dir.glob("*.exe"):
154+
try:
155+
with open(p2, 'rb') as f:
156+
existing_bytes2 = f.read(len(launcher_bytes) + 1)
157+
except OSError:
158+
LOGGER.debug("Failed to check %s contents", p2, exc_info=True)
159+
else:
160+
if existing_bytes2 == launcher_bytes:
161+
launcher2 = p2
162+
break
163+
else:
164+
LOGGER.debug("No existing launcher available")
165+
except Exception:
166+
LOGGER.debug("Failed to find existing launcher", exc_info=True)
167+
168+
if launcher2:
169+
# We know that the target either doesn't exist or needs replacing
170+
unlink(p)
171+
try:
172+
_link(launcher2, p)
173+
existing_bytes = launcher_bytes
174+
launcher_remap[launcher.name] = launcher2
175+
LOGGER.debug("Created %s as hard link to %s", p.name, launcher2.name)
176+
except FileNotFoundError:
177+
raise
178+
except OSError:
179+
LOGGER.debug("Failed to create hard link to %s", launcher2.name)
180+
launcher2 = None
181+
182+
# Recheck - existing_bytes will have been updated if we successfully linked
183+
if existing_bytes != launcher_bytes:
143184
unlink(p)
144185
try:
145186
p.write_bytes(launcher_bytes)
@@ -148,43 +189,6 @@ def _create_alias(
148189
except OSError:
149190
LOGGER.error("Failed to create global command %s.", name)
150191
LOGGER.debug("TRACEBACK", exc_info=True)
151-
elif existing_bytes == launcher_bytes:
152-
# Valid existing launcher, so save its path in case we need it later
153-
# for a hard link.
154-
launcher_remap.setdefault(launcher.name, p)
155-
else:
156-
# Links are allowed and we need to create one, so try to make a link,
157-
# falling back to a link to another existing alias (that we've checked
158-
# already during this run), and then falling back to a copy.
159-
# This handles the case where our links are on a different volume to the
160-
# install (so hard links don't work), but limits us to only a single
161-
# copy (each) of the redirector(s), thus saving space.
162-
unlink(p)
163-
try:
164-
_link(launcher, p)
165-
LOGGER.debug("Created %s as hard link to %s", p.name, launcher.name)
166-
except OSError as ex:
167-
if ex.winerror != 17:
168-
# Report errors other than cross-drive links
169-
LOGGER.debug("Failed to create hard link for command.", exc_info=True)
170-
launcher2 = launcher_remap.get(launcher.name)
171-
if launcher2:
172-
try:
173-
_link(launcher2, p)
174-
LOGGER.debug("Created %s as hard link to %s", p.name, launcher2.name)
175-
except FileNotFoundError:
176-
raise
177-
except OSError:
178-
LOGGER.debug("Failed to create hard link to fallback launcher")
179-
launcher2 = None
180-
if not launcher2:
181-
try:
182-
p.write_bytes(launcher_bytes)
183-
LOGGER.debug("Created %s as copy of %s", p.name, launcher.name)
184-
launcher_remap[launcher.name] = p
185-
except OSError:
186-
LOGGER.error("Failed to create global command %s.", name)
187-
LOGGER.debug("TRACEBACK", exc_info=True)
188192

189193
p_target = p.with_name(p.name + ".__target__")
190194
do_update = True

tests/test_alias.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class Cmd:
1717
launcher_exe = "launcher.txt"
1818
launcherw_exe = "launcherw.txt"
1919
default_platform = "-64"
20+
force = False
2021

2122
def __init__(self, platform=None):
2223
self.scratch = {}
@@ -184,7 +185,8 @@ def fake_link(x, y):
184185
)
185186
assert_log(
186187
"Create %s linking to %s",
187-
"Failed to create hard link.+",
188+
"Searching %s for suitable launcher to link",
189+
"No existing launcher available",
188190
"Created %s as copy of %s",
189191
assert_log.end_of_log(),
190192
)
@@ -218,7 +220,6 @@ def fake_link(x, y):
218220
)
219221
assert_log(
220222
"Create %s linking to %s",
221-
"Failed to create hard link.+",
222223
("Created %s as hard link to %s", ("test.exe", "actual_launcher.txt")),
223224
assert_log.end_of_log(),
224225
)

0 commit comments

Comments
 (0)