jm2 0.1.9 → 0.1.12
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/package.json +1 -1
- package/src/cli/commands/add.js +4 -0
- package/src/core/logger.js +15 -2
- package/src/core/service.js +0 -4
- package/src/daemon/scheduler.js +91 -8
package/package.json
CHANGED
package/src/cli/commands/add.js
CHANGED
|
@@ -31,6 +31,10 @@ Common examples of JM2 add:
|
|
|
31
31
|
jm2 add "weekly-backup.sh" --cron "0 0 * * 0"
|
|
32
32
|
jm2 add "monthly-task.sh" --cron "0 0 1 * *"
|
|
33
33
|
|
|
34
|
+
# Advanced cron patterns (multiple ranges, steps)
|
|
35
|
+
jm2 add "check.sh" --cron "0 */5 9-18 * * *" # Every 5 min, 9AM-6PM
|
|
36
|
+
jm2 add "offpeak.sh" --cron "*/30 0-8,18-23 * * *" # Every 30 min, off-peak hours
|
|
37
|
+
|
|
34
38
|
# Add a job with a name
|
|
35
39
|
jm2 add "backup.sh" --name "daily-backup" --cron "0 2 * * *"
|
|
36
40
|
|
package/src/core/logger.js
CHANGED
|
@@ -62,12 +62,25 @@ function shouldLog(level) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
|
-
* Format a timestamp for logging
|
|
65
|
+
* Format a timestamp for logging in local timezone
|
|
66
66
|
* @param {Date} date - Date to format
|
|
67
67
|
* @returns {string} Formatted timestamp
|
|
68
68
|
*/
|
|
69
69
|
function formatTimestamp(date = new Date()) {
|
|
70
|
-
|
|
70
|
+
const year = date.getFullYear();
|
|
71
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
72
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
73
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
74
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
75
|
+
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
76
|
+
const milliseconds = String(date.getMilliseconds()).padStart(3, '0');
|
|
77
|
+
|
|
78
|
+
const tzOffset = -date.getTimezoneOffset();
|
|
79
|
+
const tzHours = String(Math.floor(Math.abs(tzOffset) / 60)).padStart(2, '0');
|
|
80
|
+
const tzMinutes = String(Math.abs(tzOffset) % 60).padStart(2, '0');
|
|
81
|
+
const tzSign = tzOffset >= 0 ? '+' : '-';
|
|
82
|
+
|
|
83
|
+
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${milliseconds}${tzSign}${tzHours}:${tzMinutes}`;
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
/**
|
package/src/core/service.js
CHANGED
|
@@ -178,8 +178,6 @@ class DarwinService extends PlatformService {
|
|
|
178
178
|
</array>
|
|
179
179
|
<key>RunAtLoad</key>
|
|
180
180
|
<true/>
|
|
181
|
-
<key>KeepAlive</key>
|
|
182
|
-
<true/>
|
|
183
181
|
<key>StandardOutPath</key>
|
|
184
182
|
<string>${stdoutPath}</string>
|
|
185
183
|
<key>StandardErrorPath</key>
|
|
@@ -344,8 +342,6 @@ Type=forking
|
|
|
344
342
|
ExecStart=${nodePath} ${jm2Path} start
|
|
345
343
|
ExecStop=${nodePath} ${jm2Path} stop
|
|
346
344
|
ExecReload=${nodePath} ${jm2Path} restart
|
|
347
|
-
Restart=always
|
|
348
|
-
RestartSec=10
|
|
349
345
|
Environment="JM2_DATA_DIR=${dataDir}"
|
|
350
346
|
StandardOutput=append:${join(logDir, 'service-out.log')}
|
|
351
347
|
StandardError=append:${join(logDir, 'service-err.log')}
|
package/src/daemon/scheduler.js
CHANGED
|
@@ -31,6 +31,7 @@ export class Scheduler {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
this.running = true;
|
|
34
|
+
this.lastTickTime = Date.now();
|
|
34
35
|
this.logger.info('Scheduler starting...');
|
|
35
36
|
|
|
36
37
|
// Load jobs from storage
|
|
@@ -132,7 +133,7 @@ export class Scheduler {
|
|
|
132
133
|
// Calculate next run time for active jobs
|
|
133
134
|
let nextRun = null;
|
|
134
135
|
if (job.status === JobStatus.ACTIVE) {
|
|
135
|
-
nextRun = this.calculateNextRun(job);
|
|
136
|
+
nextRun = this.calculateNextRun(job, new Date());
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
this.jobs.set(job.id, {
|
|
@@ -147,15 +148,16 @@ export class Scheduler {
|
|
|
147
148
|
/**
|
|
148
149
|
* Calculate the next run time for a job
|
|
149
150
|
* @param {object} job - Job object
|
|
151
|
+
* @param {Date} fromDate - Date to calculate from
|
|
150
152
|
* @returns {Date|null} Next run time or null
|
|
151
153
|
*/
|
|
152
|
-
calculateNextRun(job) {
|
|
154
|
+
calculateNextRun(job, fromDate) {
|
|
153
155
|
if (job.status !== JobStatus.ACTIVE) {
|
|
154
156
|
return null;
|
|
155
157
|
}
|
|
156
158
|
|
|
157
159
|
if (job.type === JobType.CRON && job.cron) {
|
|
158
|
-
return getNextRunTime(job.cron);
|
|
160
|
+
return getNextRunTime(job.cron, fromDate);
|
|
159
161
|
}
|
|
160
162
|
|
|
161
163
|
if (job.type === JobType.ONCE && job.runAt) {
|
|
@@ -167,6 +169,62 @@ export class Scheduler {
|
|
|
167
169
|
return null;
|
|
168
170
|
}
|
|
169
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Calculate the next run time for a cron job, accounting for missed runs
|
|
174
|
+
* This ensures that after sleep/wake, we find the very next occurrence
|
|
175
|
+
* @param {object} job - Job object
|
|
176
|
+
* @param {Date} originalRunTime - The time the job was originally scheduled to run
|
|
177
|
+
* @returns {Date|null} Next run time or null
|
|
178
|
+
*/
|
|
179
|
+
calculateNextRunAfterExecution(job, originalRunTime) {
|
|
180
|
+
if (job.status !== JobStatus.ACTIVE || job.type !== JobType.CRON || !job.cron) {
|
|
181
|
+
return this.calculateNextRun(job, new Date());
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const now = new Date();
|
|
185
|
+
let nextRun = this.calculateNextRun(job, originalRunTime);
|
|
186
|
+
|
|
187
|
+
// Keep calculating until we find a time in the future
|
|
188
|
+
// This handles the case where the system woke from sleep and we missed multiple runs
|
|
189
|
+
while (nextRun && nextRun <= now) {
|
|
190
|
+
nextRun = this.calculateNextRun(job, nextRun);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return nextRun;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Recalculate next run times for periodic jobs that have drifted into the past
|
|
198
|
+
* This handles system sleep/wake scenarios where nextRun becomes stale
|
|
199
|
+
* Only recalculates jobs that are significantly overdue (missed multiple runs)
|
|
200
|
+
* @param {Date} now - Current time
|
|
201
|
+
*/
|
|
202
|
+
recalculateStalePeriodicJobs(now) {
|
|
203
|
+
for (const [id, job] of this.jobs) {
|
|
204
|
+
if (
|
|
205
|
+
job.status === JobStatus.ACTIVE &&
|
|
206
|
+
job.type === JobType.CRON &&
|
|
207
|
+
job.cron &&
|
|
208
|
+
job.nextRun
|
|
209
|
+
) {
|
|
210
|
+
const timeSinceNextRun = now.getTime() - job.nextRun.getTime();
|
|
211
|
+
const isSignificantlyOverdue = timeSinceNextRun > this.checkIntervalMs * 2;
|
|
212
|
+
|
|
213
|
+
if (isSignificantlyOverdue) {
|
|
214
|
+
// Job is significantly overdue - recalculate from now to find next future occurrence
|
|
215
|
+
const newNextRun = this.calculateNextRun(job, now);
|
|
216
|
+
if (newNextRun && newNextRun !== job.nextRun) {
|
|
217
|
+
this.logger.debug(
|
|
218
|
+
`Recalculating next run for job ${id} (${job.name || 'unnamed'}): ` +
|
|
219
|
+
`${job.nextRun.toISOString()} → ${newNextRun.toISOString()}`
|
|
220
|
+
);
|
|
221
|
+
this.updateJobNextRun(id, newNextRun);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
170
228
|
/**
|
|
171
229
|
* Get jobs that are due to run
|
|
172
230
|
* @returns {Array} Array of jobs that should run now
|
|
@@ -249,11 +307,32 @@ export class Scheduler {
|
|
|
249
307
|
return [];
|
|
250
308
|
}
|
|
251
309
|
|
|
310
|
+
// Detect sleep/wake events by checking if too much time has passed since last tick
|
|
311
|
+
const now = Date.now();
|
|
312
|
+
const timeSinceLastTick = this.lastTickTime ? now - this.lastTickTime : 0;
|
|
313
|
+
const sleepThreshold = this.checkIntervalMs * 5; // If more than 5 intervals passed, likely woke from sleep
|
|
314
|
+
|
|
315
|
+
if (timeSinceLastTick > sleepThreshold) {
|
|
316
|
+
const secondsAsleep = Math.round(timeSinceLastTick / 1000);
|
|
317
|
+
this.logger.info(`System wake detected - was asleep for ${secondsAsleep}s, catching up on due jobs`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
this.lastTickTime = now;
|
|
321
|
+
|
|
322
|
+
const nowDate = new Date();
|
|
323
|
+
|
|
324
|
+
// Recalculate next run for periodic jobs that have drifted into the past
|
|
325
|
+
// This handles system sleep/wake scenarios
|
|
326
|
+
this.recalculateStalePeriodicJobs(nowDate);
|
|
327
|
+
|
|
252
328
|
const dueJobs = this.getDueJobs();
|
|
253
329
|
|
|
254
330
|
for (const job of dueJobs) {
|
|
255
331
|
this.logger.debug(`Job ${job.id} (${job.name || 'unnamed'}) is due`);
|
|
256
332
|
|
|
333
|
+
// Store the original scheduled time before execution
|
|
334
|
+
const originalNextRun = job.nextRun ? new Date(job.nextRun) : new Date();
|
|
335
|
+
|
|
257
336
|
// Execute the job
|
|
258
337
|
this.executeJob(job);
|
|
259
338
|
|
|
@@ -261,8 +340,12 @@ export class Scheduler {
|
|
|
261
340
|
if (job.type === JobType.ONCE) {
|
|
262
341
|
this.updateJobStatus(job.id, JobStatus.COMPLETED);
|
|
263
342
|
} else {
|
|
264
|
-
// For cron jobs, recalculate next run time
|
|
265
|
-
|
|
343
|
+
// For cron jobs, recalculate next run time from the original scheduled time
|
|
344
|
+
// This ensures we don't miss runs after system wake from sleep
|
|
345
|
+
const nextRun = this.calculateNextRunAfterExecution(
|
|
346
|
+
{ ...job, status: JobStatus.ACTIVE },
|
|
347
|
+
originalNextRun
|
|
348
|
+
);
|
|
266
349
|
this.updateJobNextRun(job.id, nextRun);
|
|
267
350
|
}
|
|
268
351
|
}
|
|
@@ -291,7 +374,7 @@ export class Scheduler {
|
|
|
291
374
|
}
|
|
292
375
|
|
|
293
376
|
// Calculate initial next run
|
|
294
|
-
const nextRun = this.calculateNextRun(jobData);
|
|
377
|
+
const nextRun = this.calculateNextRun(jobData, new Date());
|
|
295
378
|
const job = { ...jobData, nextRun };
|
|
296
379
|
|
|
297
380
|
// Add to memory
|
|
@@ -368,7 +451,7 @@ export class Scheduler {
|
|
|
368
451
|
};
|
|
369
452
|
|
|
370
453
|
// Recalculate next run if needed
|
|
371
|
-
updatedJob.nextRun = this.calculateNextRun(updatedJob);
|
|
454
|
+
updatedJob.nextRun = this.calculateNextRun(updatedJob, new Date());
|
|
372
455
|
|
|
373
456
|
this.jobs.set(jobId, updatedJob);
|
|
374
457
|
this.persistJobs();
|
|
@@ -390,7 +473,7 @@ export class Scheduler {
|
|
|
390
473
|
// Recalculate next run when activating
|
|
391
474
|
const job = this.jobs.get(jobId);
|
|
392
475
|
if (job) {
|
|
393
|
-
updates.nextRun = this.calculateNextRun({ ...job, status });
|
|
476
|
+
updates.nextRun = this.calculateNextRun({ ...job, status }, new Date());
|
|
394
477
|
}
|
|
395
478
|
} else {
|
|
396
479
|
updates.nextRun = null;
|