diff --git a/systemdspawner/systemd.py b/systemdspawner/systemd.py index d487452..02033ff 100644 --- a/systemdspawner/systemd.py +++ b/systemdspawner/systemd.py @@ -9,6 +9,7 @@ import os import re import shlex +import shutil import subprocess import warnings @@ -143,6 +144,24 @@ async def start_transient_service( ) run_cmd.append(f"--property=EnvironmentFile={environment_file}") + # make sure cmd[0] is absolute, taking $PATH into account. + # systemd-run does not use the unit's $PATH environment + # to resolve relative paths. + if not os.path.isabs(cmd[0]): + if environment_variables and "PATH" in environment_variables: + # if unit specifies a $PATH, use it + path = environment_variables["PATH"] + else: + # search current process $PATH by default. + # this is the default behavior of shutil.which(path=None) + # but we still need the value for the error message + path = os.getenv("PATH", os.defpath) + exe = cmd[0] + abs_exe = shutil.which(exe, path=path) + if not abs_exe: + raise FileNotFoundError(f"{exe} not found on {path}") + cmd[0] = abs_exe + # Append typical Spawner "cmd" and "args" on how to start the user server run_cmd += cmd + args diff --git a/tests/test_systemd.py b/tests/test_systemd.py index c06c296..6bb3d33 100644 --- a/tests/test_systemd.py +++ b/tests/test_systemd.py @@ -126,6 +126,26 @@ async def test_workdir(): assert text == d +async def test_executable_path(): + unit_name = "systemdspawner-unittest-" + str(time.time()) + _, env_filename = tempfile.mkstemp() + with tempfile.TemporaryDirectory() as d: + await systemd.start_transient_service( + unit_name, + ["bash"], + ["-c", f"pwd > {d}/pwd"], + working_dir=d, + environment_variables={"PATH": os.environ["PATH"]}, + ) + + # Wait a tiny bit for the systemd unit to complete running + await asyncio.sleep(0.1) + + with open(os.path.join(d, "pwd")) as f: + text = f.read().strip() + assert text == d + + async def test_slice(): unit_name = "systemdspawner-unittest-" + str(time.time()) _, env_filename = tempfile.mkstemp()