Files
NetMesh/electron/autoLaunch.cjs
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

196 lines
8.6 KiB
JavaScript

/**
* Auto Launch - Registers Netcatty to start at system login, hidden to the
* tray. Thin wrapper around Electron's app.setLoginItemSettings/
* getLoginItemSettings so main.cjs and the settings IPC handlers share one
* source of truth.
*
* Development runs (`electron .`) are unsupported: process.execPath points
* at the transient electron.exe, so a login item registered there would
* break (or silently do nothing) after the dev process exits.
*
* Platform support is further limited to macOS and Windows: Electron's
* login-item API is a no-op on Linux, which ships as AppImage/deb/rpm/pacman
* with no first-party autostart hook, so reporting it as supported there
* would show an enabled toggle that does nothing.
*/
const HIDDEN_LAUNCH_ARG = "--hidden";
function isAutoLaunchSupported({ defaultApp = process.defaultApp, platform = process.platform } = {}) {
if (defaultApp) return false;
return platform === "darwin" || platform === "win32";
}
function argsEqual(a, b) {
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((value, index) => value === b[index]);
}
/**
* Find the specific Windows run-key entry our own writes create (path +
* --hidden args), not just any entry for this executable.
*/
function findMatchingLaunchItem(settings, execPath) {
if (!Array.isArray(settings?.launchItems)) return null;
return settings.launchItems.find(
(item) => item?.path === execPath && argsEqual(item?.args, [HIDDEN_LAUNCH_ARG]),
) ?? null;
}
/**
* Resolve the OS's effective auto-launch state, not just whether a
* registration exists.
*
* Windows can retain the run-key entry while the user disables it via Task
* Manager's Startup Apps UI. Electron's executableWillLaunchAtLogin surfaces
* an effective state too, but it deliberately ignores the args option and
* reports true if the executable would launch with ANY arguments — so it
* can false-positive on an unrelated no-argument entry for the same exe.
* launchItems[].enabled is scoped to one specific path+args registration
* (the same combination buildLoginItemQueryOptions queries), so look up our
* own --hidden entry there instead and use its enabled flag.
*/
function resolveEffectiveLoginState(settings, platform, execPath) {
if (platform === "win32") {
const matchingItem = findMatchingLaunchItem(settings, execPath);
if (matchingItem) return Boolean(matchingItem.enabled);
// No matching --hidden entry: openAtLogin was queried with the same
// path+args (see buildLoginItemQueryOptions), so it is still correctly
// scoped to "is that exact entry registered" — false if absent.
return Boolean(settings?.openAtLogin);
}
if (platform === "darwin" && settings?.status === "requires-approval") {
// macOS 13+ (SMAppService): a freshly registered login item sits in
// "requires-approval" until the user approves it in System Settings,
// during which openAtLogin reports false even though registration
// itself succeeded. Reporting false here would make the renderer's
// push effect see a mismatch against what the user just requested and
// immediately fire an opposite write, unregistering the pending item
// before the user ever gets a chance to approve it. Treat pending
// approval as enabled; a later read naturally reflects the real state
// once the user approves it (or the OS drops the pending request).
return true;
}
return Boolean(settings?.openAtLogin);
}
/**
* Electron's getLoginItemSettings() only reports openAtLogin for the
* specific path+args combination you ask about — it does not mean "is
* anything registered for this app". Our code only ever registers a login
* item with args:[HIDDEN_LAUNCH_ARG], so every read must query that exact
* combination (matching what setAutoLaunchEnabled writes) or Windows
* reports a false negative.
*/
function buildLoginItemQueryOptions(execPath) {
return { path: execPath, args: [HIDDEN_LAUNCH_ARG] };
}
function getAutoLaunchEnabled({
app,
execPath = process.execPath,
defaultApp = process.defaultApp,
platform = process.platform,
} = {}) {
if (!isAutoLaunchSupported({ defaultApp, platform })) {
return { success: true, enabled: false, supported: false };
}
try {
const settings = app.getLoginItemSettings(buildLoginItemQueryOptions(execPath));
return { success: true, enabled: resolveEffectiveLoginState(settings, platform, execPath), supported: true };
} catch (err) {
console.warn("[AutoLaunch] Failed to read login item settings:", err?.message || err);
// success:false signals "OS state unknown" (as opposed to a confirmed
// disabled state) so callers — notably the renderer's mount-time
// hydration — know not to trust `enabled` here. Blindly applying
// enabled:false on a transient read failure would overwrite the
// renderer's cached value and cascade into an unwanted disable write.
return { success: false, enabled: false, supported: true };
}
}
function setAutoLaunchEnabled(enabled, {
app,
execPath = process.execPath,
defaultApp = process.defaultApp,
platform = process.platform,
} = {}) {
if (!isAutoLaunchSupported({ defaultApp, platform })) {
// enabled:false is a confirmed fact here (the feature genuinely isn't
// available), not an unknown state — matches getAutoLaunchEnabled's
// equivalent branch so both functions agree on what `success` means.
return { success: true, enabled: false, supported: false };
}
const wantEnabled = Boolean(enabled);
try {
app.setLoginItemSettings({
openAtLogin: wantEnabled,
// openAsHidden only applies on macOS App Store builds; Windows relies
// on the --hidden arg below, which main.cjs checks on cold start.
openAsHidden: wantEnabled,
path: execPath,
args: wantEnabled ? [HIDDEN_LAUNCH_ARG] : [],
});
const settings = app.getLoginItemSettings(buildLoginItemQueryOptions(execPath));
return { success: true, enabled: resolveEffectiveLoginState(settings, platform, execPath), supported: true };
} catch (err) {
console.warn("[AutoLaunch] Failed to update login item settings:", err?.message || err);
// `success` means "is `enabled` trustworthy", not "did the write
// succeed" — the write itself failing does not make a subsequent,
// independent read failing too. Surface whatever the fallback read
// determines: if it succeeds, `enabled` reflects the real (unwritten)
// state and the renderer's push effect correctly rolls the optimistic
// toggle back instead of leaving it stuck on a change that never
// actually happened. Only a genuine double failure (write AND fallback
// read both throw) reports success:false, so the renderer preserves its
// last-known state instead of guessing.
const fallback = getAutoLaunchEnabled({ app, execPath, defaultApp, platform });
return { success: fallback.success, enabled: fallback.enabled, supported: true };
}
}
/**
* True when this process was launched by the OS login item (cold start
* only). Windows relies on the --hidden arg (macOS never puts args from
* setLoginItemSettings() into argv for a login launch, so it needs a
* different signal).
*
* macOS's own hidden-launch flags (openAsHidden/wasOpenedAsHidden) are
* deprecated and stop working on macOS 13+ per Electron's docs, so they
* cannot be trusted to detect an actual hidden launch there. wasOpenedAtLogin
* still works on 13+ and is not deprecated; since Netcatty only ever
* registers a macOS login item to satisfy this "launch hidden" feature (there
* is no scenario where it registers one for a normal, visible startup), any
* automatic login launch should apply our own hidden-window behavior.
*/
function wasLaunchedHidden({ argv = process.argv, app, platform = process.platform } = {}) {
if (Array.isArray(argv) && argv.includes(HIDDEN_LAUNCH_ARG)) return true;
if (platform !== "darwin" || typeof app?.getLoginItemSettings !== "function") return false;
try {
return Boolean(app.getLoginItemSettings().wasOpenedAtLogin);
} catch (err) {
console.warn("[AutoLaunch] Failed to read macOS login-item launch state:", err?.message || err);
return false;
}
}
function registerHandlers(ipcMain, { app, platform = process.platform }) {
ipcMain.handle("netcatty:autoLaunch:get", async () => {
return getAutoLaunchEnabled({ app, platform });
});
ipcMain.handle("netcatty:autoLaunch:set", async (_event, { enabled }) => {
return setAutoLaunchEnabled(enabled, { app, platform });
});
}
module.exports = {
HIDDEN_LAUNCH_ARG,
isAutoLaunchSupported,
resolveEffectiveLoginState,
buildLoginItemQueryOptions,
getAutoLaunchEnabled,
setAutoLaunchEnabled,
wasLaunchedHidden,
registerHandlers,
};