@crewx/cron 0.1.10-rc.145 → 0.1.10-rc.147
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 +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +10 -42
- package/dist/cli.js.map +1 -1
- package/dist/src/engine.d.ts +17 -5
- package/dist/src/engine.d.ts.map +1 -1
- package/dist/src/engine.js +740 -199
- 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 +63 -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/src/engine.js
CHANGED
|
@@ -33,16 +33,21 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.DATA_PATHS = exports.getProjectRoot = exports.BUILTIN_SCHEDULES = 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.acquireWorkspaceLock = acquireWorkspaceLock;
|
|
40
|
+
exports.releaseWorkspaceLock = releaseWorkspaceLock;
|
|
41
|
+
exports.migrateLegacyMemoryDebounceSchedule = migrateLegacyMemoryDebounceSchedule;
|
|
39
42
|
exports.normalizeMode = normalizeMode;
|
|
43
|
+
exports.createScheduleStore = createScheduleStore;
|
|
40
44
|
exports.loadSchedules = loadSchedules;
|
|
41
45
|
exports.saveSchedules = saveSchedules;
|
|
42
46
|
exports.getRemovedBuiltins = getRemovedBuiltins;
|
|
43
47
|
exports.seedBuiltinSchedules = seedBuiltinSchedules;
|
|
44
48
|
exports.acquireLock = acquireLock;
|
|
45
49
|
exports.releaseLock = releaseLock;
|
|
50
|
+
exports.releaseRunLock = releaseRunLock;
|
|
46
51
|
exports.shouldRunNow = shouldRunNow;
|
|
47
52
|
exports.describeCron = describeCron;
|
|
48
53
|
exports.calculateNextRun = calculateNextRun;
|
|
@@ -53,6 +58,8 @@ exports.startDaemonSafe = startDaemonSafe;
|
|
|
53
58
|
exports.ensureDaemonRunning = ensureDaemonRunning;
|
|
54
59
|
exports.getDaemonStatus = getDaemonStatus;
|
|
55
60
|
exports.executeSchedule = executeSchedule;
|
|
61
|
+
exports.getLocalMinuteKey = getLocalMinuteKey;
|
|
62
|
+
exports.buildOccurrenceKey = buildOccurrenceKey;
|
|
56
63
|
exports.daemonTick = daemonTick;
|
|
57
64
|
exports.runDaemonForeground = runDaemonForeground;
|
|
58
65
|
exports.addSchedule = addSchedule;
|
|
@@ -61,13 +68,17 @@ exports.enableSchedule = enableSchedule;
|
|
|
61
68
|
exports.disableSchedule = disableSchedule;
|
|
62
69
|
exports.updateSchedule = updateSchedule;
|
|
63
70
|
const child_process_1 = require("child_process");
|
|
71
|
+
const crypto_1 = require("crypto");
|
|
64
72
|
const path = __importStar(require("path"));
|
|
65
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; } });
|
|
66
76
|
const sdk_1 = require("@crewx/sdk");
|
|
67
77
|
const builtins_1 = require("./builtins");
|
|
68
78
|
Object.defineProperty(exports, "BUILTIN_SCHEDULES", { enumerable: true, get: function () { return builtins_1.BUILTIN_SCHEDULES; } });
|
|
69
79
|
Object.defineProperty(exports, "getProjectRoot", { enumerable: true, get: function () { return builtins_1.getProjectRoot; } });
|
|
70
|
-
const
|
|
80
|
+
const DEFAULT_WORKSPACE_PATH = path.resolve(process.cwd());
|
|
81
|
+
const DATA_DIR = path.join(DEFAULT_WORKSPACE_PATH, '.crewx');
|
|
71
82
|
const SCHEDULES_FILE = path.join(DATA_DIR, '.cron-data.json');
|
|
72
83
|
const LOG_FILE = path.join(DATA_DIR, '.cron-daemon.log');
|
|
73
84
|
const LOCK_FILE = path.join(DATA_DIR, '.cron-data.json.lock');
|
|
@@ -75,11 +86,17 @@ const USAGE_LOG_FILE = path.join(DATA_DIR, 'cron-usage.log');
|
|
|
75
86
|
const PID_FILE = path.join(DATA_DIR, '.cron-daemon.pid');
|
|
76
87
|
const BUILTINS_META_FILE = path.join(DATA_DIR, '.cron-builtins.json');
|
|
77
88
|
function resolveDataDir(dir) {
|
|
78
|
-
return
|
|
89
|
+
return path.join(path.resolve(dir ?? DEFAULT_WORKSPACE_PATH), '.crewx');
|
|
79
90
|
}
|
|
80
91
|
function resolveSchedulesFile(dir) {
|
|
81
92
|
return path.join(resolveDataDir(dir), '.cron-data.json');
|
|
82
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
|
+
}
|
|
83
100
|
function resolvePidFile(dir) {
|
|
84
101
|
return path.join(resolveDataDir(dir), '.cron-daemon.pid');
|
|
85
102
|
}
|
|
@@ -100,6 +117,9 @@ function resolveRunLockFile(scheduleId, dir) {
|
|
|
100
117
|
return path.join(resolveDataDir(dir), `.cron-run-${scheduleId}.lock`);
|
|
101
118
|
}
|
|
102
119
|
const DAEMON_INTERVAL_MS = 60 * 1000;
|
|
120
|
+
const STORE_LOCK_TIMEOUT_MS = 5000;
|
|
121
|
+
const STORE_LOCK_POLL_MS = 25;
|
|
122
|
+
const STORE_LOCK_STALE_MS = 30000;
|
|
103
123
|
const RUN_LOCK_STALE_GRACE_MS = 5000;
|
|
104
124
|
function getLocalTimestamp() {
|
|
105
125
|
const now = new Date();
|
|
@@ -111,12 +131,45 @@ function getLocalTimestamp() {
|
|
|
111
131
|
const s = String(now.getSeconds()).padStart(2, '0');
|
|
112
132
|
return `${y}-${m}-${d} ${h}:${min}:${s}`;
|
|
113
133
|
}
|
|
114
|
-
function logUsage(...args) {
|
|
115
|
-
|
|
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);
|
|
116
169
|
const timestamp = getLocalTimestamp();
|
|
117
|
-
const message =
|
|
170
|
+
const message = messageParts.join(' ');
|
|
118
171
|
const logLine = `${timestamp} | [cron] ${message}\n`;
|
|
119
|
-
fs.appendFileSync(
|
|
172
|
+
fs.appendFileSync(resolveUsageLogFile(workspacePath), logLine);
|
|
120
173
|
}
|
|
121
174
|
function ensureDataDir(dir) {
|
|
122
175
|
const dataDir = resolveDataDir(dir);
|
|
@@ -132,6 +185,76 @@ function safeReadJsonFile(filePath) {
|
|
|
132
185
|
return null;
|
|
133
186
|
}
|
|
134
187
|
}
|
|
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) {
|
|
193
|
+
try {
|
|
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;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
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
|
+
}
|
|
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
|
+
}
|
|
135
258
|
const MEMORY_DEBOUNCE_BUILTIN = (0, builtins_1.findBuiltinById)('memory-debounce');
|
|
136
259
|
function maybeRewriteLegacyMemoryCommand(command) {
|
|
137
260
|
if (!command.includes('/skills/memory-v2/memory-v2.js')) {
|
|
@@ -139,9 +262,9 @@ function maybeRewriteLegacyMemoryCommand(command) {
|
|
|
139
262
|
}
|
|
140
263
|
return MEMORY_DEBOUNCE_BUILTIN.buildCommand();
|
|
141
264
|
}
|
|
142
|
-
function migrateLegacyMemoryDebounceSchedule() {
|
|
143
|
-
|
|
144
|
-
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');
|
|
145
268
|
if (!fs.existsSync(legacyFile)) {
|
|
146
269
|
return;
|
|
147
270
|
}
|
|
@@ -153,7 +276,8 @@ function migrateLegacyMemoryDebounceSchedule() {
|
|
|
153
276
|
if (!legacySchedule) {
|
|
154
277
|
return;
|
|
155
278
|
}
|
|
156
|
-
const
|
|
279
|
+
const store = createScheduleStore(normalizedWorkspacePath);
|
|
280
|
+
const targetSchedules = store.list();
|
|
157
281
|
if (targetSchedules.some(s => s.name === MEMORY_DEBOUNCE_BUILTIN.name)) {
|
|
158
282
|
return;
|
|
159
283
|
}
|
|
@@ -163,7 +287,7 @@ function migrateLegacyMemoryDebounceSchedule() {
|
|
|
163
287
|
updated_at: new Date().toISOString(),
|
|
164
288
|
next_run: calculateNextRun(legacySchedule.cron, legacySchedule.timezone),
|
|
165
289
|
};
|
|
166
|
-
|
|
290
|
+
store.create(migratedSchedule);
|
|
167
291
|
}
|
|
168
292
|
function warnIfOverdriveInCommand(command) {
|
|
169
293
|
if (command.includes('--overdrive')) {
|
|
@@ -177,31 +301,255 @@ function normalizeMode(mode) {
|
|
|
177
301
|
return 'command';
|
|
178
302
|
return 'execute';
|
|
179
303
|
}
|
|
180
|
-
function
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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');
|
|
184
314
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
315
|
+
catch (error) {
|
|
316
|
+
const err = error;
|
|
317
|
+
if (err.code === 'ENOENT')
|
|
318
|
+
return [];
|
|
319
|
+
throw error;
|
|
188
320
|
}
|
|
321
|
+
let parsed;
|
|
189
322
|
try {
|
|
190
|
-
|
|
191
|
-
return JSON.parse(data);
|
|
323
|
+
parsed = JSON.parse(data);
|
|
192
324
|
}
|
|
193
|
-
catch (
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
325
|
+
catch (error) {
|
|
326
|
+
throw new types_1.CronStoreError('CRON_STORE_CORRUPT', `Cron schedule store is not valid JSON: ${schedulesFile}`, schedulesFile);
|
|
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;
|
|
332
|
+
}
|
|
333
|
+
function syncDirectory(directory) {
|
|
334
|
+
if (process.platform === 'win32')
|
|
335
|
+
return;
|
|
336
|
+
try {
|
|
337
|
+
const fd = fs.openSync(directory, 'r');
|
|
338
|
+
try {
|
|
339
|
+
fs.fsyncSync(fd);
|
|
340
|
+
}
|
|
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);
|
|
197
395
|
}
|
|
198
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}`);
|
|
418
|
+
}
|
|
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 };
|
|
449
|
+
}
|
|
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
|
+
}
|
|
199
542
|
function saveSchedules(schedules, dir) {
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
+
}
|
|
205
553
|
}
|
|
206
554
|
function loadBuiltinsMeta(dir) {
|
|
207
555
|
const data = safeReadJsonFile(resolveBuiltinsMetaFile(dir));
|
|
@@ -225,7 +573,8 @@ function recordRemovedBuiltin(builtinId, dir) {
|
|
|
225
573
|
}
|
|
226
574
|
}
|
|
227
575
|
function seedBuiltinSchedules(dir) {
|
|
228
|
-
const
|
|
576
|
+
const store = createScheduleStore(dir ?? DEFAULT_WORKSPACE_PATH);
|
|
577
|
+
const schedules = store.list();
|
|
229
578
|
const existingNames = new Set(schedules.map(s => s.name));
|
|
230
579
|
const removedBuiltins = new Set(getRemovedBuiltins(dir));
|
|
231
580
|
const seeded = [];
|
|
@@ -235,7 +584,7 @@ function seedBuiltinSchedules(dir) {
|
|
|
235
584
|
if (removedBuiltins.has(def.builtin_id))
|
|
236
585
|
continue;
|
|
237
586
|
const now = new Date().toISOString();
|
|
238
|
-
|
|
587
|
+
const schedule = {
|
|
239
588
|
id: (0, sdk_1.generateId)('cron'),
|
|
240
589
|
cron: def.cron,
|
|
241
590
|
command: def.buildCommand(),
|
|
@@ -250,102 +599,207 @@ function seedBuiltinSchedules(dir) {
|
|
|
250
599
|
updated_at: now,
|
|
251
600
|
run_count: 0,
|
|
252
601
|
next_run: calculateNextRun(def.cron, null),
|
|
253
|
-
}
|
|
602
|
+
};
|
|
603
|
+
store.create(schedule);
|
|
604
|
+
existingNames.add(def.name);
|
|
254
605
|
seeded.push(def.builtin_id);
|
|
255
606
|
}
|
|
256
|
-
if (seeded.length > 0) {
|
|
257
|
-
saveSchedules(schedules, dir);
|
|
258
|
-
}
|
|
259
607
|
return seeded;
|
|
260
608
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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);
|
|
286
649
|
}
|
|
650
|
+
catch { }
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
fs.unlinkSync(tempFile);
|
|
287
654
|
}
|
|
655
|
+
catch { }
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
function withRunLeaseStoreLock(workspacePath, scheduleId, operation) {
|
|
659
|
+
const owner = acquireWorkspaceLock(workspacePath);
|
|
660
|
+
try {
|
|
661
|
+
return operation(resolveRunLockFile(scheduleId, workspacePath));
|
|
662
|
+
}
|
|
663
|
+
finally {
|
|
664
|
+
releaseWorkspaceLock(owner);
|
|
288
665
|
}
|
|
289
|
-
return false;
|
|
290
666
|
}
|
|
291
|
-
function
|
|
667
|
+
function getLeaseAgeMs(lockFile) {
|
|
292
668
|
try {
|
|
293
|
-
fs.
|
|
669
|
+
return Math.max(0, Date.now() - fs.statSync(lockFile).mtimeMs);
|
|
294
670
|
}
|
|
295
671
|
catch {
|
|
672
|
+
return null;
|
|
296
673
|
}
|
|
297
674
|
}
|
|
298
|
-
function
|
|
299
|
-
|
|
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;
|
|
300
708
|
}
|
|
301
709
|
function acquireRunLock(scheduleId, taskId, timeoutMs, dir) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
catch { }
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
let ageMs;
|
|
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++) {
|
|
323
726
|
try {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
+
};
|
|
328
732
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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' };
|
|
333
740
|
}
|
|
334
|
-
|
|
335
|
-
|
|
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
|
+
};
|
|
336
757
|
}
|
|
337
|
-
return { acquired: false, runningTaskId: existing.taskId };
|
|
338
758
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
759
|
+
const existing = readRunLease(lockFile);
|
|
760
|
+
return {
|
|
761
|
+
acquired: false,
|
|
762
|
+
runningTaskId: existing?.taskId ?? 'unknown',
|
|
763
|
+
reason: 'execution_lease',
|
|
764
|
+
};
|
|
765
|
+
});
|
|
342
766
|
}
|
|
343
|
-
function
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
+
});
|
|
349
803
|
}
|
|
350
804
|
const DAY_NAMES = {
|
|
351
805
|
SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6,
|
|
@@ -622,6 +1076,30 @@ function getProcessCommandLine(pid) {
|
|
|
622
1076
|
return null;
|
|
623
1077
|
}
|
|
624
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;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
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
|
+
}
|
|
625
1103
|
const DEFAULT_DAEMON_PROCESS_PROBE = {
|
|
626
1104
|
isAlive: isPidAlive,
|
|
627
1105
|
getCommandLine: getProcessCommandLine,
|
|
@@ -702,11 +1180,16 @@ function spawnDaemon(dir, spawnImpl = child_process_1.spawn) {
|
|
|
702
1180
|
}
|
|
703
1181
|
ensureDataDir(dir);
|
|
704
1182
|
const cliPath = getDaemonCliPath();
|
|
705
|
-
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'], {
|
|
706
1188
|
cwd: dir || undefined,
|
|
707
1189
|
detached: true,
|
|
708
1190
|
stdio: 'ignore',
|
|
709
1191
|
windowsHide: true,
|
|
1192
|
+
env: daemonEnv,
|
|
710
1193
|
});
|
|
711
1194
|
if (!child.pid) {
|
|
712
1195
|
throw new Error('Failed to start daemon: no PID returned');
|
|
@@ -769,7 +1252,7 @@ function ensureDaemonRunning(dir, probe = DEFAULT_DAEMON_PROCESS_PROBE) {
|
|
|
769
1252
|
}
|
|
770
1253
|
}
|
|
771
1254
|
function getDaemonStatus(dir) {
|
|
772
|
-
const schedules =
|
|
1255
|
+
const schedules = createScheduleStore(dir ?? DEFAULT_WORKSPACE_PATH).list();
|
|
773
1256
|
const enabled = schedules.filter(s => s.enabled).length;
|
|
774
1257
|
const running = isDaemonRunning(dir);
|
|
775
1258
|
const pid = running ? readDaemonPid(dir) : null;
|
|
@@ -795,19 +1278,28 @@ function getDaemonStatus(dir) {
|
|
|
795
1278
|
}
|
|
796
1279
|
async function executeSchedule(schedule, options) {
|
|
797
1280
|
const dir = options?.dir;
|
|
1281
|
+
const workspacePath = path.resolve(dir ?? DEFAULT_WORKSPACE_PATH);
|
|
798
1282
|
const trigger = options?.trigger ?? 'cron';
|
|
799
1283
|
const timeoutMs = schedule.timeout_ms || 600000;
|
|
800
1284
|
const taskId = (0, sdk_1.generateId)('tsk');
|
|
1285
|
+
let runLease;
|
|
801
1286
|
if (!schedule.allow_concurrent) {
|
|
802
1287
|
const lock = acquireRunLock(schedule.id, taskId, timeoutMs, dir);
|
|
803
1288
|
if (!lock.acquired) {
|
|
804
1289
|
console.log(`[${getLocalTimestamp()}] Schedule ${schedule.id} already running (task ${lock.runningTaskId}), skipping`);
|
|
805
|
-
return {
|
|
1290
|
+
return {
|
|
1291
|
+
status: 'conflict',
|
|
1292
|
+
runningTaskId: lock.runningTaskId,
|
|
1293
|
+
reason: lock.reason,
|
|
1294
|
+
};
|
|
806
1295
|
}
|
|
1296
|
+
runLease = lock.lease;
|
|
807
1297
|
}
|
|
808
1298
|
const releaseLease = () => {
|
|
809
|
-
if (
|
|
810
|
-
releaseRunLock(schedule.id, dir);
|
|
1299
|
+
if (runLease) {
|
|
1300
|
+
releaseRunLock(schedule.id, runLease.leaseId, dir);
|
|
1301
|
+
runLease = undefined;
|
|
1302
|
+
}
|
|
811
1303
|
releaseAdmissionReservation(taskId);
|
|
812
1304
|
};
|
|
813
1305
|
if (!reserveAdmission(taskId, schedule.id, timeoutMs)) {
|
|
@@ -820,6 +1312,34 @@ async function executeSchedule(schedule, options) {
|
|
|
820
1312
|
releaseLease();
|
|
821
1313
|
return { status: 'blocked', reason: 'UPDATE_QUIESCING' };
|
|
822
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
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
catch (error) {
|
|
1340
|
+
releaseLease();
|
|
1341
|
+
throw error;
|
|
1342
|
+
}
|
|
823
1343
|
const workingDir = schedule.working_directory || options?.dir || (0, builtins_1.getProjectRoot)();
|
|
824
1344
|
const mode = schedule.mode;
|
|
825
1345
|
const modeFlag = mode === 'execute' ? 'x' : 'q';
|
|
@@ -859,18 +1379,16 @@ async function executeSchedule(schedule, options) {
|
|
|
859
1379
|
Object.assign(error, { code: 'CLI_UNRESOLVABLE' });
|
|
860
1380
|
throw error;
|
|
861
1381
|
}
|
|
862
|
-
const
|
|
863
|
-
...env,
|
|
864
|
-
CREWX_CLI_ARGV: JSON.stringify(resolution.argv),
|
|
865
|
-
};
|
|
1382
|
+
const childInvocation = (0, sdk_1.createCrewxChildInvocation)(resolution, { baseEnv: env });
|
|
866
1383
|
const program = (0, sdk_1.resolveWindowsSpawnProgram)({
|
|
867
|
-
command:
|
|
868
|
-
env:
|
|
1384
|
+
command: childInvocation.command,
|
|
1385
|
+
env: childInvocation.env,
|
|
869
1386
|
cwd: workingDir,
|
|
870
1387
|
packageName: 'crewx',
|
|
1388
|
+
allowShellFallback: true,
|
|
871
1389
|
});
|
|
872
1390
|
const invocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, [
|
|
873
|
-
...
|
|
1391
|
+
...childInvocation.args,
|
|
874
1392
|
...cliArgs,
|
|
875
1393
|
]);
|
|
876
1394
|
child = (0, child_process_1.spawn)(invocation.command, invocation.argv, {
|
|
@@ -878,7 +1396,7 @@ async function executeSchedule(schedule, options) {
|
|
|
878
1396
|
stdio: 'ignore',
|
|
879
1397
|
windowsHide: invocation.windowsHide,
|
|
880
1398
|
detached: true,
|
|
881
|
-
env:
|
|
1399
|
+
env: childInvocation.env,
|
|
882
1400
|
shell: invocation.shell ?? false,
|
|
883
1401
|
});
|
|
884
1402
|
}
|
|
@@ -893,9 +1411,27 @@ async function executeSchedule(schedule, options) {
|
|
|
893
1411
|
releaseLease();
|
|
894
1412
|
throw err;
|
|
895
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
|
+
}
|
|
896
1428
|
child.unref();
|
|
897
1429
|
const done = new Promise((resolve) => {
|
|
1430
|
+
let timedOut = false;
|
|
1431
|
+
let settled = false;
|
|
1432
|
+
let graceTimer;
|
|
898
1433
|
const timeout = setTimeout(() => {
|
|
1434
|
+
timedOut = true;
|
|
899
1435
|
console.log(`[${getLocalTimestamp()}] Schedule ${schedule.id} timeout, killing process`);
|
|
900
1436
|
try {
|
|
901
1437
|
child.kill('SIGTERM');
|
|
@@ -904,35 +1440,69 @@ async function executeSchedule(schedule, options) {
|
|
|
904
1440
|
const err = e;
|
|
905
1441
|
console.error(`Failed to kill process: ${err.message}`);
|
|
906
1442
|
}
|
|
907
|
-
|
|
908
|
-
|
|
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
|
+
}
|
|
909
1457
|
}, timeoutMs);
|
|
910
1458
|
child.on('close', (code) => {
|
|
911
1459
|
clearTimeout(timeout);
|
|
1460
|
+
if (graceTimer)
|
|
1461
|
+
clearTimeout(graceTimer);
|
|
912
1462
|
releaseLease();
|
|
913
|
-
const
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
+
}
|
|
1476
|
+
}
|
|
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
|
+
}
|
|
921
1483
|
}
|
|
922
|
-
if (code === 0) {
|
|
1484
|
+
else if (code === 0) {
|
|
923
1485
|
console.log(`[${getLocalTimestamp()}] Schedule ${schedule.id} completed successfully`);
|
|
924
|
-
|
|
1486
|
+
if (!settled) {
|
|
1487
|
+
settled = true;
|
|
1488
|
+
resolve({ success: true });
|
|
1489
|
+
}
|
|
925
1490
|
}
|
|
926
1491
|
else {
|
|
927
1492
|
console.log(`[${getLocalTimestamp()}] Schedule ${schedule.id} failed with code ${code}`);
|
|
928
|
-
|
|
1493
|
+
if (!settled) {
|
|
1494
|
+
settled = true;
|
|
1495
|
+
resolve({ success: false, error: `Exit code: ${code}` });
|
|
1496
|
+
}
|
|
929
1497
|
}
|
|
930
1498
|
});
|
|
931
1499
|
child.on('error', (err) => {
|
|
932
1500
|
clearTimeout(timeout);
|
|
933
|
-
releaseLease();
|
|
934
1501
|
console.error(`[${getLocalTimestamp()}] Schedule ${schedule.id} error: ${err.message}`);
|
|
935
|
-
|
|
1502
|
+
if (!settled) {
|
|
1503
|
+
settled = true;
|
|
1504
|
+
resolve({ success: false, error: err.message });
|
|
1505
|
+
}
|
|
936
1506
|
});
|
|
937
1507
|
});
|
|
938
1508
|
return { status: 'running', taskId, done };
|
|
@@ -948,13 +1518,16 @@ function getLocalMinuteKey(now, timezone) {
|
|
|
948
1518
|
});
|
|
949
1519
|
return fmt.format(now);
|
|
950
1520
|
}
|
|
1521
|
+
function buildOccurrenceKey(cronExpression, scheduledMinute) {
|
|
1522
|
+
return `${cronExpression.trim()}|${scheduledMinute}`;
|
|
1523
|
+
}
|
|
951
1524
|
async function daemonTick(dir) {
|
|
952
1525
|
if ((0, sdk_1.isDurablyQuiescing)()) {
|
|
953
1526
|
console.log(`[${getLocalTimestamp()}] Daemon tick skipped: a desktop update is draining active work`);
|
|
954
1527
|
return;
|
|
955
1528
|
}
|
|
956
1529
|
const now = new Date();
|
|
957
|
-
const schedules =
|
|
1530
|
+
const schedules = createScheduleStore(dir ?? DEFAULT_WORKSPACE_PATH).list();
|
|
958
1531
|
const enabledSchedules = schedules.filter(s => s.enabled);
|
|
959
1532
|
if (enabledSchedules.length === 0) {
|
|
960
1533
|
return;
|
|
@@ -962,17 +1535,9 @@ async function daemonTick(dir) {
|
|
|
962
1535
|
for (const schedule of enabledSchedules) {
|
|
963
1536
|
if (shouldRunNow(schedule.cron, now, schedule.timezone)) {
|
|
964
1537
|
const minuteKey = getLocalMinuteKey(now, schedule.timezone);
|
|
965
|
-
|
|
966
|
-
const lastRunMinuteKey = getLocalMinuteKey(new Date(schedule.last_run), schedule.timezone);
|
|
967
|
-
if (lastRunMinuteKey === minuteKey) {
|
|
968
|
-
continue;
|
|
969
|
-
}
|
|
970
|
-
}
|
|
1538
|
+
const occurrenceKey = buildOccurrenceKey(schedule.cron, minuteKey);
|
|
971
1539
|
try {
|
|
972
|
-
|
|
973
|
-
if (outcome.status === 'running') {
|
|
974
|
-
await outcome.done;
|
|
975
|
-
}
|
|
1540
|
+
await executeSchedule(schedule, { dir, occurrenceKey });
|
|
976
1541
|
}
|
|
977
1542
|
catch (e) {
|
|
978
1543
|
const err = e;
|
|
@@ -981,6 +1546,9 @@ async function daemonTick(dir) {
|
|
|
981
1546
|
}
|
|
982
1547
|
}
|
|
983
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; } });
|
|
984
1552
|
function runDaemonForeground(dir) {
|
|
985
1553
|
writeDaemonPid(process.pid, dir);
|
|
986
1554
|
let cleanedUp = false;
|
|
@@ -1005,7 +1573,7 @@ function runDaemonForeground(dir) {
|
|
|
1005
1573
|
console.log(`[${getLocalTimestamp()}] Cron daemon starting in foreground mode`);
|
|
1006
1574
|
console.log(`[${getLocalTimestamp()}] Check interval: 1 minute`);
|
|
1007
1575
|
console.log(`[${getLocalTimestamp()}] Press Ctrl+C to stop\n`);
|
|
1008
|
-
const schedules =
|
|
1576
|
+
const schedules = createScheduleStore(dir ?? DEFAULT_WORKSPACE_PATH).list();
|
|
1009
1577
|
const enabled = schedules.filter(s => s.enabled).length;
|
|
1010
1578
|
console.log(`[${getLocalTimestamp()}] Active schedules: ${enabled}/${schedules.length}\n`);
|
|
1011
1579
|
void daemonTick(dir);
|
|
@@ -1040,19 +1608,10 @@ function addSchedule(cron, command, options, dataDir) {
|
|
|
1040
1608
|
schedule.provider = options.provider;
|
|
1041
1609
|
}
|
|
1042
1610
|
schedule.next_run = calculateNextRun(cron, schedule.timezone);
|
|
1043
|
-
|
|
1044
|
-
schedules.push(schedule);
|
|
1045
|
-
saveSchedules(schedules, dataDir);
|
|
1046
|
-
return schedule;
|
|
1611
|
+
return createScheduleStore(dataDir ?? DEFAULT_WORKSPACE_PATH).create(schedule);
|
|
1047
1612
|
}
|
|
1048
1613
|
function removeSchedule(id, dir) {
|
|
1049
|
-
const
|
|
1050
|
-
const idx = schedules.findIndex(s => s.id === id);
|
|
1051
|
-
if (idx === -1) {
|
|
1052
|
-
throw new Error(`Schedule not found: ${id}`);
|
|
1053
|
-
}
|
|
1054
|
-
const removed = schedules.splice(idx, 1)[0];
|
|
1055
|
-
saveSchedules(schedules, dir);
|
|
1614
|
+
const removed = createScheduleStore(dir ?? DEFAULT_WORKSPACE_PATH).remove(id);
|
|
1056
1615
|
const matchedBuiltin = (0, builtins_1.findBuiltinByName)(removed.name);
|
|
1057
1616
|
if (matchedBuiltin) {
|
|
1058
1617
|
recordRemovedBuiltin(matchedBuiltin.builtin_id, dir);
|
|
@@ -1060,100 +1619,82 @@ function removeSchedule(id, dir) {
|
|
|
1060
1619
|
return removed;
|
|
1061
1620
|
}
|
|
1062
1621
|
function enableSchedule(id, dir) {
|
|
1063
|
-
|
|
1064
|
-
const schedule = schedules.find(s => s.id === id);
|
|
1065
|
-
if (!schedule) {
|
|
1066
|
-
throw new Error(`Schedule not found: ${id}`);
|
|
1067
|
-
}
|
|
1068
|
-
schedule.enabled = true;
|
|
1069
|
-
schedule.updated_at = new Date().toISOString();
|
|
1070
|
-
schedule.next_run = calculateNextRun(schedule.cron, schedule.timezone);
|
|
1071
|
-
saveSchedules(schedules, dir);
|
|
1072
|
-
return schedule;
|
|
1622
|
+
return createScheduleStore(dir ?? DEFAULT_WORKSPACE_PATH).toggle(id, true);
|
|
1073
1623
|
}
|
|
1074
1624
|
function disableSchedule(id, dir) {
|
|
1075
|
-
|
|
1076
|
-
const schedule = schedules.find(s => s.id === id);
|
|
1077
|
-
if (!schedule) {
|
|
1078
|
-
throw new Error(`Schedule not found: ${id}`);
|
|
1079
|
-
}
|
|
1080
|
-
schedule.enabled = false;
|
|
1081
|
-
schedule.updated_at = new Date().toISOString();
|
|
1082
|
-
saveSchedules(schedules, dir);
|
|
1083
|
-
return schedule;
|
|
1625
|
+
return createScheduleStore(dir ?? DEFAULT_WORKSPACE_PATH).toggle(id, false);
|
|
1084
1626
|
}
|
|
1085
1627
|
function updateSchedule(id, options, dir) {
|
|
1086
|
-
const schedules = loadSchedules(dir);
|
|
1087
|
-
const schedule = schedules.find(s => s.id === id);
|
|
1088
|
-
if (!schedule) {
|
|
1089
|
-
throw new Error(`Schedule not found: ${id}`);
|
|
1090
|
-
}
|
|
1091
1628
|
if (options.timezone !== undefined && options.timezone !== null && !isValidTimezone(options.timezone)) {
|
|
1092
1629
|
throw new Error(`Invalid timezone: ${options.timezone}`);
|
|
1093
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
|
+
}
|
|
1094
1636
|
const changes = [];
|
|
1637
|
+
const patch = {};
|
|
1095
1638
|
if (options.cron !== undefined) {
|
|
1096
|
-
|
|
1639
|
+
patch.cron = options.cron;
|
|
1097
1640
|
changes.push(`cron: ${options.cron}`);
|
|
1098
1641
|
}
|
|
1099
1642
|
if (options.command !== undefined) {
|
|
1100
1643
|
warnIfOverdriveInCommand(options.command);
|
|
1101
|
-
|
|
1644
|
+
patch.command = options.command;
|
|
1102
1645
|
changes.push(`command: ${options.command}`);
|
|
1103
1646
|
}
|
|
1104
1647
|
if (options.name !== undefined) {
|
|
1105
|
-
|
|
1648
|
+
patch.name = options.name;
|
|
1106
1649
|
changes.push(`name: ${options.name}`);
|
|
1107
1650
|
}
|
|
1108
1651
|
if (options.mode !== undefined) {
|
|
1109
|
-
|
|
1110
|
-
changes.push(`mode: ${
|
|
1652
|
+
patch.mode = normalizeMode(options.mode);
|
|
1653
|
+
changes.push(`mode: ${patch.mode}`);
|
|
1111
1654
|
}
|
|
1112
1655
|
if (options.timezone !== undefined) {
|
|
1113
|
-
|
|
1656
|
+
patch.timezone = options.timezone;
|
|
1114
1657
|
changes.push(`timezone: ${options.timezone}`);
|
|
1115
1658
|
}
|
|
1116
1659
|
if (options.working_directory !== undefined) {
|
|
1117
|
-
|
|
1660
|
+
patch.working_directory = options.working_directory;
|
|
1118
1661
|
changes.push(`working_directory: ${options.working_directory}`);
|
|
1119
1662
|
}
|
|
1120
1663
|
if (options.timeout_ms !== undefined) {
|
|
1121
|
-
|
|
1664
|
+
patch.timeout_ms = options.timeout_ms;
|
|
1122
1665
|
changes.push(`timeout: ${options.timeout_ms}ms`);
|
|
1123
1666
|
}
|
|
1124
1667
|
if (options.allow_concurrent !== undefined) {
|
|
1125
|
-
|
|
1668
|
+
patch.allow_concurrent = options.allow_concurrent;
|
|
1126
1669
|
changes.push(`allow_concurrent: ${options.allow_concurrent}`);
|
|
1127
1670
|
}
|
|
1128
1671
|
if (options.model !== undefined) {
|
|
1129
1672
|
if (options.model === null) {
|
|
1130
|
-
|
|
1673
|
+
patch.model = null;
|
|
1131
1674
|
changes.push('model: (cleared, reverts to agent default)');
|
|
1132
1675
|
}
|
|
1133
1676
|
else {
|
|
1134
|
-
|
|
1677
|
+
patch.model = options.model;
|
|
1135
1678
|
changes.push(`model: ${options.model}`);
|
|
1136
1679
|
}
|
|
1137
1680
|
}
|
|
1138
1681
|
if (options.provider !== undefined) {
|
|
1139
1682
|
if (options.provider === null) {
|
|
1140
|
-
|
|
1683
|
+
patch.provider = null;
|
|
1141
1684
|
changes.push('provider: (cleared, reverts to agent default)');
|
|
1142
1685
|
}
|
|
1143
1686
|
else {
|
|
1144
|
-
|
|
1687
|
+
patch.provider = options.provider;
|
|
1145
1688
|
changes.push(`provider: ${options.provider}`);
|
|
1146
1689
|
}
|
|
1147
1690
|
}
|
|
1148
|
-
|
|
1149
|
-
schedule.next_run = calculateNextRun(schedule.cron, schedule.timezone);
|
|
1150
|
-
saveSchedules(schedules, dir);
|
|
1151
|
-
return { schedule, changes };
|
|
1691
|
+
return { schedule: store.update(current.id, patch), changes };
|
|
1152
1692
|
}
|
|
1153
1693
|
exports.DATA_PATHS = {
|
|
1154
1694
|
DATA_DIR,
|
|
1155
1695
|
SCHEDULES_FILE,
|
|
1156
1696
|
LOG_FILE,
|
|
1697
|
+
LOCK_FILE,
|
|
1157
1698
|
USAGE_LOG_FILE,
|
|
1158
1699
|
PID_FILE,
|
|
1159
1700
|
BUILTINS_META_FILE,
|