From cfeea5dc5b0001523002c4924bb08c40aab2b9fe Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Fri, 7 Aug 2026 18:08:17 -0300 Subject: [PATCH] fix(cli): enable systray2 on Windows for Norton-friendly tray (#8609) Co-authored-by: diegosouzapw --- bin/cli/runtime/trayRuntime.ts | 3 +-- bin/cli/tray/autostart.mjs | 4 ++++ bin/cli/tray/index.mjs | 9 ++++---- changelog.d/fixes/8609-fix.plan.md | 1 + tests/unit/cli-tray-systray2.test.ts | 8 +++---- tests/unit/repro-8609.test.ts | 32 ++++++++++++++++++++++++++++ 6 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 changelog.d/fixes/8609-fix.plan.md create mode 100644 tests/unit/repro-8609.test.ts diff --git a/bin/cli/runtime/trayRuntime.ts b/bin/cli/runtime/trayRuntime.ts index 712bc720dc..98a3abfccc 100644 --- a/bin/cli/runtime/trayRuntime.ts +++ b/bin/cli/runtime/trayRuntime.ts @@ -17,7 +17,7 @@ export const SYSTRAY_VERSION = "2.1.4"; const SYSTRAY_SPEC = `${SYSTRAY_PACKAGE}@${SYSTRAY_VERSION}`; export function resolveSystrayBinName(platform: NodeJS.Platform): string | null { - if (platform === "win32") return null; + if (platform === "win32") return "tray_windows_release.exe"; if (platform === "darwin") return "tray_darwin_release"; return "tray_linux_release"; } @@ -45,7 +45,6 @@ export function chmodSystrayBinAt(runtimeRoot: string, platform: NodeJS.Platform } export async function loadSystray(): Promise<(new (...args: unknown[]) => unknown) | null> { - if (process.platform === "win32") return null; // Windows uses tray.ps1 instead ensureRuntimeDir(); if (!isInstalled()) { try { diff --git a/bin/cli/tray/autostart.mjs b/bin/cli/tray/autostart.mjs index b8318f2d79..6c1ba21aee 100644 --- a/bin/cli/tray/autostart.mjs +++ b/bin/cli/tray/autostart.mjs @@ -167,6 +167,10 @@ export function getAutostartStatus() { linger: tryReadLingerEnabled(), }; } + if (process.platform === "win32") { + const winMechanism = isAutostartEnabled() ? "vbs-startup" : null; + return { enabled: isAutostartEnabled(), mechanism: winMechanism }; + } return { enabled: isAutostartEnabled(), mechanism: null }; } diff --git a/bin/cli/tray/index.mjs b/bin/cli/tray/index.mjs index 5745062e66..dfa621b422 100644 --- a/bin/cli/tray/index.mjs +++ b/bin/cli/tray/index.mjs @@ -1,5 +1,4 @@ import { isTraySupported, initSystrayUnix, killSystrayUnix } from "./traySystray.mjs"; -import { initWinTray, killWinTray } from "./trayWindows.mjs"; let active = null; @@ -10,15 +9,17 @@ export async function initTray({ port, onQuit, onOpenDashboard, onShowLogs }) { const ctx = { port, onQuit, onOpenDashboard, onShowLogs }; // initSystrayUnix is async: it lazily installs/loads systray2 from the runtime // dir (trayRuntime.ts) rather than from node_modules. (#4605) - active = process.platform === "win32" ? initWinTray(ctx) : await initSystrayUnix(ctx); + // Use systray2 on all platforms including Windows — the tarball ships + // tray_windows_release.exe, avoiding the Norton/AVG IDP.HELU.PSE85 heuristic + // that fires on temp-dir PowerShell scripts. (#8609) + active = await initSystrayUnix(ctx); return active; } export function killTray() { if (!active) return; try { - if (process.platform === "win32") killWinTray(active); - else killSystrayUnix(active); + killSystrayUnix(active); } catch {} active = null; } diff --git a/changelog.d/fixes/8609-fix.plan.md b/changelog.d/fixes/8609-fix.plan.md new file mode 100644 index 0000000000..0f8cb8e868 --- /dev/null +++ b/changelog.d/fixes/8609-fix.plan.md @@ -0,0 +1 @@ +- fix(cli): enable systray2 on Windows for Norton-friendly tray (#8609) \ No newline at end of file diff --git a/tests/unit/cli-tray-systray2.test.ts b/tests/unit/cli-tray-systray2.test.ts index 931e926bb8..33228db41a 100644 --- a/tests/unit/cli-tray-systray2.test.ts +++ b/tests/unit/cli-tray-systray2.test.ts @@ -26,8 +26,8 @@ test("systray2 is pinned to a 2.x version (PR #1080 fix)", () => { assert.match(SYSTRAY_VERSION, /^2\./, `expected systray2@2.x, got ${SYSTRAY_VERSION}`); }); -test("resolveSystrayBinName returns null on win32 and a *_release name elsewhere", () => { - assert.equal(resolveSystrayBinName("win32"), null); +test("resolveSystrayBinName returns *_release name on all platforms (#8609)", () => { + assert.equal(resolveSystrayBinName("win32"), "tray_windows_release.exe"); assert.equal(resolveSystrayBinName("darwin"), "tray_darwin_release"); assert.equal(resolveSystrayBinName("linux"), "tray_linux_release"); }); @@ -63,12 +63,12 @@ test("chmodSystrayBinAt is a no-op when the binary doesn't exist", () => { } }); -test("chmodSystrayBinAt skips win32 (uses PowerShell tray, no Go binary)", () => { +test("chmodSystrayBinAt returns missing on win32 when binary is absent (#8609)", () => { const root = mkdtempSync(join(tmpdir(), "omniroute-systray-bin-")); try { const result = chmodSystrayBinAt(root, "win32"); assert.equal(result.changed, false); - assert.equal(result.reason, "win32-skip"); + assert.equal(result.reason, "missing"); } finally { rmSync(root, { recursive: true, force: true }); } diff --git a/tests/unit/repro-8609.test.ts b/tests/unit/repro-8609.test.ts new file mode 100644 index 0000000000..9893384509 --- /dev/null +++ b/tests/unit/repro-8609.test.ts @@ -0,0 +1,32 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync, readdirSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +test("characterize: trayWindows.mjs initWinTray writes a temp .ps1 (old behavior)", async () => { + const { initWinTray } = await import("../../bin/cli/tray/trayWindows.mjs"); + const ORIG_PLATFORM = Object.getOwnPropertyDescriptor(process, "platform"); + Object.defineProperty(process, "platform", { value: "win32", configurable: true }); + const cleanup = () => { + if (ORIG_PLATFORM) Object.defineProperty(process, "platform", ORIG_PLATFORM); + }; + try { + const proc = initWinTray({ port: 8609, onQuit() {}, onOpenDashboard() {}, onShowLogs() {} }); + if (proc && typeof proc.on === "function") proc.on("error", () => {}); + const scripts = readdirSync(tmpdir()).filter((f) => f.startsWith("omniroute-tray-") && f.endsWith(".ps1")); + assert.ok(scripts.length > 0, "initWinTray creates a temp .ps1 (expected — that is the Norton trigger)"); + const content = readFileSync(join(tmpdir(), scripts[0]), "utf8"); + assert.ok(content.includes("System.Windows.Forms.NotifyIcon"), "temp .ps1 uses WinForms tray"); + } finally { + cleanup(); + } +}); + +test("REGRESSION GUARD: index.mjs no longer imports or calls the PowerShell tray (#8609)", () => { + const source = readFileSync(join(process.cwd(), "bin/cli/tray/index.mjs"), "utf8"); + assert.ok(!source.includes("trayWindows"), "index.mjs must not import trayWindows.mjs"); + assert.ok(!source.includes("initWinTray"), "index.mjs must not reference initWinTray"); + assert.ok(!source.includes("killWinTray"), "index.mjs must not reference killWinTray"); + assert.ok(source.includes("initSystrayUnix"), "index.mjs must still import initSystrayUnix"); +});