mirror of
https://git.srhtnin.garden/sarah/SaraSort.git
synced 2026-10-14 10:48:11 +02:00
Windows support
This commit is contained in:
1 parent
f1935a119b
commit
6cb2bd74cc
13 files changed
+207
-25
No files matched your search
+107
-4
@@ -1,11 +1,22 @@
|
||||
"""SaraSortd's systemd user unit: status, start/stop/restart, autostart, and
|
||||
its journal. SaraSortd's own log files are read from its LogDir."""
|
||||
"""SaraSortd as a background service: status, start/stop/restart, autostart,
|
||||
and its journal. SaraSortd's own log files are read from its LogDir.
|
||||
|
||||
Linux: its systemd user unit (systemctl / journalctl).
|
||||
Windows: SaraSuite's stand-in for a systemd service, used the same way here:
|
||||
- it runs windowless as `pythonw.exe ServiceRun.py SaraSortd SaraSortd.py`
|
||||
from SaraSortd's config folder, and ServiceRun.py records its PID in
|
||||
%LOCALAPPDATA%\\SaraSuite\\Services\\SaraSortd.pid (that's how "running" is told);
|
||||
- autostart is a shortcut to that command in the Startup folder.
|
||||
Unit is the systemd unit on Linux and the service name ("SaraSortd") on Windows.
|
||||
"""
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from Core import ChildEnvironment
|
||||
from Core import ChildEnvironment, ConfigHome, DataHome, IsWindows, LoadConfig
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -22,11 +33,98 @@ class ServiceStatus:
|
||||
return self.Active in ("active", "activating", "reloading")
|
||||
|
||||
|
||||
_NoWindow = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
||||
|
||||
|
||||
def _Run(Arguments: list[str], Timeout: int = 15) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(Arguments, capture_output=True, text=True, timeout=Timeout, env=ChildEnvironment())
|
||||
return subprocess.run(Arguments, capture_output=True, text=True, timeout=Timeout, env=ChildEnvironment(),
|
||||
creationflags=_NoWindow)
|
||||
|
||||
|
||||
# --- Windows ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _PidFile(Name: str) -> Path:
|
||||
return DataHome / "SaraSuite" / "Services" / f"{Name}.pid"
|
||||
|
||||
|
||||
def _StartupLink(Name: str) -> Path:
|
||||
return ConfigHome / "Microsoft" / "Windows" / "Start Menu" / "Programs" / "Startup" / f"{Name}.lnk"
|
||||
|
||||
|
||||
def _WindowsCommand() -> tuple[Path, list[str], Path]:
|
||||
"""(pythonw.exe, its arguments, working folder), from SaraSuite's install layout."""
|
||||
Config = LoadConfig()
|
||||
AppDir = Path(Config.SortdAppDir).expanduser()
|
||||
ProgramDir = AppDir.parent
|
||||
PythonW = ProgramDir / ".venv" / "Scripts" / "pythonw.exe"
|
||||
Arguments = ["-X", "utf8", str(ProgramDir / "ServiceRun.py"), Config.ServiceName, str(Config.SortdScriptPath)]
|
||||
return PythonW, Arguments, Path(Config.SortdConfigDir).expanduser()
|
||||
|
||||
|
||||
def _WindowsPid(Name: str) -> str:
|
||||
try:
|
||||
Pid = int(_PidFile(Name).read_text().strip())
|
||||
Output = _Run(["tasklist", "/FI", f"PID eq {Pid}", "/NH", "/FO", "CSV"]).stdout
|
||||
except (OSError, ValueError, subprocess.SubprocessError):
|
||||
return ""
|
||||
return str(Pid) if re.search(rf'"{Pid}"', Output) else ""
|
||||
|
||||
|
||||
def _WindowsStatus(Name: str) -> ServiceStatus:
|
||||
PythonW, Arguments, _Folder = _WindowsCommand()
|
||||
Pid = _WindowsPid(Name)
|
||||
Since = ""
|
||||
if Pid:
|
||||
Since = datetime.fromtimestamp(_PidFile(Name).stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S")
|
||||
return ServiceStatus(
|
||||
Active="active" if Pid else "inactive",
|
||||
Sub="running" if Pid else "dead",
|
||||
Enabled="enabled" if _StartupLink(Name).exists() else "disabled",
|
||||
Pid=Pid,
|
||||
Since=Since,
|
||||
Found=PythonW.is_file() and Path(Arguments[2]).is_file(),
|
||||
)
|
||||
|
||||
|
||||
def _WindowsControl(Name: str, Action: str) -> str:
|
||||
PythonW, Arguments, Folder = _WindowsCommand()
|
||||
try:
|
||||
if Action in ("stop", "restart"):
|
||||
Pid = _WindowsPid(Name)
|
||||
if Pid:
|
||||
_Run(["taskkill", "/PID", Pid, "/T", "/F"])
|
||||
_PidFile(Name).unlink(missing_ok=True)
|
||||
if Action in ("start", "restart"):
|
||||
if _WindowsPid(Name):
|
||||
return ""
|
||||
Folder.mkdir(parents=True, exist_ok=True)
|
||||
subprocess.Popen(
|
||||
[str(PythonW), *Arguments], cwd=str(Folder), env=ChildEnvironment(), close_fds=True,
|
||||
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
creationflags=getattr(subprocess, "DETACHED_PROCESS", 0) | _NoWindow,
|
||||
)
|
||||
elif Action == "enable":
|
||||
import pylnk3
|
||||
|
||||
Link = _StartupLink(Name)
|
||||
Link.parent.mkdir(parents=True, exist_ok=True)
|
||||
Quoted = " ".join(f'"{Item}"' if " " in Item or "\\" in Item else Item for Item in Arguments)
|
||||
pylnk3.for_file(str(PythonW), str(Link), arguments=Quoted, work_dir=str(Folder),
|
||||
description=f"{Name} (background service)")
|
||||
elif Action == "disable":
|
||||
_StartupLink(Name).unlink(missing_ok=True)
|
||||
except (OSError, subprocess.SubprocessError) as Error:
|
||||
return str(Error)
|
||||
return ""
|
||||
|
||||
|
||||
# --- Both -----------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def Status(Unit: str) -> ServiceStatus:
|
||||
if IsWindows:
|
||||
return _WindowsStatus(Unit)
|
||||
try:
|
||||
Result = _Run(["systemctl", "--user", "show", Unit,
|
||||
"-p", "LoadState,ActiveState,SubState,UnitFileState,MainPID,ActiveEnterTimestamp"])
|
||||
@@ -45,6 +143,8 @@ def Status(Unit: str) -> ServiceStatus:
|
||||
|
||||
def Control(Unit: str, Action: str) -> str:
|
||||
"""Action: start, stop, restart, enable, disable. Returns "" or an error."""
|
||||
if IsWindows:
|
||||
return _WindowsControl(Unit, Action)
|
||||
try:
|
||||
Result = _Run(["systemctl", "--user", Action, Unit])
|
||||
except (OSError, subprocess.SubprocessError) as Error:
|
||||
@@ -60,6 +160,9 @@ def RestartIfRunning(Unit: str) -> str:
|
||||
|
||||
|
||||
def Journal(Unit: str, Lines: int) -> list[str]:
|
||||
if IsWindows:
|
||||
return ["Windows has no systemd journal. SaraSortd's own log files (the other log source) "
|
||||
"have everything it does."]
|
||||
try:
|
||||
Result = _Run(["journalctl", "--user", "-u", Unit, "-n", str(Lines), "--no-pager", "-o", "short-iso"])
|
||||
except (OSError, subprocess.SubprocessError) as Error:
|
||||
|
||||
Reference in new issue
Block a user