Background services without systemd (runit, OpenRC, ...)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
sarahandClaude Opus 5.5 committed 2026-10-13 17:51:30 +02:00
1 parent e7e233951f
commit f63e708572
8 files changed
+325 -110

No files matched your search

+57 -18
View File
@@ -19,7 +19,11 @@ from pathlib import Path, PurePosixPath
from typing import Callable, Optional
from ConfigMerge import ApplySet, MergeConfig, MergeResult
from Core import BackupsDir, ChildEnvironment, DownloadsDir, IsWindows, LoadConfig, PristineDir, StartMenuDir, StartupDir
import Services
from Core import (
AutostartDir, BackupsDir, ChildEnvironment, DownloadsDir, HasSystemd, IsWindows, LoadConfig, PristineDir,
StartMenuDir, StartupDir,
)
from Manifests import Leftover, Program, Render
from Receipts import IniRecord, LoadReceipt, PlacedFile, Receipt
from Sources import ClearDownloads, Download, LatestCommit, SourceError
@@ -55,6 +59,8 @@ def _Run(Command: list[str], Log: LogFn, Check: bool = True, Cwd: Optional[Path]
def _Systemctl(Log: LogFn, *Args: str, Check: bool = False) -> int:
if not HasSystemd:
return 0 # services run through Services.py instead
return _Run(["systemctl", "--user", *Args], Log, Check=Check)
@@ -499,11 +505,30 @@ def _ApplyShortcuts(Program: Program, Context: dict, OldReceipt: Optional[Receip
)
def _ApplyWindowsServices(Program: Program, Context: dict, OldReceipt: Optional[Receipt], NewReceipt: Receipt, Log: LogFn) -> None:
"""A Startup-folder shortcut runs the service at login (through
ServiceRun.py, which records its PID); enabling also starts it now."""
import WindowsSupport
def _PlaceAutostart(Program: Program, Service, Command: tuple, OldReceipt: Optional[Receipt],
NewReceipt: Receipt, Log: LogFn) -> None:
"""Linux without systemd: the XDG autostart entry that starts a service at
login. Recorded like a shortcut, so it goes when the service is turned off
or the program removed (and one that was already there comes back)."""
Entry = AutostartDir / f"{Service.Name}.desktop"
Previous = OldReceipt.FindFile(str(Entry)) if OldReceipt is not None else None
Placed = PlacedFile(str(Entry), "Shortcut", Displaced=Previous.Displaced if Previous else "")
if Previous is None and Entry.exists():
Displaced = BackupsDir / Program.Name / "Displaced" / _BackupName(str(Entry))
Displaced.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(Entry, Displaced)
Placed.Displaced = str(Displaced)
Entry.parent.mkdir(parents=True, exist_ok=True)
Entry.write_text(Services.AutostartEntry(Service.Name, *Command))
NewReceipt.Files.append(Placed)
Log(f"Wrote {Entry}")
def _ApplyStandInServices(Program: Program, Context: dict, OldReceipt: Optional[Receipt], NewReceipt: Receipt, Log: LogFn) -> None:
"""Services SaraSuite runs itself (Windows; Linux without systemd): at
login from a Startup-folder shortcut / an autostart entry, through
ServiceRun.py; enabling also starts it now. An update restarts it, so
it runs the new code."""
WasEnabled = set(OldReceipt.EnabledServices) if OldReceipt else set()
Runner = Program.ProgramDir / "ServiceRun.py"
for Service in Program.Services:
@@ -511,13 +536,19 @@ def _ApplyWindowsServices(Program: Program, Context: dict, OldReceipt: Optional[
WorkingDir = Path(Render(Service.WorkingDir, Context)) if Service.WorkingDir else Program.AppDir
if Program.OptionEnabled(Service.Option, NewReceipt.Options):
NewReceipt.EnabledServices.append(Service.Id)
_PlaceShortcut(
Program, StartupDir / f"{Service.Name}.lnk", f'"{Runner}" {Service.Name} "{Script}"',
WorkingDir, None, f"{Service.Name} (background service)", OldReceipt, NewReceipt, Log,
)
WindowsSupport.StartService(Service.Name, Program.PythonW, Runner, Script, WorkingDir, Log)
if IsWindows:
_PlaceShortcut(
Program, StartupDir / f"{Service.Name}.lnk", f'"{Runner}" {Service.Name} "{Script}" "{WorkingDir}"',
WorkingDir, None, f"{Service.Name} (background service)", OldReceipt, NewReceipt, Log,
)
else:
_PlaceAutostart(Program, Service, (Program.Python, Runner, Script, WorkingDir), OldReceipt, NewReceipt, Log)
if Service.Id in WasEnabled and not IsWindows: # Windows stopped it before the update
Services.Restart(Service.Name, Program.PythonW, Runner, Script, WorkingDir, Log)
else:
Services.Start(Service.Name, Program.PythonW, Runner, Script, WorkingDir, Log)
elif Service.Id in WasEnabled:
WindowsSupport.StopService(Service.Name, Log)
Services.Stop(Service.Name, Log)
def _StopWindowsServices(Program: Program, Receipt: Optional[Receipt], Log: LogFn) -> None:
@@ -525,17 +556,15 @@ def _StopWindowsServices(Program: Program, Receipt: Optional[Receipt], Log: LogF
its files or venv are replaced or removed."""
if not IsWindows or Receipt is None:
return
import WindowsSupport
for Name in Receipt.EnabledServices:
WindowsSupport.StopService(Name, Log)
Services.Stop(Name, Log)
def _ApplyServices(Program: Program, OldReceipt: Optional[Receipt], NewReceipt: Receipt, Log: LogFn, Context: Optional[dict] = None) -> None:
if not Program.Services and not (OldReceipt and OldReceipt.EnabledServices):
return
if IsWindows:
_ApplyWindowsServices(Program, Context or Program.Context(NewReceipt.Answers), OldReceipt, NewReceipt, Log)
if IsWindows or not HasSystemd:
_ApplyStandInServices(Program, Context or Program.Context(NewReceipt.Answers), OldReceipt, NewReceipt, Log)
return
_Systemctl(Log, "daemon-reload")
WasEnabled = set(OldReceipt.EnabledServices) if OldReceipt else set()
@@ -557,7 +586,12 @@ def _AfterIntegrationChanges(Program: Program, Log: LogFn) -> None:
if any(Template.Target.endswith(".desktop") for Template in Program.Templates) and Applications.exists():
_Run(["update-desktop-database", str(Applications)], Log, Check=False)
for Unit in Program.RestartUnits:
_Systemctl(Log, "try-restart", Unit)
if HasSystemd:
_Systemctl(Log, "try-restart", Unit)
else:
# No systemd to restart it: stop it, and D-Bus starts it again
# when it's next needed (xdg-desktop-portal is D-Bus activated).
_Run(["pkill", "-x", "-u", str(os.getuid()), Unit.removesuffix(".service")], Log, Check=False)
def Apply(
@@ -686,7 +720,9 @@ def PlanRemoval(Program: Program) -> RemovalPlan:
Plan.Items.append(RemovalItem(str(Downloaded), "Program", "downloaded source"))
if Current is not None:
ServiceNote = "background service, stopped and taken out of Startup" if IsWindows else "systemd user service, stopped and disabled"
ServiceNote = ("background service, stopped and taken out of Startup" if IsWindows
else "systemd user service, stopped and disabled" if HasSystemd
else "background service, stopped and taken out of autostart")
for Unit in Current.EnabledServices:
Plan.Items.append(RemovalItem(Unit, "Integration", ServiceNote))
for Placed in Current.Files:
@@ -733,6 +769,9 @@ def Remove(Program: Program, RemoveConfigs: bool, RemoveRuntime: bool, Log: LogF
if Current is not None:
if IsWindows:
_StopWindowsServices(Program, Current, Log)
elif not HasSystemd:
for Name in Current.EnabledServices:
Services.Stop(Name, Log)
else:
for Unit in Current.EnabledServices:
_Systemctl(Log, "disable", "--now", Unit)