Fix PATH for apt/git/nmcli in systemd service environment

systemd runs services with a minimal PATH that may not include
/usr/bin or /usr/sbin. Add _safe_env() helpers that ensure standard
paths are present, and expand apt-cache discovery to check common
locations directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
roberts 2026-03-14 19:20:28 -05:00
parent 9c402e3726
commit 0ec9726db4
3 changed files with 52 additions and 6 deletions

View file

@ -46,6 +46,17 @@ def _require_nmcli():
raise HTTPException(503, "NetworkManager (nmcli) not available on this system")
def _safe_env():
"""Build environment with full PATH for nmcli commands."""
env = {**os.environ, "LC_ALL": "C"}
path = env.get("PATH", "")
for p in ("/usr/bin", "/usr/sbin", "/bin", "/sbin"):
if p not in path:
path = p + ":" + path
env["PATH"] = path
return env
async def _nmcli(*args: str, timeout: float = 30) -> str:
"""Run nmcli with C locale, return stdout. Raise HTTPException on failure."""
_require_nmcli()
@ -54,7 +65,7 @@ async def _nmcli(*args: str, timeout: float = 30) -> str:
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
env={**os.environ, "LC_ALL": "C"},
env=_safe_env(),
)
try:
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)

View file

@ -16,7 +16,19 @@ router = APIRouter(prefix="/api/packages", tags=["packages"])
# Guard
# ---------------------------------------------------------------------------
_HAS_APT = bool(shutil.which("apt-cache"))
def _find_apt():
"""Find apt-cache binary, checking common paths if PATH is restricted."""
found = shutil.which("apt-cache")
if found:
return True
# systemd services may have a minimal PATH — check directly
for p in ("/usr/bin/apt-cache", "/usr/sbin/apt-cache"):
if os.path.isfile(p) and os.access(p, os.X_OK):
return True
return False
_HAS_APT = _find_apt()
def _require_apt():
@ -38,6 +50,18 @@ def _validate_name(name: str) -> str:
return name
def _safe_env():
"""Build environment with full PATH for apt/dpkg commands."""
env = {**os.environ, "LC_ALL": "C", "DEBIAN_FRONTEND": "noninteractive"}
# Ensure standard paths are included (systemd may use minimal PATH)
path = env.get("PATH", "")
for p in ("/usr/bin", "/usr/sbin", "/bin", "/sbin"):
if p not in path:
path = p + ":" + path
env["PATH"] = path
return env
async def _apt(*args: str, timeout: float = 30) -> str:
"""Run an apt/dpkg command and return stdout."""
_require_apt()
@ -46,7 +70,7 @@ async def _apt(*args: str, timeout: float = 30) -> str:
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
env={**os.environ, "LC_ALL": "C", "DEBIAN_FRONTEND": "noninteractive"},
env=_safe_env(),
)
try:
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
@ -68,7 +92,7 @@ async def _apt_nofail(*args: str, timeout: float = 30) -> tuple[int, str, str]:
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
env={**os.environ, "LC_ALL": "C", "DEBIAN_FRONTEND": "noninteractive"},
env=_safe_env(),
)
try:
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)

View file

@ -30,6 +30,17 @@ def _require_git():
# Helpers
# ---------------------------------------------------------------------------
def _safe_env():
"""Build environment with full PATH for git commands."""
env = {**os.environ, "LC_ALL": "C"}
path = env.get("PATH", "")
for p in ("/usr/bin", "/usr/sbin", "/bin", "/sbin"):
if p not in path:
path = p + ":" + path
env["PATH"] = path
return env
async def _git(*args: str, timeout: float = 30) -> str:
"""Run a git command in the Atlus install directory."""
_require_git()
@ -38,7 +49,7 @@ async def _git(*args: str, timeout: float = 30) -> str:
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
env={**os.environ, "LC_ALL": "C"},
env=_safe_env(),
)
try:
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
@ -60,7 +71,7 @@ async def _git_nofail(*args: str, timeout: float = 30) -> tuple[int, str]:
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
env={**os.environ, "LC_ALL": "C"},
env=_safe_env(),
)
try:
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)