From 4dff7d9b2e65de096dc936a1a86d127b27788603 Mon Sep 17 00:00:00 2001 From: CRT Date: Fri, 23 Oct 2026 22:48:02 +0800 Subject: [PATCH] desktop open: Windows window check uses real top-level windows (the tray helper windows and MainWindowHandle also answer for a hidden app) --- cli/lib/desktop-proc.ts | 15 ++++++++++++++- tests/desktop-proc.test.ts | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/cli/lib/desktop-proc.ts b/cli/lib/desktop-proc.ts index 3291bad..08eee60 100644 --- a/cli/lib/desktop-proc.ts +++ b/cli/lib/desktop-proc.ts @@ -15,8 +15,21 @@ export function appExe(platform: NodeJS.Platform, installedPath: string): string /** The command that lists processes with their executable and arguments (never through a shell). */ export function listCommand(platform: NodeJS.Platform): string[] { if (platform === 'win32') { + // Window = a visible, unowned, titled top-level window that is not a tool window (the tray helper windows are + // visible but tool windows / untitled; Process.MainWindowHandle also answers for a hidden window) + const cs = 'using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; ' + + 'public static class SbWin { delegate bool CB(IntPtr h, IntPtr l); ' + + '[DllImport("user32.dll")] static extern bool EnumWindows(CB c, IntPtr l); ' + + '[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint p); ' + + '[DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr h); ' + + '[DllImport("user32.dll")] static extern IntPtr GetWindow(IntPtr h, uint c); ' + + '[DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h); ' + + '[DllImport("user32.dll")] static extern int GetWindowLong(IntPtr h, int i); ' + + 'public static uint[] Visible() { var r = new List(); EnumWindows((h, l) => { uint p; GetWindowThreadProcessId(h, out p); ' + + 'if (IsWindowVisible(h) && GetWindow(h, 4) == IntPtr.Zero && GetWindowTextLength(h) > 0 && (GetWindowLong(h, -20) & 0x80) == 0) r.Add(p); return true; }, IntPtr.Zero); return r.ToArray(); } }'; return ['powershell', '-NoProfile', '-NonInteractive', '-Command', - "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"]; + `Add-Type -TypeDefinition '${cs}'; $v = [SbWin]::Visible(); ` + + "Get-CimInstance Win32_Process -Filter \"Name='storebound-desktop.exe'\" | ForEach-Object { [pscustomobject]@{ ProcessId = $_.ProcessId; ExecutablePath = $_.ExecutablePath; CommandLine = $_.CommandLine; Window = [bool]($v -contains [uint32]$_.ProcessId) } } | ConvertTo-Json -Compress"]; } return ['ps', '-axww', '-o', 'pid=,args=']; } diff --git a/tests/desktop-proc.test.ts b/tests/desktop-proc.test.ts index c1a859b..b7a3e18 100644 --- a/tests/desktop-proc.test.ts +++ b/tests/desktop-proc.test.ts @@ -92,7 +92,7 @@ describe('desktop open: a hidden tray copy that is already running', () => { expect(openState('win32', [rows[0]!], true)).toBe('hidden'); expect(openState('win32', rows, false)).toBe('ok'); expect(openState('win32', [], true)).toBe('none'); - expect(listCommand('win32').join(' ')).toContain('MainWindowHandle'); + expect(listCommand('win32').join(' ')).toContain('IsWindowVisible'); }); test('elsewhere: a visible start, or a hidden copy the new start handed over to', () => { expect(openState('linux', [hidden], false)).toBe('hidden');