fix(autostart): adopt 9Router VBS startup to suppress console flash on Windows (#7925)

Windows auto-start previously wrote a HKCU\Run registry entry that
launched node.exe directly, causing a visible console window at logon.
Closing that window also killed the background server.

Adopt the same approach as 9Router: write a OmniRoute.vbs script to
the Windows Startup folder that calls WScript.Shell.Run with SW_HIDE (0)
so OmniRoute starts fully hidden. The VBS runs serve --no-open --tray
via the existing buildServeExecLine helper.

Legacy migration — enableWin() removes the old HKCU\Run entry so stale
entries don't linger, and isEnabledWin() falls back to checking the
registry for users who enabled autostart before this change.

Co-authored-by: tientien17 <tientien17@users.noreply.github.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
ToastedPatatas
2026-07-21 22:49:57 +08:00
committed by GitHub
parent 7c39a0e065
commit 448257f8f7
3 changed files with 185 additions and 17 deletions

View File

@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
const APP_LABEL = "com.omniroute.autostart";
const WIN_REG_VALUE = "OmniRoute";
const WIN_STARTUP_FILE = "OmniRoute.vbs";
const LINUX_SERVICE_NAME = "omniroute.service";
const LINUX_DESKTOP_NAME = "omniroute.desktop";
@@ -323,38 +324,101 @@ function isEnabledMac() {
);
}
/**
* Returns the absolute path to the Windows Startup folder
* (%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\),
* or null when APPDATA is not set.
*/
function winStartupDir() {
if (!process.env.APPDATA) return null;
return join(process.env.APPDATA, "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
}
/**
* Returns the full path to the VBS autostart script in the Startup folder,
* or null when the Startup folder cannot be resolved.
*/
function winStartupPath() {
const dir = winStartupDir();
return dir ? join(dir, WIN_STARTUP_FILE) : null;
}
/**
* Builds the VBScript source that launches OmniRoute with WSH's Run method
* using SW_HIDE (0) so no console window appears.
*
* 9Router uses the same pattern: a .vbs file in the Startup folder that calls
* WScript.Shell.Run with window style 0 (hidden). This avoids the console
* window flash that HKCU\Run causes for console-mode node.exe.
*/
function buildWinVbsContent(cliPath) {
const execLine = buildServeExecLine(cliPath, { tray: true });
// VBScript doubles `"` inside a string to escape them. The execLine already
// contains quoted paths; escape each `"` to `""` so the VBS parser reads
// them as literal quote characters in the command string passed to Run().
const vbsSafe = execLine.replace(/"/g, '""');
return [
'Set WshShell = CreateObject("WScript.Shell")',
`WshShell.Run "${vbsSafe}", 0, False`,
"",
].join("\n");
}
/**
* Removes the legacy HKCU\Run registry value if present. Callers use this
* during migration so stale Run entries don't linger after switching to the
* VBS-based autostart.
*/
function cleanLegacyWinReg() {
try {
execSync(
`reg delete HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run /v ${WIN_REG_VALUE} /f 2>nul`,
{ stdio: "ignore", windowsHide: true }
);
} catch {
// entry did not exist or already removed — noop
}
}
function enableWin() {
const cliPath = resolveCliPath();
if (!cliPath) return false;
const value = `"${process.execPath}" "${cliPath}" serve --tray --no-open`;
try {
execSync(
`reg add HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run /v ${WIN_REG_VALUE} /t REG_SZ /d "${value}" /f`,
{ stdio: "ignore", windowsHide: true }
);
return true;
} catch {
return false;
}
const vbsPath = winStartupPath();
if (!vbsPath) return false;
const dir = dirname(vbsPath);
mkdirSync(dir, { recursive: true });
writeFileSync(vbsPath, buildWinVbsContent(cliPath), { mode: 0o644 });
// Migrate away from the legacy HKCU\Run entry — it launches node.exe with
// a visible console window. The VBS approach is the replacement.
cleanLegacyWinReg();
return existsSync(vbsPath);
}
function disableWin() {
const vbsPath = winStartupPath();
if (!vbsPath) return false;
try {
execSync(
`reg delete HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run /v ${WIN_REG_VALUE} /f`,
{ stdio: "ignore", windowsHide: true }
);
return true;
unlinkSync(vbsPath);
} catch {
return false;
// already removed
}
// Also clean up any lingering legacy registry entry as a safety net.
cleanLegacyWinReg();
return !existsSync(vbsPath);
}
function isEnabledWin() {
const vbsPath = winStartupPath();
if (!vbsPath) return false;
// Primary check: VBS file exists in the Startup folder.
if (existsSync(vbsPath)) return true;
// Fallback check: legacy HKCU\Run entry (for users who enabled autostart
// before the VBS migration). Treat it as enabled so the tray menu shows
// the correct toggle state, and calling disable() will clean it up.
try {
const out = execSync(
`reg query HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run /v ${WIN_REG_VALUE}`,
{ stdio: "pipe", windowsHide: true, encoding: "utf8" }
{ stdio: "pipe", windowsHide: true, encoding: "utf8", timeout: 3000 }
);
return out.includes(WIN_REG_VALUE);
} catch {

View File

@@ -0,0 +1 @@
- **fix(autostart):** Windows auto-start no longer flashes a console window — writes a `.vbs` launcher to the Startup folder instead of the `HKCU\Run` registry key, adopting the same approach as 9Router ([#7925](https://github.com/diegosouzapw/OmniRoute/pull/7925))

View File

@@ -0,0 +1,103 @@
import test from "node:test";
import assert from "node:assert/strict";
import { existsSync, readFileSync, mkdtempSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
let tmpDir: string;
let origAppData: string | undefined;
test.before(() => {
tmpDir = mkdtempSync(join(tmpdir(), "omniroute-autostart-win-"));
origAppData = process.env.APPDATA;
process.env.APPDATA = tmpDir;
});
test.after(() => {
if (origAppData === undefined) delete process.env.APPDATA;
else process.env.APPDATA = origAppData;
try {
rmSync(tmpDir, { recursive: true, force: true });
} catch {}
});
// ---------------------------------------------------------------------------
// Integration test — runs only on win32 because enable() dispatches on
// process.platform. Sets APPDATA to a temp dir so the VBS is written to a
// clean, disposable Startup folder.
// ---------------------------------------------------------------------------
test("Windows enable/disable writes and removes VBS in Startup folder", async () => {
if (process.platform !== "win32") return;
const { enable, disable, isAutostartEnabled } =
await import("../../../bin/cli/tray/autostart.mjs");
const startupDir = join(tmpDir, "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
const vbsPath = join(startupDir, "OmniRoute.vbs");
// Should start clean.
assert.equal(existsSync(vbsPath), false);
const ok = enable();
assert.equal(typeof ok, "boolean");
assert.equal(ok, true);
// VBS file must exist with correct content.
assert.equal(existsSync(vbsPath), true);
const vbs = readFileSync(vbsPath, "utf8");
assert.match(vbs, /WScript\.Shell/, "creates WScript.Shell COM object");
assert.match(vbs, /WshShell\.Run/, "calls Run method");
assert.match(vbs, /serve --no-open --tray/, "launches OmniRoute tray server");
assert.match(vbs, /, 0, False/, "uses SW_HIDE (0) and no wait");
assert.equal(vbs.endsWith("\n"), true, "trailing newline");
assert.equal(isAutostartEnabled(), true);
// Disable and verify cleanup.
const disabled = disable();
assert.equal(typeof disabled, "boolean");
assert.equal(disabled, true);
assert.equal(existsSync(vbsPath), false);
assert.equal(isAutostartEnabled(), false);
});
// ---------------------------------------------------------------------------
// Source-level assertions (run on any platform) — verify the implementation
// uses the VBS Startup-folder approach instead of the old Registry Run key.
// Mirrors the pattern used by autostart-linux.test.ts and
// autostart-macos-launchctl.test.ts.
// ---------------------------------------------------------------------------
test("Windows enableWin writes VBS to Startup folder, not reg add", () => {
const source = readFileSync(join(process.cwd(), "bin/cli/tray/autostart.mjs"), "utf8");
// Helper functions for the new VBS approach.
assert.match(source, /winStartupDir/);
assert.match(source, /winStartupPath/);
assert.match(source, /buildWinVbsContent/);
// VBS-runner scaffolding.
assert.match(source, /WScript\.Shell/);
assert.match(source, /WshShell\.Run/);
// The old approach used `reg add ... /v WIN_REG_VALUE`.
assert.doesNotMatch(source, /reg add.*WIN_REG_VALUE/);
});
test("Windows disableWin deletes VBS and cleans legacy registry", () => {
const source = readFileSync(join(process.cwd(), "bin/cli/tray/autostart.mjs"), "utf8");
assert.match(source, /unlinkSync\(vbsPath\)/);
assert.match(source, /cleanLegacyWinReg\(\)/);
});
test("Windows isEnabledWin checks VBS first, falls back to legacy reg", () => {
const source = readFileSync(join(process.cwd(), "bin/cli/tray/autostart.mjs"), "utf8");
// Primary check is VBS file existence.
assert.match(source, /existsSync\(vbsPath\)/);
// Fallback for users still on the old Registry Run key.
assert.match(source, /reg query.*WIN_REG_VALUE/);
});