feat(tray): system-tray status icon for the host (Windows + Linux)
New crates/punktfunk-tray — a small per-user companion showing the host service state at a glance (running / stopped / starting / degraded / failed + the live session in the tooltip) with one-click actions: open web console, approve a pending pairing request, start/stop/restart, open logs. No more digging through logs to learn whether the service came back after a reboot or an update. Status is service-manager-FIRST (SCM / systemd user unit — a port squatter can never fake Running), then the new loopback-only unauthenticated GET /api/v1/local/summary (counts/booleans only; the mgmt token and cert.pem are SYSTEM/Admins-DACL'd on Windows, so a non-elevated tray cannot bearer-auth). Windows: windows_subsystem binary (a console exe in the Run key would flash a terminal at sign-in), Shell_NotifyIcon + hidden window, per-session single instance, TaskbarCreated re-add, --quit for the uninstaller; service actions elevate per click via ShellExecuteW "runas" onto the new `punktfunk-host service restart` (stop → wait Stopped → start). Linux: ksni/StatusNotifierItem over zbus, systemctl --user actions (no polkit), /etc/xdg/autostart entry whose --autostart self-gates to actual host users. Icons: scripts/gen-tray-icons.py (pure stdlib) renders the brand lens + status dot into committed .ico/hicolor assets; deb/rpm/arch ship binary+autostart+icons. Live-validated: Linux on the headless KDE session (SNI registration, state transitions, menu-driven start, dbusmenu layout); Windows on the RTX box (session-1 launch with no NIM_ADD failure, single instance, --quit, restart round-trip, summary loopback-200/LAN-401). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -91,6 +91,7 @@ pub fn main(args: &[String]) -> Result<()> {
|
||||
Some("uninstall") => uninstall(),
|
||||
Some("start") => sc(&["start", SERVICE_NAME]),
|
||||
Some("stop") => sc(&["stop", SERVICE_NAME]),
|
||||
Some("restart") => restart(),
|
||||
Some("status") => sc(&["query", SERVICE_NAME]),
|
||||
_ => {
|
||||
eprintln!(
|
||||
@@ -102,6 +103,7 @@ pub fn main(args: &[String]) -> Result<()> {
|
||||
\x20 punktfunk-host service uninstall stop + remove the service + firewall rules\n\
|
||||
\x20 punktfunk-host service start start the service now\n\
|
||||
\x20 punktfunk-host service stop stop the service\n\
|
||||
\x20 punktfunk-host service restart stop, wait for exit, start again\n\
|
||||
\x20 punktfunk-host service status query the service\n\n\
|
||||
Config: %ProgramData%\\punktfunk\\host.env Logs: %ProgramData%\\punktfunk\\logs\\"
|
||||
);
|
||||
@@ -691,6 +693,40 @@ fn install(args: &[String]) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `service restart`: stop, wait for the service to actually reach Stopped (a bare
|
||||
/// `sc stop && sc start` races the stop — START fails with "instance already running" while the
|
||||
/// old process winds down), then start. The tray icon's Restart action runs this, elevated.
|
||||
fn restart() -> Result<()> {
|
||||
use windows_service::service::{ServiceAccess, ServiceState};
|
||||
use windows_service::service_manager::{ServiceManager, ServiceManagerAccess};
|
||||
|
||||
let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)
|
||||
.context("open Service Control Manager (run elevated)")?;
|
||||
let svc = manager
|
||||
.open_service(
|
||||
SERVICE_NAME,
|
||||
ServiceAccess::STOP | ServiceAccess::QUERY_STATUS | ServiceAccess::START,
|
||||
)
|
||||
.context("open service (run elevated)")?;
|
||||
// Best-effort stop: ERROR_SERVICE_NOT_ACTIVE just means restart == start.
|
||||
let _ = svc.stop();
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
|
||||
loop {
|
||||
let state = svc.query_status().context("query service status")?;
|
||||
if state.current_state == ServiceState::Stopped {
|
||||
break;
|
||||
}
|
||||
if std::time::Instant::now() >= deadline {
|
||||
anyhow::bail!("service did not stop within 30 s");
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(250));
|
||||
}
|
||||
svc.start(&[] as &[&std::ffi::OsStr])
|
||||
.context("start service")?;
|
||||
println!("Restarted service '{SERVICE_NAME}'.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn uninstall() -> Result<()> {
|
||||
use windows_service::service::ServiceAccess;
|
||||
use windows_service::service_manager::{ServiceManager, ServiceManagerAccess};
|
||||
|
||||
Reference in New Issue
Block a user