jm2 0.1.0
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/GNU-AGPL-3.0 +665 -0
- package/README.md +603 -0
- package/bin/jm2.js +24 -0
- package/package.json +70 -0
- package/src/cli/commands/add.js +206 -0
- package/src/cli/commands/config.js +212 -0
- package/src/cli/commands/edit.js +198 -0
- package/src/cli/commands/export.js +61 -0
- package/src/cli/commands/flush.js +132 -0
- package/src/cli/commands/history.js +179 -0
- package/src/cli/commands/import.js +180 -0
- package/src/cli/commands/list.js +174 -0
- package/src/cli/commands/logs.js +415 -0
- package/src/cli/commands/pause.js +97 -0
- package/src/cli/commands/remove.js +107 -0
- package/src/cli/commands/restart.js +68 -0
- package/src/cli/commands/resume.js +96 -0
- package/src/cli/commands/run.js +115 -0
- package/src/cli/commands/show.js +159 -0
- package/src/cli/commands/start.js +46 -0
- package/src/cli/commands/status.js +47 -0
- package/src/cli/commands/stop.js +48 -0
- package/src/cli/index.js +274 -0
- package/src/cli/utils/output.js +267 -0
- package/src/cli/utils/prompts.js +56 -0
- package/src/core/config.js +227 -0
- package/src/core/history-db.js +439 -0
- package/src/core/job.js +329 -0
- package/src/core/logger.js +382 -0
- package/src/core/storage.js +315 -0
- package/src/daemon/executor.js +409 -0
- package/src/daemon/index.js +873 -0
- package/src/daemon/scheduler.js +465 -0
- package/src/ipc/client.js +112 -0
- package/src/ipc/protocol.js +183 -0
- package/src/ipc/server.js +92 -0
- package/src/utils/cron.js +205 -0
- package/src/utils/datetime.js +237 -0
- package/src/utils/duration.js +226 -0
- package/src/utils/paths.js +164 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job scheduler for JM2 daemon
|
|
3
|
+
* Manages cron jobs and one-time job scheduling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getNextRunTime, isValidCronExpression } from '../utils/cron.js';
|
|
7
|
+
import { JobStatus, JobType } from '../core/job.js';
|
|
8
|
+
import { getJobs, saveJobs } from '../core/storage.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Scheduler class - manages job scheduling state and execution timing
|
|
12
|
+
*/
|
|
13
|
+
export class Scheduler {
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this.logger = options.logger || console;
|
|
16
|
+
this.executor = options.executor || null;
|
|
17
|
+
this.jobs = new Map(); // Map of job ID to scheduled job info
|
|
18
|
+
this.running = false;
|
|
19
|
+
this.checkInterval = null;
|
|
20
|
+
this.checkIntervalMs = options.checkIntervalMs || 1000; // Check every second
|
|
21
|
+
this.runningJobs = new Set(); // Track currently running job IDs
|
|
22
|
+
this.maxConcurrent = options.maxConcurrent || 10; // Max concurrent job executions
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Start the scheduler
|
|
27
|
+
*/
|
|
28
|
+
start() {
|
|
29
|
+
if (this.running) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.running = true;
|
|
34
|
+
this.logger.info('Scheduler starting...');
|
|
35
|
+
|
|
36
|
+
// Load jobs from storage
|
|
37
|
+
this.loadJobs();
|
|
38
|
+
|
|
39
|
+
// Start the check interval
|
|
40
|
+
this.checkInterval = setInterval(() => {
|
|
41
|
+
this.tick();
|
|
42
|
+
}, this.checkIntervalMs);
|
|
43
|
+
|
|
44
|
+
this.logger.info('Scheduler started');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Stop the scheduler
|
|
49
|
+
*/
|
|
50
|
+
stop() {
|
|
51
|
+
if (!this.running) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.running = false;
|
|
56
|
+
|
|
57
|
+
if (this.checkInterval) {
|
|
58
|
+
clearInterval(this.checkInterval);
|
|
59
|
+
this.checkInterval = null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.logger.info('Scheduler stopped');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Load jobs from storage into memory
|
|
67
|
+
*/
|
|
68
|
+
loadJobs() {
|
|
69
|
+
const storedJobs = getJobs();
|
|
70
|
+
this.jobs.clear();
|
|
71
|
+
|
|
72
|
+
for (const job of storedJobs) {
|
|
73
|
+
// Assign ID if missing (for backward compatibility with old imports)
|
|
74
|
+
if (!job.id) {
|
|
75
|
+
job.id = this.generateJobId();
|
|
76
|
+
}
|
|
77
|
+
this.addJobToMemory(job);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Handle expired one-time jobs that were missed while daemon was stopped
|
|
81
|
+
this.handleExpiredOneTimeJobs();
|
|
82
|
+
|
|
83
|
+
this.logger.debug(`Loaded ${this.jobs.size} jobs`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Handle expired one-time jobs on daemon restart
|
|
88
|
+
* Jobs that are past their run time and still active should be marked as failed
|
|
89
|
+
*/
|
|
90
|
+
handleExpiredOneTimeJobs() {
|
|
91
|
+
const now = new Date();
|
|
92
|
+
let expiredCount = 0;
|
|
93
|
+
|
|
94
|
+
for (const [id, job] of this.jobs) {
|
|
95
|
+
if (
|
|
96
|
+
job.type === JobType.ONCE &&
|
|
97
|
+
job.status === JobStatus.ACTIVE &&
|
|
98
|
+
job.runAt
|
|
99
|
+
) {
|
|
100
|
+
const runAt = new Date(job.runAt);
|
|
101
|
+
if (runAt < now) {
|
|
102
|
+
// Job was scheduled to run while daemon was stopped
|
|
103
|
+
this.updateJobStatus(id, JobStatus.FAILED);
|
|
104
|
+
this.updateJob(id, {
|
|
105
|
+
lastResult: 'failed',
|
|
106
|
+
error: 'Job expired - daemon was not running at scheduled time',
|
|
107
|
+
expiredAt: now.toISOString(),
|
|
108
|
+
});
|
|
109
|
+
expiredCount++;
|
|
110
|
+
this.logger.warn(
|
|
111
|
+
`Job ${id} (${job.name || 'unnamed'}) expired - was scheduled for ${runAt.toISOString()}`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (expiredCount > 0) {
|
|
118
|
+
this.logger.info(`Marked ${expiredCount} expired one-time job(s) as failed`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Add a job to the in-memory map
|
|
124
|
+
* @param {object} job - Job object
|
|
125
|
+
*/
|
|
126
|
+
addJobToMemory(job) {
|
|
127
|
+
// Ensure job has an ID
|
|
128
|
+
if (!job.id) {
|
|
129
|
+
job.id = this.generateJobId();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Calculate next run time for active jobs
|
|
133
|
+
let nextRun = null;
|
|
134
|
+
if (job.status === JobStatus.ACTIVE) {
|
|
135
|
+
nextRun = this.calculateNextRun(job);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.jobs.set(job.id, {
|
|
139
|
+
...job,
|
|
140
|
+
nextRun,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Update job in storage with next run time
|
|
144
|
+
this.updateJobNextRun(job.id, nextRun);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Calculate the next run time for a job
|
|
149
|
+
* @param {object} job - Job object
|
|
150
|
+
* @returns {Date|null} Next run time or null
|
|
151
|
+
*/
|
|
152
|
+
calculateNextRun(job) {
|
|
153
|
+
if (job.status !== JobStatus.ACTIVE) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (job.type === JobType.CRON && job.cron) {
|
|
158
|
+
return getNextRunTime(job.cron);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (job.type === JobType.ONCE && job.runAt) {
|
|
162
|
+
// Always return the runAt date for one-time jobs
|
|
163
|
+
// This allows jobs that are already due to be processed
|
|
164
|
+
return new Date(job.runAt);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get jobs that are due to run
|
|
172
|
+
* @returns {Array} Array of jobs that should run now
|
|
173
|
+
*/
|
|
174
|
+
getDueJobs() {
|
|
175
|
+
const now = new Date();
|
|
176
|
+
const dueJobs = [];
|
|
177
|
+
|
|
178
|
+
for (const [id, job] of this.jobs) {
|
|
179
|
+
if (job.status !== JobStatus.ACTIVE) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (job.nextRun && job.nextRun <= now) {
|
|
184
|
+
dueJobs.push({ ...job });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return dueJobs;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Execute a job using the executor
|
|
193
|
+
* @param {object} job - Job to execute
|
|
194
|
+
*/
|
|
195
|
+
async executeJob(job) {
|
|
196
|
+
if (!this.executor) {
|
|
197
|
+
this.logger.warn(`Cannot execute job ${job.id}: no executor configured`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (this.runningJobs.has(job.id)) {
|
|
202
|
+
this.logger.warn(`Job ${job.id} is already running`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check concurrent execution limit
|
|
207
|
+
if (this.runningJobs.size >= this.maxConcurrent) {
|
|
208
|
+
this.logger.warn(`Cannot execute job ${job.id}: max concurrent jobs (${this.maxConcurrent}) reached`);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this.runningJobs.add(job.id);
|
|
213
|
+
this.logger.info(`Executing job ${job.id} (${job.name || 'unnamed'})`);
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const result = await this.executor.executeJobWithRetry(job);
|
|
217
|
+
|
|
218
|
+
// Update job stats
|
|
219
|
+
const updatedJob = this.jobs.get(job.id);
|
|
220
|
+
if (updatedJob) {
|
|
221
|
+
updatedJob.runCount = (updatedJob.runCount || 0) + 1;
|
|
222
|
+
updatedJob.lastRun = new Date().toISOString();
|
|
223
|
+
updatedJob.lastResult = result.status === 'success' ? 'success' : 'failed';
|
|
224
|
+
this.persistJobs();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
this.logger.info(`Job ${job.id} completed: ${result.status === 'success' ? 'success' : 'failed'}`);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
this.logger.error(`Job ${job.id} failed: ${error.message}`);
|
|
230
|
+
|
|
231
|
+
const updatedJob = this.jobs.get(job.id);
|
|
232
|
+
if (updatedJob) {
|
|
233
|
+
updatedJob.runCount = (updatedJob.runCount || 0) + 1;
|
|
234
|
+
updatedJob.lastRun = new Date().toISOString();
|
|
235
|
+
updatedJob.lastResult = 'failed';
|
|
236
|
+
this.persistJobs();
|
|
237
|
+
}
|
|
238
|
+
} finally {
|
|
239
|
+
this.runningJobs.delete(job.id);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Process due jobs - called on each tick
|
|
245
|
+
* @returns {Array} Jobs that were processed
|
|
246
|
+
*/
|
|
247
|
+
tick() {
|
|
248
|
+
if (!this.running) {
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const dueJobs = this.getDueJobs();
|
|
253
|
+
|
|
254
|
+
for (const job of dueJobs) {
|
|
255
|
+
this.logger.debug(`Job ${job.id} (${job.name || 'unnamed'}) is due`);
|
|
256
|
+
|
|
257
|
+
// Execute the job
|
|
258
|
+
this.executeJob(job);
|
|
259
|
+
|
|
260
|
+
// For one-time jobs, mark as completed after scheduling
|
|
261
|
+
if (job.type === JobType.ONCE) {
|
|
262
|
+
this.updateJobStatus(job.id, JobStatus.COMPLETED);
|
|
263
|
+
} else {
|
|
264
|
+
// For cron jobs, recalculate next run time
|
|
265
|
+
const nextRun = this.calculateNextRun({ ...job, status: JobStatus.ACTIVE });
|
|
266
|
+
this.updateJobNextRun(job.id, nextRun);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return dueJobs;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Add a new job
|
|
275
|
+
* @param {object} jobData - Job data
|
|
276
|
+
* @returns {object} Added job
|
|
277
|
+
*/
|
|
278
|
+
addJob(jobData) {
|
|
279
|
+
// Generate ID if not provided
|
|
280
|
+
if (!jobData.id) {
|
|
281
|
+
jobData.id = this.generateJobId();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Determine job type
|
|
285
|
+
if (!jobData.type) {
|
|
286
|
+
if (jobData.cron) {
|
|
287
|
+
jobData.type = JobType.CRON;
|
|
288
|
+
} else if (jobData.runAt) {
|
|
289
|
+
jobData.type = JobType.ONCE;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Calculate initial next run
|
|
294
|
+
const nextRun = this.calculateNextRun(jobData);
|
|
295
|
+
const job = { ...jobData, nextRun };
|
|
296
|
+
|
|
297
|
+
// Add to memory
|
|
298
|
+
this.jobs.set(job.id, job);
|
|
299
|
+
|
|
300
|
+
// Save to storage
|
|
301
|
+
this.persistJobs();
|
|
302
|
+
|
|
303
|
+
this.logger.info(`Job ${job.id} (${job.name || 'unnamed'}) added`);
|
|
304
|
+
|
|
305
|
+
return job;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Remove a job
|
|
310
|
+
* @param {number} jobId - Job ID
|
|
311
|
+
* @returns {boolean} True if removed
|
|
312
|
+
*/
|
|
313
|
+
removeJob(jobId) {
|
|
314
|
+
const job = this.jobs.get(jobId);
|
|
315
|
+
if (!job) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
this.jobs.delete(jobId);
|
|
320
|
+
this.persistJobs();
|
|
321
|
+
|
|
322
|
+
this.logger.info(`Job ${jobId} removed`);
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Update a job
|
|
328
|
+
* @param {number} jobId - Job ID
|
|
329
|
+
* @param {object} updates - Updates to apply
|
|
330
|
+
* @returns {object|null} Updated job or null
|
|
331
|
+
*/
|
|
332
|
+
updateJob(jobId, updates) {
|
|
333
|
+
const job = this.jobs.get(jobId);
|
|
334
|
+
if (!job) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const updatedJob = {
|
|
339
|
+
...job,
|
|
340
|
+
...updates,
|
|
341
|
+
id: jobId, // Don't allow changing ID
|
|
342
|
+
updatedAt: new Date().toISOString(),
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// Recalculate next run if needed
|
|
346
|
+
updatedJob.nextRun = this.calculateNextRun(updatedJob);
|
|
347
|
+
|
|
348
|
+
this.jobs.set(jobId, updatedJob);
|
|
349
|
+
this.persistJobs();
|
|
350
|
+
|
|
351
|
+
this.logger.info(`Job ${jobId} updated`);
|
|
352
|
+
return updatedJob;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Update job status
|
|
357
|
+
* @param {number} jobId - Job ID
|
|
358
|
+
* @param {string} status - New status
|
|
359
|
+
* @returns {object|null} Updated job or null
|
|
360
|
+
*/
|
|
361
|
+
updateJobStatus(jobId, status) {
|
|
362
|
+
const updates = { status };
|
|
363
|
+
|
|
364
|
+
if (status === JobStatus.ACTIVE) {
|
|
365
|
+
// Recalculate next run when activating
|
|
366
|
+
const job = this.jobs.get(jobId);
|
|
367
|
+
if (job) {
|
|
368
|
+
updates.nextRun = this.calculateNextRun({ ...job, status });
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
updates.nextRun = null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return this.updateJob(jobId, updates);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Update job's next run time in storage
|
|
379
|
+
* @param {number} jobId - Job ID
|
|
380
|
+
* @param {Date|null} nextRun - Next run time
|
|
381
|
+
*/
|
|
382
|
+
updateJobNextRun(jobId, nextRun) {
|
|
383
|
+
const job = this.jobs.get(jobId);
|
|
384
|
+
if (job) {
|
|
385
|
+
job.nextRun = nextRun;
|
|
386
|
+
job.nextRunISO = nextRun ? nextRun.toISOString() : null;
|
|
387
|
+
}
|
|
388
|
+
this.persistJobs();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Get all jobs
|
|
393
|
+
* @returns {Array} Array of all jobs
|
|
394
|
+
*/
|
|
395
|
+
getAllJobs() {
|
|
396
|
+
return Array.from(this.jobs.values());
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Get a job by ID
|
|
401
|
+
* @param {number} jobId - Job ID
|
|
402
|
+
* @returns {object|null} Job or null
|
|
403
|
+
*/
|
|
404
|
+
getJob(jobId) {
|
|
405
|
+
return this.jobs.get(jobId) || null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Generate a unique job ID
|
|
410
|
+
* @returns {number} New job ID
|
|
411
|
+
*/
|
|
412
|
+
generateJobId() {
|
|
413
|
+
let maxId = 0;
|
|
414
|
+
for (const id of this.jobs.keys()) {
|
|
415
|
+
if (id > maxId) {
|
|
416
|
+
maxId = id;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return maxId + 1;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Persist jobs to storage
|
|
424
|
+
*/
|
|
425
|
+
persistJobs() {
|
|
426
|
+
const jobsArray = Array.from(this.jobs.values()).map(job => ({
|
|
427
|
+
...job,
|
|
428
|
+
nextRun: job.nextRun ? job.nextRun.toISOString() : null,
|
|
429
|
+
}));
|
|
430
|
+
saveJobs(jobsArray);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Get scheduler statistics
|
|
435
|
+
* @returns {object} Statistics object
|
|
436
|
+
*/
|
|
437
|
+
getStats() {
|
|
438
|
+
const jobs = this.getAllJobs();
|
|
439
|
+
const activeJobs = jobs.filter(j => j.status === JobStatus.ACTIVE);
|
|
440
|
+
const cronJobs = jobs.filter(j => j.type === JobType.CRON);
|
|
441
|
+
const onceJobs = jobs.filter(j => j.type === JobType.ONCE);
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
totalJobs: jobs.length,
|
|
445
|
+
activeJobs: activeJobs.length,
|
|
446
|
+
pausedJobs: jobs.filter(j => j.status === JobStatus.PAUSED).length,
|
|
447
|
+
completedJobs: jobs.filter(j => j.status === JobStatus.COMPLETED).length,
|
|
448
|
+
failedJobs: jobs.filter(j => j.status === JobStatus.FAILED).length,
|
|
449
|
+
cronJobs: cronJobs.length,
|
|
450
|
+
onceJobs: onceJobs.length,
|
|
451
|
+
dueJobs: this.getDueJobs().length,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Create a new scheduler instance
|
|
458
|
+
* @param {object} options - Scheduler options
|
|
459
|
+
* @returns {Scheduler} Scheduler instance
|
|
460
|
+
*/
|
|
461
|
+
export function createScheduler(options = {}) {
|
|
462
|
+
return new Scheduler(options);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export default Scheduler;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPC client for JM2 CLI
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createConnection } from 'node:net';
|
|
6
|
+
import { getSocketPath } from '../utils/paths.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Custom error class for daemon-related errors
|
|
10
|
+
*/
|
|
11
|
+
export class DaemonError extends Error {
|
|
12
|
+
constructor(message, code) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'DaemonError';
|
|
15
|
+
this.code = code;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if error is a connection refused error (daemon not running)
|
|
21
|
+
* @param {Error} error - Error object
|
|
22
|
+
* @returns {boolean} True if connection refused
|
|
23
|
+
*/
|
|
24
|
+
function isConnectionRefusedError(error) {
|
|
25
|
+
return error.code === 'ECONNREFUSED' ||
|
|
26
|
+
error.code === 'ENOENT' ||
|
|
27
|
+
error.message?.includes('connect') ||
|
|
28
|
+
error.message?.includes('No such file or directory');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Send a message to the daemon
|
|
33
|
+
* @param {object} message - Message to send
|
|
34
|
+
* @param {object} options - Client options
|
|
35
|
+
* @param {number} options.timeoutMs - Timeout in milliseconds
|
|
36
|
+
* @returns {Promise<object>} Response message
|
|
37
|
+
*/
|
|
38
|
+
export function send(message, options = {}) {
|
|
39
|
+
const { timeoutMs = 2000 } = options;
|
|
40
|
+
const socketPath = getSocketPath();
|
|
41
|
+
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const client = createConnection(socketPath);
|
|
44
|
+
let buffer = '';
|
|
45
|
+
let finished = false;
|
|
46
|
+
|
|
47
|
+
const timeout = setTimeout(() => {
|
|
48
|
+
if (!finished) {
|
|
49
|
+
finished = true;
|
|
50
|
+
client.destroy();
|
|
51
|
+
reject(new DaemonError('IPC request timed out', 'ETIMEOUT'));
|
|
52
|
+
}
|
|
53
|
+
}, timeoutMs);
|
|
54
|
+
|
|
55
|
+
client.on('error', err => {
|
|
56
|
+
if (!finished) {
|
|
57
|
+
finished = true;
|
|
58
|
+
clearTimeout(timeout);
|
|
59
|
+
|
|
60
|
+
// Provide user-friendly error for daemon not running
|
|
61
|
+
if (isConnectionRefusedError(err)) {
|
|
62
|
+
reject(new DaemonError(
|
|
63
|
+
'Daemon is not running. Start it with: jm2 start',
|
|
64
|
+
'EDAEMON_NOT_RUNNING'
|
|
65
|
+
));
|
|
66
|
+
} else {
|
|
67
|
+
reject(new DaemonError(
|
|
68
|
+
`IPC communication failed: ${err.message}`,
|
|
69
|
+
err.code || 'EIPC_ERROR'
|
|
70
|
+
));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
client.on('data', data => {
|
|
76
|
+
buffer += data.toString();
|
|
77
|
+
let index;
|
|
78
|
+
while ((index = buffer.indexOf('\n')) !== -1) {
|
|
79
|
+
const line = buffer.slice(0, index).trim();
|
|
80
|
+
buffer = buffer.slice(index + 1);
|
|
81
|
+
if (!line) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const response = JSON.parse(line);
|
|
86
|
+
if (!finished) {
|
|
87
|
+
finished = true;
|
|
88
|
+
clearTimeout(timeout);
|
|
89
|
+
client.end();
|
|
90
|
+
resolve(response);
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (!finished) {
|
|
94
|
+
finished = true;
|
|
95
|
+
clearTimeout(timeout);
|
|
96
|
+
client.end();
|
|
97
|
+
reject(new DaemonError('Invalid JSON response from daemon', 'EINVALID_JSON'));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
client.on('connect', () => {
|
|
104
|
+
client.write(`${JSON.stringify(message)}\n`);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default {
|
|
110
|
+
send,
|
|
111
|
+
DaemonError,
|
|
112
|
+
};
|