mirror of
https://git.srhtnin.garden/sarah/SaraSort.git
synced 2026-10-13 18:28:18 +02:00
223 lines
9.6 KiB
Python
223 lines
9.6 KiB
Python
"""SaraSortd's service: status, start/stop/restart, start on login, its
|
|
version, and its logs (its own log files and the systemd journal)."""
|
|
|
|
import asyncio
|
|
from pathlib import Path
|
|
|
|
import flet as ft
|
|
import tomlkit
|
|
|
|
from Core import ChipButton, ChipButtonStyle, DataHome, Heading, IsWindows, Note, Palette
|
|
from Service import Control, Journal, LogFiles, ReadTail, Status
|
|
from Widgets import App
|
|
|
|
ReceiptPath = DataHome / "SaraSuite" / "Receipts" / "SaraSortd.toml"
|
|
|
|
|
|
def LineColor(Line: str) -> str:
|
|
Lower = Line.lower()
|
|
if "error" in Lower or "traceback" in Lower or "failed" in Lower:
|
|
return Palette.Red
|
|
if "sorted" in Lower:
|
|
return Palette.Green
|
|
if "started" in Lower or "stopped" in Lower:
|
|
return Palette.Mauve
|
|
if "checking" in Lower:
|
|
return Palette.Overlay0
|
|
return Palette.Text
|
|
|
|
|
|
class ServicePage(ft.Column):
|
|
def __init__(self, TheApp: App):
|
|
super().__init__(expand=True, spacing=0)
|
|
self.App = TheApp
|
|
self.Polling = False
|
|
self.Source = "Files" # or "Journal"
|
|
|
|
self.Dot = ft.Container(width=14, height=14, border_radius=7, bgcolor=Palette.Overlay0)
|
|
self.StateText = ft.Text("", size=16)
|
|
self.Details = ft.Column(spacing=2)
|
|
self.VersionInfo = ft.Column(spacing=2)
|
|
self.Autostart = ft.Switch(label="Start SaraSortd on login", on_change=self._ToggleAutostart)
|
|
self.StartButton = ChipButton("Start", lambda e: self._Do("start"), ft.Icons.PLAY_ARROW)
|
|
self.StopButton = ChipButton("Stop", lambda e: self._Do("stop"), ft.Icons.STOP, Accent=Palette.Red)
|
|
self.RestartButton = ChipButton("Restart", lambda e: self._Do("restart"), ft.Icons.RESTART_ALT)
|
|
|
|
self.FilesButton = ChipButton("Log files", lambda e: self._SetSource("Files"))
|
|
self.JournalButton = ChipButton("Journal", lambda e: self._SetSource("Journal"))
|
|
self.FilePicker = ft.Dropdown(label="Log file", dense=True, width=260, on_select=lambda e: self._LoadLog())
|
|
self.HideChecking = ft.Checkbox(label="Hide \"Checking\" lines", value=True, on_change=lambda e: self._LoadLog())
|
|
self.Follow = ft.Checkbox(label="Follow", value=True)
|
|
self.LogView = ft.ListView(expand=True, spacing=0, padding=8, auto_scroll=True)
|
|
|
|
StatusCard = ft.Container(
|
|
ft.Column([
|
|
ft.Row([self.Dot, self.StateText], spacing=10),
|
|
self.Details,
|
|
ft.Row([self.StartButton, self.StopButton, self.RestartButton], wrap=True),
|
|
self.Autostart,
|
|
], spacing=10),
|
|
padding=14, bgcolor=Palette.Mantle, border=ft.Border.all(1, Palette.Surface1), border_radius=6, expand=True,
|
|
)
|
|
VersionCard = ft.Container(
|
|
ft.Column([ft.Text("Version", color=Palette.Mauve), self.VersionInfo], spacing=8),
|
|
padding=14, bgcolor=Palette.Mantle, border=ft.Border.all(1, Palette.Surface1), border_radius=6, expand=True,
|
|
)
|
|
self.controls = [
|
|
ft.Container(
|
|
ft.Row([Heading("Service"), ft.Container(expand=True),
|
|
ChipButton("Reload", lambda e: self.Refresh(), ft.Icons.REFRESH)]),
|
|
padding=ft.Padding.only(left=16, right=16, top=12, bottom=8),
|
|
),
|
|
ft.Container(ft.Row([StatusCard, VersionCard], vertical_alignment=ft.CrossAxisAlignment.START),
|
|
padding=ft.Padding.symmetric(horizontal=16)),
|
|
ft.Container(
|
|
ft.Row([self.FilesButton, self.JournalButton, self.FilePicker, ft.Container(width=8),
|
|
self.HideChecking, self.Follow],
|
|
vertical_alignment=ft.CrossAxisAlignment.CENTER, scroll=ft.ScrollMode.AUTO),
|
|
padding=ft.Padding.only(left=16, right=16, top=14, bottom=6),
|
|
),
|
|
ft.Container(self.LogView, expand=True, bgcolor=Palette.Crust, margin=ft.Margin.only(left=16, right=16, bottom=16),
|
|
border=ft.Border.all(1, Palette.Surface1), border_radius=6),
|
|
]
|
|
|
|
# Status
|
|
|
|
def Refresh(self) -> None:
|
|
self._RefreshStatus()
|
|
self._RefreshVersion()
|
|
self._RefreshLogFiles()
|
|
self._LoadLog()
|
|
self._StyleSourceButtons()
|
|
self.update()
|
|
|
|
def _RefreshStatus(self) -> None:
|
|
Current = Status(self.App.Config.ServiceName)
|
|
if not Current.Found:
|
|
Color, Text = Palette.Overlay0, f"{self.App.Config.ServiceName} isn't installed"
|
|
elif Current.Running:
|
|
Color, Text = Palette.Green, f"Running ({Current.Sub})"
|
|
elif Current.Active == "failed":
|
|
Color, Text = Palette.Red, "Failed"
|
|
else:
|
|
Color, Text = Palette.Yellow, f"Stopped ({Current.Active})"
|
|
self.Dot.bgcolor = Color
|
|
self.StateText.value = Text
|
|
self.StateText.color = Color
|
|
self.Details.controls = [
|
|
Note(f"{'Service' if IsWindows else 'Unit'}: {self.App.Config.ServiceName}"),
|
|
Note(f"Since: {Current.Since or '-'}"),
|
|
Note(f"PID: {Current.Pid or '-'}"),
|
|
Note(f"{'Autostart (Startup folder)' if IsWindows else 'Unit file'}: {Current.Enabled}"),
|
|
]
|
|
self.StartButton.disabled = Current.Running or not Current.Found
|
|
self.StopButton.disabled = not Current.Running
|
|
self.RestartButton.disabled = not Current.Found
|
|
self.Autostart.value = Current.Enabled == "enabled"
|
|
self.Autostart.disabled = not Current.Found
|
|
|
|
def _RefreshVersion(self) -> None:
|
|
TheSetup, Error = self.App.LoadSetup()
|
|
Lines = []
|
|
Daemon = TheSetup.DaemonVersion() if TheSetup else None
|
|
Lines.append(ft.Text(f"SaraSortd {Daemon or '?'}", size=16))
|
|
Lines.append(Note(str(self.App.Config.SortdScriptPath)))
|
|
if TheSetup:
|
|
ConfVersion = str(TheSetup.Data.get("Version", "?"))
|
|
Mismatch = Daemon and ConfVersion.split(".")[0] != Daemon.split(".")[0]
|
|
Lines.append(ft.Text(f"GlobalConf.toml {ConfVersion}" + (" (major version differs!)" if Mismatch else ""),
|
|
color=Palette.Red if Mismatch else Palette.Text, size=13))
|
|
else:
|
|
Lines.append(ft.Text(Error, color=Palette.Red, size=12))
|
|
try:
|
|
Receipt = tomlkit.parse(ReceiptPath.read_text()).unwrap()
|
|
Lines.append(Note(f"Installed by SaraSuite: commit {str(Receipt.get('Commit', '?'))[:10]}"))
|
|
Lines.append(Note(f"Installed {Receipt.get('InstalledAt', '?')}, updated {Receipt.get('UpdatedAt', '?')}"))
|
|
except (OSError, tomlkit.exceptions.TOMLKitError):
|
|
Lines.append(Note("Not installed through SaraSuite (no receipt)."))
|
|
self.VersionInfo.controls = Lines
|
|
|
|
def _Do(self, Action: str) -> None:
|
|
Error = Control(self.App.Config.ServiceName, Action)
|
|
if Error:
|
|
self.App.Notify(Error, "error")
|
|
else:
|
|
self.App.Notify(f"SaraSortd: {Action} done.", "success")
|
|
self.Refresh()
|
|
|
|
def _ToggleAutostart(self, e) -> None:
|
|
Error = Control(self.App.Config.ServiceName, "enable" if self.Autostart.value else "disable")
|
|
if Error:
|
|
self.App.Notify(Error, "error")
|
|
self._RefreshStatus()
|
|
self.update()
|
|
|
|
# Logs
|
|
|
|
def _StyleSourceButtons(self) -> None:
|
|
self.FilesButton.style = ChipButtonStyle(Palette.Mauve if self.Source == "Files" else Palette.Surface1)
|
|
self.JournalButton.style = ChipButtonStyle(Palette.Mauve if self.Source == "Journal" else Palette.Surface1)
|
|
self.FilePicker.visible = self.Source == "Files"
|
|
|
|
def _SetSource(self, Source: str) -> None:
|
|
self.Source = Source
|
|
self._StyleSourceButtons()
|
|
self._LoadLog()
|
|
self.update()
|
|
|
|
def _LogDir(self) -> Path | None:
|
|
TheSetup, _ = self.App.LoadSetup()
|
|
return TheSetup.SpecialDir("LogDir") if TheSetup else None
|
|
|
|
def _RefreshLogFiles(self) -> None:
|
|
LogDir = self._LogDir()
|
|
Files = LogFiles(LogDir) if LogDir else []
|
|
Current = self.FilePicker.value
|
|
self.FilePicker.options = [ft.DropdownOption(key=str(FilePath), text=FilePath.name) for FilePath in Files]
|
|
Keys = [str(FilePath) for FilePath in Files]
|
|
if Current not in Keys:
|
|
self.FilePicker.value = Keys[0] if Keys else None
|
|
|
|
def _LoadLog(self) -> None:
|
|
Limit = self.App.Config.LogLines
|
|
if self.Source == "Journal":
|
|
Lines = Journal(self.App.Config.ServiceName, Limit)
|
|
elif self.FilePicker.value:
|
|
Lines = ReadTail(Path(self.FilePicker.value), Limit * 4)
|
|
else:
|
|
Lines = ["No log files yet (is Logging = 1 in GlobalConf.toml?)."]
|
|
if self.HideChecking.value:
|
|
Lines = [Line for Line in Lines if "Checking" not in Line]
|
|
Lines = Lines[-Limit:]
|
|
self.LogView.auto_scroll = bool(self.Follow.value)
|
|
self.LogView.controls = [
|
|
ft.Text(Line, size=12, color=LineColor(Line), selectable=True) for Line in Lines
|
|
]
|
|
if self.page:
|
|
self.LogView.update()
|
|
|
|
# Polling while the page is shown
|
|
|
|
def StartPolling(self) -> None:
|
|
if self.Polling:
|
|
return
|
|
self.Polling = True
|
|
self.App.Page.run_task(self._Poll)
|
|
|
|
def StopPolling(self) -> None:
|
|
self.Polling = False
|
|
|
|
async def _Poll(self) -> None:
|
|
while self.Polling:
|
|
await asyncio.sleep(max(1, int(self.App.Config.RefreshSeconds)))
|
|
if not self.Polling:
|
|
break
|
|
try:
|
|
self._RefreshStatus()
|
|
if self.Follow.value:
|
|
self._RefreshLogFiles()
|
|
self._LoadLog()
|
|
self.update()
|
|
except Exception: # the page may be gone mid-update
|
|
pass
|