fix(windows): supervise the status tray, and stop the host on exit #451
@@ -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