mirror of https://github.com/openclaw/openclaw.git
fix: fall back to a startup entry for windows gateway install
This commit is contained in:
parent
a60a4b4b5e
commit
433e65711f
|
|
@ -120,6 +120,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Windows/update: mirror the native installer environment during global npm updates, including portable Git fallback and Windows-safe npm shell settings, so `openclaw update` works again on native Windows installs.
|
||||
- Gateway/status: expose `runtimeVersion` in gateway status output so install/update smoke tests can verify the running version before and after updates.
|
||||
- Windows/onboarding: explain when non-interactive local onboarding is waiting for an already-running gateway, and surface native Scheduled Task admin requirements more clearly instead of failing with an opaque gateway timeout.
|
||||
- Windows/gateway install: fall back from denied Scheduled Task creation to a per-user Startup-folder login item, so native `openclaw gateway install` and `--install-daemon` keep working without an elevated PowerShell shell.
|
||||
- Agents/text sanitization: strip leaked model control tokens (`<|...|>` and full-width `<|...|>` variants) from user-facing assistant text, preventing GLM-5 and DeepSeek internal delimiters from reaching end users. (#42173) Thanks @imwyvern.
|
||||
- iOS/gateway foreground recovery: reconnect immediately on foreground return after stale background sockets are torn down, so the app no longer stays disconnected until a later wake path happens. (#41384) Thanks @mbelinky.
|
||||
- Gateway/Control UI: keep dashboard auth tokens in session-scoped browser storage so same-tab refreshes preserve remote token auth without restoring long-lived localStorage token persistence, while scoping tokens to the selected gateway URL and fragment-only bootstrap flow. (#40892) thanks @velvet-shark.
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ Non-interactive local gateway health:
|
|||
- Unless you pass `--skip-health`, onboarding waits for a reachable local gateway before it exits successfully.
|
||||
- `--install-daemon` starts the managed gateway install path first. Without it, you must already have a local gateway running, for example `openclaw gateway run`.
|
||||
- If you only want config/workspace/bootstrap writes in automation, use `--skip-health`.
|
||||
- On native Windows, `--install-daemon` currently uses Scheduled Tasks and may require running PowerShell as Administrator.
|
||||
- On native Windows, `--install-daemon` tries Scheduled Tasks first and falls back to a per-user Startup-folder login item if task creation is denied.
|
||||
|
||||
Interactive onboarding behavior with reference mode:
|
||||
|
||||
|
|
|
|||
|
|
@ -39,8 +39,9 @@ openclaw agent --local --agent main --thinking low -m "Reply with exactly WINDOW
|
|||
Current caveats:
|
||||
|
||||
- `openclaw onboard --non-interactive` still expects a reachable local gateway unless you pass `--skip-health`
|
||||
- `openclaw onboard --non-interactive --install-daemon` and `openclaw gateway install` currently use Windows Scheduled Tasks
|
||||
- on some native Windows setups, Scheduled Task install may require running PowerShell as Administrator
|
||||
- `openclaw onboard --non-interactive --install-daemon` and `openclaw gateway install` try Windows Scheduled Tasks first
|
||||
- if Scheduled Task creation is denied, OpenClaw falls back to a per-user Startup-folder login item and starts the gateway immediately
|
||||
- Scheduled Tasks are still preferred when available because they provide better supervisor status
|
||||
|
||||
If you want the native CLI only, without gateway service install, use one of these:
|
||||
|
||||
|
|
@ -49,6 +50,15 @@ openclaw onboard --non-interactive --skip-health
|
|||
openclaw gateway run
|
||||
```
|
||||
|
||||
If you do want managed startup on native Windows:
|
||||
|
||||
```powershell
|
||||
openclaw gateway install
|
||||
openclaw gateway status --json
|
||||
```
|
||||
|
||||
If Scheduled Task creation is blocked, the fallback service mode still auto-starts after login through the current user's Startup folder.
|
||||
|
||||
## Gateway
|
||||
|
||||
- [Gateway runbook](/gateway)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,185 @@
|
|||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { PassThrough } from "node:stream";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const schtasksResponses = vi.hoisted(
|
||||
() => [] as Array<{ code: number; stdout: string; stderr: string }>,
|
||||
);
|
||||
const schtasksCalls = vi.hoisted(() => [] as string[][]);
|
||||
const inspectPortUsage = vi.hoisted(() => vi.fn());
|
||||
const killProcessTree = vi.hoisted(() => vi.fn());
|
||||
const runCommandWithTimeout = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./schtasks-exec.js", () => ({
|
||||
execSchtasks: async (argv: string[]) => {
|
||||
schtasksCalls.push(argv);
|
||||
return schtasksResponses.shift() ?? { code: 0, stdout: "", stderr: "" };
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../infra/ports.js", () => ({
|
||||
inspectPortUsage: (...args: unknown[]) => inspectPortUsage(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../process/kill-tree.js", () => ({
|
||||
killProcessTree: (...args: unknown[]) => killProcessTree(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../process/exec.js", () => ({
|
||||
runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeout(...args),
|
||||
}));
|
||||
|
||||
const {
|
||||
installScheduledTask,
|
||||
isScheduledTaskInstalled,
|
||||
readScheduledTaskRuntime,
|
||||
restartScheduledTask,
|
||||
resolveTaskScriptPath,
|
||||
} = await import("./schtasks.js");
|
||||
|
||||
function resolveStartupEntryPath(env: Record<string, string>) {
|
||||
return path.join(
|
||||
env.APPDATA,
|
||||
"Microsoft",
|
||||
"Windows",
|
||||
"Start Menu",
|
||||
"Programs",
|
||||
"Startup",
|
||||
"OpenClaw Gateway.cmd",
|
||||
);
|
||||
}
|
||||
|
||||
async function withWindowsEnv(
|
||||
run: (params: { tmpDir: string; env: Record<string, string> }) => Promise<void>,
|
||||
) {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-win-startup-"));
|
||||
const env = {
|
||||
USERPROFILE: tmpDir,
|
||||
APPDATA: path.join(tmpDir, "AppData", "Roaming"),
|
||||
OPENCLAW_PROFILE: "default",
|
||||
OPENCLAW_GATEWAY_PORT: "18789",
|
||||
};
|
||||
try {
|
||||
await run({ tmpDir, env });
|
||||
} finally {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
schtasksResponses.length = 0;
|
||||
schtasksCalls.length = 0;
|
||||
inspectPortUsage.mockReset();
|
||||
killProcessTree.mockReset();
|
||||
runCommandWithTimeout.mockReset();
|
||||
runCommandWithTimeout.mockResolvedValue({
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit",
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("Windows startup fallback", () => {
|
||||
it("falls back to a Startup-folder launcher when schtasks create is denied", async () => {
|
||||
await withWindowsEnv(async ({ env }) => {
|
||||
schtasksResponses.push(
|
||||
{ code: 0, stdout: "", stderr: "" },
|
||||
{ code: 5, stdout: "", stderr: "ERROR: Access is denied." },
|
||||
);
|
||||
|
||||
const stdout = new PassThrough();
|
||||
let printed = "";
|
||||
stdout.on("data", (chunk) => {
|
||||
printed += String(chunk);
|
||||
});
|
||||
|
||||
const result = await installScheduledTask({
|
||||
env,
|
||||
stdout,
|
||||
programArguments: ["node", "gateway.js", "--port", "18789"],
|
||||
environment: { OPENCLAW_GATEWAY_PORT: "18789" },
|
||||
});
|
||||
|
||||
const startupEntryPath = resolveStartupEntryPath(env);
|
||||
const startupScript = await fs.readFile(startupEntryPath, "utf8");
|
||||
expect(result.scriptPath).toBe(resolveTaskScriptPath(env));
|
||||
expect(startupScript).toContain('start "" /min cmd.exe /d /c');
|
||||
expect(startupScript).toContain("gateway.cmd");
|
||||
expect(runCommandWithTimeout).toHaveBeenCalledWith(
|
||||
["cmd.exe", "/d", "/s", "/c", startupEntryPath],
|
||||
expect.objectContaining({ timeoutMs: 3000, windowsVerbatimArguments: true }),
|
||||
);
|
||||
expect(printed).toContain("Installed Windows login item");
|
||||
});
|
||||
});
|
||||
|
||||
it("treats an installed Startup-folder launcher as loaded", async () => {
|
||||
await withWindowsEnv(async ({ env }) => {
|
||||
schtasksResponses.push(
|
||||
{ code: 0, stdout: "", stderr: "" },
|
||||
{ code: 1, stdout: "", stderr: "not found" },
|
||||
);
|
||||
await fs.mkdir(path.dirname(resolveStartupEntryPath(env)), { recursive: true });
|
||||
await fs.writeFile(resolveStartupEntryPath(env), "@echo off\r\n", "utf8");
|
||||
|
||||
await expect(isScheduledTaskInstalled({ env })).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("reports runtime from the gateway listener when using the Startup fallback", async () => {
|
||||
await withWindowsEnv(async ({ env }) => {
|
||||
schtasksResponses.push(
|
||||
{ code: 0, stdout: "", stderr: "" },
|
||||
{ code: 1, stdout: "", stderr: "not found" },
|
||||
);
|
||||
await fs.mkdir(path.dirname(resolveStartupEntryPath(env)), { recursive: true });
|
||||
await fs.writeFile(resolveStartupEntryPath(env), "@echo off\r\n", "utf8");
|
||||
inspectPortUsage.mockResolvedValue({
|
||||
port: 18789,
|
||||
status: "busy",
|
||||
listeners: [{ pid: 4242, command: "node.exe" }],
|
||||
hints: [],
|
||||
});
|
||||
|
||||
await expect(readScheduledTaskRuntime(env)).resolves.toMatchObject({
|
||||
status: "running",
|
||||
pid: 4242,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("restarts the Startup fallback by killing the current pid and relaunching the entry", async () => {
|
||||
await withWindowsEnv(async ({ env }) => {
|
||||
schtasksResponses.push(
|
||||
{ code: 0, stdout: "", stderr: "" },
|
||||
{ code: 1, stdout: "", stderr: "not found" },
|
||||
{ code: 0, stdout: "", stderr: "" },
|
||||
{ code: 1, stdout: "", stderr: "not found" },
|
||||
);
|
||||
await fs.mkdir(path.dirname(resolveStartupEntryPath(env)), { recursive: true });
|
||||
await fs.writeFile(resolveStartupEntryPath(env), "@echo off\r\n", "utf8");
|
||||
inspectPortUsage.mockResolvedValue({
|
||||
port: 18789,
|
||||
status: "busy",
|
||||
listeners: [{ pid: 5151, command: "node.exe" }],
|
||||
hints: [],
|
||||
});
|
||||
|
||||
const stdout = new PassThrough();
|
||||
await expect(restartScheduledTask({ env, stdout })).resolves.toEqual({
|
||||
outcome: "completed",
|
||||
});
|
||||
expect(killProcessTree).toHaveBeenCalledWith(5151, { graceMs: 300 });
|
||||
expect(runCommandWithTimeout).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { inspectPortUsage } from "../infra/ports.js";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import { killProcessTree } from "../process/kill-tree.js";
|
||||
import { parseCmdScriptCommandLine, quoteCmdScriptArg } from "./cmd-argv.js";
|
||||
import { assertNoCmdLineBreak, parseCmdSetAssignment, renderCmdSetAssignment } from "./cmd-set.js";
|
||||
import { resolveGatewayServiceDescription, resolveGatewayWindowsTaskName } from "./constants.js";
|
||||
|
|
@ -37,6 +40,36 @@ export function resolveTaskScriptPath(env: GatewayServiceEnv): string {
|
|||
return path.join(stateDir, scriptName);
|
||||
}
|
||||
|
||||
function resolveWindowsStartupDir(env: GatewayServiceEnv): string {
|
||||
const appData = env.APPDATA?.trim();
|
||||
if (appData) {
|
||||
return path.join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
|
||||
}
|
||||
const home = env.USERPROFILE?.trim() || env.HOME?.trim();
|
||||
if (!home) {
|
||||
throw new Error("Windows startup folder unavailable: APPDATA/USERPROFILE not set");
|
||||
}
|
||||
return path.join(
|
||||
home,
|
||||
"AppData",
|
||||
"Roaming",
|
||||
"Microsoft",
|
||||
"Windows",
|
||||
"Start Menu",
|
||||
"Programs",
|
||||
"Startup",
|
||||
);
|
||||
}
|
||||
|
||||
function sanitizeWindowsFilename(value: string): string {
|
||||
return value.replace(/[<>:"/\\|?*]/g, "_").replace(/\p{Cc}/gu, "_");
|
||||
}
|
||||
|
||||
function resolveStartupEntryPath(env: GatewayServiceEnv): string {
|
||||
const taskName = resolveTaskName(env);
|
||||
return path.join(resolveWindowsStartupDir(env), `${sanitizeWindowsFilename(taskName)}.cmd`);
|
||||
}
|
||||
|
||||
// `/TR` is parsed by schtasks itself, while the generated `gateway.cmd` line is parsed by cmd.exe.
|
||||
// Keep their quoting strategies separate so each parser gets the encoding it expects.
|
||||
function quoteSchtasksArg(value: string): string {
|
||||
|
|
@ -103,6 +136,7 @@ export async function readScheduledTaskCommand(
|
|||
programArguments: parseCmdScriptCommandLine(commandLine),
|
||||
...(workingDirectory ? { workingDirectory } : {}),
|
||||
...(Object.keys(environment).length > 0 ? { environment } : {}),
|
||||
sourcePath: scriptPath,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
|
|
@ -211,6 +245,17 @@ function buildTaskScript({
|
|||
return `${lines.join("\r\n")}\r\n`;
|
||||
}
|
||||
|
||||
function buildStartupLauncherScript(params: { description?: string; scriptPath: string }): string {
|
||||
const lines = ["@echo off"];
|
||||
const trimmedDescription = params.description?.trim();
|
||||
if (trimmedDescription) {
|
||||
assertNoCmdLineBreak(trimmedDescription, "Startup launcher description");
|
||||
lines.push(`rem ${trimmedDescription}`);
|
||||
}
|
||||
lines.push(`start "" /min cmd.exe /d /c ${quoteCmdScriptArg(params.scriptPath)}`);
|
||||
return `${lines.join("\r\n")}\r\n`;
|
||||
}
|
||||
|
||||
async function assertSchtasksAvailable() {
|
||||
const res = await execSchtasks(["/Query"]);
|
||||
if (res.code === 0) {
|
||||
|
|
@ -220,6 +265,92 @@ async function assertSchtasksAvailable() {
|
|||
throw new Error(`schtasks unavailable: ${detail || "unknown error"}`.trim());
|
||||
}
|
||||
|
||||
async function isStartupEntryInstalled(env: GatewayServiceEnv): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(resolveStartupEntryPath(env));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function isRegisteredScheduledTask(env: GatewayServiceEnv): Promise<boolean> {
|
||||
const taskName = resolveTaskName(env);
|
||||
const res = await execSchtasks(["/Query", "/TN", taskName]).catch(() => ({
|
||||
code: 1,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
}));
|
||||
return res.code === 0;
|
||||
}
|
||||
|
||||
async function launchStartupEntry(env: GatewayServiceEnv): Promise<void> {
|
||||
const startupEntryPath = resolveStartupEntryPath(env);
|
||||
await runCommandWithTimeout(["cmd.exe", "/d", "/s", "/c", startupEntryPath], {
|
||||
timeoutMs: 3000,
|
||||
windowsVerbatimArguments: true,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveConfiguredGatewayPort(env: GatewayServiceEnv): number | null {
|
||||
const raw = env.OPENCLAW_GATEWAY_PORT?.trim();
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const parsed = Number.parseInt(raw, 10);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
||||
}
|
||||
|
||||
async function resolveFallbackRuntime(env: GatewayServiceEnv): Promise<GatewayServiceRuntime> {
|
||||
const port = resolveConfiguredGatewayPort(env);
|
||||
if (!port) {
|
||||
return {
|
||||
status: "unknown",
|
||||
detail: "Startup-folder login item installed; gateway port unknown.",
|
||||
};
|
||||
}
|
||||
const diagnostics = await inspectPortUsage(port).catch(() => null);
|
||||
if (!diagnostics) {
|
||||
return {
|
||||
status: "unknown",
|
||||
detail: `Startup-folder login item installed; could not inspect port ${port}.`,
|
||||
};
|
||||
}
|
||||
const listener = diagnostics.listeners.find((item) => typeof item.pid === "number");
|
||||
return {
|
||||
status: diagnostics.status === "busy" ? "running" : "stopped",
|
||||
...(listener?.pid ? { pid: listener.pid } : {}),
|
||||
detail:
|
||||
diagnostics.status === "busy"
|
||||
? `Startup-folder login item installed; listener detected on port ${port}.`
|
||||
: `Startup-folder login item installed; no listener detected on port ${port}.`,
|
||||
};
|
||||
}
|
||||
|
||||
async function stopStartupEntry(
|
||||
env: GatewayServiceEnv,
|
||||
stdout: NodeJS.WritableStream,
|
||||
): Promise<void> {
|
||||
const runtime = await resolveFallbackRuntime(env);
|
||||
if (typeof runtime.pid === "number" && runtime.pid > 0) {
|
||||
killProcessTree(runtime.pid, { graceMs: 300 });
|
||||
}
|
||||
stdout.write(`${formatLine("Stopped Windows login item", resolveTaskName(env))}\n`);
|
||||
}
|
||||
|
||||
async function restartStartupEntry(
|
||||
env: GatewayServiceEnv,
|
||||
stdout: NodeJS.WritableStream,
|
||||
): Promise<GatewayServiceRestartResult> {
|
||||
const runtime = await resolveFallbackRuntime(env);
|
||||
if (typeof runtime.pid === "number" && runtime.pid > 0) {
|
||||
killProcessTree(runtime.pid, { graceMs: 300 });
|
||||
}
|
||||
await launchStartupEntry(env);
|
||||
stdout.write(`${formatLine("Restarted Windows login item", resolveTaskName(env))}\n`);
|
||||
return { outcome: "completed" };
|
||||
}
|
||||
|
||||
export async function installScheduledTask({
|
||||
env,
|
||||
stdout,
|
||||
|
|
@ -263,10 +394,23 @@ export async function installScheduledTask({
|
|||
}
|
||||
if (create.code !== 0) {
|
||||
const detail = create.stderr || create.stdout;
|
||||
const hint = /access is denied/i.test(detail)
|
||||
? " Run PowerShell as Administrator or rerun without installing the daemon."
|
||||
: "";
|
||||
throw new Error(`schtasks create failed: ${detail}${hint}`.trim());
|
||||
if (/access is denied/i.test(detail)) {
|
||||
const startupEntryPath = resolveStartupEntryPath(env);
|
||||
await fs.mkdir(path.dirname(startupEntryPath), { recursive: true });
|
||||
const launcher = buildStartupLauncherScript({ description: taskDescription, scriptPath });
|
||||
await fs.writeFile(startupEntryPath, launcher, "utf8");
|
||||
await launchStartupEntry(env);
|
||||
writeFormattedLines(
|
||||
stdout,
|
||||
[
|
||||
{ label: "Installed Windows login item", value: startupEntryPath },
|
||||
{ label: "Task script", value: scriptPath },
|
||||
],
|
||||
{ leadingBlankLine: true },
|
||||
);
|
||||
return { scriptPath };
|
||||
}
|
||||
throw new Error(`schtasks create failed: ${detail}`.trim());
|
||||
}
|
||||
|
||||
await execSchtasks(["/Run", "/TN", taskName]);
|
||||
|
|
@ -288,7 +432,16 @@ export async function uninstallScheduledTask({
|
|||
}: GatewayServiceManageArgs): Promise<void> {
|
||||
await assertSchtasksAvailable();
|
||||
const taskName = resolveTaskName(env);
|
||||
await execSchtasks(["/Delete", "/F", "/TN", taskName]);
|
||||
const taskInstalled = await isRegisteredScheduledTask(env).catch(() => false);
|
||||
if (taskInstalled) {
|
||||
await execSchtasks(["/Delete", "/F", "/TN", taskName]);
|
||||
}
|
||||
|
||||
const startupEntryPath = resolveStartupEntryPath(env);
|
||||
try {
|
||||
await fs.unlink(startupEntryPath);
|
||||
stdout.write(`${formatLine("Removed Windows login item", startupEntryPath)}\n`);
|
||||
} catch {}
|
||||
|
||||
const scriptPath = resolveTaskScriptPath(env);
|
||||
try {
|
||||
|
|
@ -305,8 +458,23 @@ function isTaskNotRunning(res: { stdout: string; stderr: string; code: number })
|
|||
}
|
||||
|
||||
export async function stopScheduledTask({ stdout, env }: GatewayServiceControlArgs): Promise<void> {
|
||||
await assertSchtasksAvailable();
|
||||
const taskName = resolveTaskName(env ?? (process.env as GatewayServiceEnv));
|
||||
const effectiveEnv = env ?? (process.env as GatewayServiceEnv);
|
||||
try {
|
||||
await assertSchtasksAvailable();
|
||||
} catch (err) {
|
||||
if (await isStartupEntryInstalled(effectiveEnv)) {
|
||||
await stopStartupEntry(effectiveEnv, stdout);
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
if (!(await isRegisteredScheduledTask(effectiveEnv))) {
|
||||
if (await isStartupEntryInstalled(effectiveEnv)) {
|
||||
await stopStartupEntry(effectiveEnv, stdout);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const taskName = resolveTaskName(effectiveEnv);
|
||||
const res = await execSchtasks(["/End", "/TN", taskName]);
|
||||
if (res.code !== 0 && !isTaskNotRunning(res)) {
|
||||
throw new Error(`schtasks end failed: ${res.stderr || res.stdout}`.trim());
|
||||
|
|
@ -318,8 +486,21 @@ export async function restartScheduledTask({
|
|||
stdout,
|
||||
env,
|
||||
}: GatewayServiceControlArgs): Promise<GatewayServiceRestartResult> {
|
||||
await assertSchtasksAvailable();
|
||||
const taskName = resolveTaskName(env ?? (process.env as GatewayServiceEnv));
|
||||
const effectiveEnv = env ?? (process.env as GatewayServiceEnv);
|
||||
try {
|
||||
await assertSchtasksAvailable();
|
||||
} catch (err) {
|
||||
if (await isStartupEntryInstalled(effectiveEnv)) {
|
||||
return await restartStartupEntry(effectiveEnv, stdout);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
if (!(await isRegisteredScheduledTask(effectiveEnv))) {
|
||||
if (await isStartupEntryInstalled(effectiveEnv)) {
|
||||
return await restartStartupEntry(effectiveEnv, stdout);
|
||||
}
|
||||
}
|
||||
const taskName = resolveTaskName(effectiveEnv);
|
||||
await execSchtasks(["/End", "/TN", taskName]);
|
||||
const res = await execSchtasks(["/Run", "/TN", taskName]);
|
||||
if (res.code !== 0) {
|
||||
|
|
@ -330,10 +511,11 @@ export async function restartScheduledTask({
|
|||
}
|
||||
|
||||
export async function isScheduledTaskInstalled(args: GatewayServiceEnvArgs): Promise<boolean> {
|
||||
await assertSchtasksAvailable();
|
||||
const taskName = resolveTaskName(args.env ?? (process.env as GatewayServiceEnv));
|
||||
const res = await execSchtasks(["/Query", "/TN", taskName]);
|
||||
return res.code === 0;
|
||||
const effectiveEnv = args.env ?? (process.env as GatewayServiceEnv);
|
||||
if (await isRegisteredScheduledTask(effectiveEnv)) {
|
||||
return true;
|
||||
}
|
||||
return await isStartupEntryInstalled(effectiveEnv);
|
||||
}
|
||||
|
||||
export async function readScheduledTaskRuntime(
|
||||
|
|
@ -342,6 +524,9 @@ export async function readScheduledTaskRuntime(
|
|||
try {
|
||||
await assertSchtasksAvailable();
|
||||
} catch (err) {
|
||||
if (await isStartupEntryInstalled(env)) {
|
||||
return await resolveFallbackRuntime(env);
|
||||
}
|
||||
return {
|
||||
status: "unknown",
|
||||
detail: String(err),
|
||||
|
|
@ -350,6 +535,9 @@ export async function readScheduledTaskRuntime(
|
|||
const taskName = resolveTaskName(env);
|
||||
const res = await execSchtasks(["/Query", "/TN", taskName, "/V", "/FO", "LIST"]);
|
||||
if (res.code !== 0) {
|
||||
if (await isStartupEntryInstalled(env)) {
|
||||
return await resolveFallbackRuntime(env);
|
||||
}
|
||||
const detail = (res.stderr || res.stdout).trim();
|
||||
const missing = detail.toLowerCase().includes("cannot find the file");
|
||||
return {
|
||||
|
|
|
|||
Loading…
Reference in New Issue