@crewx/cron 0.1.10-rc.144 → 0.1.10-rc.146
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 +14 -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 +758 -192
- 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 +68 -1
- 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,21 +86,40 @@ 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
|
}
|
|
86
103
|
function resolveBuiltinsMetaFile(dir) {
|
|
87
104
|
return path.join(resolveDataDir(dir), '.cron-builtins.json');
|
|
88
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
|
+
}
|
|
89
116
|
function resolveRunLockFile(scheduleId, dir) {
|
|
90
117
|
return path.join(resolveDataDir(dir), `.cron-run-${scheduleId}.lock`);
|
|
91
118
|
}
|
|
92
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;
|
|
93
123
|
const RUN_LOCK_STALE_GRACE_MS = 5000;
|
|
94
124
|
function getLocalTimestamp() {
|
|
95
125
|
const now = new Date();
|
|
@@ -101,12 +131,45 @@ function getLocalTimestamp() {
|
|
|
101
131
|
const s = String(now.getSeconds()).padStart(2, '0');
|
|
102
132
|
return `${y}-${m}-${d} ${h}:${min}:${s}`;
|
|
103
133
|
}
|
|
104
|
-
function logUsage(...args) {
|
|
105
|
-
|
|
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);
|
|
106
169
|
const timestamp = getLocalTimestamp();
|
|
107
|
-
const message =
|
|
170
|
+
const message = messageParts.join(' ');
|
|
108
171
|
const logLine = `${timestamp} | [cron] ${message}\n`;
|
|
109
|
-
fs.appendFileSync(
|
|
172
|
+
fs.appendFileSync(resolveUsageLogFile(workspacePath), logLine);
|
|
110
173
|
}
|
|
111
174
|
function ensureDataDir(dir) {
|
|
112
175
|
const dataDir = resolveDataDir(dir);
|
|
@@ -122,6 +185,76 @@ function safeReadJsonFile(filePath) {
|
|
|
122
185
|
return null;
|
|
123
186
|
}
|
|
124
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
|
+
}
|
|
125
258
|
const MEMORY_DEBOUNCE_BUILTIN = (0, builtins_1.findBuiltinById)('memory-debounce');
|
|
126
259
|
function maybeRewriteLegacyMemoryCommand(command) {
|
|
127
260
|
if (!command.includes('/skills/memory-v2/memory-v2.js')) {
|
|
@@ -129,9 +262,9 @@ function maybeRewriteLegacyMemoryCommand(command) {
|
|
|
129
262
|
}
|
|
130
263
|
return MEMORY_DEBOUNCE_BUILTIN.buildCommand();
|
|
131
264
|
}
|
|
132
|
-
function migrateLegacyMemoryDebounceSchedule() {
|
|
133
|
-
|
|
134
|
-
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');
|
|
135
268
|
if (!fs.existsSync(legacyFile)) {
|
|
136
269
|
return;
|
|
137
270
|
}
|
|
@@ -143,7 +276,8 @@ function migrateLegacyMemoryDebounceSchedule() {
|
|
|
143
276
|
if (!legacySchedule) {
|
|
144
277
|
return;
|
|
145
278
|
}
|
|
146
|
-
const
|
|
279
|
+
const store = createScheduleStore(normalizedWorkspacePath);
|
|
280
|
+
const targetSchedules = store.list();
|
|
147
281
|
if (targetSchedules.some(s => s.name === MEMORY_DEBOUNCE_BUILTIN.name)) {
|
|
148
282
|
return;
|
|
149
283
|
}
|
|
@@ -153,7 +287,7 @@ function migrateLegacyMemoryDebounceSchedule() {
|
|
|
153
287
|
updated_at: new Date().toISOString(),
|
|
154
288
|
next_run: calculateNextRun(legacySchedule.cron, legacySchedule.timezone),
|
|
155
289
|
};
|
|
156
|
-
|
|
290
|
+
store.create(migratedSchedule);
|
|
157
291
|
}
|
|
158
292
|
function warnIfOverdriveInCommand(command) {
|
|
159
293
|
if (command.includes('--overdrive')) {
|
|
@@ -167,31 +301,255 @@ function normalizeMode(mode) {
|
|
|
167
301
|
return 'command';
|
|
168
302
|
return 'execute';
|
|
169
303
|
}
|
|
170
|
-
function
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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');
|
|
174
314
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
315
|
+
catch (error) {
|
|
316
|
+
const err = error;
|
|
317
|
+
if (err.code === 'ENOENT')
|
|
318
|
+
return [];
|
|
319
|
+
throw error;
|
|
178
320
|
}
|
|
321
|
+
let parsed;
|
|
179
322
|
try {
|
|
180
|
-
|
|
181
|
-
return JSON.parse(data);
|
|
323
|
+
parsed = JSON.parse(data);
|
|
182
324
|
}
|
|
183
|
-
catch (
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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);
|
|
187
395
|
}
|
|
188
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
|
+
}
|
|
189
542
|
function saveSchedules(schedules, dir) {
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
+
}
|
|
195
553
|
}
|
|
196
554
|
function loadBuiltinsMeta(dir) {
|
|
197
555
|
const data = safeReadJsonFile(resolveBuiltinsMetaFile(dir));
|
|
@@ -215,7 +573,8 @@ function recordRemovedBuiltin(builtinId, dir) {
|
|
|
215
573
|
}
|
|
216
574
|
}
|
|
217
575
|
function seedBuiltinSchedules(dir) {
|
|
218
|
-
const
|
|
576
|
+
const store = createScheduleStore(dir ?? DEFAULT_WORKSPACE_PATH);
|
|
577
|
+
const schedules = store.list();
|
|
219
578
|
const existingNames = new Set(schedules.map(s => s.name));
|
|
220
579
|
const removedBuiltins = new Set(getRemovedBuiltins(dir));
|
|
221
580
|
const seeded = [];
|
|
@@ -225,7 +584,7 @@ function seedBuiltinSchedules(dir) {
|
|
|
225
584
|
if (removedBuiltins.has(def.builtin_id))
|
|
226
585
|
continue;
|
|
227
586
|
const now = new Date().toISOString();
|
|
228
|
-
|
|
587
|
+
const schedule = {
|
|
229
588
|
id: (0, sdk_1.generateId)('cron'),
|
|
230
589
|
cron: def.cron,
|
|
231
590
|
command: def.buildCommand(),
|
|
@@ -240,102 +599,207 @@ function seedBuiltinSchedules(dir) {
|
|
|
240
599
|
updated_at: now,
|
|
241
600
|
run_count: 0,
|
|
242
601
|
next_run: calculateNextRun(def.cron, null),
|
|
243
|
-
}
|
|
602
|
+
};
|
|
603
|
+
store.create(schedule);
|
|
604
|
+
existingNames.add(def.name);
|
|
244
605
|
seeded.push(def.builtin_id);
|
|
245
606
|
}
|
|
246
|
-
if (seeded.length > 0) {
|
|
247
|
-
saveSchedules(schedules, dir);
|
|
248
|
-
}
|
|
249
607
|
return seeded;
|
|
250
608
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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);
|
|
276
649
|
}
|
|
650
|
+
catch { }
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
fs.unlinkSync(tempFile);
|
|
277
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);
|
|
278
665
|
}
|
|
279
|
-
return false;
|
|
280
666
|
}
|
|
281
|
-
function
|
|
667
|
+
function getLeaseAgeMs(lockFile) {
|
|
282
668
|
try {
|
|
283
|
-
fs.
|
|
669
|
+
return Math.max(0, Date.now() - fs.statSync(lockFile).mtimeMs);
|
|
284
670
|
}
|
|
285
671
|
catch {
|
|
672
|
+
return null;
|
|
286
673
|
}
|
|
287
674
|
}
|
|
288
|
-
function
|
|
289
|
-
|
|
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;
|
|
290
708
|
}
|
|
291
709
|
function acquireRunLock(scheduleId, taskId, timeoutMs, dir) {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
catch { }
|
|
310
|
-
continue;
|
|
311
|
-
}
|
|
312
|
-
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++) {
|
|
313
726
|
try {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
+
};
|
|
318
732
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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' };
|
|
323
740
|
}
|
|
324
|
-
|
|
325
|
-
|
|
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
|
+
};
|
|
326
757
|
}
|
|
327
|
-
return { acquired: false, runningTaskId: existing.taskId };
|
|
328
758
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
759
|
+
const existing = readRunLease(lockFile);
|
|
760
|
+
return {
|
|
761
|
+
acquired: false,
|
|
762
|
+
runningTaskId: existing?.taskId ?? 'unknown',
|
|
763
|
+
reason: 'execution_lease',
|
|
764
|
+
};
|
|
765
|
+
});
|
|
332
766
|
}
|
|
333
|
-
function
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
+
});
|
|
339
803
|
}
|
|
340
804
|
const DAY_NAMES = {
|
|
341
805
|
SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6,
|
|
@@ -612,6 +1076,30 @@ function getProcessCommandLine(pid) {
|
|
|
612
1076
|
return null;
|
|
613
1077
|
}
|
|
614
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
|
+
}
|
|
615
1103
|
const DEFAULT_DAEMON_PROCESS_PROBE = {
|
|
616
1104
|
isAlive: isPidAlive,
|
|
617
1105
|
getCommandLine: getProcessCommandLine,
|
|
@@ -687,6 +1175,9 @@ function isDaemonRunning(dir, probe = DEFAULT_DAEMON_PROCESS_PROBE) {
|
|
|
687
1175
|
return inspectDaemon(dir, probe) === 'running';
|
|
688
1176
|
}
|
|
689
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');
|
|
1180
|
+
}
|
|
690
1181
|
ensureDataDir(dir);
|
|
691
1182
|
const cliPath = getDaemonCliPath();
|
|
692
1183
|
const child = spawnImpl('node', [cliPath, 'daemon', '--foreground'], {
|
|
@@ -756,7 +1247,7 @@ function ensureDaemonRunning(dir, probe = DEFAULT_DAEMON_PROCESS_PROBE) {
|
|
|
756
1247
|
}
|
|
757
1248
|
}
|
|
758
1249
|
function getDaemonStatus(dir) {
|
|
759
|
-
const schedules =
|
|
1250
|
+
const schedules = createScheduleStore(dir ?? DEFAULT_WORKSPACE_PATH).list();
|
|
760
1251
|
const enabled = schedules.filter(s => s.enabled).length;
|
|
761
1252
|
const running = isDaemonRunning(dir);
|
|
762
1253
|
const pid = running ? readDaemonPid(dir) : null;
|
|
@@ -782,16 +1273,68 @@ function getDaemonStatus(dir) {
|
|
|
782
1273
|
}
|
|
783
1274
|
async function executeSchedule(schedule, options) {
|
|
784
1275
|
const dir = options?.dir;
|
|
1276
|
+
const workspacePath = path.resolve(dir ?? DEFAULT_WORKSPACE_PATH);
|
|
785
1277
|
const trigger = options?.trigger ?? 'cron';
|
|
786
1278
|
const timeoutMs = schedule.timeout_ms || 600000;
|
|
787
1279
|
const taskId = (0, sdk_1.generateId)('tsk');
|
|
1280
|
+
let runLease;
|
|
788
1281
|
if (!schedule.allow_concurrent) {
|
|
789
1282
|
const lock = acquireRunLock(schedule.id, taskId, timeoutMs, dir);
|
|
790
1283
|
if (!lock.acquired) {
|
|
791
1284
|
console.log(`[${getLocalTimestamp()}] Schedule ${schedule.id} already running (task ${lock.runningTaskId}), skipping`);
|
|
792
|
-
return {
|
|
1285
|
+
return {
|
|
1286
|
+
status: 'conflict',
|
|
1287
|
+
runningTaskId: lock.runningTaskId,
|
|
1288
|
+
reason: lock.reason,
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
runLease = lock.lease;
|
|
1292
|
+
}
|
|
1293
|
+
const releaseLease = () => {
|
|
1294
|
+
if (runLease) {
|
|
1295
|
+
releaseRunLock(schedule.id, runLease.leaseId, dir);
|
|
1296
|
+
runLease = undefined;
|
|
1297
|
+
}
|
|
1298
|
+
releaseAdmissionReservation(taskId);
|
|
1299
|
+
};
|
|
1300
|
+
if (!reserveAdmission(taskId, schedule.id, timeoutMs)) {
|
|
1301
|
+
console.log(`[${getLocalTimestamp()}] Schedule ${schedule.id} blocked: could not durably record the update-admission reservation`);
|
|
1302
|
+
releaseLease();
|
|
1303
|
+
return { status: 'blocked', reason: 'ADMISSION_UNKNOWN' };
|
|
1304
|
+
}
|
|
1305
|
+
if ((0, sdk_1.isDurablyQuiescing)()) {
|
|
1306
|
+
console.log(`[${getLocalTimestamp()}] Schedule ${schedule.id} blocked: a desktop update is draining active work`);
|
|
1307
|
+
releaseLease();
|
|
1308
|
+
return { status: 'blocked', reason: 'UPDATE_QUIESCING' };
|
|
1309
|
+
}
|
|
1310
|
+
try {
|
|
1311
|
+
const store = createScheduleStore(workspacePath);
|
|
1312
|
+
const startedAt = new Date().toISOString();
|
|
1313
|
+
if (options?.occurrenceKey !== undefined) {
|
|
1314
|
+
const claim = store.claimOccurrence(schedule.id, {
|
|
1315
|
+
occurrenceKey: options.occurrenceKey,
|
|
1316
|
+
startedAt,
|
|
1317
|
+
});
|
|
1318
|
+
if (!claim.claimed) {
|
|
1319
|
+
releaseLease();
|
|
1320
|
+
console.log(`[${getLocalTimestamp()}] Schedule ${schedule.id} occurrence ${options.occurrenceKey} already claimed, skipping`);
|
|
1321
|
+
return {
|
|
1322
|
+
status: 'conflict',
|
|
1323
|
+
runningTaskId: '',
|
|
1324
|
+
reason: 'occurrence_claim',
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
else {
|
|
1329
|
+
if (store.get(schedule.id)) {
|
|
1330
|
+
store.recordStarted(schedule.id, { startedAt });
|
|
1331
|
+
}
|
|
793
1332
|
}
|
|
794
1333
|
}
|
|
1334
|
+
catch (error) {
|
|
1335
|
+
releaseLease();
|
|
1336
|
+
throw error;
|
|
1337
|
+
}
|
|
795
1338
|
const workingDir = schedule.working_directory || options?.dir || (0, builtins_1.getProjectRoot)();
|
|
796
1339
|
const mode = schedule.mode;
|
|
797
1340
|
const modeFlag = mode === 'execute' ? 'x' : 'q';
|
|
@@ -799,10 +1342,6 @@ async function executeSchedule(schedule, options) {
|
|
|
799
1342
|
const effectiveProvider = options?.overrides?.provider ?? schedule.provider;
|
|
800
1343
|
console.log(`[${getLocalTimestamp()}] Executing schedule ${schedule.id}: ${schedule.name ?? schedule.cron} (task ${taskId})`);
|
|
801
1344
|
console.log(`[${getLocalTimestamp()}] Command: ${schedule.command}`);
|
|
802
|
-
const releaseLease = () => {
|
|
803
|
-
if (!schedule.allow_concurrent)
|
|
804
|
-
releaseRunLock(schedule.id, dir);
|
|
805
|
-
};
|
|
806
1345
|
const env = (0, sdk_1.withoutCrewxRootContext)({ ...process.env, ...schedule.env, CREWX_TRACE_ID: taskId });
|
|
807
1346
|
delete env['CLAUDECODE'];
|
|
808
1347
|
if (!schedule.env || !('CREWX_CONFIG' in schedule.env)) {
|
|
@@ -869,9 +1408,27 @@ async function executeSchedule(schedule, options) {
|
|
|
869
1408
|
releaseLease();
|
|
870
1409
|
throw err;
|
|
871
1410
|
}
|
|
1411
|
+
if (runLease && child.pid) {
|
|
1412
|
+
const handedOff = handoffRunLock(runLease, child.pid, captureProcessIdentity(child.pid));
|
|
1413
|
+
if (!handedOff) {
|
|
1414
|
+
const error = new Error('Cron execution lease was replaced before child ownership was established');
|
|
1415
|
+
console.error(`[${getLocalTimestamp()}] Schedule ${schedule.id} failed to hand off its execution lease: ${error.message}`);
|
|
1416
|
+
try {
|
|
1417
|
+
child.kill('SIGTERM');
|
|
1418
|
+
}
|
|
1419
|
+
catch {
|
|
1420
|
+
}
|
|
1421
|
+
releaseLease();
|
|
1422
|
+
throw error;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
872
1425
|
child.unref();
|
|
873
1426
|
const done = new Promise((resolve) => {
|
|
1427
|
+
let timedOut = false;
|
|
1428
|
+
let settled = false;
|
|
1429
|
+
let graceTimer;
|
|
874
1430
|
const timeout = setTimeout(() => {
|
|
1431
|
+
timedOut = true;
|
|
875
1432
|
console.log(`[${getLocalTimestamp()}] Schedule ${schedule.id} timeout, killing process`);
|
|
876
1433
|
try {
|
|
877
1434
|
child.kill('SIGTERM');
|
|
@@ -880,35 +1437,69 @@ async function executeSchedule(schedule, options) {
|
|
|
880
1437
|
const err = e;
|
|
881
1438
|
console.error(`Failed to kill process: ${err.message}`);
|
|
882
1439
|
}
|
|
883
|
-
|
|
884
|
-
|
|
1440
|
+
graceTimer = setTimeout(() => {
|
|
1441
|
+
try {
|
|
1442
|
+
child.kill('SIGKILL');
|
|
1443
|
+
}
|
|
1444
|
+
catch {
|
|
1445
|
+
}
|
|
1446
|
+
}, RUN_LOCK_STALE_GRACE_MS);
|
|
1447
|
+
if (typeof graceTimer === 'object' && graceTimer !== null && 'unref' in graceTimer) {
|
|
1448
|
+
graceTimer.unref();
|
|
1449
|
+
}
|
|
1450
|
+
if (!settled) {
|
|
1451
|
+
settled = true;
|
|
1452
|
+
resolve({ success: false, error: 'Timeout' });
|
|
1453
|
+
}
|
|
885
1454
|
}, timeoutMs);
|
|
886
1455
|
child.on('close', (code) => {
|
|
887
1456
|
clearTimeout(timeout);
|
|
1457
|
+
if (graceTimer)
|
|
1458
|
+
clearTimeout(graceTimer);
|
|
888
1459
|
releaseLease();
|
|
889
|
-
const
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
saveSchedules(schedules, dir);
|
|
1460
|
+
const finishedAt = new Date().toISOString();
|
|
1461
|
+
const outcome = timedOut ? 'timeout' : code === 0 ? 'success' : 'failure';
|
|
1462
|
+
try {
|
|
1463
|
+
createScheduleStore(workspacePath).recordFinished(schedule.id, {
|
|
1464
|
+
finishedAt,
|
|
1465
|
+
outcome,
|
|
1466
|
+
});
|
|
897
1467
|
}
|
|
898
|
-
|
|
1468
|
+
catch (error) {
|
|
1469
|
+
const err = error;
|
|
1470
|
+
if (!(error instanceof types_1.CronStoreError && error.code === 'CRON_SCHEDULE_NOT_FOUND')) {
|
|
1471
|
+
console.error(`[${getLocalTimestamp()}] Failed to record schedule ${schedule.id} completion: ${err.message}`);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
if (timedOut) {
|
|
1475
|
+
console.log(`[${getLocalTimestamp()}] Schedule ${schedule.id} terminated after timeout`);
|
|
1476
|
+
if (!settled) {
|
|
1477
|
+
settled = true;
|
|
1478
|
+
resolve({ success: false, error: 'Timeout' });
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
else if (code === 0) {
|
|
899
1482
|
console.log(`[${getLocalTimestamp()}] Schedule ${schedule.id} completed successfully`);
|
|
900
|
-
|
|
1483
|
+
if (!settled) {
|
|
1484
|
+
settled = true;
|
|
1485
|
+
resolve({ success: true });
|
|
1486
|
+
}
|
|
901
1487
|
}
|
|
902
1488
|
else {
|
|
903
1489
|
console.log(`[${getLocalTimestamp()}] Schedule ${schedule.id} failed with code ${code}`);
|
|
904
|
-
|
|
1490
|
+
if (!settled) {
|
|
1491
|
+
settled = true;
|
|
1492
|
+
resolve({ success: false, error: `Exit code: ${code}` });
|
|
1493
|
+
}
|
|
905
1494
|
}
|
|
906
1495
|
});
|
|
907
1496
|
child.on('error', (err) => {
|
|
908
1497
|
clearTimeout(timeout);
|
|
909
|
-
releaseLease();
|
|
910
1498
|
console.error(`[${getLocalTimestamp()}] Schedule ${schedule.id} error: ${err.message}`);
|
|
911
|
-
|
|
1499
|
+
if (!settled) {
|
|
1500
|
+
settled = true;
|
|
1501
|
+
resolve({ success: false, error: err.message });
|
|
1502
|
+
}
|
|
912
1503
|
});
|
|
913
1504
|
});
|
|
914
1505
|
return { status: 'running', taskId, done };
|
|
@@ -924,9 +1515,16 @@ function getLocalMinuteKey(now, timezone) {
|
|
|
924
1515
|
});
|
|
925
1516
|
return fmt.format(now);
|
|
926
1517
|
}
|
|
1518
|
+
function buildOccurrenceKey(cronExpression, scheduledMinute) {
|
|
1519
|
+
return `${cronExpression.trim()}|${scheduledMinute}`;
|
|
1520
|
+
}
|
|
927
1521
|
async function daemonTick(dir) {
|
|
1522
|
+
if ((0, sdk_1.isDurablyQuiescing)()) {
|
|
1523
|
+
console.log(`[${getLocalTimestamp()}] Daemon tick skipped: a desktop update is draining active work`);
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
928
1526
|
const now = new Date();
|
|
929
|
-
const schedules =
|
|
1527
|
+
const schedules = createScheduleStore(dir ?? DEFAULT_WORKSPACE_PATH).list();
|
|
930
1528
|
const enabledSchedules = schedules.filter(s => s.enabled);
|
|
931
1529
|
if (enabledSchedules.length === 0) {
|
|
932
1530
|
return;
|
|
@@ -934,17 +1532,9 @@ async function daemonTick(dir) {
|
|
|
934
1532
|
for (const schedule of enabledSchedules) {
|
|
935
1533
|
if (shouldRunNow(schedule.cron, now, schedule.timezone)) {
|
|
936
1534
|
const minuteKey = getLocalMinuteKey(now, schedule.timezone);
|
|
937
|
-
|
|
938
|
-
const lastRunMinuteKey = getLocalMinuteKey(new Date(schedule.last_run), schedule.timezone);
|
|
939
|
-
if (lastRunMinuteKey === minuteKey) {
|
|
940
|
-
continue;
|
|
941
|
-
}
|
|
942
|
-
}
|
|
1535
|
+
const occurrenceKey = buildOccurrenceKey(schedule.cron, minuteKey);
|
|
943
1536
|
try {
|
|
944
|
-
|
|
945
|
-
if (outcome.status === 'running') {
|
|
946
|
-
await outcome.done;
|
|
947
|
-
}
|
|
1537
|
+
await executeSchedule(schedule, { dir, occurrenceKey });
|
|
948
1538
|
}
|
|
949
1539
|
catch (e) {
|
|
950
1540
|
const err = e;
|
|
@@ -953,6 +1543,9 @@ async function daemonTick(dir) {
|
|
|
953
1543
|
}
|
|
954
1544
|
}
|
|
955
1545
|
}
|
|
1546
|
+
var runtime_1 = require("./runtime");
|
|
1547
|
+
Object.defineProperty(exports, "createCronRuntime", { enumerable: true, get: function () { return runtime_1.createCronRuntime; } });
|
|
1548
|
+
Object.defineProperty(exports, "CRON_RUNTIME_PROCESS_LEASE_FILENAME", { enumerable: true, get: function () { return runtime_1.CRON_RUNTIME_PROCESS_LEASE_FILENAME; } });
|
|
956
1549
|
function runDaemonForeground(dir) {
|
|
957
1550
|
writeDaemonPid(process.pid, dir);
|
|
958
1551
|
let cleanedUp = false;
|
|
@@ -977,7 +1570,7 @@ function runDaemonForeground(dir) {
|
|
|
977
1570
|
console.log(`[${getLocalTimestamp()}] Cron daemon starting in foreground mode`);
|
|
978
1571
|
console.log(`[${getLocalTimestamp()}] Check interval: 1 minute`);
|
|
979
1572
|
console.log(`[${getLocalTimestamp()}] Press Ctrl+C to stop\n`);
|
|
980
|
-
const schedules =
|
|
1573
|
+
const schedules = createScheduleStore(dir ?? DEFAULT_WORKSPACE_PATH).list();
|
|
981
1574
|
const enabled = schedules.filter(s => s.enabled).length;
|
|
982
1575
|
console.log(`[${getLocalTimestamp()}] Active schedules: ${enabled}/${schedules.length}\n`);
|
|
983
1576
|
void daemonTick(dir);
|
|
@@ -1012,19 +1605,10 @@ function addSchedule(cron, command, options, dataDir) {
|
|
|
1012
1605
|
schedule.provider = options.provider;
|
|
1013
1606
|
}
|
|
1014
1607
|
schedule.next_run = calculateNextRun(cron, schedule.timezone);
|
|
1015
|
-
|
|
1016
|
-
schedules.push(schedule);
|
|
1017
|
-
saveSchedules(schedules, dataDir);
|
|
1018
|
-
return schedule;
|
|
1608
|
+
return createScheduleStore(dataDir ?? DEFAULT_WORKSPACE_PATH).create(schedule);
|
|
1019
1609
|
}
|
|
1020
1610
|
function removeSchedule(id, dir) {
|
|
1021
|
-
const
|
|
1022
|
-
const idx = schedules.findIndex(s => s.id === id);
|
|
1023
|
-
if (idx === -1) {
|
|
1024
|
-
throw new Error(`Schedule not found: ${id}`);
|
|
1025
|
-
}
|
|
1026
|
-
const removed = schedules.splice(idx, 1)[0];
|
|
1027
|
-
saveSchedules(schedules, dir);
|
|
1611
|
+
const removed = createScheduleStore(dir ?? DEFAULT_WORKSPACE_PATH).remove(id);
|
|
1028
1612
|
const matchedBuiltin = (0, builtins_1.findBuiltinByName)(removed.name);
|
|
1029
1613
|
if (matchedBuiltin) {
|
|
1030
1614
|
recordRemovedBuiltin(matchedBuiltin.builtin_id, dir);
|
|
@@ -1032,100 +1616,82 @@ function removeSchedule(id, dir) {
|
|
|
1032
1616
|
return removed;
|
|
1033
1617
|
}
|
|
1034
1618
|
function enableSchedule(id, dir) {
|
|
1035
|
-
|
|
1036
|
-
const schedule = schedules.find(s => s.id === id);
|
|
1037
|
-
if (!schedule) {
|
|
1038
|
-
throw new Error(`Schedule not found: ${id}`);
|
|
1039
|
-
}
|
|
1040
|
-
schedule.enabled = true;
|
|
1041
|
-
schedule.updated_at = new Date().toISOString();
|
|
1042
|
-
schedule.next_run = calculateNextRun(schedule.cron, schedule.timezone);
|
|
1043
|
-
saveSchedules(schedules, dir);
|
|
1044
|
-
return schedule;
|
|
1619
|
+
return createScheduleStore(dir ?? DEFAULT_WORKSPACE_PATH).toggle(id, true);
|
|
1045
1620
|
}
|
|
1046
1621
|
function disableSchedule(id, dir) {
|
|
1047
|
-
|
|
1048
|
-
const schedule = schedules.find(s => s.id === id);
|
|
1049
|
-
if (!schedule) {
|
|
1050
|
-
throw new Error(`Schedule not found: ${id}`);
|
|
1051
|
-
}
|
|
1052
|
-
schedule.enabled = false;
|
|
1053
|
-
schedule.updated_at = new Date().toISOString();
|
|
1054
|
-
saveSchedules(schedules, dir);
|
|
1055
|
-
return schedule;
|
|
1622
|
+
return createScheduleStore(dir ?? DEFAULT_WORKSPACE_PATH).toggle(id, false);
|
|
1056
1623
|
}
|
|
1057
1624
|
function updateSchedule(id, options, dir) {
|
|
1058
|
-
const schedules = loadSchedules(dir);
|
|
1059
|
-
const schedule = schedules.find(s => s.id === id);
|
|
1060
|
-
if (!schedule) {
|
|
1061
|
-
throw new Error(`Schedule not found: ${id}`);
|
|
1062
|
-
}
|
|
1063
1625
|
if (options.timezone !== undefined && options.timezone !== null && !isValidTimezone(options.timezone)) {
|
|
1064
1626
|
throw new Error(`Invalid timezone: ${options.timezone}`);
|
|
1065
1627
|
}
|
|
1628
|
+
const store = createScheduleStore(dir ?? DEFAULT_WORKSPACE_PATH);
|
|
1629
|
+
const current = store.get(id);
|
|
1630
|
+
if (!current) {
|
|
1631
|
+
throw new Error(`Schedule not found: ${id}`);
|
|
1632
|
+
}
|
|
1066
1633
|
const changes = [];
|
|
1634
|
+
const patch = {};
|
|
1067
1635
|
if (options.cron !== undefined) {
|
|
1068
|
-
|
|
1636
|
+
patch.cron = options.cron;
|
|
1069
1637
|
changes.push(`cron: ${options.cron}`);
|
|
1070
1638
|
}
|
|
1071
1639
|
if (options.command !== undefined) {
|
|
1072
1640
|
warnIfOverdriveInCommand(options.command);
|
|
1073
|
-
|
|
1641
|
+
patch.command = options.command;
|
|
1074
1642
|
changes.push(`command: ${options.command}`);
|
|
1075
1643
|
}
|
|
1076
1644
|
if (options.name !== undefined) {
|
|
1077
|
-
|
|
1645
|
+
patch.name = options.name;
|
|
1078
1646
|
changes.push(`name: ${options.name}`);
|
|
1079
1647
|
}
|
|
1080
1648
|
if (options.mode !== undefined) {
|
|
1081
|
-
|
|
1082
|
-
changes.push(`mode: ${
|
|
1649
|
+
patch.mode = normalizeMode(options.mode);
|
|
1650
|
+
changes.push(`mode: ${patch.mode}`);
|
|
1083
1651
|
}
|
|
1084
1652
|
if (options.timezone !== undefined) {
|
|
1085
|
-
|
|
1653
|
+
patch.timezone = options.timezone;
|
|
1086
1654
|
changes.push(`timezone: ${options.timezone}`);
|
|
1087
1655
|
}
|
|
1088
1656
|
if (options.working_directory !== undefined) {
|
|
1089
|
-
|
|
1657
|
+
patch.working_directory = options.working_directory;
|
|
1090
1658
|
changes.push(`working_directory: ${options.working_directory}`);
|
|
1091
1659
|
}
|
|
1092
1660
|
if (options.timeout_ms !== undefined) {
|
|
1093
|
-
|
|
1661
|
+
patch.timeout_ms = options.timeout_ms;
|
|
1094
1662
|
changes.push(`timeout: ${options.timeout_ms}ms`);
|
|
1095
1663
|
}
|
|
1096
1664
|
if (options.allow_concurrent !== undefined) {
|
|
1097
|
-
|
|
1665
|
+
patch.allow_concurrent = options.allow_concurrent;
|
|
1098
1666
|
changes.push(`allow_concurrent: ${options.allow_concurrent}`);
|
|
1099
1667
|
}
|
|
1100
1668
|
if (options.model !== undefined) {
|
|
1101
1669
|
if (options.model === null) {
|
|
1102
|
-
|
|
1670
|
+
patch.model = null;
|
|
1103
1671
|
changes.push('model: (cleared, reverts to agent default)');
|
|
1104
1672
|
}
|
|
1105
1673
|
else {
|
|
1106
|
-
|
|
1674
|
+
patch.model = options.model;
|
|
1107
1675
|
changes.push(`model: ${options.model}`);
|
|
1108
1676
|
}
|
|
1109
1677
|
}
|
|
1110
1678
|
if (options.provider !== undefined) {
|
|
1111
1679
|
if (options.provider === null) {
|
|
1112
|
-
|
|
1680
|
+
patch.provider = null;
|
|
1113
1681
|
changes.push('provider: (cleared, reverts to agent default)');
|
|
1114
1682
|
}
|
|
1115
1683
|
else {
|
|
1116
|
-
|
|
1684
|
+
patch.provider = options.provider;
|
|
1117
1685
|
changes.push(`provider: ${options.provider}`);
|
|
1118
1686
|
}
|
|
1119
1687
|
}
|
|
1120
|
-
|
|
1121
|
-
schedule.next_run = calculateNextRun(schedule.cron, schedule.timezone);
|
|
1122
|
-
saveSchedules(schedules, dir);
|
|
1123
|
-
return { schedule, changes };
|
|
1688
|
+
return { schedule: store.update(current.id, patch), changes };
|
|
1124
1689
|
}
|
|
1125
1690
|
exports.DATA_PATHS = {
|
|
1126
1691
|
DATA_DIR,
|
|
1127
1692
|
SCHEDULES_FILE,
|
|
1128
1693
|
LOG_FILE,
|
|
1694
|
+
LOCK_FILE,
|
|
1129
1695
|
USAGE_LOG_FILE,
|
|
1130
1696
|
PID_FILE,
|
|
1131
1697
|
BUILTINS_META_FILE,
|