fix(gateway): quote systemd service paths to handle spaces

This commit is contained in:
Es1la 2026-04-22 09:32:19 +03:00
parent ff9752410a
commit d75278e26f
2 changed files with 39 additions and 4 deletions

View file

@ -1176,6 +1176,14 @@ def _hermes_home_for_target_user(target_home_dir: str) -> str:
return str(current_hermes) return str(current_hermes)
def _systemd_quote_arg(value: str) -> str:
"""Quote a systemd command/path argument only when needed."""
text = str(value)
if text and not any(ch.isspace() for ch in text) and '"' not in text and "\\" not in text:
return text
return '"' + text.replace("\\", "\\\\").replace('"', '\\"') + '"'
def generate_systemd_unit(system: bool = False, run_as_user: str | None = None) -> str: def generate_systemd_unit(system: bool = False, run_as_user: str | None = None) -> str:
python_path = get_python_path() python_path = get_python_path()
working_dir = str(PROJECT_ROOT) working_dir = str(PROJECT_ROOT)
@ -1210,6 +1218,11 @@ def generate_systemd_unit(system: bool = False, run_as_user: str | None = None)
path_entries.extend(_build_user_local_paths(Path(home_dir), path_entries)) path_entries.extend(_build_user_local_paths(Path(home_dir), path_entries))
path_entries.extend(common_bin_paths) path_entries.extend(common_bin_paths)
sane_path = ":".join(path_entries) sane_path = ":".join(path_entries)
profile_suffix = f" {profile_arg}" if profile_arg else ""
exec_start = (
f"{_systemd_quote_arg(python_path)} -m hermes_cli.main"
f"{profile_suffix} gateway run --replace"
)
return f"""[Unit] return f"""[Unit]
Description={SERVICE_DESCRIPTION} Description={SERVICE_DESCRIPTION}
After=network-online.target After=network-online.target
@ -1221,8 +1234,8 @@ StartLimitBurst=5
Type=simple Type=simple
User={username} User={username}
Group={group_name} Group={group_name}
ExecStart={python_path} -m hermes_cli.main{f" {profile_arg}" if profile_arg else ""} gateway run --replace ExecStart={exec_start}
WorkingDirectory={working_dir} WorkingDirectory={_systemd_quote_arg(working_dir)}
Environment="HOME={home_dir}" Environment="HOME={home_dir}"
Environment="USER={username}" Environment="USER={username}"
Environment="LOGNAME={username}" Environment="LOGNAME={username}"
@ -1248,6 +1261,11 @@ WantedBy=multi-user.target
path_entries.extend(_build_user_local_paths(Path.home(), path_entries)) path_entries.extend(_build_user_local_paths(Path.home(), path_entries))
path_entries.extend(common_bin_paths) path_entries.extend(common_bin_paths)
sane_path = ":".join(path_entries) sane_path = ":".join(path_entries)
profile_suffix = f" {profile_arg}" if profile_arg else ""
exec_start = (
f"{_systemd_quote_arg(python_path)} -m hermes_cli.main"
f"{profile_suffix} gateway run --replace"
)
return f"""[Unit] return f"""[Unit]
Description={SERVICE_DESCRIPTION} Description={SERVICE_DESCRIPTION}
After=network.target After=network.target
@ -1256,8 +1274,8 @@ StartLimitBurst=5
[Service] [Service]
Type=simple Type=simple
ExecStart={python_path} -m hermes_cli.main{f" {profile_arg}" if profile_arg else ""} gateway run --replace ExecStart={exec_start}
WorkingDirectory={working_dir} WorkingDirectory={_systemd_quote_arg(working_dir)}
Environment="PATH={sane_path}" Environment="PATH={sane_path}"
Environment="VIRTUAL_ENV={venv_dir}" Environment="VIRTUAL_ENV={venv_dir}"
Environment="HERMES_HOME={hermes_home}" Environment="HERMES_HOME={hermes_home}"

View file

@ -100,6 +100,23 @@ class TestGeneratedSystemdUnits:
assert "/home/test/.nvm/versions/node/v24.14.0/bin" in unit assert "/home/test/.nvm/versions/node/v24.14.0/bin" in unit
def test_user_unit_quotes_execstart_and_workdir_paths_with_spaces(self, tmp_path, monkeypatch):
project_root = tmp_path / "Hermes Agent"
venv_dir = project_root / "venv"
python_path = venv_dir / "bin" / "python"
python_path.parent.mkdir(parents=True)
python_path.write_text("", encoding="utf-8")
monkeypatch.setattr(gateway_cli, "PROJECT_ROOT", project_root)
monkeypatch.setattr(gateway_cli, "_detect_venv_dir", lambda: venv_dir)
monkeypatch.setattr(gateway_cli, "get_python_path", lambda: str(python_path))
monkeypatch.setattr(gateway_cli.shutil, "which", lambda cmd: None)
unit = gateway_cli.generate_systemd_unit(system=False)
assert f'ExecStart="{python_path}" -m hermes_cli.main gateway run --replace' in unit
assert f'WorkingDirectory="{project_root}"' in unit
def test_system_unit_avoids_recursive_execstop_and_uses_extended_stop_timeout(self): def test_system_unit_avoids_recursive_execstop_and_uses_extended_stop_timeout(self):
unit = gateway_cli.generate_systemd_unit(system=True) unit = gateway_cli.generate_systemd_unit(system=True)