Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bin/cli/commands/autostart.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function registerAutostart(program) {
.description(t("autostart.status") || "Show autostart status")
.action(async (opts, c) => {
const globalOpts = c.optsWithGlobals();
const { isAutostartEnabled } = await import("../tray/autostart.mjs");
emit({ enabled: isAutostartEnabled() }, globalOpts);
const { getAutostartStatus } = await import("../tray/autostart.mjs");
emit(getAutostartStatus(), globalOpts);
});
}
6 changes: 3 additions & 3 deletions bin/cli/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1225,9 +1225,9 @@
"quit": "Quit OmniRoute via tray"
},
"autostart": {
"description": "Manage OmniRoute autostart at login",
"enable": "Enable autostart at login",
"disable": "Disable autostart at login",
"description": "Manage OmniRoute autostart at boot (Linux: systemd user service)",
"enable": "Enable autostart at boot",
"disable": "Disable autostart at boot",
"status": "Show autostart status"
},
"runtime": {
Expand Down
227 changes: 201 additions & 26 deletions bin/cli/tray/autostart.mjs
Original file line number Diff line number Diff line change
@@ -1,16 +1,177 @@
import { existsSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs";
import { existsSync, writeFileSync, unlinkSync, mkdirSync, realpathSync } from "node:fs";
import { execSync } from "node:child_process";
import { homedir } from "node:os";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const APP_LABEL = "com.omniroute.autostart";
const WIN_REG_VALUE = "OmniRoute";
const LINUX_SERVICE_NAME = "omniroute.service";
const LINUX_DESKTOP_NAME = "omniroute.desktop";

function resolveCliPath() {
return (
process.argv[1] || join(dirname(fileURLToPath(import.meta.url)), "..", "..", "omniroute.mjs")
);
const candidates = [];
if (process.argv[1]) candidates.push(process.argv[1]);
try {
const which = execSync("command -v omniroute 2>/dev/null", { encoding: "utf8" }).trim();
if (which) candidates.push(which);
} catch {
// command -v unavailable
}
candidates.push(join(dirname(fileURLToPath(import.meta.url)), "..", "..", "omniroute.mjs"));

for (const candidate of candidates) {
if (!candidate) continue;
try {
const resolved = realpathSync(candidate);
if (resolved.endsWith("omniroute.mjs") && existsSync(resolved)) return resolved;
} catch {
// try next candidate
}
}

const fallback = candidates[candidates.length - 1];
return existsSync(fallback) ? fallback : null;
}

function quoteExecArg(value) {
if (!/[ \t"'\\]/.test(value)) return value;
return `"${value.replace(/(["\\])/g, "\\$1")}"`;
}

function buildServeExecLine(cliPath, { tray = false } = {}) {
const parts = [quoteExecArg(process.execPath), quoteExecArg(cliPath), "serve", "--no-open"];
if (tray) parts.push("--tray");
return parts.join(" ");
}

function isGraphicalLinuxSession() {
if (process.env.DISPLAY || process.env.WAYLAND_DISPLAY) return true;
if (process.env.XDG_CURRENT_DESKTOP) return true;
return false;
}

function linuxSystemdUnitPath() {
return join(homedir(), ".config", "systemd", "user", LINUX_SERVICE_NAME);
}

function linuxDesktopPath() {
return join(homedir(), ".config", "autostart", LINUX_DESKTOP_NAME);
}

function runUserSystemctl(args, { ignoreFailure = true } = {}) {
try {
execSync(`systemctl --user ${args}`, { stdio: "ignore" });
return true;
} catch {
return ignoreFailure ? false : false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The ternary operator ignoreFailure ? false : false is redundant as it always returns false. Additionally, the ignoreFailure parameter is currently ignored in the error path. If the intention is to allow errors to propagate when ignoreFailure is false, the logic should be updated to throw the error.

Suggested change
return ignoreFailure ? false : false;
return false;

}
}

function isSystemdUserAvailable() {
try {
execSync("systemctl --user --version", { stdio: "ignore" });
return true;
} catch {
return false;
}
}

function isSystemdServiceEnabled() {
if (!existsSync(linuxSystemdUnitPath())) return false;
try {
execSync(`systemctl --user is-enabled ${LINUX_SERVICE_NAME}`, { stdio: "ignore" });
return true;
} catch {
return false;
}
}

function tryEnableLinger() {
try {
const user =
process.env.USER ||
process.env.LOGNAME ||
execSync("whoami", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
if (!user) return false;
execSync(`loginctl enable-linger ${JSON.stringify(user)}`, { stdio: "ignore" });
return true;
} catch {
return false;
}
}

function writeLinuxSystemdUnit(cliPath) {
const unitDir = dirname(linuxSystemdUnitPath());
mkdirSync(unitDir, { recursive: true });
const envFile = join(homedir(), ".omniroute", ".env");
const lines = [
"[Unit]",
"Description=OmniRoute AI proxy router",
"After=network-online.target",
"Wants=network-online.target",
"",
"[Service]",
"Type=simple",
`ExecStart=${buildServeExecLine(cliPath, { tray: false })}`,
"Restart=on-failure",
"RestartSec=5",
];
if (existsSync(envFile)) lines.push(`EnvironmentFile=-${envFile}`);
lines.push("", "[Install]", "WantedBy=default.target", "");
writeFileSync(linuxSystemdUnitPath(), `${lines.join("\n")}\n`, { mode: 0o644 });
}

function writeLinuxDesktopEntry(cliPath) {
const dir = dirname(linuxDesktopPath());
mkdirSync(dir, { recursive: true });
const desktop =
[
"[Desktop Entry]",
"Type=Application",
"Name=OmniRoute",
"Comment=AI proxy router with auto fallback",
`Exec=${buildServeExecLine(cliPath, { tray: true })}`,
"Terminal=false",
"Hidden=false",
"X-GNOME-Autostart-enabled=true",
].join("\n") + "\n";
writeFileSync(linuxDesktopPath(), desktop, { mode: 0o644 });
}

export function getAutostartStatus() {
if (process.platform === "linux") {
const systemdUnit = linuxSystemdUnitPath();
const desktopFile = linuxDesktopPath();
const systemdEnabled = isSystemdServiceEnabled();
const desktopEnabled = existsSync(desktopFile);
const enabled = systemdEnabled || desktopEnabled;
let mechanism = null;
if (systemdEnabled) mechanism = "systemd-user";
else if (desktopEnabled) mechanism = "xdg-desktop";
return {
enabled,
mechanism,
systemdUnit: existsSync(systemdUnit) ? systemdUnit : null,
desktopFile: desktopEnabled ? desktopFile : null,
linger: tryReadLingerEnabled(),
};
}
return { enabled: isAutostartEnabled(), mechanism: null };
}

function tryReadLingerEnabled() {
try {
const user = process.env.USER || process.env.LOGNAME;
if (!user) return null;
Comment on lines +165 to +166

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For consistency with tryEnableLinger (lines 92-95), tryReadLingerEnabled should also attempt to fall back to whoami if the USER and LOGNAME environment variables are missing. This ensures the autostart status report is accurate in environments where these variables aren't set.

    const user =
      process.env.USER ||
      process.env.LOGNAME ||
      execSync("whoami", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
    if (!user) return null;

const out = execSync(`loginctl show-user ${JSON.stringify(user)} -p Linger`, {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
});
return out.includes("Linger=yes");
} catch {
return null;
}
}

export function enable() {
Expand Down Expand Up @@ -39,6 +200,7 @@ function enableMac() {
mkdirSync(plistDir, { recursive: true });
const plistPath = join(plistDir, `${APP_LABEL}.plist`);
const cliPath = resolveCliPath();
if (!cliPath) return false;
const plist = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
Expand Down Expand Up @@ -77,6 +239,7 @@ function isEnabledMac() {

function enableWin() {
const cliPath = resolveCliPath();
if (!cliPath) return false;
const value = `"${process.execPath}" "${cliPath}" serve --tray --no-open`;
try {
execSync(
Expand Down Expand Up @@ -114,34 +277,46 @@ function isEnabledWin() {
}

function enableLinux() {
const dir = join(homedir(), ".config", "autostart");
mkdirSync(dir, { recursive: true });
const cliPath = resolveCliPath();
const desktop =
[
"[Desktop Entry]",
"Type=Application",
"Name=OmniRoute",
"Comment=AI proxy router with auto fallback",
`Exec=${process.execPath} ${cliPath} serve --tray --no-open`,
"Terminal=false",
"Hidden=false",
"X-GNOME-Autostart-enabled=true",
].join("\n") + "\n";
writeFileSync(join(dir, "omniroute.desktop"), desktop, { mode: 0o644 });
return true;
if (!cliPath) return false;

if (!isSystemdUserAvailable() && !isGraphicalLinuxSession()) {
return false;
}

let ok = false;

if (isSystemdUserAvailable()) {
writeLinuxSystemdUnit(cliPath);
runUserSystemctl("daemon-reload");
ok = runUserSystemctl(`enable ${LINUX_SERVICE_NAME}`) || existsSync(linuxSystemdUnitPath());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The fallback || existsSync(linuxSystemdUnitPath()) is problematic because the unit file was just written in the previous step (line 290), so this check will almost always be true. This causes ok to be true even if the systemctl --user enable command failed, leading to a false positive report that autostart is enabled when it actually isn't.

Suggested change
ok = runUserSystemctl(`enable ${LINUX_SERVICE_NAME}`) || existsSync(linuxSystemdUnitPath());
ok = runUserSystemctl(`enable ${LINUX_SERVICE_NAME}`);

runUserSystemctl(`start ${LINUX_SERVICE_NAME}`);
tryEnableLinger();
}

if (isGraphicalLinuxSession()) {
writeLinuxDesktopEntry(cliPath);
ok = true;
}

return ok || isEnabledLinux();
}

function disableLinux() {
const desktopPath = join(homedir(), ".config", "autostart", "omniroute.desktop");
try {
unlinkSync(desktopPath);
return true;
} catch {
return false;
if (isSystemdUserAvailable()) {
runUserSystemctl(`disable --now ${LINUX_SERVICE_NAME}`);
runUserSystemctl("daemon-reload");
}
try {
unlinkSync(linuxSystemdUnitPath());
} catch {}
try {
unlinkSync(linuxDesktopPath());
} catch {}
return !isEnabledLinux();
}

function isEnabledLinux() {
return existsSync(join(homedir(), ".config", "autostart", "omniroute.desktop"));
if (isSystemdServiceEnabled()) return true;
return existsSync(linuxDesktopPath());
}
Loading