mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
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:
@@ -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 {
|
||||
|
||||
1
changelog.d/fixes/7925-windows-autostart-vbs.md
Normal file
1
changelog.d/fixes/7925-windows-autostart-vbs.md
Normal 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))
|
||||
103
tests/unit/cli/autostart-windows.test.ts
Normal file
103
tests/unit/cli/autostart-windows.test.ts
Normal 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/);
|
||||
});
|
||||
Reference in New Issue
Block a user