@crewx/cron 0.1.10-rc.18 → 0.1.10-rc.180
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/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +46 -53
- package/dist/cli.js.map +1 -1
- package/dist/src/builtins.d.ts +14 -0
- package/dist/src/builtins.d.ts.map +1 -0
- package/dist/src/builtins.js +71 -0
- package/dist/src/builtins.js.map +1 -0
- package/dist/src/engine.d.ts +38 -11
- package/dist/src/engine.d.ts.map +1 -1
- package/dist/src/engine.js +1079 -196
- package/dist/src/engine.js.map +1 -1
- package/dist/src/runtime.d.ts +128 -0
- package/dist/src/runtime.d.ts.map +1 -0
- package/dist/src/runtime.js +612 -0
- package/dist/src/runtime.js.map +1 -0
- package/dist/src/types.d.ts +94 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +11 -0
- package/dist/src/types.js.map +1 -1
- package/package.json +3 -3
- package/dist/tsconfig.tsbuildinfo +0 -1
package/dist/src/engine.js
CHANGED
|
@@ -33,24 +33,33 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.DATA_PATHS = void 0;
|
|
36
|
+
exports.DATA_PATHS = exports.CRON_RUNTIME_PROCESS_LEASE_FILENAME = exports.createCronRuntime = exports.getProjectRoot = exports.BUILTIN_SCHEDULES = exports.CronStoreError = void 0;
|
|
37
37
|
exports.getLocalTimestamp = getLocalTimestamp;
|
|
38
38
|
exports.logUsage = logUsage;
|
|
39
|
-
exports.
|
|
39
|
+
exports.acquireWorkspaceLock = acquireWorkspaceLock;
|
|
40
|
+
exports.releaseWorkspaceLock = releaseWorkspaceLock;
|
|
41
|
+
exports.migrateLegacyMemoryDebounceSchedule = migrateLegacyMemoryDebounceSchedule;
|
|
40
42
|
exports.normalizeMode = normalizeMode;
|
|
43
|
+
exports.createScheduleStore = createScheduleStore;
|
|
41
44
|
exports.loadSchedules = loadSchedules;
|
|
42
45
|
exports.saveSchedules = saveSchedules;
|
|
46
|
+
exports.getRemovedBuiltins = getRemovedBuiltins;
|
|
47
|
+
exports.seedBuiltinSchedules = seedBuiltinSchedules;
|
|
43
48
|
exports.acquireLock = acquireLock;
|
|
44
49
|
exports.releaseLock = releaseLock;
|
|
50
|
+
exports.releaseRunLock = releaseRunLock;
|
|
45
51
|
exports.shouldRunNow = shouldRunNow;
|
|
46
52
|
exports.describeCron = describeCron;
|
|
47
53
|
exports.calculateNextRun = calculateNextRun;
|
|
48
54
|
exports.isDaemonRunning = isDaemonRunning;
|
|
49
55
|
exports.startDaemon = startDaemon;
|
|
50
56
|
exports.stopDaemon = stopDaemon;
|
|
57
|
+
exports.startDaemonSafe = startDaemonSafe;
|
|
51
58
|
exports.ensureDaemonRunning = ensureDaemonRunning;
|
|
52
59
|
exports.getDaemonStatus = getDaemonStatus;
|
|
53
60
|
exports.executeSchedule = executeSchedule;
|
|
61
|
+
exports.getLocalMinuteKey = getLocalMinuteKey;
|
|
62
|
+
exports.buildOccurrenceKey = buildOccurrenceKey;
|
|
54
63
|
exports.daemonTick = daemonTick;
|
|
55
64
|
exports.runDaemonForeground = runDaemonForeground;
|
|
56
65
|
exports.addSchedule = addSchedule;
|
|
@@ -59,26 +68,59 @@ exports.enableSchedule = enableSchedule;
|
|
|
59
68
|
exports.disableSchedule = disableSchedule;
|
|
60
69
|
exports.updateSchedule = updateSchedule;
|
|
61
70
|
const child_process_1 = require("child_process");
|
|
71
|
+
const crypto_1 = require("crypto");
|
|
62
72
|
const path = __importStar(require("path"));
|
|
63
73
|
const fs = __importStar(require("fs"));
|
|
74
|
+
const types_1 = require("./types");
|
|
75
|
+
Object.defineProperty(exports, "CronStoreError", { enumerable: true, get: function () { return types_1.CronStoreError; } });
|
|
64
76
|
const sdk_1 = require("@crewx/sdk");
|
|
65
|
-
const
|
|
77
|
+
const builtins_1 = require("./builtins");
|
|
78
|
+
Object.defineProperty(exports, "BUILTIN_SCHEDULES", { enumerable: true, get: function () { return builtins_1.BUILTIN_SCHEDULES; } });
|
|
79
|
+
Object.defineProperty(exports, "getProjectRoot", { enumerable: true, get: function () { return builtins_1.getProjectRoot; } });
|
|
80
|
+
const DEFAULT_WORKSPACE_PATH = path.resolve(process.cwd());
|
|
81
|
+
const DATA_DIR = path.join(DEFAULT_WORKSPACE_PATH, '.crewx');
|
|
66
82
|
const SCHEDULES_FILE = path.join(DATA_DIR, '.cron-data.json');
|
|
67
83
|
const LOG_FILE = path.join(DATA_DIR, '.cron-daemon.log');
|
|
68
84
|
const LOCK_FILE = path.join(DATA_DIR, '.cron-data.json.lock');
|
|
69
85
|
const USAGE_LOG_FILE = path.join(DATA_DIR, 'cron-usage.log');
|
|
70
86
|
const PID_FILE = path.join(DATA_DIR, '.cron-daemon.pid');
|
|
87
|
+
const BUILTINS_META_FILE = path.join(DATA_DIR, '.cron-builtins.json');
|
|
71
88
|
function resolveDataDir(dir) {
|
|
72
|
-
return
|
|
89
|
+
return path.join(path.resolve(dir ?? DEFAULT_WORKSPACE_PATH), '.crewx');
|
|
73
90
|
}
|
|
74
91
|
function resolveSchedulesFile(dir) {
|
|
75
92
|
return path.join(resolveDataDir(dir), '.cron-data.json');
|
|
76
93
|
}
|
|
94
|
+
function resolveLockFile(dir) {
|
|
95
|
+
return path.join(resolveDataDir(dir), '.cron-data.json.lock');
|
|
96
|
+
}
|
|
97
|
+
function resolveUsageLogFile(dir) {
|
|
98
|
+
return path.join(resolveDataDir(dir), 'cron-usage.log');
|
|
99
|
+
}
|
|
77
100
|
function resolvePidFile(dir) {
|
|
78
101
|
return path.join(resolveDataDir(dir), '.cron-daemon.pid');
|
|
79
102
|
}
|
|
103
|
+
function resolveBuiltinsMetaFile(dir) {
|
|
104
|
+
return path.join(resolveDataDir(dir), '.cron-builtins.json');
|
|
105
|
+
}
|
|
106
|
+
function reserveAdmission(taskId, scheduleId, timeoutMs) {
|
|
107
|
+
const reserved = (0, sdk_1.reserveUpdateAdmission)(taskId, scheduleId, timeoutMs);
|
|
108
|
+
if (!reserved) {
|
|
109
|
+
console.error(`[${getLocalTimestamp()}] Failed to record update-admission reservation for task ${taskId}`);
|
|
110
|
+
}
|
|
111
|
+
return reserved;
|
|
112
|
+
}
|
|
113
|
+
function releaseAdmissionReservation(taskId) {
|
|
114
|
+
(0, sdk_1.releaseUpdateAdmissionReservation)(taskId);
|
|
115
|
+
}
|
|
116
|
+
function resolveRunLockFile(scheduleId, dir) {
|
|
117
|
+
return path.join(resolveDataDir(dir), `.cron-run-${scheduleId}.lock`);
|
|
118
|
+
}
|
|
80
119
|
const DAEMON_INTERVAL_MS = 60 * 1000;
|
|
81
|
-
const
|
|
120
|
+
const STORE_LOCK_TIMEOUT_MS = 5000;
|
|
121
|
+
const STORE_LOCK_POLL_MS = 25;
|
|
122
|
+
const STORE_LOCK_STALE_MS = 30000;
|
|
123
|
+
const RUN_LOCK_STALE_GRACE_MS = 5000;
|
|
82
124
|
function getLocalTimestamp() {
|
|
83
125
|
const now = new Date();
|
|
84
126
|
const y = now.getFullYear();
|
|
@@ -89,12 +131,45 @@ function getLocalTimestamp() {
|
|
|
89
131
|
const s = String(now.getSeconds()).padStart(2, '0');
|
|
90
132
|
return `${y}-${m}-${d} ${h}:${min}:${s}`;
|
|
91
133
|
}
|
|
92
|
-
function logUsage(...args) {
|
|
93
|
-
|
|
134
|
+
function logUsage(dirOrMessage, ...args) {
|
|
135
|
+
const isWorkspacePath = (value) => value !== undefined && (value === '.' ||
|
|
136
|
+
value === '..' ||
|
|
137
|
+
path.isAbsolute(value) ||
|
|
138
|
+
value.includes(path.sep) ||
|
|
139
|
+
value.includes('/') ||
|
|
140
|
+
value.includes('\\') ||
|
|
141
|
+
value.endsWith('.crewx') ||
|
|
142
|
+
(() => {
|
|
143
|
+
try {
|
|
144
|
+
return fs.statSync(value).isDirectory();
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
})());
|
|
150
|
+
let workspacePath;
|
|
151
|
+
let messageParts;
|
|
152
|
+
if (args.length > 0 && (dirOrMessage === undefined || isWorkspacePath(dirOrMessage))) {
|
|
153
|
+
workspacePath = dirOrMessage;
|
|
154
|
+
messageParts = args;
|
|
155
|
+
}
|
|
156
|
+
else if (args.length === 0 && isWorkspacePath(dirOrMessage)) {
|
|
157
|
+
workspacePath = dirOrMessage;
|
|
158
|
+
messageParts = [];
|
|
159
|
+
}
|
|
160
|
+
else if (args.length === 1 && isWorkspacePath(args[0])) {
|
|
161
|
+
workspacePath = args[0];
|
|
162
|
+
messageParts = dirOrMessage === undefined ? [] : [dirOrMessage];
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
workspacePath = undefined;
|
|
166
|
+
messageParts = dirOrMessage === undefined ? args : [dirOrMessage, ...args];
|
|
167
|
+
}
|
|
168
|
+
ensureDataDir(workspacePath);
|
|
94
169
|
const timestamp = getLocalTimestamp();
|
|
95
|
-
const message =
|
|
170
|
+
const message = messageParts.join(' ');
|
|
96
171
|
const logLine = `${timestamp} | [cron] ${message}\n`;
|
|
97
|
-
fs.appendFileSync(
|
|
172
|
+
fs.appendFileSync(resolveUsageLogFile(workspacePath), logLine);
|
|
98
173
|
}
|
|
99
174
|
function ensureDataDir(dir) {
|
|
100
175
|
const dataDir = resolveDataDir(dir);
|
|
@@ -110,27 +185,86 @@ function safeReadJsonFile(filePath) {
|
|
|
110
185
|
return null;
|
|
111
186
|
}
|
|
112
187
|
}
|
|
113
|
-
|
|
188
|
+
const STORE_LOCK_WAIT_BUFFER = new Int32Array(new SharedArrayBuffer(4));
|
|
189
|
+
function waitForStoreLock() {
|
|
190
|
+
Atomics.wait(STORE_LOCK_WAIT_BUFFER, 0, 0, STORE_LOCK_POLL_MS);
|
|
191
|
+
}
|
|
192
|
+
function readStoreLock(lockPath) {
|
|
114
193
|
try {
|
|
115
|
-
|
|
194
|
+
const parsed = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
195
|
+
if (typeof parsed.token !== 'string' ||
|
|
196
|
+
typeof parsed.pid !== 'number' ||
|
|
197
|
+
typeof parsed.created_at !== 'number') {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
return parsed;
|
|
116
201
|
}
|
|
117
202
|
catch {
|
|
118
|
-
return
|
|
203
|
+
return null;
|
|
119
204
|
}
|
|
120
205
|
}
|
|
121
|
-
function
|
|
122
|
-
|
|
206
|
+
function acquireWorkspaceLock(workspacePath, timeout = STORE_LOCK_TIMEOUT_MS) {
|
|
207
|
+
const normalizedWorkspacePath = path.resolve(workspacePath);
|
|
208
|
+
ensureDataDir(normalizedWorkspacePath);
|
|
209
|
+
const lockPath = resolveLockFile(normalizedWorkspacePath);
|
|
210
|
+
const token = (0, crypto_1.randomUUID)();
|
|
211
|
+
const deadline = Date.now() + Math.max(0, timeout);
|
|
212
|
+
const owner = {
|
|
213
|
+
workspacePath: normalizedWorkspacePath,
|
|
214
|
+
lockPath,
|
|
215
|
+
token,
|
|
216
|
+
};
|
|
217
|
+
while (true) {
|
|
218
|
+
const payload = {
|
|
219
|
+
token,
|
|
220
|
+
pid: process.pid,
|
|
221
|
+
created_at: Date.now(),
|
|
222
|
+
};
|
|
223
|
+
try {
|
|
224
|
+
fs.writeFileSync(lockPath, JSON.stringify(payload), { flag: 'wx', mode: 0o600 });
|
|
225
|
+
return owner;
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
const err = error;
|
|
229
|
+
if (err.code !== 'EEXIST')
|
|
230
|
+
throw error;
|
|
231
|
+
const existing = readStoreLock(lockPath);
|
|
232
|
+
if (existing && Date.now() - existing.created_at > STORE_LOCK_STALE_MS) {
|
|
233
|
+
releaseWorkspaceLock({ ...owner, token: existing.token });
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (Date.now() >= deadline) {
|
|
237
|
+
throw new types_1.CronStoreError('CRON_STORE_LOCK_TIMEOUT', `Timed out waiting for cron store lock: ${lockPath}`, lockPath);
|
|
238
|
+
}
|
|
239
|
+
waitForStoreLock();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
123
242
|
}
|
|
243
|
+
function releaseWorkspaceLock(owner) {
|
|
244
|
+
const current = readStoreLock(owner.lockPath);
|
|
245
|
+
if (!current || current.token !== owner.token)
|
|
246
|
+
return false;
|
|
247
|
+
try {
|
|
248
|
+
fs.unlinkSync(owner.lockPath);
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
const err = error;
|
|
253
|
+
if (err.code === 'ENOENT')
|
|
254
|
+
return false;
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const MEMORY_DEBOUNCE_BUILTIN = (0, builtins_1.findBuiltinById)('memory-debounce');
|
|
124
259
|
function maybeRewriteLegacyMemoryCommand(command) {
|
|
125
260
|
if (!command.includes('/skills/memory-v2/memory-v2.js')) {
|
|
126
261
|
return command;
|
|
127
262
|
}
|
|
128
|
-
|
|
129
|
-
return `node ${builtinMemoryCli} summarize-dirty`;
|
|
263
|
+
return MEMORY_DEBOUNCE_BUILTIN.buildCommand();
|
|
130
264
|
}
|
|
131
|
-
function migrateLegacyMemoryDebounceSchedule() {
|
|
132
|
-
|
|
133
|
-
const legacyFile = path.join(
|
|
265
|
+
function migrateLegacyMemoryDebounceSchedule(workspacePath = (0, builtins_1.getProjectRoot)()) {
|
|
266
|
+
const normalizedWorkspacePath = path.resolve(workspacePath);
|
|
267
|
+
const legacyFile = path.join(normalizedWorkspacePath, 'skills', 'cron', '.cron-data.json');
|
|
134
268
|
if (!fs.existsSync(legacyFile)) {
|
|
135
269
|
return;
|
|
136
270
|
}
|
|
@@ -138,12 +272,13 @@ function migrateLegacyMemoryDebounceSchedule() {
|
|
|
138
272
|
if (!Array.isArray(legacySchedules)) {
|
|
139
273
|
return;
|
|
140
274
|
}
|
|
141
|
-
const legacySchedule = legacySchedules.find(s => s.name ===
|
|
275
|
+
const legacySchedule = legacySchedules.find(s => s.name === MEMORY_DEBOUNCE_BUILTIN.name);
|
|
142
276
|
if (!legacySchedule) {
|
|
143
277
|
return;
|
|
144
278
|
}
|
|
145
|
-
const
|
|
146
|
-
|
|
279
|
+
const store = createScheduleStore(normalizedWorkspacePath);
|
|
280
|
+
const targetSchedules = store.list();
|
|
281
|
+
if (targetSchedules.some(s => s.name === MEMORY_DEBOUNCE_BUILTIN.name)) {
|
|
147
282
|
return;
|
|
148
283
|
}
|
|
149
284
|
const migratedSchedule = {
|
|
@@ -152,7 +287,12 @@ function migrateLegacyMemoryDebounceSchedule() {
|
|
|
152
287
|
updated_at: new Date().toISOString(),
|
|
153
288
|
next_run: calculateNextRun(legacySchedule.cron, legacySchedule.timezone),
|
|
154
289
|
};
|
|
155
|
-
|
|
290
|
+
store.create(migratedSchedule);
|
|
291
|
+
}
|
|
292
|
+
function warnIfOverdriveInCommand(command) {
|
|
293
|
+
if (command.includes('--overdrive')) {
|
|
294
|
+
console.warn('warning: --overdrive in cron command is ignored and may silently override --model');
|
|
295
|
+
}
|
|
156
296
|
}
|
|
157
297
|
function normalizeMode(mode) {
|
|
158
298
|
if (mode === 'query')
|
|
@@ -161,69 +301,506 @@ function normalizeMode(mode) {
|
|
|
161
301
|
return 'command';
|
|
162
302
|
return 'execute';
|
|
163
303
|
}
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
304
|
+
function cloneSchedule(schedule) {
|
|
305
|
+
return {
|
|
306
|
+
...schedule,
|
|
307
|
+
...(schedule.env ? { env: { ...schedule.env } } : {}),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function readSchedulesFile(schedulesFile) {
|
|
311
|
+
let data;
|
|
312
|
+
try {
|
|
313
|
+
data = fs.readFileSync(schedulesFile, 'utf8');
|
|
168
314
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
315
|
+
catch (error) {
|
|
316
|
+
const err = error;
|
|
317
|
+
if (err.code === 'ENOENT')
|
|
318
|
+
return [];
|
|
319
|
+
throw error;
|
|
172
320
|
}
|
|
321
|
+
let parsed;
|
|
173
322
|
try {
|
|
174
|
-
|
|
175
|
-
return JSON.parse(data);
|
|
323
|
+
parsed = JSON.parse(data);
|
|
176
324
|
}
|
|
177
|
-
catch (
|
|
178
|
-
|
|
179
|
-
console.error('Failed to load schedules:', err.message);
|
|
180
|
-
return [];
|
|
325
|
+
catch (error) {
|
|
326
|
+
throw new types_1.CronStoreError('CRON_STORE_CORRUPT', `Cron schedule store is not valid JSON: ${schedulesFile}`, schedulesFile);
|
|
181
327
|
}
|
|
328
|
+
if (!Array.isArray(parsed)) {
|
|
329
|
+
throw new types_1.CronStoreError('CRON_STORE_CORRUPT', `Cron schedule store must contain a top-level array: ${schedulesFile}`, schedulesFile);
|
|
330
|
+
}
|
|
331
|
+
return parsed;
|
|
182
332
|
}
|
|
183
|
-
function
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
fs.renameSync(tempFile, schedulesFile);
|
|
189
|
-
}
|
|
190
|
-
async function acquireLock(timeout = 5000) {
|
|
191
|
-
ensureDataDir();
|
|
192
|
-
const startTime = Date.now();
|
|
193
|
-
while (Date.now() - startTime < timeout) {
|
|
333
|
+
function syncDirectory(directory) {
|
|
334
|
+
if (process.platform === 'win32')
|
|
335
|
+
return;
|
|
336
|
+
try {
|
|
337
|
+
const fd = fs.openSync(directory, 'r');
|
|
194
338
|
try {
|
|
195
|
-
fs.
|
|
196
|
-
return true;
|
|
339
|
+
fs.fsyncSync(fd);
|
|
197
340
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
341
|
+
finally {
|
|
342
|
+
fs.closeSync(fd);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function writeSchedulesAtomic(schedulesFile, schedules) {
|
|
349
|
+
const directory = path.dirname(schedulesFile);
|
|
350
|
+
const tempFile = path.join(directory, `.${path.basename(schedulesFile)}.${process.pid}.${(0, crypto_1.randomUUID)()}.tmp`);
|
|
351
|
+
const contents = JSON.stringify(schedules, null, 2);
|
|
352
|
+
let fd;
|
|
353
|
+
try {
|
|
354
|
+
fd = fs.openSync(tempFile, 'wx', 0o600);
|
|
355
|
+
fs.writeFileSync(fd, contents);
|
|
356
|
+
fs.fsyncSync(fd);
|
|
357
|
+
fs.closeSync(fd);
|
|
358
|
+
fd = undefined;
|
|
359
|
+
fs.renameSync(tempFile, schedulesFile);
|
|
360
|
+
syncDirectory(directory);
|
|
361
|
+
}
|
|
362
|
+
finally {
|
|
363
|
+
if (fd !== undefined) {
|
|
364
|
+
try {
|
|
365
|
+
fs.closeSync(fd);
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
fs.unlinkSync(tempFile);
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function requireSchedule(schedules, id) {
|
|
378
|
+
const schedule = schedules.find(item => item.id === id);
|
|
379
|
+
if (!schedule) {
|
|
380
|
+
throw new types_1.CronStoreError('CRON_SCHEDULE_NOT_FOUND', `Schedule not found: ${id}`);
|
|
381
|
+
}
|
|
382
|
+
return schedule;
|
|
383
|
+
}
|
|
384
|
+
function withStoreLock(workspacePath, operation) {
|
|
385
|
+
const lock = acquireWorkspaceLock(workspacePath);
|
|
386
|
+
try {
|
|
387
|
+
const schedulesFile = resolveSchedulesFile(workspacePath);
|
|
388
|
+
const schedules = readSchedulesFile(schedulesFile);
|
|
389
|
+
const result = operation(schedules);
|
|
390
|
+
writeSchedulesAtomic(schedulesFile, schedules);
|
|
391
|
+
return result;
|
|
392
|
+
}
|
|
393
|
+
finally {
|
|
394
|
+
releaseWorkspaceLock(lock);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function createScheduleStore(workspacePath = DEFAULT_WORKSPACE_PATH) {
|
|
398
|
+
const normalizedWorkspacePath = path.resolve(workspacePath);
|
|
399
|
+
const dataDir = resolveDataDir(normalizedWorkspacePath);
|
|
400
|
+
const schedulesFile = resolveSchedulesFile(normalizedWorkspacePath);
|
|
401
|
+
const lockFile = resolveLockFile(normalizedWorkspacePath);
|
|
402
|
+
return {
|
|
403
|
+
workspacePath: normalizedWorkspacePath,
|
|
404
|
+
dataDir,
|
|
405
|
+
schedulesFile,
|
|
406
|
+
lockFile,
|
|
407
|
+
list() {
|
|
408
|
+
return readSchedulesFile(schedulesFile).map(cloneSchedule);
|
|
409
|
+
},
|
|
410
|
+
get(id) {
|
|
411
|
+
const schedule = readSchedulesFile(schedulesFile).find(item => item.id === id);
|
|
412
|
+
return schedule ? cloneSchedule(schedule) : undefined;
|
|
413
|
+
},
|
|
414
|
+
create(schedule) {
|
|
415
|
+
return withStoreLock(normalizedWorkspacePath, schedules => {
|
|
416
|
+
if (schedules.some(item => item.id === schedule.id)) {
|
|
417
|
+
throw new types_1.CronStoreError('CRON_SCHEDULE_EXISTS', `Schedule already exists: ${schedule.id}`);
|
|
207
418
|
}
|
|
208
|
-
|
|
209
|
-
|
|
419
|
+
schedules.push(cloneSchedule(schedule));
|
|
420
|
+
return cloneSchedule(schedule);
|
|
421
|
+
});
|
|
422
|
+
},
|
|
423
|
+
update(id, changes) {
|
|
424
|
+
return withStoreLock(normalizedWorkspacePath, schedules => {
|
|
425
|
+
const schedule = requireSchedule(schedules, id);
|
|
426
|
+
if (changes.cron !== undefined)
|
|
427
|
+
schedule.cron = changes.cron;
|
|
428
|
+
if (changes.command !== undefined)
|
|
429
|
+
schedule.command = changes.command;
|
|
430
|
+
if (changes.mode !== undefined)
|
|
431
|
+
schedule.mode = changes.mode;
|
|
432
|
+
if (changes.enabled !== undefined)
|
|
433
|
+
schedule.enabled = changes.enabled;
|
|
434
|
+
if (changes.name !== undefined)
|
|
435
|
+
schedule.name = changes.name;
|
|
436
|
+
if (changes.working_directory !== undefined)
|
|
437
|
+
schedule.working_directory = changes.working_directory;
|
|
438
|
+
if (changes.timezone !== undefined)
|
|
439
|
+
schedule.timezone = changes.timezone;
|
|
440
|
+
if (changes.timeout_ms !== undefined)
|
|
441
|
+
schedule.timeout_ms = changes.timeout_ms;
|
|
442
|
+
if (changes.allow_concurrent !== undefined)
|
|
443
|
+
schedule.allow_concurrent = changes.allow_concurrent;
|
|
444
|
+
if (changes.env !== undefined) {
|
|
445
|
+
if (changes.env === null)
|
|
446
|
+
delete schedule.env;
|
|
447
|
+
else
|
|
448
|
+
schedule.env = { ...changes.env };
|
|
210
449
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
450
|
+
if (changes.model !== undefined) {
|
|
451
|
+
if (changes.model === null)
|
|
452
|
+
delete schedule.model;
|
|
453
|
+
else
|
|
454
|
+
schedule.model = changes.model;
|
|
455
|
+
}
|
|
456
|
+
if (changes.provider !== undefined) {
|
|
457
|
+
if (changes.provider === null)
|
|
458
|
+
delete schedule.provider;
|
|
459
|
+
else
|
|
460
|
+
schedule.provider = changes.provider;
|
|
461
|
+
}
|
|
462
|
+
schedule.updated_at = new Date().toISOString();
|
|
463
|
+
schedule.next_run = calculateNextRun(schedule.cron, schedule.timezone);
|
|
464
|
+
return cloneSchedule(schedule);
|
|
465
|
+
});
|
|
466
|
+
},
|
|
467
|
+
remove(id) {
|
|
468
|
+
return withStoreLock(normalizedWorkspacePath, schedules => {
|
|
469
|
+
const index = schedules.findIndex(item => item.id === id);
|
|
470
|
+
if (index === -1) {
|
|
471
|
+
throw new types_1.CronStoreError('CRON_SCHEDULE_NOT_FOUND', `Schedule not found: ${id}`);
|
|
472
|
+
}
|
|
473
|
+
const [removed] = schedules.splice(index, 1);
|
|
474
|
+
return cloneSchedule(removed);
|
|
475
|
+
});
|
|
476
|
+
},
|
|
477
|
+
toggle(id, enabled) {
|
|
478
|
+
return withStoreLock(normalizedWorkspacePath, schedules => {
|
|
479
|
+
const schedule = requireSchedule(schedules, id);
|
|
480
|
+
schedule.enabled = enabled ?? !schedule.enabled;
|
|
481
|
+
schedule.updated_at = new Date().toISOString();
|
|
482
|
+
if (schedule.enabled) {
|
|
483
|
+
schedule.next_run = calculateNextRun(schedule.cron, schedule.timezone);
|
|
484
|
+
}
|
|
485
|
+
return cloneSchedule(schedule);
|
|
486
|
+
});
|
|
487
|
+
},
|
|
488
|
+
claimOccurrence(id, options) {
|
|
489
|
+
return withStoreLock(normalizedWorkspacePath, schedules => {
|
|
490
|
+
const schedule = requireSchedule(schedules, typeof id === 'string' ? id : id.id);
|
|
491
|
+
if (schedule.last_occurrence_key === options.occurrenceKey) {
|
|
492
|
+
return {
|
|
493
|
+
claimed: false,
|
|
494
|
+
schedule: cloneSchedule(schedule),
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
const startedAt = options.startedAt ?? new Date().toISOString();
|
|
498
|
+
schedule.last_started_at = startedAt;
|
|
499
|
+
schedule.last_occurrence_key = options.occurrenceKey;
|
|
500
|
+
schedule.updated_at = startedAt;
|
|
501
|
+
return {
|
|
502
|
+
claimed: true,
|
|
503
|
+
schedule: cloneSchedule(schedule),
|
|
504
|
+
};
|
|
505
|
+
});
|
|
506
|
+
},
|
|
507
|
+
recordStarted(id, options) {
|
|
508
|
+
return withStoreLock(normalizedWorkspacePath, schedules => {
|
|
509
|
+
const schedule = requireSchedule(schedules, typeof id === 'string' ? id : id.id);
|
|
510
|
+
const normalizedOptions = typeof options === 'string' ? { startedAt: options } : options;
|
|
511
|
+
const startedAt = normalizedOptions?.startedAt ?? new Date().toISOString();
|
|
512
|
+
schedule.last_started_at = startedAt;
|
|
513
|
+
if (normalizedOptions?.occurrenceKey !== undefined) {
|
|
514
|
+
schedule.last_occurrence_key = normalizedOptions.occurrenceKey;
|
|
515
|
+
}
|
|
516
|
+
schedule.updated_at = startedAt;
|
|
517
|
+
return cloneSchedule(schedule);
|
|
518
|
+
});
|
|
519
|
+
},
|
|
520
|
+
recordFinished(id, options) {
|
|
521
|
+
return withStoreLock(normalizedWorkspacePath, schedules => {
|
|
522
|
+
const schedule = requireSchedule(schedules, typeof id === 'string' ? id : id.id);
|
|
523
|
+
const finishedAt = options?.finishedAt ?? new Date().toISOString();
|
|
524
|
+
schedule.last_run = finishedAt;
|
|
525
|
+
schedule.run_count = (schedule.run_count || 0) + 1;
|
|
526
|
+
schedule.next_run = options?.nextRun !== undefined
|
|
527
|
+
? options.nextRun
|
|
528
|
+
: calculateNextRun(schedule.cron, schedule.timezone);
|
|
529
|
+
if (options?.outcome !== undefined)
|
|
530
|
+
schedule.last_outcome = options.outcome;
|
|
531
|
+
else if (options?.success !== undefined)
|
|
532
|
+
schedule.last_outcome = options.success ? 'success' : 'failure';
|
|
533
|
+
schedule.updated_at = finishedAt;
|
|
534
|
+
return cloneSchedule(schedule);
|
|
535
|
+
});
|
|
536
|
+
},
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function loadSchedules(dir) {
|
|
540
|
+
return createScheduleStore(dir ?? DEFAULT_WORKSPACE_PATH).list();
|
|
541
|
+
}
|
|
542
|
+
function saveSchedules(schedules, dir) {
|
|
543
|
+
const workspacePath = path.resolve(dir ?? DEFAULT_WORKSPACE_PATH);
|
|
544
|
+
const lock = acquireWorkspaceLock(workspacePath);
|
|
545
|
+
try {
|
|
546
|
+
const schedulesFile = resolveSchedulesFile(workspacePath);
|
|
547
|
+
readSchedulesFile(schedulesFile);
|
|
548
|
+
writeSchedulesAtomic(schedulesFile, schedules.map(cloneSchedule));
|
|
549
|
+
}
|
|
550
|
+
finally {
|
|
551
|
+
releaseWorkspaceLock(lock);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function loadBuiltinsMeta(dir) {
|
|
555
|
+
const data = safeReadJsonFile(resolveBuiltinsMetaFile(dir));
|
|
556
|
+
return { removed_builtins: Array.isArray(data?.removed_builtins) ? data.removed_builtins : [] };
|
|
557
|
+
}
|
|
558
|
+
function saveBuiltinsMeta(meta, dir) {
|
|
559
|
+
ensureDataDir(dir);
|
|
560
|
+
const metaFile = resolveBuiltinsMetaFile(dir);
|
|
561
|
+
const tempFile = `${metaFile}.tmp`;
|
|
562
|
+
fs.writeFileSync(tempFile, JSON.stringify(meta, null, 2));
|
|
563
|
+
fs.renameSync(tempFile, metaFile);
|
|
564
|
+
}
|
|
565
|
+
function getRemovedBuiltins(dir) {
|
|
566
|
+
return loadBuiltinsMeta(dir).removed_builtins;
|
|
567
|
+
}
|
|
568
|
+
function recordRemovedBuiltin(builtinId, dir) {
|
|
569
|
+
const meta = loadBuiltinsMeta(dir);
|
|
570
|
+
if (!meta.removed_builtins.includes(builtinId)) {
|
|
571
|
+
meta.removed_builtins.push(builtinId);
|
|
572
|
+
saveBuiltinsMeta(meta, dir);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function seedBuiltinSchedules(dir) {
|
|
576
|
+
const store = createScheduleStore(dir ?? DEFAULT_WORKSPACE_PATH);
|
|
577
|
+
const schedules = store.list();
|
|
578
|
+
const existingNames = new Set(schedules.map(s => s.name));
|
|
579
|
+
const removedBuiltins = new Set(getRemovedBuiltins(dir));
|
|
580
|
+
const seeded = [];
|
|
581
|
+
for (const def of builtins_1.BUILTIN_SCHEDULES) {
|
|
582
|
+
if (existingNames.has(def.name))
|
|
583
|
+
continue;
|
|
584
|
+
if (removedBuiltins.has(def.builtin_id))
|
|
585
|
+
continue;
|
|
586
|
+
const now = new Date().toISOString();
|
|
587
|
+
const schedule = {
|
|
588
|
+
id: (0, sdk_1.generateId)('cron'),
|
|
589
|
+
cron: def.cron,
|
|
590
|
+
command: def.buildCommand(),
|
|
591
|
+
mode: def.mode,
|
|
592
|
+
enabled: true,
|
|
593
|
+
name: def.name,
|
|
594
|
+
working_directory: null,
|
|
595
|
+
timezone: null,
|
|
596
|
+
timeout_ms: 600000,
|
|
597
|
+
allow_concurrent: false,
|
|
598
|
+
created_at: now,
|
|
599
|
+
updated_at: now,
|
|
600
|
+
run_count: 0,
|
|
601
|
+
next_run: calculateNextRun(def.cron, null),
|
|
602
|
+
};
|
|
603
|
+
store.create(schedule);
|
|
604
|
+
existingNames.add(def.name);
|
|
605
|
+
seeded.push(def.builtin_id);
|
|
606
|
+
}
|
|
607
|
+
return seeded;
|
|
608
|
+
}
|
|
609
|
+
const legacyLockOwners = new Map();
|
|
610
|
+
async function acquireLock(timeout = STORE_LOCK_TIMEOUT_MS, dir) {
|
|
611
|
+
const workspacePath = path.resolve(dir ?? DEFAULT_WORKSPACE_PATH);
|
|
612
|
+
try {
|
|
613
|
+
legacyLockOwners.set(workspacePath, acquireWorkspaceLock(workspacePath, timeout));
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
if (error instanceof types_1.CronStoreError && error.code === 'CRON_STORE_LOCK_TIMEOUT')
|
|
618
|
+
return false;
|
|
619
|
+
throw error;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
function releaseLock(dir) {
|
|
623
|
+
const workspacePath = path.resolve(dir ?? DEFAULT_WORKSPACE_PATH);
|
|
624
|
+
const owner = legacyLockOwners.get(workspacePath);
|
|
625
|
+
if (!owner)
|
|
626
|
+
return;
|
|
627
|
+
legacyLockOwners.delete(workspacePath);
|
|
628
|
+
releaseWorkspaceLock(owner);
|
|
629
|
+
}
|
|
630
|
+
function readRunLease(lockFile) {
|
|
631
|
+
return safeReadJsonFile(lockFile);
|
|
632
|
+
}
|
|
633
|
+
function writeRunLeaseAtomic(lockFile, lease) {
|
|
634
|
+
const directory = path.dirname(lockFile);
|
|
635
|
+
const tempFile = path.join(directory, `.${path.basename(lockFile)}.${process.pid}.${(0, crypto_1.randomUUID)()}.tmp`);
|
|
636
|
+
let fd;
|
|
637
|
+
try {
|
|
638
|
+
fd = fs.openSync(tempFile, 'wx', 0o600);
|
|
639
|
+
fs.writeFileSync(fd, JSON.stringify(lease));
|
|
640
|
+
fs.fsyncSync(fd);
|
|
641
|
+
fs.closeSync(fd);
|
|
642
|
+
fd = undefined;
|
|
643
|
+
fs.renameSync(tempFile, lockFile);
|
|
644
|
+
}
|
|
645
|
+
finally {
|
|
646
|
+
if (fd !== undefined) {
|
|
647
|
+
try {
|
|
648
|
+
fs.closeSync(fd);
|
|
215
649
|
}
|
|
650
|
+
catch { }
|
|
216
651
|
}
|
|
652
|
+
try {
|
|
653
|
+
fs.unlinkSync(tempFile);
|
|
654
|
+
}
|
|
655
|
+
catch { }
|
|
217
656
|
}
|
|
218
|
-
return false;
|
|
219
657
|
}
|
|
220
|
-
function
|
|
658
|
+
function withRunLeaseStoreLock(workspacePath, scheduleId, operation) {
|
|
659
|
+
const owner = acquireWorkspaceLock(workspacePath);
|
|
221
660
|
try {
|
|
222
|
-
|
|
661
|
+
return operation(resolveRunLockFile(scheduleId, workspacePath));
|
|
662
|
+
}
|
|
663
|
+
finally {
|
|
664
|
+
releaseWorkspaceLock(owner);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
function getLeaseAgeMs(lockFile) {
|
|
668
|
+
try {
|
|
669
|
+
return Math.max(0, Date.now() - fs.statSync(lockFile).mtimeMs);
|
|
223
670
|
}
|
|
224
671
|
catch {
|
|
672
|
+
return null;
|
|
225
673
|
}
|
|
226
674
|
}
|
|
675
|
+
function compareProcessIdentity(expected, actual) {
|
|
676
|
+
if (expected.startTime !== null && actual.startTime !== null) {
|
|
677
|
+
return expected.startTime === actual.startTime;
|
|
678
|
+
}
|
|
679
|
+
if (expected.commandLine !== null && actual.commandLine !== null) {
|
|
680
|
+
return expected.commandLine === actual.commandLine;
|
|
681
|
+
}
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
function isRunLeaseStale(lease, lockFile, timeoutMs) {
|
|
685
|
+
if (lease.phase !== 'child') {
|
|
686
|
+
const ageMs = getLeaseAgeMs(lockFile);
|
|
687
|
+
if (ageMs === null)
|
|
688
|
+
return false;
|
|
689
|
+
const ownerPid = lease.hostPid ?? lease.pid;
|
|
690
|
+
const ownerAlive = isPidAlive(ownerPid);
|
|
691
|
+
return ownerAlive === false || ageMs > timeoutMs + RUN_LOCK_STALE_GRACE_MS;
|
|
692
|
+
}
|
|
693
|
+
const childPid = lease.childPid ?? lease.pid;
|
|
694
|
+
const childAlive = isPidAlive(childPid);
|
|
695
|
+
if (childAlive === false)
|
|
696
|
+
return true;
|
|
697
|
+
if (childAlive !== true)
|
|
698
|
+
return false;
|
|
699
|
+
if (!lease.childIdentity)
|
|
700
|
+
return false;
|
|
701
|
+
const currentIdentity = captureProcessIdentity(childPid);
|
|
702
|
+
if (!currentIdentity)
|
|
703
|
+
return false;
|
|
704
|
+
const identityMatch = compareProcessIdentity(lease.childIdentity, currentIdentity);
|
|
705
|
+
if (identityMatch === false)
|
|
706
|
+
return true;
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
709
|
+
function acquireRunLock(scheduleId, taskId, timeoutMs, dir) {
|
|
710
|
+
const workspacePath = path.resolve(dir ?? DEFAULT_WORKSPACE_PATH);
|
|
711
|
+
ensureDataDir(workspacePath);
|
|
712
|
+
const lockFile = resolveRunLockFile(scheduleId, workspacePath);
|
|
713
|
+
const leaseId = (0, crypto_1.randomUUID)();
|
|
714
|
+
const ownerToken = (0, crypto_1.randomUUID)();
|
|
715
|
+
const lease = {
|
|
716
|
+
leaseId,
|
|
717
|
+
ownerToken,
|
|
718
|
+
phase: 'admitting',
|
|
719
|
+
hostPid: process.pid,
|
|
720
|
+
pid: process.pid,
|
|
721
|
+
taskId,
|
|
722
|
+
startedAt: new Date().toISOString(),
|
|
723
|
+
};
|
|
724
|
+
return withRunLeaseStoreLock(workspacePath, scheduleId, () => {
|
|
725
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
726
|
+
try {
|
|
727
|
+
fs.writeFileSync(lockFile, JSON.stringify(lease), { flag: 'wx', mode: 0o600 });
|
|
728
|
+
return {
|
|
729
|
+
acquired: true,
|
|
730
|
+
lease: { scheduleId, workspacePath, lockFile, leaseId, ownerToken, taskId },
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
catch (e) {
|
|
734
|
+
const err = e;
|
|
735
|
+
if (err.code !== 'EEXIST')
|
|
736
|
+
throw err;
|
|
737
|
+
const existing = readRunLease(lockFile);
|
|
738
|
+
if (!existing) {
|
|
739
|
+
return { acquired: false, runningTaskId: 'unknown', reason: 'execution_lease' };
|
|
740
|
+
}
|
|
741
|
+
if (isRunLeaseStale(existing, lockFile, timeoutMs)) {
|
|
742
|
+
const current = readRunLease(lockFile);
|
|
743
|
+
if (current?.leaseId !== undefined && current.leaseId !== existing.leaseId) {
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
try {
|
|
747
|
+
fs.unlinkSync(lockFile);
|
|
748
|
+
}
|
|
749
|
+
catch { }
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
return {
|
|
753
|
+
acquired: false,
|
|
754
|
+
runningTaskId: existing.taskId,
|
|
755
|
+
reason: 'execution_lease',
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
const existing = readRunLease(lockFile);
|
|
760
|
+
return {
|
|
761
|
+
acquired: false,
|
|
762
|
+
runningTaskId: existing?.taskId ?? 'unknown',
|
|
763
|
+
reason: 'execution_lease',
|
|
764
|
+
};
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
function handoffRunLock(lease, childPid, childIdentity) {
|
|
768
|
+
return withRunLeaseStoreLock(lease.workspacePath, lease.scheduleId, lockFile => {
|
|
769
|
+
const current = readRunLease(lockFile);
|
|
770
|
+
if (!current ||
|
|
771
|
+
current.leaseId !== lease.leaseId ||
|
|
772
|
+
current.ownerToken !== lease.ownerToken) {
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
writeRunLeaseAtomic(lockFile, {
|
|
776
|
+
...current,
|
|
777
|
+
phase: 'child',
|
|
778
|
+
pid: childPid,
|
|
779
|
+
childPid,
|
|
780
|
+
childIdentity,
|
|
781
|
+
});
|
|
782
|
+
return true;
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
function releaseRunLock(scheduleId, leaseId, dir) {
|
|
786
|
+
const workspacePath = path.resolve(dir ?? DEFAULT_WORKSPACE_PATH);
|
|
787
|
+
ensureDataDir(workspacePath);
|
|
788
|
+
return withRunLeaseStoreLock(workspacePath, scheduleId, lockFile => {
|
|
789
|
+
const current = readRunLease(lockFile);
|
|
790
|
+
if (!current || current.leaseId !== leaseId)
|
|
791
|
+
return false;
|
|
792
|
+
try {
|
|
793
|
+
fs.unlinkSync(lockFile);
|
|
794
|
+
return true;
|
|
795
|
+
}
|
|
796
|
+
catch (error) {
|
|
797
|
+
const err = error;
|
|
798
|
+
if (err.code === 'ENOENT')
|
|
799
|
+
return false;
|
|
800
|
+
throw error;
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
}
|
|
227
804
|
const DAY_NAMES = {
|
|
228
805
|
SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6,
|
|
229
806
|
};
|
|
@@ -452,8 +1029,10 @@ function calculateNextRun(cronExpr, timezone) {
|
|
|
452
1029
|
function readDaemonPid(dir) {
|
|
453
1030
|
try {
|
|
454
1031
|
const raw = fs.readFileSync(resolvePidFile(dir), 'utf8').trim();
|
|
455
|
-
|
|
456
|
-
|
|
1032
|
+
if (!/^\d+$/.test(raw))
|
|
1033
|
+
return null;
|
|
1034
|
+
const pid = Number(raw);
|
|
1035
|
+
return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
|
|
457
1036
|
}
|
|
458
1037
|
catch {
|
|
459
1038
|
return null;
|
|
@@ -476,44 +1055,141 @@ function isPidAlive(pid) {
|
|
|
476
1055
|
return true;
|
|
477
1056
|
}
|
|
478
1057
|
catch (e) {
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
return true;
|
|
482
|
-
return false;
|
|
1058
|
+
const error = e;
|
|
1059
|
+
return error.code === 'ESRCH' ? false : 'unknown';
|
|
483
1060
|
}
|
|
484
1061
|
}
|
|
485
|
-
function
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
1062
|
+
function getProcessCommandLine(pid) {
|
|
1063
|
+
try {
|
|
1064
|
+
const output = process.platform === 'win32'
|
|
1065
|
+
? (0, child_process_1.execFileSync)('powershell.exe', [
|
|
1066
|
+
'-NoLogo',
|
|
1067
|
+
'-NoProfile',
|
|
1068
|
+
'-NonInteractive',
|
|
1069
|
+
'-Command',
|
|
1070
|
+
`(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`,
|
|
1071
|
+
], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
|
|
1072
|
+
: (0, child_process_1.execFileSync)('ps', ['-ww', '-p', String(pid), '-o', 'command='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
1073
|
+
return output.trim() || null;
|
|
1074
|
+
}
|
|
1075
|
+
catch {
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
function getProcessStartTime(pid) {
|
|
1080
|
+
try {
|
|
1081
|
+
const output = process.platform === 'win32'
|
|
1082
|
+
? (0, child_process_1.execFileSync)('powershell.exe', [
|
|
1083
|
+
'-NoLogo',
|
|
1084
|
+
'-NoProfile',
|
|
1085
|
+
'-NonInteractive',
|
|
1086
|
+
'-Command',
|
|
1087
|
+
`(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CreationDate.ToUniversalTime().ToString("o")`,
|
|
1088
|
+
], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
|
|
1089
|
+
: (0, child_process_1.execFileSync)('ps', ['-p', String(pid), '-o', 'lstart='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
1090
|
+
return output.trim() || null;
|
|
1091
|
+
}
|
|
1092
|
+
catch {
|
|
1093
|
+
return null;
|
|
489
1094
|
}
|
|
490
|
-
throw new Error(`Cron CLI entrypoint not found: ${distCli}`);
|
|
491
1095
|
}
|
|
492
|
-
function
|
|
1096
|
+
function captureProcessIdentity(pid) {
|
|
1097
|
+
const identity = {
|
|
1098
|
+
commandLine: getProcessCommandLine(pid),
|
|
1099
|
+
startTime: getProcessStartTime(pid),
|
|
1100
|
+
};
|
|
1101
|
+
return identity.commandLine !== null || identity.startTime !== null ? identity : null;
|
|
1102
|
+
}
|
|
1103
|
+
const DEFAULT_DAEMON_PROCESS_PROBE = {
|
|
1104
|
+
isAlive: isPidAlive,
|
|
1105
|
+
getCommandLine: getProcessCommandLine,
|
|
1106
|
+
};
|
|
1107
|
+
function normalizeCommandLine(value) {
|
|
1108
|
+
const normalized = value.replace(/\\/g, '/');
|
|
1109
|
+
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
1110
|
+
}
|
|
1111
|
+
function getDaemonCliPathTail() {
|
|
1112
|
+
try {
|
|
1113
|
+
const segments = normalizeCommandLine(getDaemonCliPath()).split('/').filter(Boolean);
|
|
1114
|
+
const tail = segments.slice(-3).join('/');
|
|
1115
|
+
return tail || null;
|
|
1116
|
+
}
|
|
1117
|
+
catch {
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
function classifyDaemonIdentity(commandLine) {
|
|
1122
|
+
if (!commandLine?.trim())
|
|
1123
|
+
return 'unknown';
|
|
1124
|
+
const cliPathTail = getDaemonCliPathTail();
|
|
1125
|
+
if (!cliPathTail)
|
|
1126
|
+
return 'unknown';
|
|
1127
|
+
const normalizedCommandLine = normalizeCommandLine(commandLine);
|
|
1128
|
+
return normalizedCommandLine.includes('daemon --foreground')
|
|
1129
|
+
&& normalizedCommandLine.includes(cliPathTail)
|
|
1130
|
+
? 'running'
|
|
1131
|
+
: 'stale';
|
|
1132
|
+
}
|
|
1133
|
+
function getDaemonCliPath() {
|
|
1134
|
+
const candidates = [
|
|
1135
|
+
path.join(__dirname, '..', 'cli.js'),
|
|
1136
|
+
path.join(__dirname, '..', 'dist', 'cli.js'),
|
|
1137
|
+
];
|
|
1138
|
+
const cliPath = candidates.find(candidate => fs.existsSync(candidate));
|
|
1139
|
+
if (cliPath) {
|
|
1140
|
+
return cliPath;
|
|
1141
|
+
}
|
|
1142
|
+
throw new Error(`Cron CLI entrypoint not found: ${candidates[0]}`);
|
|
1143
|
+
}
|
|
1144
|
+
function inspectDaemon(dir, probe = DEFAULT_DAEMON_PROCESS_PROBE) {
|
|
493
1145
|
const pid = readDaemonPid(dir);
|
|
494
1146
|
if (!pid) {
|
|
495
|
-
|
|
1147
|
+
removeDaemonPidFile(dir);
|
|
1148
|
+
return 'stale';
|
|
1149
|
+
}
|
|
1150
|
+
let alive;
|
|
1151
|
+
try {
|
|
1152
|
+
alive = probe.isAlive(pid);
|
|
1153
|
+
}
|
|
1154
|
+
catch {
|
|
1155
|
+
alive = 'unknown';
|
|
496
1156
|
}
|
|
497
|
-
const alive = isPidAlive(pid);
|
|
498
1157
|
if (!alive) {
|
|
499
1158
|
removeDaemonPidFile(dir);
|
|
1159
|
+
return 'stale';
|
|
500
1160
|
}
|
|
501
|
-
|
|
1161
|
+
let commandLine;
|
|
1162
|
+
try {
|
|
1163
|
+
commandLine = probe.getCommandLine(pid);
|
|
1164
|
+
}
|
|
1165
|
+
catch {
|
|
1166
|
+
return 'unknown';
|
|
1167
|
+
}
|
|
1168
|
+
const liveness = classifyDaemonIdentity(commandLine);
|
|
1169
|
+
if (liveness === 'stale') {
|
|
1170
|
+
removeDaemonPidFile(dir);
|
|
1171
|
+
}
|
|
1172
|
+
return liveness;
|
|
502
1173
|
}
|
|
503
|
-
function
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
throw new Error('
|
|
1174
|
+
function isDaemonRunning(dir, probe = DEFAULT_DAEMON_PROCESS_PROBE) {
|
|
1175
|
+
return inspectDaemon(dir, probe) === 'running';
|
|
1176
|
+
}
|
|
1177
|
+
function spawnDaemon(dir, spawnImpl = child_process_1.spawn) {
|
|
1178
|
+
if ((0, sdk_1.isDurablyQuiescing)()) {
|
|
1179
|
+
throw new Error('Cannot start cron daemon: a desktop update is draining active work');
|
|
509
1180
|
}
|
|
510
1181
|
ensureDataDir(dir);
|
|
511
1182
|
const cliPath = getDaemonCliPath();
|
|
512
|
-
const
|
|
1183
|
+
const daemonEnv = { ...process.env };
|
|
1184
|
+
if (process.versions.electron) {
|
|
1185
|
+
daemonEnv.ELECTRON_RUN_AS_NODE = '1';
|
|
1186
|
+
}
|
|
1187
|
+
const child = spawnImpl(process.execPath, [cliPath, 'daemon', '--foreground'], {
|
|
513
1188
|
cwd: dir || undefined,
|
|
514
1189
|
detached: true,
|
|
515
1190
|
stdio: 'ignore',
|
|
516
1191
|
windowsHide: true,
|
|
1192
|
+
env: daemonEnv,
|
|
517
1193
|
});
|
|
518
1194
|
if (!child.pid) {
|
|
519
1195
|
throw new Error('Failed to start daemon: no PID returned');
|
|
@@ -522,37 +1198,64 @@ function startDaemon(dir) {
|
|
|
522
1198
|
writeDaemonPid(child.pid, dir);
|
|
523
1199
|
return child.pid;
|
|
524
1200
|
}
|
|
525
|
-
function
|
|
526
|
-
const
|
|
527
|
-
if (
|
|
528
|
-
|
|
529
|
-
|
|
1201
|
+
function startDaemon(dir, probe = DEFAULT_DAEMON_PROCESS_PROBE) {
|
|
1202
|
+
const liveness = inspectDaemon(dir, probe);
|
|
1203
|
+
if (liveness === 'running') {
|
|
1204
|
+
const pid = readDaemonPid(dir);
|
|
1205
|
+
if (pid)
|
|
1206
|
+
return pid;
|
|
1207
|
+
throw new Error('Daemon is already running');
|
|
530
1208
|
}
|
|
531
|
-
if (
|
|
532
|
-
|
|
533
|
-
|
|
1209
|
+
if (liveness === 'unknown') {
|
|
1210
|
+
const pid = readDaemonPid(dir);
|
|
1211
|
+
throw new Error(`Unable to determine cron daemon liveness (pid=${pid ?? 'unknown'}, pidFile=${resolvePidFile(dir)}); refusing to start`);
|
|
534
1212
|
}
|
|
1213
|
+
return spawnDaemon(dir, probe.spawnImpl);
|
|
1214
|
+
}
|
|
1215
|
+
function stopDaemon(dir, probe = DEFAULT_DAEMON_PROCESS_PROBE) {
|
|
1216
|
+
if (inspectDaemon(dir, probe) !== 'running')
|
|
1217
|
+
return false;
|
|
1218
|
+
const pid = readDaemonPid(dir);
|
|
1219
|
+
if (!pid)
|
|
1220
|
+
return false;
|
|
535
1221
|
process.kill(pid, 'SIGTERM');
|
|
536
1222
|
return true;
|
|
537
1223
|
}
|
|
538
|
-
function
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
1224
|
+
function startDaemonSafe(dir, probe = DEFAULT_DAEMON_PROCESS_PROBE) {
|
|
1225
|
+
const liveness = inspectDaemon(dir, probe);
|
|
1226
|
+
if (liveness === 'running') {
|
|
1227
|
+
return { started: false };
|
|
1228
|
+
}
|
|
1229
|
+
if (liveness === 'unknown') {
|
|
1230
|
+
return {
|
|
1231
|
+
started: false,
|
|
1232
|
+
error: `Unable to determine cron daemon liveness (pid=${readDaemonPid(dir) ?? 'unknown'}, pidFile=${resolvePidFile(dir)}); refusing to start`,
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
542
1235
|
try {
|
|
543
|
-
|
|
1236
|
+
const pid = spawnDaemon(dir, probe.spawnImpl);
|
|
1237
|
+
return { started: true, pid };
|
|
544
1238
|
}
|
|
545
1239
|
catch (e) {
|
|
546
1240
|
const err = e;
|
|
547
|
-
|
|
1241
|
+
return { started: false, error: err.message };
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
function ensureDaemonRunning(dir, probe = DEFAULT_DAEMON_PROCESS_PROBE) {
|
|
1245
|
+
if (isDaemonRunning(dir, probe))
|
|
1246
|
+
return;
|
|
1247
|
+
console.log('⚡ Cron daemon is not running; starting it automatically...');
|
|
1248
|
+
const result = startDaemonSafe(dir, probe);
|
|
1249
|
+
if (!result.started) {
|
|
1250
|
+
console.error('Failed to auto-start daemon:', result.error);
|
|
548
1251
|
process.exit(1);
|
|
549
1252
|
}
|
|
550
1253
|
}
|
|
551
1254
|
function getDaemonStatus(dir) {
|
|
552
|
-
const schedules =
|
|
1255
|
+
const schedules = createScheduleStore(dir ?? DEFAULT_WORKSPACE_PATH).list();
|
|
553
1256
|
const enabled = schedules.filter(s => s.enabled).length;
|
|
554
|
-
const
|
|
555
|
-
const
|
|
1257
|
+
const running = isDaemonRunning(dir);
|
|
1258
|
+
const pid = running ? readDaemonPid(dir) : null;
|
|
556
1259
|
let started_at = null;
|
|
557
1260
|
const pidFile = resolvePidFile(dir);
|
|
558
1261
|
if (running && fs.existsSync(pidFile)) {
|
|
@@ -573,34 +1276,162 @@ function getDaemonStatus(dir) {
|
|
|
573
1276
|
},
|
|
574
1277
|
};
|
|
575
1278
|
}
|
|
576
|
-
async function executeSchedule(schedule,
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
1279
|
+
async function executeSchedule(schedule, options) {
|
|
1280
|
+
const dir = options?.dir;
|
|
1281
|
+
const workspacePath = path.resolve(dir ?? DEFAULT_WORKSPACE_PATH);
|
|
1282
|
+
const trigger = options?.trigger ?? 'cron';
|
|
1283
|
+
const timeoutMs = schedule.timeout_ms || 600000;
|
|
1284
|
+
const taskId = (0, sdk_1.generateId)('tsk');
|
|
1285
|
+
let runLease;
|
|
1286
|
+
if (!schedule.allow_concurrent) {
|
|
1287
|
+
const lock = acquireRunLock(schedule.id, taskId, timeoutMs, dir);
|
|
1288
|
+
if (!lock.acquired) {
|
|
1289
|
+
console.log(`[${getLocalTimestamp()}] Schedule ${schedule.id} already running (task ${lock.runningTaskId}), skipping`);
|
|
1290
|
+
return {
|
|
1291
|
+
status: 'conflict',
|
|
1292
|
+
runningTaskId: lock.runningTaskId,
|
|
1293
|
+
reason: lock.reason,
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
runLease = lock.lease;
|
|
1297
|
+
}
|
|
1298
|
+
const releaseLease = () => {
|
|
1299
|
+
if (runLease) {
|
|
1300
|
+
releaseRunLock(schedule.id, runLease.leaseId, dir);
|
|
1301
|
+
runLease = undefined;
|
|
1302
|
+
}
|
|
1303
|
+
releaseAdmissionReservation(taskId);
|
|
1304
|
+
};
|
|
1305
|
+
if (!reserveAdmission(taskId, schedule.id, timeoutMs)) {
|
|
1306
|
+
console.log(`[${getLocalTimestamp()}] Schedule ${schedule.id} blocked: could not durably record the update-admission reservation`);
|
|
1307
|
+
releaseLease();
|
|
1308
|
+
return { status: 'blocked', reason: 'ADMISSION_UNKNOWN' };
|
|
1309
|
+
}
|
|
1310
|
+
if ((0, sdk_1.isDurablyQuiescing)()) {
|
|
1311
|
+
console.log(`[${getLocalTimestamp()}] Schedule ${schedule.id} blocked: a desktop update is draining active work`);
|
|
1312
|
+
releaseLease();
|
|
1313
|
+
return { status: 'blocked', reason: 'UPDATE_QUIESCING' };
|
|
1314
|
+
}
|
|
1315
|
+
try {
|
|
1316
|
+
const store = createScheduleStore(workspacePath);
|
|
1317
|
+
const startedAt = new Date().toISOString();
|
|
1318
|
+
if (options?.occurrenceKey !== undefined) {
|
|
1319
|
+
const claim = store.claimOccurrence(schedule.id, {
|
|
1320
|
+
occurrenceKey: options.occurrenceKey,
|
|
1321
|
+
startedAt,
|
|
1322
|
+
});
|
|
1323
|
+
if (!claim.claimed) {
|
|
1324
|
+
releaseLease();
|
|
1325
|
+
console.log(`[${getLocalTimestamp()}] Schedule ${schedule.id} occurrence ${options.occurrenceKey} already claimed, skipping`);
|
|
1326
|
+
return {
|
|
1327
|
+
status: 'conflict',
|
|
1328
|
+
runningTaskId: '',
|
|
1329
|
+
reason: 'occurrence_claim',
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
else {
|
|
1334
|
+
if (store.get(schedule.id)) {
|
|
1335
|
+
store.recordStarted(schedule.id, { startedAt });
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
580
1338
|
}
|
|
581
|
-
|
|
582
|
-
|
|
1339
|
+
catch (error) {
|
|
1340
|
+
releaseLease();
|
|
1341
|
+
throw error;
|
|
1342
|
+
}
|
|
1343
|
+
const workingDir = schedule.working_directory || options?.dir || (0, builtins_1.getProjectRoot)();
|
|
583
1344
|
const mode = schedule.mode;
|
|
584
1345
|
const modeFlag = mode === 'execute' ? 'x' : 'q';
|
|
585
|
-
|
|
1346
|
+
const effectiveModel = options?.overrides?.model ?? schedule.model;
|
|
1347
|
+
const effectiveProvider = options?.overrides?.provider ?? schedule.provider;
|
|
1348
|
+
console.log(`[${getLocalTimestamp()}] Executing schedule ${schedule.id}: ${schedule.name ?? schedule.cron} (task ${taskId})`);
|
|
586
1349
|
console.log(`[${getLocalTimestamp()}] Command: ${schedule.command}`);
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
1350
|
+
const env = (0, sdk_1.withoutCrewxRootContext)({ ...process.env, ...schedule.env, CREWX_TRACE_ID: taskId });
|
|
1351
|
+
delete env['CLAUDECODE'];
|
|
1352
|
+
if (!schedule.env || !('CREWX_CONFIG' in schedule.env)) {
|
|
1353
|
+
delete env['CREWX_CONFIG'];
|
|
1354
|
+
}
|
|
1355
|
+
env['CREWX_WORKSPACE'] = workingDir;
|
|
1356
|
+
const cliArgs = [modeFlag, schedule.command];
|
|
1357
|
+
if (effectiveModel) {
|
|
1358
|
+
cliArgs.push('--model', effectiveModel);
|
|
1359
|
+
}
|
|
1360
|
+
if (mode !== 'command' && effectiveProvider) {
|
|
1361
|
+
cliArgs.push('--provider', effectiveProvider);
|
|
1362
|
+
}
|
|
1363
|
+
cliArgs.push('--metadata', JSON.stringify({ trigger, schedule_id: schedule.id }));
|
|
1364
|
+
let child;
|
|
1365
|
+
try {
|
|
1366
|
+
if (mode === 'command') {
|
|
1367
|
+
child = (0, child_process_1.spawn)('sh', ['-c', schedule.command], {
|
|
592
1368
|
cwd: workingDir,
|
|
593
|
-
stdio: '
|
|
1369
|
+
stdio: 'ignore',
|
|
594
1370
|
windowsHide: true,
|
|
1371
|
+
detached: true,
|
|
595
1372
|
env,
|
|
596
|
-
})
|
|
597
|
-
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
else {
|
|
1376
|
+
const resolution = (0, sdk_1.resolveCrewxExecutable)({ env, cwd: workingDir });
|
|
1377
|
+
if (!resolution.ok) {
|
|
1378
|
+
const error = new Error((0, sdk_1.formatCrewxExecutableFailure)(resolution));
|
|
1379
|
+
Object.assign(error, { code: 'CLI_UNRESOLVABLE' });
|
|
1380
|
+
throw error;
|
|
1381
|
+
}
|
|
1382
|
+
const childInvocation = (0, sdk_1.createCrewxChildInvocation)(resolution, { baseEnv: env });
|
|
1383
|
+
const program = (0, sdk_1.resolveWindowsSpawnProgram)({
|
|
1384
|
+
command: childInvocation.command,
|
|
1385
|
+
env: childInvocation.env,
|
|
598
1386
|
cwd: workingDir,
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
1387
|
+
packageName: 'crewx',
|
|
1388
|
+
allowShellFallback: true,
|
|
1389
|
+
});
|
|
1390
|
+
const invocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, [
|
|
1391
|
+
...childInvocation.args,
|
|
1392
|
+
...cliArgs,
|
|
1393
|
+
]);
|
|
1394
|
+
child = (0, child_process_1.spawn)(invocation.command, invocation.argv, {
|
|
1395
|
+
cwd: workingDir,
|
|
1396
|
+
stdio: 'ignore',
|
|
1397
|
+
windowsHide: invocation.windowsHide,
|
|
1398
|
+
detached: true,
|
|
1399
|
+
env: childInvocation.env,
|
|
1400
|
+
shell: invocation.shell ?? false,
|
|
602
1401
|
});
|
|
1402
|
+
}
|
|
1403
|
+
await new Promise((resolve, reject) => {
|
|
1404
|
+
child.once('spawn', () => resolve());
|
|
1405
|
+
child.once('error', (err) => reject(err));
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
catch (e) {
|
|
1409
|
+
const err = e;
|
|
1410
|
+
console.error(`[${getLocalTimestamp()}] Schedule ${schedule.id} failed to spawn: ${err.message}`);
|
|
1411
|
+
releaseLease();
|
|
1412
|
+
throw err;
|
|
1413
|
+
}
|
|
1414
|
+
if (runLease && child.pid) {
|
|
1415
|
+
const handedOff = handoffRunLock(runLease, child.pid, captureProcessIdentity(child.pid));
|
|
1416
|
+
if (!handedOff) {
|
|
1417
|
+
const error = new Error('Cron execution lease was replaced before child ownership was established');
|
|
1418
|
+
console.error(`[${getLocalTimestamp()}] Schedule ${schedule.id} failed to hand off its execution lease: ${error.message}`);
|
|
1419
|
+
try {
|
|
1420
|
+
child.kill('SIGTERM');
|
|
1421
|
+
}
|
|
1422
|
+
catch {
|
|
1423
|
+
}
|
|
1424
|
+
releaseLease();
|
|
1425
|
+
throw error;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
child.unref();
|
|
1429
|
+
const done = new Promise((resolve) => {
|
|
1430
|
+
let timedOut = false;
|
|
1431
|
+
let settled = false;
|
|
1432
|
+
let graceTimer;
|
|
603
1433
|
const timeout = setTimeout(() => {
|
|
1434
|
+
timedOut = true;
|
|
604
1435
|
console.log(`[${getLocalTimestamp()}] Schedule ${schedule.id} timeout, killing process`);
|
|
605
1436
|
try {
|
|
606
1437
|
child.kill('SIGTERM');
|
|
@@ -609,37 +1440,72 @@ async function executeSchedule(schedule, dir) {
|
|
|
609
1440
|
const err = e;
|
|
610
1441
|
console.error(`Failed to kill process: ${err.message}`);
|
|
611
1442
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
1443
|
+
graceTimer = setTimeout(() => {
|
|
1444
|
+
try {
|
|
1445
|
+
child.kill('SIGKILL');
|
|
1446
|
+
}
|
|
1447
|
+
catch {
|
|
1448
|
+
}
|
|
1449
|
+
}, RUN_LOCK_STALE_GRACE_MS);
|
|
1450
|
+
if (typeof graceTimer === 'object' && graceTimer !== null && 'unref' in graceTimer) {
|
|
1451
|
+
graceTimer.unref();
|
|
1452
|
+
}
|
|
1453
|
+
if (!settled) {
|
|
1454
|
+
settled = true;
|
|
1455
|
+
resolve({ success: false, error: 'Timeout' });
|
|
1456
|
+
}
|
|
1457
|
+
}, timeoutMs);
|
|
615
1458
|
child.on('close', (code) => {
|
|
616
1459
|
clearTimeout(timeout);
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
1460
|
+
if (graceTimer)
|
|
1461
|
+
clearTimeout(graceTimer);
|
|
1462
|
+
releaseLease();
|
|
1463
|
+
const finishedAt = new Date().toISOString();
|
|
1464
|
+
const outcome = timedOut ? 'timeout' : code === 0 ? 'success' : 'failure';
|
|
1465
|
+
try {
|
|
1466
|
+
createScheduleStore(workspacePath).recordFinished(schedule.id, {
|
|
1467
|
+
finishedAt,
|
|
1468
|
+
outcome,
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
catch (error) {
|
|
1472
|
+
const err = error;
|
|
1473
|
+
if (!(error instanceof types_1.CronStoreError && error.code === 'CRON_SCHEDULE_NOT_FOUND')) {
|
|
1474
|
+
console.error(`[${getLocalTimestamp()}] Failed to record schedule ${schedule.id} completion: ${err.message}`);
|
|
1475
|
+
}
|
|
626
1476
|
}
|
|
627
|
-
if (
|
|
1477
|
+
if (timedOut) {
|
|
1478
|
+
console.log(`[${getLocalTimestamp()}] Schedule ${schedule.id} terminated after timeout`);
|
|
1479
|
+
if (!settled) {
|
|
1480
|
+
settled = true;
|
|
1481
|
+
resolve({ success: false, error: 'Timeout' });
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
else if (code === 0) {
|
|
628
1485
|
console.log(`[${getLocalTimestamp()}] Schedule ${schedule.id} completed successfully`);
|
|
629
|
-
|
|
1486
|
+
if (!settled) {
|
|
1487
|
+
settled = true;
|
|
1488
|
+
resolve({ success: true });
|
|
1489
|
+
}
|
|
630
1490
|
}
|
|
631
1491
|
else {
|
|
632
1492
|
console.log(`[${getLocalTimestamp()}] Schedule ${schedule.id} failed with code ${code}`);
|
|
633
|
-
|
|
1493
|
+
if (!settled) {
|
|
1494
|
+
settled = true;
|
|
1495
|
+
resolve({ success: false, error: `Exit code: ${code}` });
|
|
1496
|
+
}
|
|
634
1497
|
}
|
|
635
1498
|
});
|
|
636
1499
|
child.on('error', (err) => {
|
|
637
1500
|
clearTimeout(timeout);
|
|
638
|
-
runningSchedules.delete(schedule.id);
|
|
639
1501
|
console.error(`[${getLocalTimestamp()}] Schedule ${schedule.id} error: ${err.message}`);
|
|
640
|
-
|
|
1502
|
+
if (!settled) {
|
|
1503
|
+
settled = true;
|
|
1504
|
+
resolve({ success: false, error: err.message });
|
|
1505
|
+
}
|
|
641
1506
|
});
|
|
642
1507
|
});
|
|
1508
|
+
return { status: 'running', taskId, done };
|
|
643
1509
|
}
|
|
644
1510
|
function getLocalMinuteKey(now, timezone) {
|
|
645
1511
|
if (!timezone || !isValidTimezone(timezone)) {
|
|
@@ -652,9 +1518,16 @@ function getLocalMinuteKey(now, timezone) {
|
|
|
652
1518
|
});
|
|
653
1519
|
return fmt.format(now);
|
|
654
1520
|
}
|
|
1521
|
+
function buildOccurrenceKey(cronExpression, scheduledMinute) {
|
|
1522
|
+
return `${cronExpression.trim()}|${scheduledMinute}`;
|
|
1523
|
+
}
|
|
655
1524
|
async function daemonTick(dir) {
|
|
1525
|
+
if ((0, sdk_1.isDurablyQuiescing)()) {
|
|
1526
|
+
console.log(`[${getLocalTimestamp()}] Daemon tick skipped: a desktop update is draining active work`);
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
656
1529
|
const now = new Date();
|
|
657
|
-
const schedules =
|
|
1530
|
+
const schedules = createScheduleStore(dir ?? DEFAULT_WORKSPACE_PATH).list();
|
|
658
1531
|
const enabledSchedules = schedules.filter(s => s.enabled);
|
|
659
1532
|
if (enabledSchedules.length === 0) {
|
|
660
1533
|
return;
|
|
@@ -662,16 +1535,20 @@ async function daemonTick(dir) {
|
|
|
662
1535
|
for (const schedule of enabledSchedules) {
|
|
663
1536
|
if (shouldRunNow(schedule.cron, now, schedule.timezone)) {
|
|
664
1537
|
const minuteKey = getLocalMinuteKey(now, schedule.timezone);
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
1538
|
+
const occurrenceKey = buildOccurrenceKey(schedule.cron, minuteKey);
|
|
1539
|
+
try {
|
|
1540
|
+
await executeSchedule(schedule, { dir, occurrenceKey });
|
|
1541
|
+
}
|
|
1542
|
+
catch (e) {
|
|
1543
|
+
const err = e;
|
|
1544
|
+
console.error(`[${getLocalTimestamp()}] Schedule ${schedule.id} failed to spawn: ${err.message}`);
|
|
670
1545
|
}
|
|
671
|
-
await executeSchedule(schedule, dir);
|
|
672
1546
|
}
|
|
673
1547
|
}
|
|
674
1548
|
}
|
|
1549
|
+
var runtime_1 = require("./runtime");
|
|
1550
|
+
Object.defineProperty(exports, "createCronRuntime", { enumerable: true, get: function () { return runtime_1.createCronRuntime; } });
|
|
1551
|
+
Object.defineProperty(exports, "CRON_RUNTIME_PROCESS_LEASE_FILENAME", { enumerable: true, get: function () { return runtime_1.CRON_RUNTIME_PROCESS_LEASE_FILENAME; } });
|
|
675
1552
|
function runDaemonForeground(dir) {
|
|
676
1553
|
writeDaemonPid(process.pid, dir);
|
|
677
1554
|
let cleanedUp = false;
|
|
@@ -696,7 +1573,7 @@ function runDaemonForeground(dir) {
|
|
|
696
1573
|
console.log(`[${getLocalTimestamp()}] Cron daemon starting in foreground mode`);
|
|
697
1574
|
console.log(`[${getLocalTimestamp()}] Check interval: 1 minute`);
|
|
698
1575
|
console.log(`[${getLocalTimestamp()}] Press Ctrl+C to stop\n`);
|
|
699
|
-
const schedules =
|
|
1576
|
+
const schedules = createScheduleStore(dir ?? DEFAULT_WORKSPACE_PATH).list();
|
|
700
1577
|
const enabled = schedules.filter(s => s.enabled).length;
|
|
701
1578
|
console.log(`[${getLocalTimestamp()}] Active schedules: ${enabled}/${schedules.length}\n`);
|
|
702
1579
|
void daemonTick(dir);
|
|
@@ -708,6 +1585,7 @@ function addSchedule(cron, command, options, dataDir) {
|
|
|
708
1585
|
if (options.timezone && !isValidTimezone(options.timezone)) {
|
|
709
1586
|
throw new Error(`Invalid timezone: ${options.timezone}`);
|
|
710
1587
|
}
|
|
1588
|
+
warnIfOverdriveInCommand(command);
|
|
711
1589
|
const schedule = {
|
|
712
1590
|
id: (0, sdk_1.generateId)('cron'),
|
|
713
1591
|
cron,
|
|
@@ -723,97 +1601,102 @@ function addSchedule(cron, command, options, dataDir) {
|
|
|
723
1601
|
updated_at: new Date().toISOString(),
|
|
724
1602
|
run_count: 0,
|
|
725
1603
|
};
|
|
1604
|
+
if (options.model) {
|
|
1605
|
+
schedule.model = options.model;
|
|
1606
|
+
}
|
|
1607
|
+
if (options.provider) {
|
|
1608
|
+
schedule.provider = options.provider;
|
|
1609
|
+
}
|
|
726
1610
|
schedule.next_run = calculateNextRun(cron, schedule.timezone);
|
|
727
|
-
|
|
728
|
-
schedules.push(schedule);
|
|
729
|
-
saveSchedules(schedules, dataDir);
|
|
730
|
-
return schedule;
|
|
1611
|
+
return createScheduleStore(dataDir ?? DEFAULT_WORKSPACE_PATH).create(schedule);
|
|
731
1612
|
}
|
|
732
1613
|
function removeSchedule(id, dir) {
|
|
733
|
-
const
|
|
734
|
-
const
|
|
735
|
-
if (
|
|
736
|
-
|
|
1614
|
+
const removed = createScheduleStore(dir ?? DEFAULT_WORKSPACE_PATH).remove(id);
|
|
1615
|
+
const matchedBuiltin = (0, builtins_1.findBuiltinByName)(removed.name);
|
|
1616
|
+
if (matchedBuiltin) {
|
|
1617
|
+
recordRemovedBuiltin(matchedBuiltin.builtin_id, dir);
|
|
737
1618
|
}
|
|
738
|
-
const removed = schedules.splice(idx, 1)[0];
|
|
739
|
-
saveSchedules(schedules, dir);
|
|
740
1619
|
return removed;
|
|
741
1620
|
}
|
|
742
1621
|
function enableSchedule(id, dir) {
|
|
743
|
-
|
|
744
|
-
const schedule = schedules.find(s => s.id === id);
|
|
745
|
-
if (!schedule) {
|
|
746
|
-
throw new Error(`Schedule not found: ${id}`);
|
|
747
|
-
}
|
|
748
|
-
schedule.enabled = true;
|
|
749
|
-
schedule.updated_at = new Date().toISOString();
|
|
750
|
-
schedule.next_run = calculateNextRun(schedule.cron, schedule.timezone);
|
|
751
|
-
saveSchedules(schedules, dir);
|
|
752
|
-
return schedule;
|
|
1622
|
+
return createScheduleStore(dir ?? DEFAULT_WORKSPACE_PATH).toggle(id, true);
|
|
753
1623
|
}
|
|
754
1624
|
function disableSchedule(id, dir) {
|
|
755
|
-
|
|
756
|
-
const schedule = schedules.find(s => s.id === id);
|
|
757
|
-
if (!schedule) {
|
|
758
|
-
throw new Error(`Schedule not found: ${id}`);
|
|
759
|
-
}
|
|
760
|
-
schedule.enabled = false;
|
|
761
|
-
schedule.updated_at = new Date().toISOString();
|
|
762
|
-
saveSchedules(schedules, dir);
|
|
763
|
-
return schedule;
|
|
1625
|
+
return createScheduleStore(dir ?? DEFAULT_WORKSPACE_PATH).toggle(id, false);
|
|
764
1626
|
}
|
|
765
1627
|
function updateSchedule(id, options, dir) {
|
|
766
|
-
const schedules = loadSchedules(dir);
|
|
767
|
-
const schedule = schedules.find(s => s.id === id);
|
|
768
|
-
if (!schedule) {
|
|
769
|
-
throw new Error(`Schedule not found: ${id}`);
|
|
770
|
-
}
|
|
771
1628
|
if (options.timezone !== undefined && options.timezone !== null && !isValidTimezone(options.timezone)) {
|
|
772
1629
|
throw new Error(`Invalid timezone: ${options.timezone}`);
|
|
773
1630
|
}
|
|
1631
|
+
const store = createScheduleStore(dir ?? DEFAULT_WORKSPACE_PATH);
|
|
1632
|
+
const current = store.get(id);
|
|
1633
|
+
if (!current) {
|
|
1634
|
+
throw new Error(`Schedule not found: ${id}`);
|
|
1635
|
+
}
|
|
774
1636
|
const changes = [];
|
|
1637
|
+
const patch = {};
|
|
775
1638
|
if (options.cron !== undefined) {
|
|
776
|
-
|
|
1639
|
+
patch.cron = options.cron;
|
|
777
1640
|
changes.push(`cron: ${options.cron}`);
|
|
778
1641
|
}
|
|
779
1642
|
if (options.command !== undefined) {
|
|
780
|
-
|
|
1643
|
+
warnIfOverdriveInCommand(options.command);
|
|
1644
|
+
patch.command = options.command;
|
|
781
1645
|
changes.push(`command: ${options.command}`);
|
|
782
1646
|
}
|
|
783
1647
|
if (options.name !== undefined) {
|
|
784
|
-
|
|
1648
|
+
patch.name = options.name;
|
|
785
1649
|
changes.push(`name: ${options.name}`);
|
|
786
1650
|
}
|
|
787
1651
|
if (options.mode !== undefined) {
|
|
788
|
-
|
|
789
|
-
changes.push(`mode: ${
|
|
1652
|
+
patch.mode = normalizeMode(options.mode);
|
|
1653
|
+
changes.push(`mode: ${patch.mode}`);
|
|
790
1654
|
}
|
|
791
1655
|
if (options.timezone !== undefined) {
|
|
792
|
-
|
|
1656
|
+
patch.timezone = options.timezone;
|
|
793
1657
|
changes.push(`timezone: ${options.timezone}`);
|
|
794
1658
|
}
|
|
795
1659
|
if (options.working_directory !== undefined) {
|
|
796
|
-
|
|
1660
|
+
patch.working_directory = options.working_directory;
|
|
797
1661
|
changes.push(`working_directory: ${options.working_directory}`);
|
|
798
1662
|
}
|
|
799
1663
|
if (options.timeout_ms !== undefined) {
|
|
800
|
-
|
|
1664
|
+
patch.timeout_ms = options.timeout_ms;
|
|
801
1665
|
changes.push(`timeout: ${options.timeout_ms}ms`);
|
|
802
1666
|
}
|
|
803
1667
|
if (options.allow_concurrent !== undefined) {
|
|
804
|
-
|
|
1668
|
+
patch.allow_concurrent = options.allow_concurrent;
|
|
805
1669
|
changes.push(`allow_concurrent: ${options.allow_concurrent}`);
|
|
806
1670
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
1671
|
+
if (options.model !== undefined) {
|
|
1672
|
+
if (options.model === null) {
|
|
1673
|
+
patch.model = null;
|
|
1674
|
+
changes.push('model: (cleared, reverts to agent default)');
|
|
1675
|
+
}
|
|
1676
|
+
else {
|
|
1677
|
+
patch.model = options.model;
|
|
1678
|
+
changes.push(`model: ${options.model}`);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
if (options.provider !== undefined) {
|
|
1682
|
+
if (options.provider === null) {
|
|
1683
|
+
patch.provider = null;
|
|
1684
|
+
changes.push('provider: (cleared, reverts to agent default)');
|
|
1685
|
+
}
|
|
1686
|
+
else {
|
|
1687
|
+
patch.provider = options.provider;
|
|
1688
|
+
changes.push(`provider: ${options.provider}`);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
return { schedule: store.update(current.id, patch), changes };
|
|
811
1692
|
}
|
|
812
1693
|
exports.DATA_PATHS = {
|
|
813
1694
|
DATA_DIR,
|
|
814
1695
|
SCHEDULES_FILE,
|
|
815
1696
|
LOG_FILE,
|
|
1697
|
+
LOCK_FILE,
|
|
816
1698
|
USAGE_LOG_FILE,
|
|
817
1699
|
PID_FILE,
|
|
1700
|
+
BUILTINS_META_FILE,
|
|
818
1701
|
};
|
|
819
1702
|
//# sourceMappingURL=engine.js.map
|