desktop open: a hidden tray copy that is already running counts once its window is visible (Windows), graceful stop wait 3 s on Windows

This commit is contained in:
CRTkafa 2026-10-23 22:42:12 +08:00
parent d4cf1ad3a1
commit 8df41a26b0
5 changed files with 58 additions and 17 deletions

View file

@ -5,7 +5,7 @@
import { posix } from 'node:path';
import { compareVersions } from './desktop-install';
export interface Proc { pid: number; exe: string; args: string }
export interface Proc { pid: number; exe: string; args: string; /** Windows only: the process has a visible top-level window. */ window?: boolean }
/** The executable a process of the installed app runs from (the .app bundle's binary on macOS). */
export function appExe(platform: NodeJS.Platform, installedPath: string): string {
@ -16,7 +16,7 @@ export function appExe(platform: NodeJS.Platform, installedPath: string): string
export function listCommand(platform: NodeJS.Platform): string[] {
if (platform === 'win32') {
return ['powershell', '-NoProfile', '-NonInteractive', '-Command',
"Get-CimInstance Win32_Process -Filter \"Name='storebound-desktop.exe'\" | Select-Object ProcessId,ExecutablePath,CommandLine | ConvertTo-Json -Compress"];
"Get-CimInstance Win32_Process -Filter \"Name='storebound-desktop.exe'\" | ForEach-Object { $g = Get-Process -Id $_.ProcessId -ErrorAction SilentlyContinue; [pscustomobject]@{ ProcessId = $_.ProcessId; ExecutablePath = $_.ExecutablePath; CommandLine = $_.CommandLine; Window = [bool]($g -and $g.MainWindowHandle -ne 0) } } | ConvertTo-Json -Compress"];
}
return ['ps', '-axww', '-o', 'pid=,args='];
}
@ -28,8 +28,8 @@ export function parseProcs(platform: NodeJS.Platform, out: string): Proc[] {
if (!t) return [];
let j: unknown;
try { j = JSON.parse(t); } catch { return []; }
const rows = (Array.isArray(j) ? j : [j]) as { ProcessId?: number; ExecutablePath?: string | null; CommandLine?: string | null }[];
return rows.filter(r => typeof r?.ProcessId === 'number').map(r => ({ pid: r.ProcessId!, exe: r.ExecutablePath ?? '', args: r.CommandLine ?? '' }));
const rows = (Array.isArray(j) ? j : [j]) as { ProcessId?: number; ExecutablePath?: string | null; CommandLine?: string | null; Window?: boolean }[];
return rows.filter(r => typeof r?.ProcessId === 'number').map(r => ({ pid: r.ProcessId!, exe: r.ExecutablePath ?? '', args: r.CommandLine ?? '', window: r.Window === true }));
}
const procs: Proc[] = [];
for (const line of out.split('\n')) {
@ -51,6 +51,20 @@ export function matchByPath(platform: NodeJS.Platform, procs: Proc[], exe: strin
/** Was the process started hidden (the logon entry: tray only, no window)? */
export const startedHidden = (p: Proc): boolean => /(?:^|\s|")--hidden(?:\s|"|$)/.test(p.args);
/** Is the app open for the user? Windows: a process of ours has a visible window (a running tray copy started with
* --hidden counts once the single-instance handoff showed its window). Elsewhere a window cannot be seen from here:
* a process started without --hidden, or a hidden copy that the new start handed over to (`handedOver`: the new
* process exited normally), counts. 'none' = no process, 'hidden' = only tray copies and no visible window. */
export function openState(platform: NodeJS.Platform, procs: Proc[], handedOver: boolean): 'ok' | 'none' | 'hidden' {
if (!procs.length) return 'none';
if (platform === 'win32') return procs.some(p => p.window) ? 'ok' : 'hidden';
return procs.some(p => !startedHidden(p)) || handedOver ? 'ok' : 'hidden';
}
/** Seconds to wait for a graceful quit before the forced stop: closing the window of a tray app only hides it, so on
* Windows the graceful step is short. */
export const graceMs = (platform: NodeJS.Platform): number => (platform === 'win32' ? 3_000 : 10_000);
/** The graceful stop (Windows: WM_CLOSE through taskkill without /F; elsewhere TERM) and the forced one, by PID. */
export function stopCommand(platform: NodeJS.Platform, pid: number, force: boolean): string[] {
if (platform === 'win32') return force ? ['taskkill', '/F', '/PID', String(pid)] : ['taskkill', '/PID', String(pid)];