@serviceme/devtools-core 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +558 -263
- package/dist/index.mjs +555 -262
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -39,7 +39,8 @@ async function runCommand(command, options = {}) {
|
|
|
39
39
|
cwd: options.cwd,
|
|
40
40
|
env: options.env,
|
|
41
41
|
shell: options.shell,
|
|
42
|
-
stdio: "pipe"
|
|
42
|
+
stdio: "pipe",
|
|
43
|
+
windowsHide: true
|
|
43
44
|
});
|
|
44
45
|
let stdout = "";
|
|
45
46
|
let stderr = "";
|
|
@@ -243,8 +244,14 @@ import {
|
|
|
243
244
|
createServicemeError as createServicemeError3,
|
|
244
245
|
KNOWN_ENVIRONMENT_TOOLS
|
|
245
246
|
} from "@serviceme/devtools-protocol";
|
|
246
|
-
var
|
|
247
|
+
var DEFAULT_TOOL_CHECK_TIMEOUT_MS = 5e3;
|
|
248
|
+
var TOOL_CHECK_TIMEOUT_MS = {
|
|
249
|
+
nuget: 12e3,
|
|
250
|
+
nvm: 8e3,
|
|
251
|
+
dotnet: 8e3
|
|
252
|
+
};
|
|
247
253
|
var ERROR_CODE_NOT_FOUND = 127;
|
|
254
|
+
var ERROR_CODE_TIMEOUT = "ETIMEDOUT";
|
|
248
255
|
var EnvironmentInspector = class {
|
|
249
256
|
async checkEnvironment() {
|
|
250
257
|
const results = await Promise.all(
|
|
@@ -281,19 +288,18 @@ var EnvironmentInspector = class {
|
|
|
281
288
|
const isWindows = process.platform === "win32";
|
|
282
289
|
const result = await runCommand(isWindows ? "where" : "which", {
|
|
283
290
|
args: [toolName],
|
|
284
|
-
timeoutMs:
|
|
291
|
+
timeoutMs: this.getToolTimeout(toolName)
|
|
285
292
|
});
|
|
286
|
-
return result.stdout.trim().
|
|
293
|
+
return result.stdout.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0);
|
|
287
294
|
} catch {
|
|
288
295
|
return void 0;
|
|
289
296
|
}
|
|
290
297
|
}
|
|
291
298
|
async getToolVersion(toolName) {
|
|
292
|
-
const
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
timeoutMs: TOOL_CHECK_TIMEOUT_MS
|
|
299
|
+
const invocation = this.getVersionInvocation(toolName);
|
|
300
|
+
const result = await runCommand(invocation.command, {
|
|
301
|
+
args: invocation.args,
|
|
302
|
+
timeoutMs: this.getToolTimeout(toolName)
|
|
297
303
|
});
|
|
298
304
|
return this.parseVersion(toolName, result.stdout || result.stderr);
|
|
299
305
|
}
|
|
@@ -302,7 +308,7 @@ var EnvironmentInspector = class {
|
|
|
302
308
|
try {
|
|
303
309
|
const result = await runCommand("cmd.exe", {
|
|
304
310
|
args: ["/c", "nvm version"],
|
|
305
|
-
timeoutMs:
|
|
311
|
+
timeoutMs: this.getToolTimeout("nvm")
|
|
306
312
|
});
|
|
307
313
|
return {
|
|
308
314
|
installed: true,
|
|
@@ -322,7 +328,7 @@ var EnvironmentInspector = class {
|
|
|
322
328
|
"-lc",
|
|
323
329
|
'export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; nvm --version'
|
|
324
330
|
],
|
|
325
|
-
timeoutMs:
|
|
331
|
+
timeoutMs: this.getToolTimeout("nvm")
|
|
326
332
|
});
|
|
327
333
|
return {
|
|
328
334
|
installed: true,
|
|
@@ -347,7 +353,7 @@ var EnvironmentInspector = class {
|
|
|
347
353
|
try {
|
|
348
354
|
const result = await runCommand("dotnet", {
|
|
349
355
|
args: ["nuget", "list", "source"],
|
|
350
|
-
timeoutMs:
|
|
356
|
+
timeoutMs: this.getToolTimeout("nuget")
|
|
351
357
|
});
|
|
352
358
|
const privateSourceUrl = "http://192.168.20.209:10010/nuget";
|
|
353
359
|
if (result.stdout.includes(privateSourceUrl)) {
|
|
@@ -365,17 +371,28 @@ var EnvironmentInspector = class {
|
|
|
365
371
|
return this.handleToolCheckError(error);
|
|
366
372
|
}
|
|
367
373
|
}
|
|
368
|
-
|
|
374
|
+
getVersionInvocation(toolName) {
|
|
375
|
+
const isWindows = process.platform === "win32";
|
|
376
|
+
if (isWindows && ["npm", "pnpm", "nrm"].includes(toolName)) {
|
|
377
|
+
return {
|
|
378
|
+
command: "cmd.exe",
|
|
379
|
+
args: ["/d", "/s", "/c", `${toolName} --version`]
|
|
380
|
+
};
|
|
381
|
+
}
|
|
369
382
|
const commands = {
|
|
370
|
-
git: "git --version",
|
|
371
|
-
node: "node --version",
|
|
372
|
-
npm: "npm --version",
|
|
373
|
-
pnpm: "pnpm --version",
|
|
374
|
-
nvm: "nvm --version",
|
|
375
|
-
nrm: "nrm --version",
|
|
376
|
-
|
|
383
|
+
git: { command: "git", args: ["--version"] },
|
|
384
|
+
node: { command: "node", args: ["--version"] },
|
|
385
|
+
npm: { command: "npm", args: ["--version"] },
|
|
386
|
+
pnpm: { command: "pnpm", args: ["--version"] },
|
|
387
|
+
nvm: { command: "nvm", args: ["--version"] },
|
|
388
|
+
nrm: { command: "nrm", args: ["--version"] },
|
|
389
|
+
rtk: { command: "rtk", args: ["--version"] },
|
|
390
|
+
dotnet: { command: "dotnet", args: ["--version"] }
|
|
377
391
|
};
|
|
378
|
-
return commands[toolName] ||
|
|
392
|
+
return commands[toolName] || { command: toolName, args: ["--version"] };
|
|
393
|
+
}
|
|
394
|
+
getToolTimeout(toolName) {
|
|
395
|
+
return TOOL_CHECK_TIMEOUT_MS[toolName] ?? DEFAULT_TOOL_CHECK_TIMEOUT_MS;
|
|
379
396
|
}
|
|
380
397
|
parseVersion(toolName, output) {
|
|
381
398
|
const cleaned = output.trim();
|
|
@@ -402,10 +419,11 @@ var EnvironmentInspector = class {
|
|
|
402
419
|
const execError = error;
|
|
403
420
|
const message = execError.message || String(error);
|
|
404
421
|
const code = execError.code;
|
|
405
|
-
const isNotFound = message.includes("command not found") || message.includes("not recognized") || code === ERROR_CODE_NOT_FOUND;
|
|
422
|
+
const isNotFound = message.includes("command not found") || message.includes("not recognized") || message.includes("ENOENT") || code === "ENOENT" || code === ERROR_CODE_NOT_FOUND;
|
|
423
|
+
const isTimeout = code === ERROR_CODE_TIMEOUT || message.toLowerCase().includes("timed out");
|
|
406
424
|
return {
|
|
407
425
|
installed: false,
|
|
408
|
-
error: isNotFound ? "Not installed" : message
|
|
426
|
+
error: isNotFound ? "Not installed" : isTimeout ? "Check timed out" : message
|
|
409
427
|
};
|
|
410
428
|
}
|
|
411
429
|
};
|
|
@@ -1081,12 +1099,75 @@ var PidManager = class {
|
|
|
1081
1099
|
};
|
|
1082
1100
|
|
|
1083
1101
|
// src/scheduled-tasks/daemon/SchedulerDaemon.ts
|
|
1084
|
-
import * as
|
|
1102
|
+
import * as fs9 from "fs";
|
|
1103
|
+
import * as os from "os";
|
|
1085
1104
|
|
|
1086
1105
|
// src/scheduled-tasks/executors/GithubCopilotCliExecutor.ts
|
|
1087
1106
|
import { spawn as spawn2 } from "child_process";
|
|
1088
|
-
|
|
1107
|
+
import * as fs5 from "fs";
|
|
1108
|
+
|
|
1109
|
+
// src/scheduled-tasks/executors/timeout.ts
|
|
1110
|
+
function resolveConfiguredTimeoutMs(timeoutSeconds, defaultTimeoutMs) {
|
|
1111
|
+
if (timeoutSeconds === 0) {
|
|
1112
|
+
return void 0;
|
|
1113
|
+
}
|
|
1114
|
+
if (typeof timeoutSeconds !== "number" || !Number.isFinite(timeoutSeconds) || timeoutSeconds < 0) {
|
|
1115
|
+
return defaultTimeoutMs;
|
|
1116
|
+
}
|
|
1117
|
+
return timeoutSeconds * 1e3;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// src/scheduled-tasks/executors/GithubCopilotCliExecutor.ts
|
|
1089
1121
|
var MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
1122
|
+
var DEFAULT_TIMEOUT_MS2 = 3e5;
|
|
1123
|
+
function redactArgs(args) {
|
|
1124
|
+
const redacted = [];
|
|
1125
|
+
let redactNext = false;
|
|
1126
|
+
for (const arg of args) {
|
|
1127
|
+
if (redactNext) {
|
|
1128
|
+
redacted.push("<redacted>");
|
|
1129
|
+
redactNext = false;
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
redacted.push(arg);
|
|
1133
|
+
if (arg === "--prompt") {
|
|
1134
|
+
redactNext = true;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
return redacted;
|
|
1138
|
+
}
|
|
1139
|
+
function writeDiagnostic(message) {
|
|
1140
|
+
const logPath = process.env.SERVICEME_SCHEDULER_LOG_PATH;
|
|
1141
|
+
if (logPath) {
|
|
1142
|
+
fs5.appendFileSync(logPath, message);
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
process.stderr.write(message);
|
|
1146
|
+
}
|
|
1147
|
+
function resolveGithubCopilotCliExecution(payload) {
|
|
1148
|
+
const args = ["copilot", "prompt", "--prompt", payload.prompt];
|
|
1149
|
+
if (payload.autopilot) {
|
|
1150
|
+
args.push("--autopilot");
|
|
1151
|
+
}
|
|
1152
|
+
if (payload.allowTools && payload.allowTools.length > 0) {
|
|
1153
|
+
args.push("--allow-tools", payload.allowTools.join(","));
|
|
1154
|
+
}
|
|
1155
|
+
if (payload.model) {
|
|
1156
|
+
args.push("--model", payload.model);
|
|
1157
|
+
}
|
|
1158
|
+
if (payload.agent) {
|
|
1159
|
+
args.push("--agent", payload.agent);
|
|
1160
|
+
}
|
|
1161
|
+
args.push("--timeout", String(payload.timeout ?? 0));
|
|
1162
|
+
return {
|
|
1163
|
+
command: "serviceme",
|
|
1164
|
+
args,
|
|
1165
|
+
cwd: payload.workspace,
|
|
1166
|
+
timeoutMs: resolveConfiguredTimeoutMs(payload.timeout, DEFAULT_TIMEOUT_MS2),
|
|
1167
|
+
diagnosticArgs: redactArgs(args),
|
|
1168
|
+
promptLen: payload.prompt.length
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1090
1171
|
var GithubCopilotCliExecutor = class {
|
|
1091
1172
|
async execute(payload, abortSignal) {
|
|
1092
1173
|
const authenticated = await isCopilotAuthenticated();
|
|
@@ -1109,7 +1190,7 @@ var GithubCopilotCliExecutor = class {
|
|
|
1109
1190
|
}
|
|
1110
1191
|
executeStreaming(payload, onOutput, abortSignal) {
|
|
1111
1192
|
const p = payload;
|
|
1112
|
-
const
|
|
1193
|
+
const execution = resolveGithubCopilotCliExecution(p);
|
|
1113
1194
|
let resolve;
|
|
1114
1195
|
const resultPromise = new Promise((r) => {
|
|
1115
1196
|
resolve = r;
|
|
@@ -1118,7 +1199,7 @@ var GithubCopilotCliExecutor = class {
|
|
|
1118
1199
|
const settle = (result) => {
|
|
1119
1200
|
if (settled) return;
|
|
1120
1201
|
settled = true;
|
|
1121
|
-
clearTimeout(timer);
|
|
1202
|
+
if (timer) clearTimeout(timer);
|
|
1122
1203
|
resolve?.(result);
|
|
1123
1204
|
};
|
|
1124
1205
|
if (abortSignal?.aborted) {
|
|
@@ -1131,36 +1212,32 @@ var GithubCopilotCliExecutor = class {
|
|
|
1131
1212
|
}
|
|
1132
1213
|
};
|
|
1133
1214
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
args.push("--model", p.model);
|
|
1143
|
-
}
|
|
1144
|
-
if (p.agent) {
|
|
1145
|
-
args.push("--agent", p.agent);
|
|
1146
|
-
}
|
|
1147
|
-
if (p.timeout != null) {
|
|
1148
|
-
args.push("--timeout", String(p.timeout));
|
|
1149
|
-
}
|
|
1150
|
-
const child = spawn2("serviceme", args, {
|
|
1151
|
-
cwd: p.workspace,
|
|
1152
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1215
|
+
writeDiagnostic(
|
|
1216
|
+
`[GithubCopilotCliExecutor] spawn: command=${execution.command}, args=${JSON.stringify(execution.diagnosticArgs)}, promptLen=${execution.promptLen}, cwd=${execution.cwd ?? "(default)"}, platform=${process.platform}, windowsHide=true, pid=${process.pid}
|
|
1217
|
+
`
|
|
1218
|
+
);
|
|
1219
|
+
const child = spawn2(execution.command, execution.args, {
|
|
1220
|
+
cwd: execution.cwd,
|
|
1221
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1222
|
+
windowsHide: true
|
|
1153
1223
|
});
|
|
1154
|
-
|
|
1224
|
+
if (child.pid) {
|
|
1225
|
+
writeDiagnostic(
|
|
1226
|
+
`[GithubCopilotCliExecutor] spawned child PID=${child.pid}
|
|
1227
|
+
`
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
const timeoutMs = execution.timeoutMs;
|
|
1231
|
+
const timer = timeoutMs != null ? setTimeout(() => {
|
|
1155
1232
|
child.kill("SIGTERM");
|
|
1156
1233
|
setTimeout(() => {
|
|
1157
1234
|
if (!child.killed) child.kill("SIGKILL");
|
|
1158
1235
|
}, 5e3);
|
|
1159
1236
|
settle({
|
|
1160
1237
|
status: "timeout",
|
|
1161
|
-
error: `Copilot CLI execution timed out after ${
|
|
1238
|
+
error: `Copilot CLI execution timed out after ${timeoutMs / 1e3}s`
|
|
1162
1239
|
});
|
|
1163
|
-
},
|
|
1240
|
+
}, timeoutMs) : void 0;
|
|
1164
1241
|
let stdoutBuf = "";
|
|
1165
1242
|
let stderrBuf = "";
|
|
1166
1243
|
child.stdout.on("data", (chunk) => {
|
|
@@ -1204,19 +1281,19 @@ var GithubCopilotCliExecutor = class {
|
|
|
1204
1281
|
};
|
|
1205
1282
|
|
|
1206
1283
|
// src/scheduled-tasks/executors/HttpRequestExecutor.ts
|
|
1207
|
-
var
|
|
1284
|
+
var DEFAULT_TIMEOUT_MS3 = 3e4;
|
|
1208
1285
|
var HttpRequestExecutor = class {
|
|
1209
1286
|
async execute(payload, abortSignal) {
|
|
1210
1287
|
const p = payload;
|
|
1211
|
-
const
|
|
1288
|
+
const timeoutMs = resolveConfiguredTimeoutMs(p.timeout, DEFAULT_TIMEOUT_MS3);
|
|
1212
1289
|
const ac = new AbortController();
|
|
1213
|
-
let
|
|
1214
|
-
const timer = setTimeout(() => {
|
|
1215
|
-
|
|
1290
|
+
let timedOut = false;
|
|
1291
|
+
const timer = timeoutMs != null ? setTimeout(() => {
|
|
1292
|
+
timedOut = true;
|
|
1216
1293
|
ac.abort();
|
|
1217
|
-
},
|
|
1294
|
+
}, timeoutMs) : void 0;
|
|
1218
1295
|
if (abortSignal?.aborted) {
|
|
1219
|
-
clearTimeout(timer);
|
|
1296
|
+
if (timer) clearTimeout(timer);
|
|
1220
1297
|
return { status: "cancelled", error: "Execution aborted" };
|
|
1221
1298
|
}
|
|
1222
1299
|
let externalAbort = false;
|
|
@@ -1224,7 +1301,7 @@ var HttpRequestExecutor = class {
|
|
|
1224
1301
|
"abort",
|
|
1225
1302
|
() => {
|
|
1226
1303
|
externalAbort = true;
|
|
1227
|
-
clearTimeout(timer);
|
|
1304
|
+
if (timer) clearTimeout(timer);
|
|
1228
1305
|
ac.abort();
|
|
1229
1306
|
},
|
|
1230
1307
|
{ once: true }
|
|
@@ -1236,7 +1313,7 @@ var HttpRequestExecutor = class {
|
|
|
1236
1313
|
body: p.body,
|
|
1237
1314
|
signal: ac.signal
|
|
1238
1315
|
});
|
|
1239
|
-
clearTimeout(timer);
|
|
1316
|
+
if (timer) clearTimeout(timer);
|
|
1240
1317
|
const body = await response.text();
|
|
1241
1318
|
if (response.ok) {
|
|
1242
1319
|
return {
|
|
@@ -1251,14 +1328,17 @@ ${body}`.trim()
|
|
|
1251
1328
|
${body}`.trim()
|
|
1252
1329
|
};
|
|
1253
1330
|
} catch (err) {
|
|
1254
|
-
clearTimeout(timer);
|
|
1331
|
+
if (timer) clearTimeout(timer);
|
|
1255
1332
|
if (err instanceof Error && err.name === "AbortError") {
|
|
1256
1333
|
if (externalAbort) {
|
|
1257
1334
|
return { status: "cancelled", error: "Execution cancelled" };
|
|
1258
1335
|
}
|
|
1336
|
+
if (!timedOut || timeoutMs == null) {
|
|
1337
|
+
return { status: "failure", error: err.message };
|
|
1338
|
+
}
|
|
1259
1339
|
return {
|
|
1260
1340
|
status: "timeout",
|
|
1261
|
-
error: `HTTP request timed out after ${
|
|
1341
|
+
error: `HTTP request timed out after ${timeoutMs / 1e3}s`
|
|
1262
1342
|
};
|
|
1263
1343
|
}
|
|
1264
1344
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1269,8 +1349,82 @@ ${body}`.trim()
|
|
|
1269
1349
|
|
|
1270
1350
|
// src/scheduled-tasks/executors/ShellExecutor.ts
|
|
1271
1351
|
import { spawn as spawn3 } from "child_process";
|
|
1272
|
-
|
|
1352
|
+
import * as fs6 from "fs";
|
|
1353
|
+
import * as path5 from "path";
|
|
1273
1354
|
var MAX_OUTPUT_BYTES2 = 1024 * 1024;
|
|
1355
|
+
var DEFAULT_TIMEOUT_MS4 = 6e4;
|
|
1356
|
+
var POSIX_SHELL_CANDIDATES = ["bash.exe", "sh.exe"];
|
|
1357
|
+
function resolveShellExecution(script, options = {}) {
|
|
1358
|
+
const platform = options.platform ?? process.platform;
|
|
1359
|
+
const env = options.env ?? process.env;
|
|
1360
|
+
const fileExists = options.fileExists ?? fs6.existsSync;
|
|
1361
|
+
if (platform === "win32") {
|
|
1362
|
+
const posixShell = usesPosixShellSyntax(script) ? findWindowsPosixShell(env, fileExists) : null;
|
|
1363
|
+
if (posixShell) {
|
|
1364
|
+
return {
|
|
1365
|
+
command: posixShell,
|
|
1366
|
+
args: ["-s"],
|
|
1367
|
+
stdinScript: script,
|
|
1368
|
+
outputEncoding: "utf8",
|
|
1369
|
+
shellKind: "posix"
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
return {
|
|
1373
|
+
command: env.ComSpec ?? env.COMSPEC ?? "cmd.exe",
|
|
1374
|
+
args: ["/d", "/s", "/c", script],
|
|
1375
|
+
outputEncoding: "utf8",
|
|
1376
|
+
shellKind: "cmd"
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
return {
|
|
1380
|
+
command: "sh",
|
|
1381
|
+
args: ["-s"],
|
|
1382
|
+
stdinScript: script,
|
|
1383
|
+
outputEncoding: "utf8",
|
|
1384
|
+
shellKind: "posix"
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
function usesPosixShellSyntax(script) {
|
|
1388
|
+
return /\$\([^)]*\)|`[^`]*`/.test(script);
|
|
1389
|
+
}
|
|
1390
|
+
function findWindowsPosixShell(env, fileExists) {
|
|
1391
|
+
const explicitShell = env.SERVICEME_POSIX_SHELL;
|
|
1392
|
+
if (explicitShell && fileExists(explicitShell)) {
|
|
1393
|
+
return explicitShell;
|
|
1394
|
+
}
|
|
1395
|
+
const knownGitBashPaths = [
|
|
1396
|
+
"C:\\Program Files\\Git\\bin\\bash.exe",
|
|
1397
|
+
"C:\\Program Files\\Git\\usr\\bin\\bash.exe",
|
|
1398
|
+
"C:\\Program Files (x86)\\Git\\bin\\bash.exe",
|
|
1399
|
+
"C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe"
|
|
1400
|
+
];
|
|
1401
|
+
for (const candidate of knownGitBashPaths) {
|
|
1402
|
+
if (fileExists(candidate)) return candidate;
|
|
1403
|
+
}
|
|
1404
|
+
const pathValue = env.Path ?? env.PATH ?? "";
|
|
1405
|
+
for (const dir of pathValue.split(path5.win32.delimiter)) {
|
|
1406
|
+
if (!dir) continue;
|
|
1407
|
+
for (const executable of POSIX_SHELL_CANDIDATES) {
|
|
1408
|
+
const candidate = path5.win32.join(dir, executable);
|
|
1409
|
+
if (fileExists(candidate) && !isWindowsWslLauncher(candidate)) {
|
|
1410
|
+
return candidate;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
return null;
|
|
1415
|
+
}
|
|
1416
|
+
function isWindowsWslLauncher(candidate) {
|
|
1417
|
+
const normalized = path5.win32.normalize(candidate).toLowerCase();
|
|
1418
|
+
return normalized.endsWith("\\windows\\system32\\bash.exe") || normalized.endsWith("\\windows\\syswow64\\bash.exe");
|
|
1419
|
+
}
|
|
1420
|
+
function writeDiagnostic2(message) {
|
|
1421
|
+
const logPath = process.env.SERVICEME_SCHEDULER_LOG_PATH;
|
|
1422
|
+
if (logPath) {
|
|
1423
|
+
fs6.appendFileSync(logPath, message);
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
process.stderr.write(message);
|
|
1427
|
+
}
|
|
1274
1428
|
var ShellExecutor = class {
|
|
1275
1429
|
async execute(payload, abortSignal) {
|
|
1276
1430
|
let output = "";
|
|
@@ -1286,7 +1440,7 @@ var ShellExecutor = class {
|
|
|
1286
1440
|
}
|
|
1287
1441
|
executeStreaming(payload, onOutput, abortSignal) {
|
|
1288
1442
|
const p = payload;
|
|
1289
|
-
const
|
|
1443
|
+
const timeoutMs = resolveConfiguredTimeoutMs(p.timeout, DEFAULT_TIMEOUT_MS4);
|
|
1290
1444
|
let resolve;
|
|
1291
1445
|
const resultPromise = new Promise((r) => {
|
|
1292
1446
|
resolve = r;
|
|
@@ -1295,7 +1449,7 @@ var ShellExecutor = class {
|
|
|
1295
1449
|
const settle = (result) => {
|
|
1296
1450
|
if (settled) return;
|
|
1297
1451
|
settled = true;
|
|
1298
|
-
clearTimeout(timer);
|
|
1452
|
+
if (timer) clearTimeout(timer);
|
|
1299
1453
|
resolve?.(result);
|
|
1300
1454
|
};
|
|
1301
1455
|
if (abortSignal?.aborted) {
|
|
@@ -1308,37 +1462,65 @@ var ShellExecutor = class {
|
|
|
1308
1462
|
}
|
|
1309
1463
|
};
|
|
1310
1464
|
}
|
|
1311
|
-
const
|
|
1465
|
+
const shellExecution = resolveShellExecution(p.script);
|
|
1466
|
+
const spawnOpts = {
|
|
1312
1467
|
cwd: p.cwd,
|
|
1313
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1314
|
-
|
|
1315
|
-
|
|
1468
|
+
stdio: [shellExecution.stdinScript ? "pipe" : "ignore", "pipe", "pipe"],
|
|
1469
|
+
windowsHide: true
|
|
1470
|
+
};
|
|
1471
|
+
writeDiagnostic2(
|
|
1472
|
+
`[ShellExecutor] spawn:
|
|
1473
|
+
platform=${process.platform}
|
|
1474
|
+
shell=${shellExecution.shellKind}
|
|
1475
|
+
command=${shellExecution.command}
|
|
1476
|
+
args=${JSON.stringify(shellExecution.args.filter((a) => a !== p.script))}
|
|
1477
|
+
stdin=${shellExecution.stdinScript ? "true" : "false"}
|
|
1478
|
+
cwd=${p.cwd ?? "(default)"}
|
|
1479
|
+
timeout=${timeoutMs != null ? `${timeoutMs}ms` : "unlimited"}
|
|
1480
|
+
scriptLen=${p.script.length}
|
|
1481
|
+
windowsHide=true
|
|
1482
|
+
daemonPid=${process.pid}
|
|
1483
|
+
`
|
|
1484
|
+
);
|
|
1485
|
+
const child = spawn3(shellExecution.command, shellExecution.args, spawnOpts);
|
|
1486
|
+
if (shellExecution.stdinScript && child.stdin) {
|
|
1487
|
+
child.stdin.end(shellExecution.stdinScript);
|
|
1488
|
+
}
|
|
1489
|
+
if (child.pid) {
|
|
1490
|
+
writeDiagnostic2(`[ShellExecutor] spawned child PID=${child.pid}
|
|
1491
|
+
`);
|
|
1492
|
+
}
|
|
1493
|
+
const timer = timeoutMs != null ? setTimeout(() => {
|
|
1316
1494
|
child.kill("SIGTERM");
|
|
1317
1495
|
setTimeout(() => {
|
|
1318
1496
|
if (!child.killed) child.kill("SIGKILL");
|
|
1319
1497
|
}, 5e3);
|
|
1320
1498
|
settle({
|
|
1321
1499
|
status: "timeout",
|
|
1322
|
-
error: `Shell execution timed out after ${
|
|
1500
|
+
error: `Shell execution timed out after ${timeoutMs / 1e3}s`
|
|
1323
1501
|
});
|
|
1324
|
-
},
|
|
1502
|
+
}, timeoutMs) : void 0;
|
|
1325
1503
|
let stdoutBuf = "";
|
|
1326
1504
|
let stderrBuf = "";
|
|
1327
|
-
child.stdout
|
|
1328
|
-
const data = chunk.toString();
|
|
1505
|
+
child.stdout?.on("data", (chunk) => {
|
|
1506
|
+
const data = chunk.toString(shellExecution.outputEncoding);
|
|
1329
1507
|
stdoutBuf += data;
|
|
1330
1508
|
if (stdoutBuf.length > MAX_OUTPUT_BYTES2)
|
|
1331
1509
|
stdoutBuf = stdoutBuf.slice(-MAX_OUTPUT_BYTES2);
|
|
1332
1510
|
onOutput("stdout", data);
|
|
1333
1511
|
});
|
|
1334
|
-
child.stderr
|
|
1335
|
-
const data = chunk.toString();
|
|
1512
|
+
child.stderr?.on("data", (chunk) => {
|
|
1513
|
+
const data = chunk.toString(shellExecution.outputEncoding);
|
|
1336
1514
|
stderrBuf += data;
|
|
1337
1515
|
if (stderrBuf.length > MAX_OUTPUT_BYTES2)
|
|
1338
1516
|
stderrBuf = stderrBuf.slice(-MAX_OUTPUT_BYTES2);
|
|
1339
1517
|
onOutput("stderr", data);
|
|
1340
1518
|
});
|
|
1341
1519
|
child.on("close", (code) => {
|
|
1520
|
+
writeDiagnostic2(
|
|
1521
|
+
`[ShellExecutor] child PID=${child.pid} exited: code=${code}
|
|
1522
|
+
`
|
|
1523
|
+
);
|
|
1342
1524
|
if (code === 0) {
|
|
1343
1525
|
settle({ status: "success", output: stdoutBuf || void 0 });
|
|
1344
1526
|
} else {
|
|
@@ -1385,36 +1567,65 @@ function getExecutor(taskType) {
|
|
|
1385
1567
|
|
|
1386
1568
|
// src/scheduled-tasks/TaskConfigManager.ts
|
|
1387
1569
|
import { randomUUID } from "crypto";
|
|
1388
|
-
import * as
|
|
1389
|
-
import * as
|
|
1570
|
+
import * as fs7 from "fs";
|
|
1571
|
+
import * as path6 from "path";
|
|
1572
|
+
import { createServicemeError as createServicemeError7 } from "@serviceme/devtools-protocol";
|
|
1390
1573
|
var CONFIG_DIR3 = ".serviceme";
|
|
1391
1574
|
var CONFIG_FILE = "scheduled-tasks.json";
|
|
1392
1575
|
function emptyConfig() {
|
|
1393
1576
|
return { version: 1, tasks: [] };
|
|
1394
1577
|
}
|
|
1578
|
+
function isRecord(value) {
|
|
1579
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1580
|
+
}
|
|
1581
|
+
function requireNonEmptyString(payload, field, taskType) {
|
|
1582
|
+
if (!isRecord(payload) || typeof payload[field] !== "string" || !payload[field].trim()) {
|
|
1583
|
+
throw createServicemeError7(
|
|
1584
|
+
"invalid_payload",
|
|
1585
|
+
`${taskType} payload ${field} is required`
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
function validateTaskPayload(taskType, payload) {
|
|
1590
|
+
switch (taskType) {
|
|
1591
|
+
case "command":
|
|
1592
|
+
requireNonEmptyString(payload, "command", taskType);
|
|
1593
|
+
return;
|
|
1594
|
+
case "shell":
|
|
1595
|
+
requireNonEmptyString(payload, "script", taskType);
|
|
1596
|
+
return;
|
|
1597
|
+
case "http_request":
|
|
1598
|
+
requireNonEmptyString(payload, "url", taskType);
|
|
1599
|
+
requireNonEmptyString(payload, "method", taskType);
|
|
1600
|
+
return;
|
|
1601
|
+
case "github_copilot_cli":
|
|
1602
|
+
requireNonEmptyString(payload, "prompt", taskType);
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1395
1606
|
var TaskConfigManager = class {
|
|
1396
1607
|
constructor(workspacePath) {
|
|
1397
|
-
this.configPath =
|
|
1608
|
+
this.configPath = path6.join(workspacePath, CONFIG_DIR3, CONFIG_FILE);
|
|
1398
1609
|
}
|
|
1399
1610
|
getConfigPath() {
|
|
1400
1611
|
return this.configPath;
|
|
1401
1612
|
}
|
|
1402
1613
|
readConfig() {
|
|
1403
|
-
if (!
|
|
1614
|
+
if (!fs7.existsSync(this.configPath)) {
|
|
1404
1615
|
return emptyConfig();
|
|
1405
1616
|
}
|
|
1406
|
-
const raw =
|
|
1617
|
+
const raw = fs7.readFileSync(this.configPath, "utf-8");
|
|
1407
1618
|
const parsed = JSON.parse(raw);
|
|
1408
1619
|
return parsed;
|
|
1409
1620
|
}
|
|
1410
1621
|
writeConfig(config) {
|
|
1411
|
-
const dir =
|
|
1412
|
-
if (!
|
|
1413
|
-
|
|
1622
|
+
const dir = path6.dirname(this.configPath);
|
|
1623
|
+
if (!fs7.existsSync(dir)) {
|
|
1624
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
1414
1625
|
}
|
|
1415
1626
|
const tmp = `${this.configPath}.tmp`;
|
|
1416
|
-
|
|
1417
|
-
|
|
1627
|
+
fs7.writeFileSync(tmp, JSON.stringify(config, null, " "), "utf-8");
|
|
1628
|
+
fs7.renameSync(tmp, this.configPath);
|
|
1418
1629
|
}
|
|
1419
1630
|
listTasks() {
|
|
1420
1631
|
return this.readConfig().tasks;
|
|
@@ -1426,6 +1637,7 @@ var TaskConfigManager = class {
|
|
|
1426
1637
|
return this.readConfig().tasks.find((t) => t.name === name);
|
|
1427
1638
|
}
|
|
1428
1639
|
createTask(input) {
|
|
1640
|
+
validateTaskPayload(input.taskType, input.payload);
|
|
1429
1641
|
const config = this.readConfig();
|
|
1430
1642
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1431
1643
|
const task = {
|
|
@@ -1454,6 +1666,9 @@ var TaskConfigManager = class {
|
|
|
1454
1666
|
if (!existing) {
|
|
1455
1667
|
throw new Error(`Task '${id}' not found`);
|
|
1456
1668
|
}
|
|
1669
|
+
const nextTaskType = input.taskType ?? existing.taskType;
|
|
1670
|
+
const nextPayload = input.payload ?? existing.payload;
|
|
1671
|
+
validateTaskPayload(nextTaskType, nextPayload);
|
|
1457
1672
|
const updated = {
|
|
1458
1673
|
...existing,
|
|
1459
1674
|
...input.name !== void 0 && { name: input.name },
|
|
@@ -1492,10 +1707,197 @@ var TaskConfigManager = class {
|
|
|
1492
1707
|
}
|
|
1493
1708
|
};
|
|
1494
1709
|
|
|
1710
|
+
// src/scheduled-tasks/TaskExecutionEngine.ts
|
|
1711
|
+
function hasNonEmptyString(value) {
|
|
1712
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
1713
|
+
}
|
|
1714
|
+
function resolveTaskExecutionPayload(taskType, payload, workspacePath) {
|
|
1715
|
+
if (!hasNonEmptyString(workspacePath)) {
|
|
1716
|
+
return payload;
|
|
1717
|
+
}
|
|
1718
|
+
if (taskType === "shell") {
|
|
1719
|
+
const shellPayload = payload;
|
|
1720
|
+
if (hasNonEmptyString(shellPayload.cwd)) {
|
|
1721
|
+
return shellPayload;
|
|
1722
|
+
}
|
|
1723
|
+
return { ...shellPayload, cwd: workspacePath };
|
|
1724
|
+
}
|
|
1725
|
+
if (taskType === "github_copilot_cli") {
|
|
1726
|
+
const copilotPayload = payload;
|
|
1727
|
+
if (hasNonEmptyString(copilotPayload.workspace)) {
|
|
1728
|
+
return copilotPayload;
|
|
1729
|
+
}
|
|
1730
|
+
return { ...copilotPayload, workspace: workspacePath };
|
|
1731
|
+
}
|
|
1732
|
+
return payload;
|
|
1733
|
+
}
|
|
1734
|
+
var TaskExecutionEngine = class {
|
|
1735
|
+
constructor(getExecutor2) {
|
|
1736
|
+
this.getExecutor = getExecutor2;
|
|
1737
|
+
this.running = /* @__PURE__ */ new Map();
|
|
1738
|
+
this.taskExecutions = /* @__PURE__ */ new Map();
|
|
1739
|
+
this.listener = null;
|
|
1740
|
+
}
|
|
1741
|
+
setListener(listener) {
|
|
1742
|
+
this.listener = listener;
|
|
1743
|
+
}
|
|
1744
|
+
async execute(snapshot) {
|
|
1745
|
+
const policy = snapshot.concurrencyPolicy ?? "reject";
|
|
1746
|
+
if (policy === "reject") {
|
|
1747
|
+
const existingIds = this.taskExecutions.get(snapshot.taskId);
|
|
1748
|
+
if (existingIds && existingIds.size > 0) {
|
|
1749
|
+
throw new Error(
|
|
1750
|
+
`TASK_ALREADY_RUNNING: Task ${snapshot.taskId} is already running`
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1755
|
+
if (!this.taskExecutions.has(snapshot.taskId)) {
|
|
1756
|
+
this.taskExecutions.set(snapshot.taskId, /* @__PURE__ */ new Set());
|
|
1757
|
+
}
|
|
1758
|
+
this.taskExecutions.get(snapshot.taskId)?.add(snapshot.executionId);
|
|
1759
|
+
const ac = new AbortController();
|
|
1760
|
+
const runningExec = {
|
|
1761
|
+
executionId: snapshot.executionId,
|
|
1762
|
+
taskId: snapshot.taskId,
|
|
1763
|
+
startedAt,
|
|
1764
|
+
cancel: () => ac.abort()
|
|
1765
|
+
};
|
|
1766
|
+
this.running.set(snapshot.executionId, runningExec);
|
|
1767
|
+
this.listener?.onStarted({
|
|
1768
|
+
executionId: snapshot.executionId,
|
|
1769
|
+
taskId: snapshot.taskId,
|
|
1770
|
+
startedAt
|
|
1771
|
+
});
|
|
1772
|
+
try {
|
|
1773
|
+
const executionPayload = resolveTaskExecutionPayload(
|
|
1774
|
+
snapshot.type,
|
|
1775
|
+
snapshot.payload,
|
|
1776
|
+
snapshot.workspacePath
|
|
1777
|
+
);
|
|
1778
|
+
validateTaskPayload(snapshot.type, executionPayload);
|
|
1779
|
+
const executor = this.getExecutor(snapshot.type);
|
|
1780
|
+
let result;
|
|
1781
|
+
if (isStreamingTaskExecutor(executor)) {
|
|
1782
|
+
const handle = executor.executeStreaming(
|
|
1783
|
+
executionPayload,
|
|
1784
|
+
(stream, data) => {
|
|
1785
|
+
this.listener?.onOutput({
|
|
1786
|
+
executionId: snapshot.executionId,
|
|
1787
|
+
stream,
|
|
1788
|
+
data
|
|
1789
|
+
});
|
|
1790
|
+
},
|
|
1791
|
+
ac.signal
|
|
1792
|
+
);
|
|
1793
|
+
runningExec.cancel = () => {
|
|
1794
|
+
handle.cancel();
|
|
1795
|
+
ac.abort();
|
|
1796
|
+
};
|
|
1797
|
+
result = await handle.result;
|
|
1798
|
+
} else {
|
|
1799
|
+
result = await executor.execute(executionPayload, ac.signal);
|
|
1800
|
+
}
|
|
1801
|
+
const log = {
|
|
1802
|
+
id: snapshot.executionId,
|
|
1803
|
+
taskId: snapshot.taskId,
|
|
1804
|
+
taskName: snapshot.name,
|
|
1805
|
+
startedAt,
|
|
1806
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1807
|
+
status: result.status === "running" ? "failure" : result.status,
|
|
1808
|
+
output: result.output,
|
|
1809
|
+
error: result.error
|
|
1810
|
+
};
|
|
1811
|
+
switch (log.status) {
|
|
1812
|
+
case "success":
|
|
1813
|
+
this.listener?.onCompleted({
|
|
1814
|
+
executionId: snapshot.executionId,
|
|
1815
|
+
log
|
|
1816
|
+
});
|
|
1817
|
+
break;
|
|
1818
|
+
case "cancelled":
|
|
1819
|
+
this.listener?.onCancelled({
|
|
1820
|
+
executionId: snapshot.executionId,
|
|
1821
|
+
log
|
|
1822
|
+
});
|
|
1823
|
+
break;
|
|
1824
|
+
case "timeout":
|
|
1825
|
+
this.listener?.onFailed({
|
|
1826
|
+
executionId: snapshot.executionId,
|
|
1827
|
+
status: "timeout",
|
|
1828
|
+
reason: "timeout",
|
|
1829
|
+
log
|
|
1830
|
+
});
|
|
1831
|
+
break;
|
|
1832
|
+
default:
|
|
1833
|
+
this.listener?.onFailed({
|
|
1834
|
+
executionId: snapshot.executionId,
|
|
1835
|
+
status: "failure",
|
|
1836
|
+
reason: "error",
|
|
1837
|
+
log
|
|
1838
|
+
});
|
|
1839
|
+
break;
|
|
1840
|
+
}
|
|
1841
|
+
} catch (err) {
|
|
1842
|
+
const log = {
|
|
1843
|
+
id: snapshot.executionId,
|
|
1844
|
+
taskId: snapshot.taskId,
|
|
1845
|
+
taskName: snapshot.name,
|
|
1846
|
+
startedAt,
|
|
1847
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1848
|
+
status: ac.signal.aborted ? "cancelled" : "failure",
|
|
1849
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1850
|
+
};
|
|
1851
|
+
if (ac.signal.aborted) {
|
|
1852
|
+
this.listener?.onCancelled({
|
|
1853
|
+
executionId: snapshot.executionId,
|
|
1854
|
+
log
|
|
1855
|
+
});
|
|
1856
|
+
} else {
|
|
1857
|
+
this.listener?.onFailed({
|
|
1858
|
+
executionId: snapshot.executionId,
|
|
1859
|
+
status: "failure",
|
|
1860
|
+
reason: "error",
|
|
1861
|
+
log
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
} finally {
|
|
1865
|
+
this.running.delete(snapshot.executionId);
|
|
1866
|
+
const taskExecIds = this.taskExecutions.get(snapshot.taskId);
|
|
1867
|
+
if (taskExecIds) {
|
|
1868
|
+
taskExecIds.delete(snapshot.executionId);
|
|
1869
|
+
if (taskExecIds.size === 0) {
|
|
1870
|
+
this.taskExecutions.delete(snapshot.taskId);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
cancel(executionId) {
|
|
1876
|
+
const exec = this.running.get(executionId);
|
|
1877
|
+
if (!exec) return false;
|
|
1878
|
+
exec.cancel();
|
|
1879
|
+
return true;
|
|
1880
|
+
}
|
|
1881
|
+
listRunning() {
|
|
1882
|
+
return Array.from(this.running.values()).map((e) => ({
|
|
1883
|
+
executionId: e.executionId,
|
|
1884
|
+
taskId: e.taskId,
|
|
1885
|
+
startedAt: e.startedAt
|
|
1886
|
+
}));
|
|
1887
|
+
}
|
|
1888
|
+
dispose() {
|
|
1889
|
+
for (const exec of this.running.values()) {
|
|
1890
|
+
exec.cancel();
|
|
1891
|
+
}
|
|
1892
|
+
this.running.clear();
|
|
1893
|
+
this.taskExecutions.clear();
|
|
1894
|
+
}
|
|
1895
|
+
};
|
|
1896
|
+
|
|
1495
1897
|
// src/scheduled-tasks/TaskLogManager.ts
|
|
1496
1898
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1497
|
-
import * as
|
|
1498
|
-
import * as
|
|
1899
|
+
import * as fs8 from "fs";
|
|
1900
|
+
import * as path7 from "path";
|
|
1499
1901
|
var CONFIG_DIR4 = ".serviceme";
|
|
1500
1902
|
var LOG_FILE2 = "scheduled-tasks-log.json";
|
|
1501
1903
|
var MAX_LOGS = 200;
|
|
@@ -1520,17 +1922,17 @@ function validateAndRepairLogFile(raw) {
|
|
|
1520
1922
|
}
|
|
1521
1923
|
var TaskLogManager = class {
|
|
1522
1924
|
constructor(workspacePath) {
|
|
1523
|
-
this.logPath =
|
|
1925
|
+
this.logPath = path7.join(workspacePath, CONFIG_DIR4, LOG_FILE2);
|
|
1524
1926
|
}
|
|
1525
1927
|
getLogPath() {
|
|
1526
1928
|
return this.logPath;
|
|
1527
1929
|
}
|
|
1528
1930
|
readLogFile() {
|
|
1529
|
-
if (!
|
|
1931
|
+
if (!fs8.existsSync(this.logPath)) {
|
|
1530
1932
|
return emptyLogFile();
|
|
1531
1933
|
}
|
|
1532
1934
|
try {
|
|
1533
|
-
const raw =
|
|
1935
|
+
const raw = fs8.readFileSync(this.logPath, "utf-8");
|
|
1534
1936
|
const parsed = JSON.parse(raw);
|
|
1535
1937
|
const file = validateAndRepairLogFile(parsed);
|
|
1536
1938
|
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.logs) || parsed.logs.length !== file.logs.length) {
|
|
@@ -1546,21 +1948,21 @@ var TaskLogManager = class {
|
|
|
1546
1948
|
}
|
|
1547
1949
|
backupCorruptedFile() {
|
|
1548
1950
|
try {
|
|
1549
|
-
if (
|
|
1951
|
+
if (fs8.existsSync(this.logPath)) {
|
|
1550
1952
|
const backupPath = `${this.logPath}.corrupted.${Date.now()}`;
|
|
1551
|
-
|
|
1953
|
+
fs8.copyFileSync(this.logPath, backupPath);
|
|
1552
1954
|
}
|
|
1553
1955
|
} catch {
|
|
1554
1956
|
}
|
|
1555
1957
|
}
|
|
1556
1958
|
writeLogFile(file) {
|
|
1557
|
-
const dir =
|
|
1558
|
-
if (!
|
|
1559
|
-
|
|
1959
|
+
const dir = path7.dirname(this.logPath);
|
|
1960
|
+
if (!fs8.existsSync(dir)) {
|
|
1961
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
1560
1962
|
}
|
|
1561
1963
|
const tmp = `${this.logPath}.tmp`;
|
|
1562
|
-
|
|
1563
|
-
|
|
1964
|
+
fs8.writeFileSync(tmp, JSON.stringify(file, null, " "), "utf-8");
|
|
1965
|
+
fs8.renameSync(tmp, this.logPath);
|
|
1564
1966
|
}
|
|
1565
1967
|
appendLog(input) {
|
|
1566
1968
|
const file = this.readLogFile();
|
|
@@ -1630,7 +2032,23 @@ var SchedulerDaemon = class {
|
|
|
1630
2032
|
this.running = true;
|
|
1631
2033
|
this.startTime = Date.now();
|
|
1632
2034
|
this.pidManager.writePid(process.pid);
|
|
1633
|
-
this.
|
|
2035
|
+
const configPath = this.configManager.getConfigPath();
|
|
2036
|
+
const logPath = this.logger.getLogPath();
|
|
2037
|
+
this.logger.log("info", "=".repeat(60));
|
|
2038
|
+
this.logger.log(
|
|
2039
|
+
"info",
|
|
2040
|
+
`Scheduler daemon started
|
|
2041
|
+
PID: ${process.pid}
|
|
2042
|
+
Node: ${process.version}
|
|
2043
|
+
OS: ${os.type()} ${os.release()} (${process.arch})
|
|
2044
|
+
Platform: ${process.platform}
|
|
2045
|
+
Hostname: ${os.hostname()}
|
|
2046
|
+
User: ${os.userInfo().username}
|
|
2047
|
+
Workspace: ${this.workspacePath}
|
|
2048
|
+
Config: ${configPath}
|
|
2049
|
+
LogPath: ${logPath}
|
|
2050
|
+
ExecPath: ${process.execPath}`
|
|
2051
|
+
);
|
|
1634
2052
|
this.tickTimer = setInterval(() => this.tick(), TICK_INTERVAL);
|
|
1635
2053
|
this.setupConfigWatch();
|
|
1636
2054
|
process.on("SIGTERM", () => this.stop());
|
|
@@ -1655,8 +2073,8 @@ var SchedulerDaemon = class {
|
|
|
1655
2073
|
const configPath = this.configManager.getConfigPath();
|
|
1656
2074
|
const dir = configPath.substring(0, configPath.lastIndexOf("/"));
|
|
1657
2075
|
try {
|
|
1658
|
-
if (
|
|
1659
|
-
this.watcher =
|
|
2076
|
+
if (fs9.existsSync(dir)) {
|
|
2077
|
+
this.watcher = fs9.watch(dir, (_eventType, filename) => {
|
|
1660
2078
|
if (filename === "scheduled-tasks.json") {
|
|
1661
2079
|
this.logger.log("info", "Config file changed, reconciling...");
|
|
1662
2080
|
}
|
|
@@ -1672,7 +2090,11 @@ var SchedulerDaemon = class {
|
|
|
1672
2090
|
for (const task of config.tasks) {
|
|
1673
2091
|
if (!task.enabled) continue;
|
|
1674
2092
|
if (this.taskRunning.has(task.id)) continue;
|
|
1675
|
-
|
|
2093
|
+
let lastExec = this.lastRun.get(task.id);
|
|
2094
|
+
if (lastExec === void 0) {
|
|
2095
|
+
this.lastRun.set(task.id, now);
|
|
2096
|
+
lastExec = now;
|
|
2097
|
+
}
|
|
1676
2098
|
if (this.shouldRun(task, lastExec, now)) {
|
|
1677
2099
|
this.executeTask(task, now);
|
|
1678
2100
|
}
|
|
@@ -1694,11 +2116,25 @@ var SchedulerDaemon = class {
|
|
|
1694
2116
|
this.taskRunning.add(task.id);
|
|
1695
2117
|
this.lastRun.set(task.id, now);
|
|
1696
2118
|
const startedAt = new Date(now).toISOString();
|
|
1697
|
-
|
|
2119
|
+
const startMs = Date.now();
|
|
2120
|
+
this.logger.log(
|
|
2121
|
+
"info",
|
|
2122
|
+
`Executing task: ${task.name} (${task.id})
|
|
2123
|
+
type: ${task.taskType}
|
|
2124
|
+
schedule: ${task.schedule} (${task.scheduleType})
|
|
2125
|
+
enabled: ${task.enabled}`
|
|
2126
|
+
);
|
|
1698
2127
|
try {
|
|
2128
|
+
const executionPayload = resolveTaskExecutionPayload(
|
|
2129
|
+
task.taskType,
|
|
2130
|
+
task.payload,
|
|
2131
|
+
this.workspacePath
|
|
2132
|
+
);
|
|
2133
|
+
validateTaskPayload(task.taskType, executionPayload);
|
|
1699
2134
|
const executor = getExecutor(task.taskType);
|
|
1700
|
-
const result = await executor.execute(
|
|
2135
|
+
const result = await executor.execute(executionPayload);
|
|
1701
2136
|
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2137
|
+
const durationMs = Date.now() - startMs;
|
|
1702
2138
|
this.logManager.appendLog({
|
|
1703
2139
|
taskId: task.id,
|
|
1704
2140
|
taskName: task.name,
|
|
@@ -1708,9 +2144,17 @@ var SchedulerDaemon = class {
|
|
|
1708
2144
|
output: result.output,
|
|
1709
2145
|
error: result.error
|
|
1710
2146
|
});
|
|
1711
|
-
|
|
2147
|
+
const outputBytes = result.output ? Buffer.byteLength(result.output) : 0;
|
|
2148
|
+
this.logger.log(
|
|
2149
|
+
"info",
|
|
2150
|
+
`Task completed: ${task.name} (${task.id})
|
|
2151
|
+
status: ${result.status}
|
|
2152
|
+
duration: ${durationMs}ms
|
|
2153
|
+
outputBytes: ${outputBytes}`
|
|
2154
|
+
);
|
|
1712
2155
|
} catch (err) {
|
|
1713
2156
|
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2157
|
+
const durationMs = Date.now() - startMs;
|
|
1714
2158
|
const message = err instanceof Error ? err.message : String(err);
|
|
1715
2159
|
this.logManager.appendLog({
|
|
1716
2160
|
taskId: task.id,
|
|
@@ -1720,7 +2164,12 @@ var SchedulerDaemon = class {
|
|
|
1720
2164
|
status: "failure",
|
|
1721
2165
|
error: message
|
|
1722
2166
|
});
|
|
1723
|
-
this.logger.log(
|
|
2167
|
+
this.logger.log(
|
|
2168
|
+
"error",
|
|
2169
|
+
`Task failed: ${task.name} (${task.id})
|
|
2170
|
+
duration: ${durationMs}ms
|
|
2171
|
+
error: ${message}`
|
|
2172
|
+
);
|
|
1724
2173
|
} finally {
|
|
1725
2174
|
this.taskRunning.delete(task.id);
|
|
1726
2175
|
}
|
|
@@ -1777,164 +2226,6 @@ function matchCronField(field, value) {
|
|
|
1777
2226
|
const values = field.split(",");
|
|
1778
2227
|
return values.some((v) => Number.parseInt(v, 10) === value);
|
|
1779
2228
|
}
|
|
1780
|
-
|
|
1781
|
-
// src/scheduled-tasks/TaskExecutionEngine.ts
|
|
1782
|
-
var TaskExecutionEngine = class {
|
|
1783
|
-
constructor(getExecutor2) {
|
|
1784
|
-
this.getExecutor = getExecutor2;
|
|
1785
|
-
this.running = /* @__PURE__ */ new Map();
|
|
1786
|
-
this.taskExecutions = /* @__PURE__ */ new Map();
|
|
1787
|
-
this.listener = null;
|
|
1788
|
-
}
|
|
1789
|
-
setListener(listener) {
|
|
1790
|
-
this.listener = listener;
|
|
1791
|
-
}
|
|
1792
|
-
async execute(snapshot) {
|
|
1793
|
-
const policy = snapshot.concurrencyPolicy ?? "reject";
|
|
1794
|
-
if (policy === "reject") {
|
|
1795
|
-
const existingIds = this.taskExecutions.get(snapshot.taskId);
|
|
1796
|
-
if (existingIds && existingIds.size > 0) {
|
|
1797
|
-
throw new Error(
|
|
1798
|
-
`TASK_ALREADY_RUNNING: Task ${snapshot.taskId} is already running`
|
|
1799
|
-
);
|
|
1800
|
-
}
|
|
1801
|
-
}
|
|
1802
|
-
const executor = this.getExecutor(snapshot.type);
|
|
1803
|
-
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1804
|
-
if (!this.taskExecutions.has(snapshot.taskId)) {
|
|
1805
|
-
this.taskExecutions.set(snapshot.taskId, /* @__PURE__ */ new Set());
|
|
1806
|
-
}
|
|
1807
|
-
this.taskExecutions.get(snapshot.taskId)?.add(snapshot.executionId);
|
|
1808
|
-
const ac = new AbortController();
|
|
1809
|
-
const runningExec = {
|
|
1810
|
-
executionId: snapshot.executionId,
|
|
1811
|
-
taskId: snapshot.taskId,
|
|
1812
|
-
startedAt,
|
|
1813
|
-
cancel: () => ac.abort()
|
|
1814
|
-
};
|
|
1815
|
-
this.running.set(snapshot.executionId, runningExec);
|
|
1816
|
-
this.listener?.onStarted({
|
|
1817
|
-
executionId: snapshot.executionId,
|
|
1818
|
-
taskId: snapshot.taskId,
|
|
1819
|
-
startedAt
|
|
1820
|
-
});
|
|
1821
|
-
try {
|
|
1822
|
-
let result;
|
|
1823
|
-
if (isStreamingTaskExecutor(executor)) {
|
|
1824
|
-
const handle = executor.executeStreaming(
|
|
1825
|
-
snapshot.payload,
|
|
1826
|
-
(stream, data) => {
|
|
1827
|
-
this.listener?.onOutput({
|
|
1828
|
-
executionId: snapshot.executionId,
|
|
1829
|
-
stream,
|
|
1830
|
-
data
|
|
1831
|
-
});
|
|
1832
|
-
},
|
|
1833
|
-
ac.signal
|
|
1834
|
-
);
|
|
1835
|
-
runningExec.cancel = () => {
|
|
1836
|
-
handle.cancel();
|
|
1837
|
-
ac.abort();
|
|
1838
|
-
};
|
|
1839
|
-
result = await handle.result;
|
|
1840
|
-
} else {
|
|
1841
|
-
result = await executor.execute(snapshot.payload, ac.signal);
|
|
1842
|
-
}
|
|
1843
|
-
const log = {
|
|
1844
|
-
id: snapshot.executionId,
|
|
1845
|
-
taskId: snapshot.taskId,
|
|
1846
|
-
taskName: snapshot.name,
|
|
1847
|
-
startedAt,
|
|
1848
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1849
|
-
status: result.status === "running" ? "failure" : result.status,
|
|
1850
|
-
output: result.output,
|
|
1851
|
-
error: result.error
|
|
1852
|
-
};
|
|
1853
|
-
switch (log.status) {
|
|
1854
|
-
case "success":
|
|
1855
|
-
this.listener?.onCompleted({
|
|
1856
|
-
executionId: snapshot.executionId,
|
|
1857
|
-
log
|
|
1858
|
-
});
|
|
1859
|
-
break;
|
|
1860
|
-
case "cancelled":
|
|
1861
|
-
this.listener?.onCancelled({
|
|
1862
|
-
executionId: snapshot.executionId,
|
|
1863
|
-
log
|
|
1864
|
-
});
|
|
1865
|
-
break;
|
|
1866
|
-
case "timeout":
|
|
1867
|
-
this.listener?.onFailed({
|
|
1868
|
-
executionId: snapshot.executionId,
|
|
1869
|
-
status: "timeout",
|
|
1870
|
-
reason: "timeout",
|
|
1871
|
-
log
|
|
1872
|
-
});
|
|
1873
|
-
break;
|
|
1874
|
-
default:
|
|
1875
|
-
this.listener?.onFailed({
|
|
1876
|
-
executionId: snapshot.executionId,
|
|
1877
|
-
status: "failure",
|
|
1878
|
-
reason: "error",
|
|
1879
|
-
log
|
|
1880
|
-
});
|
|
1881
|
-
break;
|
|
1882
|
-
}
|
|
1883
|
-
} catch (err) {
|
|
1884
|
-
const log = {
|
|
1885
|
-
id: snapshot.executionId,
|
|
1886
|
-
taskId: snapshot.taskId,
|
|
1887
|
-
taskName: snapshot.name,
|
|
1888
|
-
startedAt,
|
|
1889
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1890
|
-
status: ac.signal.aborted ? "cancelled" : "failure",
|
|
1891
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1892
|
-
};
|
|
1893
|
-
if (ac.signal.aborted) {
|
|
1894
|
-
this.listener?.onCancelled({
|
|
1895
|
-
executionId: snapshot.executionId,
|
|
1896
|
-
log
|
|
1897
|
-
});
|
|
1898
|
-
} else {
|
|
1899
|
-
this.listener?.onFailed({
|
|
1900
|
-
executionId: snapshot.executionId,
|
|
1901
|
-
status: "failure",
|
|
1902
|
-
reason: "error",
|
|
1903
|
-
log
|
|
1904
|
-
});
|
|
1905
|
-
}
|
|
1906
|
-
} finally {
|
|
1907
|
-
this.running.delete(snapshot.executionId);
|
|
1908
|
-
const taskExecIds = this.taskExecutions.get(snapshot.taskId);
|
|
1909
|
-
if (taskExecIds) {
|
|
1910
|
-
taskExecIds.delete(snapshot.executionId);
|
|
1911
|
-
if (taskExecIds.size === 0) {
|
|
1912
|
-
this.taskExecutions.delete(snapshot.taskId);
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
}
|
|
1916
|
-
}
|
|
1917
|
-
cancel(executionId) {
|
|
1918
|
-
const exec = this.running.get(executionId);
|
|
1919
|
-
if (!exec) return false;
|
|
1920
|
-
exec.cancel();
|
|
1921
|
-
return true;
|
|
1922
|
-
}
|
|
1923
|
-
listRunning() {
|
|
1924
|
-
return Array.from(this.running.values()).map((e) => ({
|
|
1925
|
-
executionId: e.executionId,
|
|
1926
|
-
taskId: e.taskId,
|
|
1927
|
-
startedAt: e.startedAt
|
|
1928
|
-
}));
|
|
1929
|
-
}
|
|
1930
|
-
dispose() {
|
|
1931
|
-
for (const exec of this.running.values()) {
|
|
1932
|
-
exec.cancel();
|
|
1933
|
-
}
|
|
1934
|
-
this.running.clear();
|
|
1935
|
-
this.taskExecutions.clear();
|
|
1936
|
-
}
|
|
1937
|
-
};
|
|
1938
2229
|
export {
|
|
1939
2230
|
DaemonLogger,
|
|
1940
2231
|
EnvironmentInspector,
|
|
@@ -1963,5 +2254,7 @@ export {
|
|
|
1963
2254
|
moveFiles,
|
|
1964
2255
|
noopLogger,
|
|
1965
2256
|
parseIntervalMs,
|
|
1966
|
-
|
|
2257
|
+
resolveTaskExecutionPayload,
|
|
2258
|
+
unzipFile,
|
|
2259
|
+
validateTaskPayload
|
|
1967
2260
|
};
|