forked from unom/punktfunk
fix(windows): supervise the status tray, and stop the host on exit
The tray died on every upgrade and stayed dead until the next sign-in. The update-specific remedy only covered console-initiated updates, and only when the previous binary already recorded the intent — winget, a hand-run setup and a plain crash all still ended the same way. Worse, the relaunch it did manage joined the service worker's kill-on-close job object, because spawn_in_active_session never asked to break away. Every process that call launches — the tray, the user's game, a hook — was therefore reaped when the service stopped, contradicting its own documented contract. The host now supervises the tray for its whole lifetime, gated on the HKLM Run value the trayicon task writes. That covers every way a tray can die and needs nothing from the version that ran before, so the intent record's tray_was_running flag goes. The icon's lifetime tracks the host's in both directions: the menu's exit entry stops the host, and says so. Only that entry does — a sign-out and the uninstaller's --quit still leave a headless host running. The uninstaller now removes the service before the tray, so the supervisor cannot put one back.
This commit is contained in:
@@ -218,6 +218,11 @@ pub async fn run(
|
||||
// Close out any update-intent record a previous apply left behind (the update reports its
|
||||
// own outcome across its own restart — update/jobs.rs). Once per boot, before serving.
|
||||
crate::update::reconcile_at_boot();
|
||||
// Keep a status tray alive for as long as this host runs. The tray has no supervisor of its
|
||||
// own — the HKLM `Run` value is a sign-in trigger — so every upgrade's `StopTrays` (and every
|
||||
// crash) left the box without an icon until the next logon. See `windows::tray::supervise`.
|
||||
#[cfg(target_os = "windows")]
|
||||
crate::tray::supervise();
|
||||
|
||||
// The mgmt API is HTTPS + token-authenticated ALWAYS (even on loopback): `parse_serve`
|
||||
// guarantees a token (CLI flag / env / persisted ~/.config/punktfunk/mgmt-token / generated).
|
||||
|
||||
@@ -575,10 +575,6 @@ enum PostApply {
|
||||
pub(crate) fn reconcile_at_boot() {
|
||||
let path = jobs::intent_path();
|
||||
let intent = jobs::read_intent(&path);
|
||||
// Read off the intent BEFORE `reconcile` consumes it. Restored on both terminal outcomes: a
|
||||
// rolled-back or aborted install killed the tray just as thoroughly as a successful one. NOT on
|
||||
// StillApplying — the installer may still be running and would only kill it again.
|
||||
let restore_tray = intent.as_ref().is_some_and(|i| i.tray_was_running);
|
||||
match jobs::reconcile(intent, env!("PUNKTFUNK_VERSION"), now_unix()) {
|
||||
jobs::Reconciled::None | jobs::Reconciled::StillApplying => {}
|
||||
jobs::Reconciled::Success(record) => {
|
||||
@@ -601,12 +597,6 @@ pub(crate) fn reconcile_at_boot() {
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
if restore_tray {
|
||||
windows::relaunch_tray();
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let _ = restore_tray; // the Linux packages never kill a running tray
|
||||
}
|
||||
|
||||
/// What status hands to the API layer.
|
||||
|
||||
@@ -42,23 +42,6 @@ pub(crate) struct IntentRecord {
|
||||
/// intent with this flag still present at boot ⟹ the rebuild succeeded.
|
||||
#[serde(default)]
|
||||
pub source_build: bool,
|
||||
/// A per-user status tray was running when we spawned the installer, so boot reconciliation
|
||||
/// should put it back.
|
||||
///
|
||||
/// The installer's `StopTrays` force-kills every session's `punktfunk-tray.exe` (it is one of
|
||||
/// the files being replaced), and its `[Run]` relaunch carries `skipifsilent` — which a
|
||||
/// console-initiated update, spawned with `/VERYSILENT`, always trips. The tray therefore died
|
||||
/// on every in-console update and stayed dead until the next sign-in. The installer cannot fix
|
||||
/// this itself: spawned from the SYSTEM host service, its `runasoriginaluser` resolves to
|
||||
/// SYSTEM, which would put a SYSTEM-owned tray in the user's session squatting the
|
||||
/// `Local\PunktfunkTray` mutex and blocking the real one. The host relaunches it instead — it
|
||||
/// already owns the `WTSQueryUserToken` primitive for landing a process in the interactive
|
||||
/// session as the logged-in user.
|
||||
///
|
||||
/// `#[serde(default)]`: an intent written by an older host reads as false (no relaunch), which
|
||||
/// is the pre-existing behaviour.
|
||||
#[serde(default)]
|
||||
pub tray_was_running: bool,
|
||||
}
|
||||
|
||||
/// The durable outcome of the most recent apply attempt.
|
||||
@@ -206,7 +189,6 @@ mod tests {
|
||||
installer_sha256: "ab".repeat(32),
|
||||
log_path: "/logs/update-0.23.200.log".into(),
|
||||
source_build: false,
|
||||
tray_was_running: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +199,6 @@ mod tests {
|
||||
let json = r#"{"from":"0.23.100","to":"0.23.200","serial":42,"started_unix":1000,
|
||||
"installer_sha256":"ab","log_path":"/logs/x.log"}"#;
|
||||
let i: IntentRecord = serde_json::from_str(json).expect("older intent still parses");
|
||||
assert!(!i.tray_was_running);
|
||||
assert!(!i.source_build);
|
||||
}
|
||||
|
||||
|
||||
@@ -140,9 +140,6 @@ pub(super) fn run_apply_steamos(
|
||||
installer_sha256: String::new(),
|
||||
log_path: log.display().to_string(),
|
||||
source_build: true,
|
||||
// Windows-only concern: no Linux package force-kills a running tray, and the desktop
|
||||
// autostart entry owns bringing it up.
|
||||
tray_was_running: false,
|
||||
},
|
||||
)
|
||||
.map_err(|e| ("applying", format!("write intent record: {e}")))?;
|
||||
@@ -352,7 +349,6 @@ pub(super) fn run_apply(
|
||||
installer_sha256: String::new(),
|
||||
log_path: "journalctl -u punktfunk-update.service".into(),
|
||||
source_build: false,
|
||||
tray_was_running: false, // Windows-only concern (see the source-build intent above)
|
||||
},
|
||||
)
|
||||
.map_err(|e| ("restarting", format!("write intent record: {e}")))?;
|
||||
|
||||
@@ -51,24 +51,6 @@ fn staging_dir() -> PathBuf {
|
||||
pf_paths::config_dir().join("updates")
|
||||
}
|
||||
|
||||
/// Put the per-user tray back after an update that killed it.
|
||||
///
|
||||
/// Runs from boot reconciliation, i.e. in the NEW host, as SYSTEM — so `crate::tray::start` takes
|
||||
/// its session-crossing path and lands the tray in the active console session under the logged-in
|
||||
/// user's own token. That is the whole reason this is the host's job and not the installer's (see
|
||||
/// `IntentRecord::tray_was_running`).
|
||||
///
|
||||
/// Best-effort throughout: nobody's update outcome depends on the icon, and the common benign
|
||||
/// failure is simply that nobody has signed in yet — which the HKLM `Run` value covers at the next
|
||||
/// logon. Hence `info`, not a warning the operator must act on.
|
||||
pub(crate) fn relaunch_tray() {
|
||||
match crate::tray::start() {
|
||||
Ok((Some(pid), how)) => tracing::info!(pid, how, "status tray relaunched after the update"),
|
||||
Ok((None, _)) => tracing::debug!("status tray was already running after the update"),
|
||||
Err(e) => tracing::info!(error = %e, "could not relaunch the status tray after the update"),
|
||||
}
|
||||
}
|
||||
|
||||
fn log_path(version: &str) -> PathBuf {
|
||||
pf_paths::config_dir()
|
||||
.join("logs")
|
||||
@@ -112,6 +94,10 @@ pub(super) fn run_apply(
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
// The point of no return: after this record exists, boot reconciliation owns the outcome.
|
||||
//
|
||||
// Nothing about the status tray is recorded here: the installer force-kills every tray to
|
||||
// unlock punktfunk-tray.exe and, under /VERYSILENT, never runs its own relaunch entry, but
|
||||
// `tray::supervise` in the new host puts one back without needing to be told.
|
||||
jobs::write_json_atomic(
|
||||
&jobs::intent_path(),
|
||||
&IntentRecord {
|
||||
@@ -122,10 +108,6 @@ pub(super) fn run_apply(
|
||||
installer_sha256: asset.sha256.to_ascii_lowercase(),
|
||||
log_path: log.display().to_string(),
|
||||
source_build: false,
|
||||
// Captured BEFORE the installer runs: it force-kills every tray to unlock
|
||||
// punktfunk-tray.exe and, under /VERYSILENT, never runs its relaunch entry. See
|
||||
// `IntentRecord::tray_was_running`; `relaunch_tray` puts it back at reconcile.
|
||||
tray_was_running: crate::tray::is_running(),
|
||||
},
|
||||
)
|
||||
.map_err(|e| ("applying", format!("write intent record: {e}")))?;
|
||||
|
||||
@@ -27,7 +27,8 @@ use windows::Win32::Security::{
|
||||
use windows::Win32::System::Environment::{CreateEnvironmentBlock, DestroyEnvironmentBlock};
|
||||
use windows::Win32::System::RemoteDesktop::{WTSGetActiveConsoleSessionId, WTSQueryUserToken};
|
||||
use windows::Win32::System::Threading::{
|
||||
CreateProcessAsUserW, CREATE_UNICODE_ENVIRONMENT, PROCESS_INFORMATION, STARTUPINFOW,
|
||||
CreateProcessAsUserW, CREATE_BREAKAWAY_FROM_JOB, CREATE_UNICODE_ENVIRONMENT,
|
||||
PROCESS_INFORMATION, STARTUPINFOW,
|
||||
};
|
||||
|
||||
/// `Some((own_session, console_session))` when this process is NOT in the active console session —
|
||||
@@ -133,24 +134,44 @@ pub fn spawn_in_active_session(cmdline: &str, workdir: Option<&Path>) -> Result<
|
||||
};
|
||||
|
||||
let mut pi = PROCESS_INFORMATION::default();
|
||||
// SAFETY: `primary` is the live primary token; `cmd`, `desktop` (via `si.lpDesktop`), `workdir_w`
|
||||
// (via `cwd`) and `merged_env` are locals that outlive the call, each NUL-terminated as the API
|
||||
// requires — `merged_env` doubly so, per `merged_env_block`. `pi` is a live local out-param, and
|
||||
// the API retains none of these pointers.
|
||||
let created = unsafe {
|
||||
CreateProcessAsUserW(
|
||||
Some(primary),
|
||||
None,
|
||||
Some(PWSTR(cmd.as_mut_ptr())),
|
||||
None,
|
||||
None,
|
||||
false, // no handle inheritance — fire-and-forget GUI launch, no stdio relay
|
||||
CREATE_UNICODE_ENVIRONMENT,
|
||||
Some(merged_env.as_ptr() as *const core::ffi::c_void),
|
||||
cwd,
|
||||
&si,
|
||||
&mut pi,
|
||||
)
|
||||
// BREAKAWAY: the service worker that calls this sits in a kill-on-close job object
|
||||
// (`crate::service`, `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_BREAKAWAY_OK`), and
|
||||
// a child joins its parent's job by default. Without the flag, everything launched here — the
|
||||
// status tray, the user's game, a hook — is reaped the moment the service stops or restarts,
|
||||
// which flatly contradicts this function's "the launched process outlives this call" contract.
|
||||
// The job exists to stop a service crash orphaning the SYSTEM streamer; it was never meant to
|
||||
// own processes running under the USER's token in the console session. Same reasoning, same
|
||||
// flag, as the update installer spawn (`update/windows.rs`).
|
||||
//
|
||||
// The retry covers the one case the flag can fail: a job that does NOT permit breakaway (a
|
||||
// hand-run host under some job-owning launcher) rejects `CreateProcessAsUserW` with
|
||||
// ACCESS_DENIED. Launching into that job beats not launching at all.
|
||||
let mut flags = CREATE_UNICODE_ENVIRONMENT | CREATE_BREAKAWAY_FROM_JOB;
|
||||
let created = loop {
|
||||
// SAFETY: `primary` is the live primary token; `cmd`, `desktop` (via `si.lpDesktop`),
|
||||
// `workdir_w` (via `cwd`) and `merged_env` are locals that outlive the call, each
|
||||
// NUL-terminated as the API requires — `merged_env` doubly so, per `merged_env_block`.
|
||||
// `pi` is a live local out-param, and the API retains none of these pointers.
|
||||
let r = unsafe {
|
||||
CreateProcessAsUserW(
|
||||
Some(primary),
|
||||
None,
|
||||
Some(PWSTR(cmd.as_mut_ptr())),
|
||||
None,
|
||||
None,
|
||||
false, // no handle inheritance — fire-and-forget GUI launch, no stdio relay
|
||||
flags,
|
||||
Some(merged_env.as_ptr() as *const core::ffi::c_void),
|
||||
cwd,
|
||||
&si,
|
||||
&mut pi,
|
||||
)
|
||||
};
|
||||
if r.is_ok() || !flags.contains(CREATE_BREAKAWAY_FROM_JOB) {
|
||||
break r;
|
||||
}
|
||||
tracing::debug!("breakaway launch refused ({r:?}) — retrying inside the job");
|
||||
flags &= !CREATE_BREAKAWAY_FROM_JOB;
|
||||
};
|
||||
// SAFETY: `primary` is live and owned here, closed exactly once and not used after.
|
||||
let _ = unsafe { CloseHandle(primary) };
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
//! Tray lifecycle: the one place that knows how to find, start, stop and check the per-user status
|
||||
//! tray. Shared by the `tray` CLI subcommand and by post-update reconciliation
|
||||
//! (`update::windows::relaunch_tray`).
|
||||
//! Tray lifecycle: the one place that knows how to find, start, stop, check and SUPERVISE the
|
||||
//! per-user status tray. Shared by the `tray` CLI subcommand and by [`supervise`], which the host
|
||||
//! service runs for its whole lifetime.
|
||||
//!
|
||||
//! Why this exists at all: `punktfunk-tray.exe` is a per-USER, per-SESSION GUI process with no
|
||||
//! recovery path of its own. The HKLM `Run` value only fires at sign-in, and nothing in the product
|
||||
//! restarts a tray that died — so anything that kills one (an upgrade's `StopTrays`, a crash) left
|
||||
//! the operator without an icon until they signed out and back in.
|
||||
//! restarted a tray that died — so anything that killed one (an upgrade's `StopTrays`, a crash) left
|
||||
//! the operator without an icon until they signed out and back in. [`supervise`] closes that.
|
||||
//!
|
||||
//! The launch has to cross a privilege boundary in one direction but not the other, so [`start`]
|
||||
//! tries the session-crossing path first and falls back to a plain spawn:
|
||||
@@ -83,6 +83,62 @@ pub fn is_running() -> bool {
|
||||
.any(|n| n == stem)
|
||||
}
|
||||
|
||||
/// How often [`supervise`] re-checks that a tray is up.
|
||||
const WATCH_TICK: std::time::Duration = std::time::Duration::from_secs(30);
|
||||
|
||||
/// The installer's `trayicon` task writes this HKLM `Run` value. Its presence is the ONLY honest
|
||||
/// "this box wants a status icon" signal: `punktfunk-tray.exe` is installed unconditionally
|
||||
/// (it is small), so its mere existence on disk means nothing.
|
||||
fn wanted() -> bool {
|
||||
winreg::RegKey::predef(winreg::enums::HKEY_LOCAL_MACHINE)
|
||||
.open_subkey(r"SOFTWARE\Microsoft\Windows\CurrentVersion\Run")
|
||||
.and_then(|k| k.get_value::<String, _>("PunktfunkTray"))
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Keep a status tray alive for as long as the host runs. Spawned once from `mgmt::run`.
|
||||
///
|
||||
/// This is the tray's missing supervisor. The HKLM `Run` value fires at sign-in and never again, so
|
||||
/// EVERY way a tray dies used to be terminal until the next logon: the installer's `StopTrays`
|
||||
/// (which every upgrade runs — console-initiated, winget, or a hand-run setup), an Explorer-level
|
||||
/// crash, an operator's `taskkill`. An update-specific remedy only ever covered the update path,
|
||||
/// and only when the *previous* binary already knew to record the intent. A tick that just asks
|
||||
/// "is one running?" covers all of them and depends on nothing the old version wrote.
|
||||
///
|
||||
/// The first check is immediate — that is the post-update restore — and later ones need TWO
|
||||
/// consecutive misses before acting. That grace tick is what keeps the watchdog from fighting the
|
||||
/// tray's own "Exit tray", which stops this service and takes the watchdog down with it a few
|
||||
/// seconds later.
|
||||
pub fn supervise() {
|
||||
std::thread::spawn(|| {
|
||||
if !wanted() {
|
||||
tracing::debug!("no HKLM Run entry for the status tray — not supervising it");
|
||||
return;
|
||||
}
|
||||
ensure();
|
||||
let mut missed = false;
|
||||
loop {
|
||||
std::thread::sleep(WATCH_TICK);
|
||||
let absent = !is_running();
|
||||
if absent && missed {
|
||||
ensure();
|
||||
}
|
||||
missed = absent;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// One supervision beat. Best-effort by design: nobody's stream depends on the icon, and the
|
||||
/// ordinary "failure" is simply that nobody has signed in yet — so this stays at `debug`/`info`
|
||||
/// rather than handing the operator a warning they cannot act on.
|
||||
fn ensure() {
|
||||
match start() {
|
||||
Ok((Some(pid), how)) => tracing::info!(pid, how, "status tray started"),
|
||||
Ok((None, _)) => tracing::trace!("status tray is already running"),
|
||||
Err(e) => tracing::debug!(error = %e, "could not start the status tray"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the tray if it is not already up.
|
||||
///
|
||||
/// `Ok(None)` = one was already running (idempotent by design: the tray also guards itself with a
|
||||
@@ -124,6 +180,10 @@ pub fn start() -> Result<(Option<u32>, &'static str)> {
|
||||
|
||||
/// Stop every tray instance. Returns whether one was running.
|
||||
///
|
||||
/// Note that [`supervise`] will put one back within a minute while this host runs — `tray stop` is
|
||||
/// a diagnostic, not a way to turn the icon off. Turning it off for good means clearing the
|
||||
/// installer's HKLM `Run` value (see [`wanted`]).
|
||||
///
|
||||
/// Graceful first, mirroring the uninstaller's own order (`[UninstallRun]`): `--quit` posts
|
||||
/// WM_CLOSE to the tray window, which lets it remove its icon via `NIM_DELETE` instead of leaving a
|
||||
/// ghost in the notification area until the shell next sweeps it. `--quit` only reaches the session
|
||||
|
||||
@@ -3,9 +3,14 @@
|
||||
//! The host service (`PunktfunkHost`, LocalSystem) supervises from session 0 and its `serve`
|
||||
//! child runs as SYSTEM — neither can own a per-user tray icon, so this is a separate small
|
||||
//! process the installer puts in the HKLM `Run` key (one instance per interactive session,
|
||||
//! enforced by a `Local\` mutex). Start/Stop/Restart open one UAC consent prompt each
|
||||
//! (`ShellExecuteW "runas"` on `punktfunk-host.exe service …`) — service control is deliberately
|
||||
//! left admin-gated rather than DACL-opened to every local user.
|
||||
//! enforced by a `Local\` mutex). Start/Stop/Restart — and "Stop host and exit tray" — open one UAC
|
||||
//! consent prompt each (`ShellExecuteW "runas"` on `punktfunk-host.exe service …`); service control
|
||||
//! is deliberately left admin-gated rather than DACL-opened to every local user.
|
||||
//!
|
||||
//! The icon's lifetime tracks the host's in BOTH directions: the host supervises this process
|
||||
//! (`punktfunk-host`'s `windows/tray.rs`), and the menu's exit entry stops the host. Only that
|
||||
//! entry does — a sign-out (`WM_ENDSESSION`) or the uninstaller's `--quit` (`WM_CLOSE`) leaves a
|
||||
//! headless host running, which is the whole point of a headless host.
|
||||
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use std::sync::atomic::{AtomicBool, AtomicIsize, AtomicU8, Ordering};
|
||||
@@ -502,7 +507,19 @@ fn show_menu(hwnd: HWND) {
|
||||
Some(win_theme::GLYPH_FOLDER),
|
||||
);
|
||||
let _ = AppendMenuW(menu, MF_SEPARATOR, 0, PCWSTR::null());
|
||||
add(IDM_EXIT, "Exit tray", false, Some(win_theme::GLYPH_POWER));
|
||||
// Exiting takes the host down with it (see the IDM_EXIT arm), so the entry says so and
|
||||
// carries the same shield as the other service actions — an operator must never be
|
||||
// surprised by either the UAC prompt or the stop.
|
||||
if can_control && running {
|
||||
add(
|
||||
IDM_EXIT,
|
||||
"Stop host and exit tray",
|
||||
false,
|
||||
Some(win_theme::GLYPH_SHIELD),
|
||||
);
|
||||
} else {
|
||||
add(IDM_EXIT, "Exit tray", false, Some(win_theme::GLYPH_POWER));
|
||||
}
|
||||
|
||||
let mut pt = Default::default();
|
||||
let _ = GetCursorPos(&mut pt);
|
||||
@@ -537,16 +554,20 @@ fn shell_open(hwnd: HWND, target: &str) {
|
||||
}
|
||||
|
||||
/// One UAC prompt per service action: relaunch the host exe elevated with `service <verb>`.
|
||||
/// A declined prompt (ERROR_CANCELLED) is deliberately ignored.
|
||||
fn elevate_service(hwnd: HWND, verb: &str) {
|
||||
///
|
||||
/// Returns whether the elevated child was actually launched. `false` covers a declined prompt
|
||||
/// (`ERROR_CANCELLED`) — harmless for start/stop/restart, which simply do nothing, but `IDM_EXIT`
|
||||
/// needs it: an exit that stops the host must not close the icon when the operator said no.
|
||||
/// ShellExecute reports failure as a return value at or below 32, per its contract.
|
||||
fn elevate_service(hwnd: HWND, verb: &str) -> bool {
|
||||
let Some(exe) = app().host_exe.as_ref() else {
|
||||
return;
|
||||
return false;
|
||||
};
|
||||
let exe_w = to_wide(&exe.to_string_lossy());
|
||||
let params = to_wide(&format!("service {verb}"));
|
||||
// SAFETY: nul-terminated strings live across the call; "runas" spawns the elevated child
|
||||
// (hidden console — the tray re-polls for the outcome instead of scraping its output).
|
||||
unsafe {
|
||||
let rc = unsafe {
|
||||
ShellExecuteW(
|
||||
Some(hwnd),
|
||||
w!("runas"),
|
||||
@@ -554,11 +575,12 @@ fn elevate_service(hwnd: HWND, verb: &str) {
|
||||
PCWSTR(params.as_ptr()),
|
||||
PCWSTR::null(),
|
||||
SW_HIDE,
|
||||
);
|
||||
}
|
||||
)
|
||||
};
|
||||
if let Some(p) = app().poller.get() {
|
||||
p.poke();
|
||||
}
|
||||
rc.0 as isize > 32
|
||||
}
|
||||
|
||||
/// Open the web console at `path` ("" = dashboard). Deep links land the operator on the page the
|
||||
@@ -627,14 +649,40 @@ extern "system" fn wndproc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM)
|
||||
IDM_OPEN_WEB => open_web_console(hwnd, ""),
|
||||
IDM_PAIRING => open_web_console(hwnd, "pairing"),
|
||||
IDM_DISPLAYS => open_web_console(hwnd, "displays"),
|
||||
IDM_START => elevate_service(hwnd, "start"),
|
||||
IDM_STOP => elevate_service(hwnd, "stop"),
|
||||
IDM_RESTART => elevate_service(hwnd, "restart"),
|
||||
IDM_START => {
|
||||
let _ = elevate_service(hwnd, "start");
|
||||
}
|
||||
IDM_STOP => {
|
||||
let _ = elevate_service(hwnd, "stop");
|
||||
}
|
||||
IDM_RESTART => {
|
||||
let _ = elevate_service(hwnd, "restart");
|
||||
}
|
||||
IDM_LOGS => open_logs(hwnd),
|
||||
// SAFETY: DestroyWindow on the wndproc's own window/thread.
|
||||
IDM_EXIT => unsafe {
|
||||
let _ = DestroyWindow(hwnd);
|
||||
},
|
||||
IDM_EXIT => {
|
||||
// The icon is the host's only visible surface on this box, so closing it
|
||||
// stops the host rather than leaving a service running with no face. Only
|
||||
// this menu entry does that: WM_CLOSE (the uninstaller's `--quit`) and
|
||||
// WM_ENDSESSION (sign-out, shutdown) must leave a headless host alone.
|
||||
//
|
||||
// A declined UAC prompt cancels the exit too — the host is still up, and an
|
||||
// icon that vanished while its host kept running would be a lie.
|
||||
// Same condition the menu labelled this entry with (`show_menu`): without a
|
||||
// host exe there is no stop to attempt, and a refusal there would leave the
|
||||
// operator with an icon they cannot close.
|
||||
let stop_first = app.host_exe.is_some()
|
||||
&& matches!(
|
||||
*app.status.lock().unwrap(),
|
||||
TrayStatus::Running(_) | TrayStatus::Starting | TrayStatus::Degraded
|
||||
);
|
||||
if stop_first && !elevate_service(hwnd, "stop") {
|
||||
return LRESULT(0);
|
||||
}
|
||||
// SAFETY: DestroyWindow on the wndproc's own window/thread.
|
||||
unsafe {
|
||||
let _ = DestroyWindow(hwnd);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
LRESULT(0)
|
||||
|
||||
@@ -327,14 +327,19 @@ Filename: "powershell.exe"; Parameters: "{code:RestoreTasksParams}"; \
|
||||
Filename: "{app}\punktfunk-tray.exe"; Flags: runasoriginaluser nowait skipifsilent; Tasks: trayicon
|
||||
|
||||
[UninstallRun]
|
||||
; Quit the tray FIRST - it is this exe being deleted, so it must not be running. --quit closes the
|
||||
; Uninstall the SERVICE FIRST, then the tray. Order matters since the host supervises the tray
|
||||
; (windows/tray.rs `supervise`): kill the tray while the service still runs and it can put a fresh
|
||||
; one back, re-locking punktfunk-tray.exe just as file deletion starts. Stopping the service first
|
||||
; takes its supervisor down with it, so the two entries below face nothing that fights them.
|
||||
;
|
||||
; Then quit the tray - it is this exe being deleted, so it must not be running. --quit closes the
|
||||
; current session's instance (an elevated caller may message a medium-IL window; UIPI only blocks
|
||||
; low->high); the taskkill then reaps instances in OTHER signed-in sessions. [UninstallRun] runs
|
||||
; before file deletion, so a raced survivor only means a delete-on-reboot leftover, nothing worse.
|
||||
; (runasoriginaluser is not valid in [UninstallRun] - both entries run elevated, which is fine.)
|
||||
Filename: "{app}\punktfunk-host.exe"; Parameters: "service uninstall"; Flags: runhidden waituntilterminated; RunOnceId: "PunktfunkHostServiceUninstall"
|
||||
Filename: "{app}\punktfunk-tray.exe"; Parameters: "--quit"; Flags: runhidden waituntilterminated; RunOnceId: "PunktfunkTrayQuit"
|
||||
Filename: "{sys}\taskkill.exe"; Parameters: "/F /IM punktfunk-tray.exe"; Flags: runhidden waituntilterminated; RunOnceId: "PunktfunkTrayKill"
|
||||
Filename: "{app}\punktfunk-host.exe"; Parameters: "service uninstall"; Flags: runhidden waituntilterminated; RunOnceId: "PunktfunkHostServiceUninstall"
|
||||
; Remove the punktfunk drivers we installed (pf-vdisplay devnode + driver package, then the gamepad
|
||||
; driver packages). AFTER service uninstall so the host no longer holds the devices. Unconditional
|
||||
; (not #ifdef'd on this build's bundled payload - an upgrade may have dropped a payload the original
|
||||
|
||||
Reference in New Issue
Block a user