Skip to content

Commit b80ae7a

Browse files
LXTBradGroux
authored andcommitted
Daemon: query Windows task runtime directly
1 parent 53efd63 commit b80ae7a

5 files changed

Lines changed: 74 additions & 35 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ Docs: https://docs.openclaw.ai
6464
- Codex app-server: disarm the short post-tool completion watchdog after current-turn activity, expose `appServer.turnCompletionIdleTimeoutMs`, and include raw assistant item context in idle-timeout diagnostics so status-only post-tool stalls stop failing as idle. Fixes #77984. Thanks @roseware-dev and @rubencu.
6565
- Plugin skills/Windows: publish plugin-provided skill directories as junctions on Windows so standard users without Developer Mode can register plugin skills without symlink EPERM failures. Fixes #77958. (#77971) Thanks @hclsys and @jarro.
6666
- Shell env/Windows: hide the login-shell environment probe child window so gateway startup and shell-env refreshes do not flash a console on Windows. Fixes #78159. (#78266) Thanks @BradGroux.
67+
- Gateway/Windows: read scheduled-task runtime from the named task query directly so a fragile broad `schtasks /Query` preflight cannot make `openclaw status` report the gateway as unavailable. Fixes #49187. (#51486) Thanks @wangji0923.
6768
- MS Teams: surface blocked Bot Framework egress by logging JWKS fetch network failures and adding a Bot Connector send hint for transport-level reply failures. Fixes #77674. (#78081) Thanks @Beandon13.
6869
- Gateway/sessions: fast-path already-qualified model refs while building session-list rows so `openclaw sessions` and Control UI session lists avoid heavyweight model resolution on large stores. (#77902) Thanks @ragesaq.
6970
- Contributor PRs: remind external contributors to redact private information like IP addresses, API keys, phone numbers, and non-public endpoints from real behavior proof. Thanks @pashpashpash.

src/daemon/schtasks.startup-fallback.test.ts

Lines changed: 59 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
inspectPortUsage,
88
killProcessTree,
99
resetSchtasksBaseMocks,
10+
schtasksCalls,
11+
schtasksThrownErrors,
1012
schtasksResponses,
1113
withWindowsEnv,
1214
writeGatewayScript,
@@ -122,12 +124,12 @@ function expectGatewayTermination(pid: number) {
122124

123125
function addStartupFallbackMissingResponses(
124126
extraResponses: Array<{ code: number; stdout: string; stderr: string }> = [],
127+
opts?: { includePreflight?: boolean },
125128
) {
126-
schtasksResponses.push(
127-
{ code: 0, stdout: "", stderr: "" },
128-
{ code: 1, stdout: "", stderr: "not found" },
129-
...extraResponses,
130-
);
129+
if (opts?.includePreflight ?? true) {
130+
schtasksResponses.push({ code: 0, stdout: "", stderr: "" });
131+
}
132+
schtasksResponses.push({ code: 1, stdout: "", stderr: "not found" }, ...extraResponses);
131133
}
132134

133135
function installGatewayScheduledTask(env: Record<string, string>, stdout = new PassThrough()) {
@@ -147,11 +149,9 @@ function fastForwardTaskStartWait(): void {
147149

148150
function addAcceptedRunNeverStartsResponses(): void {
149151
addStartupFallbackMissingResponses([
150-
{ code: 0, stdout: "", stderr: "" },
151152
{ code: 0, stdout: "", stderr: "" },
152153
{ code: 0, stdout: "", stderr: "" },
153154
{ code: 0, stdout: notYetRunTaskQueryOutput(), stderr: "" },
154-
{ code: 0, stdout: "", stderr: "" },
155155
{ code: 0, stdout: notYetRunTaskQueryOutput(), stderr: "" },
156156
]);
157157
}
@@ -341,10 +341,7 @@ describe("Windows startup fallback", () => {
341341
await withWindowsEnv("openclaw-win-startup-", async ({ env }) => {
342342
await writeGatewayScript(env);
343343
findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([4242]);
344-
schtasksResponses.push(
345-
{ code: 0, stdout: "", stderr: "" },
346-
{ code: 0, stdout: notYetRunTaskQueryOutput(), stderr: "" },
347-
);
344+
schtasksResponses.push({ code: 0, stdout: notYetRunTaskQueryOutput(), stderr: "" });
348345

349346
await expect(readScheduledTaskRuntime(env)).resolves.toMatchObject({
350347
status: "running",
@@ -364,10 +361,7 @@ describe("Windows startup fallback", () => {
364361
listeners: [{ pid: 4242, command: "node.exe" }],
365362
hints: [],
366363
});
367-
schtasksResponses.push(
368-
{ code: 0, stdout: "", stderr: "" },
369-
{ code: 0, stdout: notYetRunTaskQueryOutput(), stderr: "" },
370-
);
364+
schtasksResponses.push({ code: 0, stdout: notYetRunTaskQueryOutput(), stderr: "" });
371365

372366
await expect(readScheduledTaskRuntime(env)).resolves.toMatchObject({
373367
status: "stopped",
@@ -379,7 +373,7 @@ describe("Windows startup fallback", () => {
379373

380374
it("treats an installed Startup-folder launcher as loaded", async () => {
381375
await withWindowsEnv("openclaw-win-startup-", async ({ env }) => {
382-
addStartupFallbackMissingResponses();
376+
addStartupFallbackMissingResponses([], { includePreflight: false });
383377
await writeStartupFallbackEntry(env);
384378

385379
await expect(isScheduledTaskInstalled({ env })).resolves.toBe(true);
@@ -388,7 +382,47 @@ describe("Windows startup fallback", () => {
388382

389383
it("reports runtime from the gateway listener when using the Startup fallback", async () => {
390384
await withWindowsEnv("openclaw-win-startup-", async ({ env }) => {
391-
addStartupFallbackMissingResponses();
385+
addStartupFallbackMissingResponses([], { includePreflight: false });
386+
await writeStartupFallbackEntry(env);
387+
inspectPortUsage.mockResolvedValue({
388+
port: 18789,
389+
status: "busy",
390+
listeners: [{ pid: 4242, command: "node.exe" }],
391+
hints: [],
392+
});
393+
394+
await expect(readScheduledTaskRuntime(env)).resolves.toMatchObject({
395+
status: "running",
396+
pid: 4242,
397+
});
398+
});
399+
});
400+
401+
it("reads runtime from the task-scoped schtasks query without a global preflight", async () => {
402+
await withWindowsEnv("openclaw-win-startup-", async ({ env }) => {
403+
schtasksResponses.push({
404+
code: 0,
405+
stdout: [
406+
"TaskName: \\OpenClaw Gateway",
407+
"Status: Running",
408+
"Last Run Time: 2026/3/21 14:00:00",
409+
"Last Run Result: 267009",
410+
].join("\r\n"),
411+
stderr: "",
412+
});
413+
414+
await expect(readScheduledTaskRuntime(env)).resolves.toMatchObject({
415+
status: "running",
416+
state: "Running",
417+
lastRunResult: "267009",
418+
});
419+
expect(schtasksCalls).toEqual([["/Query", "/TN", "OpenClaw Gateway", "/V", "/FO", "LIST"]]);
420+
});
421+
});
422+
423+
it("falls back to Startup runtime when the task-scoped schtasks query throws", async () => {
424+
await withWindowsEnv("openclaw-win-startup-", async ({ env }) => {
425+
schtasksThrownErrors.push(new Error("spawn ENOENT"));
392426
await writeStartupFallbackEntry(env);
393427
inspectPortUsage.mockResolvedValue({
394428
port: 18789,
@@ -401,15 +435,19 @@ describe("Windows startup fallback", () => {
401435
status: "running",
402436
pid: 4242,
403437
});
438+
expect(schtasksCalls).toEqual([["/Query", "/TN", "OpenClaw Gateway", "/V", "/FO", "LIST"]]);
404439
});
405440
});
406441

407442
it("restarts the Startup fallback by killing the current pid and relaunching the entry", async () => {
408443
await withWindowsEnv("openclaw-win-startup-", async ({ env }) => {
409-
addStartupFallbackMissingResponses([
410-
{ code: 0, stdout: "", stderr: "" },
411-
{ code: 1, stdout: "", stderr: "not found" },
412-
]);
444+
addStartupFallbackMissingResponses(
445+
[
446+
{ code: 0, stdout: "", stderr: "" },
447+
{ code: 1, stdout: "", stderr: "not found" },
448+
],
449+
{ includePreflight: true },
450+
);
413451
await writeGatewayScript(env);
414452
await writeStartupFallbackEntry(env);
415453
inspectPortUsage.mockResolvedValue({
@@ -445,9 +483,7 @@ describe("Windows startup fallback", () => {
445483
{ code: 0, stdout: "", stderr: "" },
446484
{ code: 0, stdout: "", stderr: "" },
447485
{ code: 0, stdout: "", stderr: "" },
448-
{ code: 0, stdout: "", stderr: "" },
449486
{ code: 0, stdout: notYetRunTaskQueryOutput(), stderr: "" },
450-
{ code: 0, stdout: "", stderr: "" },
451487
{ code: 0, stdout: notYetRunTaskQueryOutput(), stderr: "" },
452488
);
453489

src/daemon/schtasks.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,19 +1025,14 @@ export async function isScheduledTaskInstalled(args: GatewayServiceEnvArgs): Pro
10251025
export async function readScheduledTaskRuntime(
10261026
env: GatewayServiceEnv = process.env as GatewayServiceEnv,
10271027
): Promise<GatewayServiceRuntime> {
1028-
try {
1029-
await assertSchtasksAvailable();
1030-
} catch (err) {
1031-
if (await isStartupEntryInstalled(env)) {
1032-
return await resolveFallbackRuntime(env);
1033-
}
1034-
return {
1035-
status: "unknown",
1036-
detail: String(err),
1037-
};
1038-
}
10391028
const taskName = resolveTaskName(env);
1040-
const res = await execSchtasks(["/Query", "/TN", taskName, "/V", "/FO", "LIST"]);
1029+
const res = await execSchtasks(["/Query", "/TN", taskName, "/V", "/FO", "LIST"]).catch(
1030+
(err: unknown) => ({
1031+
code: 1,
1032+
stdout: "",
1033+
stderr: String(err),
1034+
}),
1035+
);
10411036
if (res.code !== 0) {
10421037
if (await isStartupEntryInstalled(env)) {
10431038
return await resolveFallbackRuntime(env);

src/daemon/test-helpers/schtasks-base-mocks.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@ import {
33
inspectPortUsage,
44
killProcessTree,
55
schtasksCalls,
6+
schtasksThrownErrors,
67
schtasksResponses,
78
} from "./schtasks-fixtures.js";
89

910
vi.mock("../schtasks-exec.js", () => ({
1011
execSchtasks: async (argv: string[]) => {
1112
schtasksCalls.push(argv);
13+
const thrown = schtasksThrownErrors.shift();
14+
if (thrown !== undefined) {
15+
throw thrown;
16+
}
1217
return schtasksResponses.shift() ?? { code: 0, stdout: "", stderr: "" };
1318
},
1419
}));

src/daemon/test-helpers/schtasks-fixtures.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { MockFn } from "../../test-utils/vitest-mock-fn.js";
88
import { resolveTaskScriptPath } from "../schtasks.js";
99

1010
export const schtasksResponses: Array<{ code: number; stdout: string; stderr: string }> = [];
11+
export const schtasksThrownErrors: unknown[] = [];
1112
export const schtasksCalls: string[][] = [];
1213

1314
export const inspectPortUsage: MockFn<(port: number) => Promise<PortUsage>> = vi.fn();
@@ -33,6 +34,7 @@ export async function withWindowsEnv(
3334

3435
export function resetSchtasksBaseMocks() {
3536
schtasksResponses.length = 0;
37+
schtasksThrownErrors.length = 0;
3638
schtasksCalls.length = 0;
3739
inspectPortUsage.mockReset();
3840
killProcessTree.mockReset();

0 commit comments

Comments
 (0)