fix(gateway): probe the target user's Node tree when generating a system unit
`_append_node_dir_for_service()` had two bugs, both caught by `test_system_unit_uses_target_user_home_not_calling_user`: 1. It crashed. `iter_hermes_node_dirs()` defaults to the *calling* user's Hermes home, so under sudo it stats `/root/.hermes/node/bin` — which raises `PermissionError` for a non-root caller rather than returning False. An unreadable candidate dir means "skip this rung", not "kill the generator", so the probe now swallows OSError. 2. Worse than the crash: had the stat succeeded, a `--system` unit targeting alice would have baked *root's* managed Node into alice's PATH. The generator now skips the managed-Node rung on the system path and re-runs it after `_hermes_home_for_target_user()` resolves, passing that home explicitly. Entries are prepended so the managed Node still outranks the remapped shell-PATH entries, matching the user-unit ordering. The launchd generator is unaffected — it has no target-user remapping, so the default home is already correct there.
This commit is contained in:
parent
b13148d354
commit
0c3551849f
|
|
@ -2749,7 +2749,9 @@ def _systemd_watchdog_service_fields(
|
|||
return "notify", f"NotifyAccess=main\nWatchdogSec={seconds}s\n"
|
||||
|
||||
|
||||
def _append_node_dir_for_service(path_entries: list[str]) -> None:
|
||||
def _append_node_dir_for_service(
|
||||
path_entries: list[str], hermes_root: Path | None = None
|
||||
) -> None:
|
||||
"""Add the Node directory a generated service unit should use to *path_entries*.
|
||||
|
||||
The Hermes-managed Node under ``$HERMES_HOME/node`` goes first when it
|
||||
|
|
@ -2757,16 +2759,27 @@ def _append_node_dir_for_service(path_entries: list[str]) -> None:
|
|||
a service unit is written once and then survives reboots, so resolving a
|
||||
system Node that happens to be ahead on the installing shell's PATH bakes
|
||||
the wrong interpreter in permanently — the exact failure the desktop
|
||||
backend spawn was fixed for. Managed dirs are profile-scoped
|
||||
(``get_hermes_home()``), so each profile's unit still names its own Node.
|
||||
backend spawn was fixed for. Managed dirs are profile-scoped, so each
|
||||
profile's unit still names its own Node.
|
||||
|
||||
*hermes_root* is the Hermes home the unit will run against. System units
|
||||
installed via sudo MUST pass the **target user's** home: probing the
|
||||
default (the calling user's — root's — tree) would bake root's Node into
|
||||
the target user's unit. The probe swallows OSError: an unreadable
|
||||
candidate dir (hardened home) means "skip the rung", not "crash the
|
||||
generator".
|
||||
|
||||
PATH lookup remains the fallback rung for installs with no managed Node.
|
||||
"""
|
||||
from hermes_constants import iter_hermes_node_dirs
|
||||
|
||||
for directory in iter_hermes_node_dirs():
|
||||
for directory in iter_hermes_node_dirs(hermes_root):
|
||||
entry = str(directory)
|
||||
if directory.is_dir() and entry not in path_entries:
|
||||
try:
|
||||
present = directory.is_dir()
|
||||
except OSError:
|
||||
present = False
|
||||
if present and entry not in path_entries:
|
||||
path_entries.append(entry)
|
||||
|
||||
resolved_node = shutil.which("node")
|
||||
|
|
@ -2793,7 +2806,11 @@ def generate_systemd_unit(system: bool = False, run_as_user: str | None = None)
|
|||
venv_dir = str(detected_venv) if detected_venv else str(PROJECT_ROOT / "venv")
|
||||
|
||||
path_entries = _build_service_path_dirs()
|
||||
_append_node_dir_for_service(path_entries)
|
||||
if not system:
|
||||
# System units append the managed Node dirs later, once the TARGET
|
||||
# user's Hermes home is known — probing here would stat the calling
|
||||
# (sudo → root's) tree and bake the wrong user's Node into the unit.
|
||||
_append_node_dir_for_service(path_entries)
|
||||
|
||||
common_bin_paths = [
|
||||
"/usr/local/sbin",
|
||||
|
|
@ -2827,6 +2844,17 @@ def generate_systemd_unit(system: bool = False, run_as_user: str | None = None)
|
|||
working_dir = str(hermes_home) if hermes_home else _remap_path_for_user(working_dir, home_dir)
|
||||
venv_dir = _remap_path_for_user(venv_dir, home_dir)
|
||||
path_entries = [_remap_path_for_user(p, home_dir) for p in path_entries]
|
||||
# Managed Node for the TARGET user's tree (see the skip above): probe
|
||||
# the remapped hermes_home, not the calling user's. Prepend — the
|
||||
# managed Node must outrank remapped shell-PATH entries, matching the
|
||||
# user-unit ordering where it's appended before PATH capture.
|
||||
_target_node_entries: list[str] = []
|
||||
_append_node_dir_for_service(
|
||||
_target_node_entries, Path(hermes_home) if hermes_home else None
|
||||
)
|
||||
path_entries = [
|
||||
e for e in _target_node_entries if e not in path_entries
|
||||
] + path_entries
|
||||
path_entries.extend(_build_user_local_paths(Path(home_dir), path_entries))
|
||||
path_entries.extend(_build_wsl_interop_paths(path_entries))
|
||||
path_entries.extend(common_bin_paths)
|
||||
|
|
|
|||
Loading…
Reference in New Issue