lsh-framework 0.5.4
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/.env.example +51 -0
- package/README.md +399 -0
- package/dist/app.js +33 -0
- package/dist/cicd/analytics.js +261 -0
- package/dist/cicd/auth.js +269 -0
- package/dist/cicd/cache-manager.js +172 -0
- package/dist/cicd/data-retention.js +305 -0
- package/dist/cicd/performance-monitor.js +224 -0
- package/dist/cicd/webhook-receiver.js +634 -0
- package/dist/cli.js +500 -0
- package/dist/commands/api.js +343 -0
- package/dist/commands/self.js +318 -0
- package/dist/commands/theme.js +257 -0
- package/dist/commands/zsh-import.js +240 -0
- package/dist/components/App.js +1 -0
- package/dist/components/Divider.js +29 -0
- package/dist/components/REPL.js +43 -0
- package/dist/components/Terminal.js +232 -0
- package/dist/components/UserInput.js +30 -0
- package/dist/daemon/api-server.js +315 -0
- package/dist/daemon/job-registry.js +554 -0
- package/dist/daemon/lshd.js +822 -0
- package/dist/daemon/monitoring-api.js +220 -0
- package/dist/examples/supabase-integration.js +106 -0
- package/dist/lib/api-error-handler.js +183 -0
- package/dist/lib/associative-arrays.js +285 -0
- package/dist/lib/base-api-server.js +290 -0
- package/dist/lib/base-command-registrar.js +286 -0
- package/dist/lib/base-job-manager.js +293 -0
- package/dist/lib/brace-expansion.js +160 -0
- package/dist/lib/builtin-commands.js +439 -0
- package/dist/lib/cloud-config-manager.js +347 -0
- package/dist/lib/command-validator.js +190 -0
- package/dist/lib/completion-system.js +344 -0
- package/dist/lib/cron-job-manager.js +364 -0
- package/dist/lib/daemon-client-helper.js +141 -0
- package/dist/lib/daemon-client.js +501 -0
- package/dist/lib/database-persistence.js +638 -0
- package/dist/lib/database-schema.js +259 -0
- package/dist/lib/enhanced-history-system.js +246 -0
- package/dist/lib/env-validator.js +265 -0
- package/dist/lib/executors/builtin-executor.js +52 -0
- package/dist/lib/extended-globbing.js +411 -0
- package/dist/lib/extended-parameter-expansion.js +227 -0
- package/dist/lib/floating-point-arithmetic.js +256 -0
- package/dist/lib/history-system.js +245 -0
- package/dist/lib/interactive-shell.js +460 -0
- package/dist/lib/job-builtins.js +580 -0
- package/dist/lib/job-manager.js +386 -0
- package/dist/lib/job-storage-database.js +156 -0
- package/dist/lib/job-storage-memory.js +73 -0
- package/dist/lib/logger.js +274 -0
- package/dist/lib/lshrc-init.js +177 -0
- package/dist/lib/pathname-expansion.js +216 -0
- package/dist/lib/prompt-system.js +328 -0
- package/dist/lib/script-runner.js +226 -0
- package/dist/lib/secrets-manager.js +193 -0
- package/dist/lib/shell-executor.js +2504 -0
- package/dist/lib/shell-parser.js +958 -0
- package/dist/lib/shell-types.js +6 -0
- package/dist/lib/shell.lib.js +40 -0
- package/dist/lib/supabase-client.js +58 -0
- package/dist/lib/theme-manager.js +476 -0
- package/dist/lib/variable-expansion.js +385 -0
- package/dist/lib/zsh-compatibility.js +658 -0
- package/dist/lib/zsh-import-manager.js +699 -0
- package/dist/lib/zsh-options.js +328 -0
- package/dist/pipeline/job-tracker.js +491 -0
- package/dist/pipeline/mcli-bridge.js +302 -0
- package/dist/pipeline/pipeline-service.js +1116 -0
- package/dist/pipeline/workflow-engine.js +867 -0
- package/dist/services/api/api.js +58 -0
- package/dist/services/api/auth.js +35 -0
- package/dist/services/api/config.js +7 -0
- package/dist/services/api/file.js +22 -0
- package/dist/services/cron/cron-registrar.js +235 -0
- package/dist/services/cron/cron.js +9 -0
- package/dist/services/daemon/daemon-registrar.js +565 -0
- package/dist/services/daemon/daemon.js +9 -0
- package/dist/services/lib/lib.js +86 -0
- package/dist/services/log-file-extractor.js +170 -0
- package/dist/services/secrets/secrets.js +94 -0
- package/dist/services/shell/shell.js +28 -0
- package/dist/services/supabase/supabase-registrar.js +367 -0
- package/dist/services/supabase/supabase.js +9 -0
- package/dist/services/zapier.js +16 -0
- package/dist/simple-api-server.js +148 -0
- package/dist/store/store.js +31 -0
- package/dist/util/lib.util.js +11 -0
- package/package.json +144 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job Management System for LSH Shell
|
|
3
|
+
* Supports CRUD operations on shell jobs and system processes
|
|
4
|
+
*
|
|
5
|
+
* REFACTORED: Now extends BaseJobManager to eliminate duplication
|
|
6
|
+
*/
|
|
7
|
+
import { spawn, exec } from 'child_process';
|
|
8
|
+
import { promisify } from 'util';
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import { BaseJobManager, } from './base-job-manager.js';
|
|
11
|
+
import MemoryJobStorage from './job-storage-memory.js';
|
|
12
|
+
const execAsync = promisify(exec);
|
|
13
|
+
export class JobManager extends BaseJobManager {
|
|
14
|
+
nextJobId = 1;
|
|
15
|
+
persistenceFile;
|
|
16
|
+
schedulerInterval;
|
|
17
|
+
constructor(persistenceFile = '/tmp/lsh-jobs.json') {
|
|
18
|
+
super(new MemoryJobStorage(), 'JobManager');
|
|
19
|
+
this.persistenceFile = persistenceFile;
|
|
20
|
+
this.loadPersistedJobs();
|
|
21
|
+
this.startScheduler();
|
|
22
|
+
this.setupCleanupHandlers();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Start a job (execute it as a process)
|
|
26
|
+
*/
|
|
27
|
+
async startJob(jobId) {
|
|
28
|
+
const baseJob = await this.getJob(jobId);
|
|
29
|
+
if (!baseJob) {
|
|
30
|
+
throw new Error(`Job ${jobId} not found`);
|
|
31
|
+
}
|
|
32
|
+
const job = baseJob;
|
|
33
|
+
if (job.status === 'running') {
|
|
34
|
+
throw new Error(`Job ${jobId} is already running`);
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
// Spawn the process
|
|
38
|
+
if (job.type === 'shell') {
|
|
39
|
+
job.process = spawn('sh', ['-c', job.command], {
|
|
40
|
+
cwd: job.cwd,
|
|
41
|
+
env: job.env,
|
|
42
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const [cmd, ...args] = job.command.split(' ');
|
|
47
|
+
job.process = spawn(cmd, args.concat(job.args || []), {
|
|
48
|
+
cwd: job.cwd,
|
|
49
|
+
env: job.env,
|
|
50
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
job.pid = job.process.pid;
|
|
54
|
+
// Handle output
|
|
55
|
+
job.process.stdout?.on('data', (data) => {
|
|
56
|
+
job.stdout = (job.stdout || '') + data.toString();
|
|
57
|
+
if (job.logFile) {
|
|
58
|
+
fs.appendFileSync(job.logFile, data);
|
|
59
|
+
}
|
|
60
|
+
this.emit('jobOutput', job.id, 'stdout', data.toString());
|
|
61
|
+
});
|
|
62
|
+
job.process.stderr?.on('data', (data) => {
|
|
63
|
+
job.stderr = (job.stderr || '') + data.toString();
|
|
64
|
+
if (job.logFile) {
|
|
65
|
+
fs.appendFileSync(job.logFile, data);
|
|
66
|
+
}
|
|
67
|
+
this.emit('jobOutput', job.id, 'stderr', data.toString());
|
|
68
|
+
});
|
|
69
|
+
// Handle completion
|
|
70
|
+
job.process.on('exit', (code, signal) => {
|
|
71
|
+
const status = code === 0 ? 'completed' : (signal === 'SIGKILL' ? 'killed' : 'failed');
|
|
72
|
+
this.updateJobStatus(job.id, status, {
|
|
73
|
+
completedAt: new Date(),
|
|
74
|
+
exitCode: code || undefined,
|
|
75
|
+
});
|
|
76
|
+
this.emit('jobCompleted', job, code, signal);
|
|
77
|
+
this.persistJobs();
|
|
78
|
+
});
|
|
79
|
+
// Set timeout if specified
|
|
80
|
+
if (job.timeout) {
|
|
81
|
+
job.timer = setTimeout(() => {
|
|
82
|
+
this.stopJob(job.id, 'SIGKILL');
|
|
83
|
+
}, job.timeout);
|
|
84
|
+
}
|
|
85
|
+
// Update status to running
|
|
86
|
+
const updatedJob = await this.updateJobStatus(job.id, 'running', {
|
|
87
|
+
startedAt: new Date(),
|
|
88
|
+
pid: job.pid,
|
|
89
|
+
});
|
|
90
|
+
await this.persistJobs();
|
|
91
|
+
return updatedJob;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
await this.updateJobStatus(job.id, 'failed', {
|
|
95
|
+
completedAt: new Date(),
|
|
96
|
+
stderr: error.message,
|
|
97
|
+
});
|
|
98
|
+
this.emit('jobFailed', job, error);
|
|
99
|
+
await this.persistJobs();
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Stop a running job
|
|
105
|
+
*/
|
|
106
|
+
async stopJob(jobId, signal = 'SIGTERM') {
|
|
107
|
+
const baseJob = await this.getJob(jobId);
|
|
108
|
+
if (!baseJob) {
|
|
109
|
+
throw new Error(`Job ${jobId} not found`);
|
|
110
|
+
}
|
|
111
|
+
const job = baseJob;
|
|
112
|
+
if (job.status !== 'running') {
|
|
113
|
+
throw new Error(`Job ${jobId} is not running`);
|
|
114
|
+
}
|
|
115
|
+
if (!job.process || !job.pid) {
|
|
116
|
+
throw new Error(`Job ${jobId} has no associated process`);
|
|
117
|
+
}
|
|
118
|
+
// Clear timeout if exists
|
|
119
|
+
if (job.timer) {
|
|
120
|
+
clearTimeout(job.timer);
|
|
121
|
+
job.timer = undefined;
|
|
122
|
+
}
|
|
123
|
+
// Kill the process
|
|
124
|
+
try {
|
|
125
|
+
job.process.kill(signal);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
this.logger.error(`Failed to kill job ${jobId}`, error);
|
|
129
|
+
}
|
|
130
|
+
// Update status
|
|
131
|
+
const updatedJob = await this.updateJobStatus(jobId, 'stopped', {
|
|
132
|
+
completedAt: new Date(),
|
|
133
|
+
});
|
|
134
|
+
await this.persistJobs();
|
|
135
|
+
return updatedJob;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Create and immediately start a job
|
|
139
|
+
*/
|
|
140
|
+
async runJob(spec) {
|
|
141
|
+
const job = await this.createJob(spec);
|
|
142
|
+
return await this.startJob(job.id);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Pause a job (stop it but keep for later resumption)
|
|
146
|
+
*/
|
|
147
|
+
async pauseJob(jobId) {
|
|
148
|
+
await this.stopJob(jobId, 'SIGSTOP');
|
|
149
|
+
return await this.updateJobStatus(jobId, 'paused');
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Resume a paused job
|
|
153
|
+
*/
|
|
154
|
+
async resumeJob(jobId) {
|
|
155
|
+
const baseJob = await this.getJob(jobId);
|
|
156
|
+
if (!baseJob) {
|
|
157
|
+
throw new Error(`Job ${jobId} not found`);
|
|
158
|
+
}
|
|
159
|
+
const job = baseJob;
|
|
160
|
+
if (job.status !== 'paused') {
|
|
161
|
+
throw new Error(`Job ${jobId} is not paused`);
|
|
162
|
+
}
|
|
163
|
+
if (!job.process || !job.pid) {
|
|
164
|
+
throw new Error(`Job ${jobId} has no associated process`);
|
|
165
|
+
}
|
|
166
|
+
// Send SIGCONT to resume
|
|
167
|
+
try {
|
|
168
|
+
job.process.kill('SIGCONT');
|
|
169
|
+
return await this.updateJobStatus(jobId, 'running');
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
throw new Error(`Failed to resume job ${jobId}: ${error}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Kill a job forcefully
|
|
177
|
+
*/
|
|
178
|
+
async killJob(jobId, signal = 'SIGKILL') {
|
|
179
|
+
return await this.stopJob(jobId, signal);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Monitor a job's resource usage
|
|
183
|
+
*/
|
|
184
|
+
async monitorJob(jobId) {
|
|
185
|
+
const baseJob = await this.getJob(jobId);
|
|
186
|
+
if (!baseJob) {
|
|
187
|
+
throw new Error(`Job ${jobId} not found`);
|
|
188
|
+
}
|
|
189
|
+
const job = baseJob;
|
|
190
|
+
if (!job.pid) {
|
|
191
|
+
throw new Error(`Job ${jobId} is not running`);
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const { stdout } = await execAsync(`ps -p ${job.pid} -o pid,ppid,pcpu,pmem,etime,state`);
|
|
195
|
+
const lines = stdout.split('\n');
|
|
196
|
+
if (lines.length < 2) {
|
|
197
|
+
return null; // Process not found
|
|
198
|
+
}
|
|
199
|
+
const parts = lines[1].trim().split(/\s+/);
|
|
200
|
+
const monitoring = {
|
|
201
|
+
pid: parseInt(parts[0]),
|
|
202
|
+
ppid: parseInt(parts[1]),
|
|
203
|
+
cpu: parseFloat(parts[2]),
|
|
204
|
+
memory: parseFloat(parts[3]),
|
|
205
|
+
elapsed: parts[4],
|
|
206
|
+
state: parts[5],
|
|
207
|
+
timestamp: new Date(),
|
|
208
|
+
};
|
|
209
|
+
// Update job with current resource usage
|
|
210
|
+
job.cpuUsage = monitoring.cpu;
|
|
211
|
+
job.memoryUsage = monitoring.memory;
|
|
212
|
+
this.emit('jobMonitoring', job, monitoring);
|
|
213
|
+
return monitoring;
|
|
214
|
+
}
|
|
215
|
+
catch (_error) {
|
|
216
|
+
return null; // Process likely terminated
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Get system processes
|
|
221
|
+
*/
|
|
222
|
+
async getSystemProcesses() {
|
|
223
|
+
try {
|
|
224
|
+
const { stdout } = await execAsync('ps -eo pid,ppid,user,pcpu,pmem,lstart,comm,args');
|
|
225
|
+
const lines = stdout.split('\n').slice(1); // Skip header
|
|
226
|
+
return lines
|
|
227
|
+
.filter(line => line.trim())
|
|
228
|
+
.map(line => {
|
|
229
|
+
const parts = line.trim().split(/\s+/);
|
|
230
|
+
return {
|
|
231
|
+
pid: parseInt(parts[0]),
|
|
232
|
+
ppid: parseInt(parts[1]),
|
|
233
|
+
user: parts[2],
|
|
234
|
+
cpu: parseFloat(parts[3]),
|
|
235
|
+
memory: parseFloat(parts[4]),
|
|
236
|
+
startTime: new Date(parts.slice(5, 9).join(' ')),
|
|
237
|
+
name: parts[9],
|
|
238
|
+
command: parts.slice(10).join(' ') || parts[9],
|
|
239
|
+
status: 'running'
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
this.logger.error('Failed to get system processes', error);
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Get job statistics
|
|
250
|
+
*/
|
|
251
|
+
getJobStats() {
|
|
252
|
+
const jobs = Array.from(this.jobs.values());
|
|
253
|
+
const stats = {
|
|
254
|
+
total: jobs.length,
|
|
255
|
+
byStatus: {},
|
|
256
|
+
byType: {},
|
|
257
|
+
running: jobs.filter(j => j.status === 'running').length,
|
|
258
|
+
completed: jobs.filter(j => j.status === 'completed').length,
|
|
259
|
+
failed: jobs.filter(j => j.status === 'failed').length,
|
|
260
|
+
};
|
|
261
|
+
jobs.forEach(job => {
|
|
262
|
+
stats.byStatus[job.status] = (stats.byStatus[job.status] || 0) + 1;
|
|
263
|
+
stats.byType[job.type] = (stats.byType[job.type] || 0) + 1;
|
|
264
|
+
});
|
|
265
|
+
return stats;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Clean up old jobs
|
|
269
|
+
*/
|
|
270
|
+
async cleanupJobs(olderThanHours = 24) {
|
|
271
|
+
const cutoff = new Date(Date.now() - olderThanHours * 60 * 60 * 1000);
|
|
272
|
+
const jobs = await this.listJobs();
|
|
273
|
+
let cleaned = 0;
|
|
274
|
+
for (const job of jobs) {
|
|
275
|
+
if (job.status === 'completed' || job.status === 'failed') {
|
|
276
|
+
if (job.completedAt && job.completedAt < cutoff) {
|
|
277
|
+
await this.removeJob(job.id, true);
|
|
278
|
+
cleaned++;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
this.logger.info(`Cleaned up ${cleaned} old jobs`);
|
|
283
|
+
return cleaned;
|
|
284
|
+
}
|
|
285
|
+
// ================================
|
|
286
|
+
// PRIVATE: Persistence & Scheduling
|
|
287
|
+
// ================================
|
|
288
|
+
async loadPersistedJobs() {
|
|
289
|
+
try {
|
|
290
|
+
if (fs.existsSync(this.persistenceFile)) {
|
|
291
|
+
const data = fs.readFileSync(this.persistenceFile, 'utf8');
|
|
292
|
+
const persistedJobs = JSON.parse(data);
|
|
293
|
+
for (const job of persistedJobs) {
|
|
294
|
+
// Convert date strings back to Date objects
|
|
295
|
+
job.createdAt = new Date(job.createdAt);
|
|
296
|
+
if (job.startedAt)
|
|
297
|
+
job.startedAt = new Date(job.startedAt);
|
|
298
|
+
if (job.completedAt)
|
|
299
|
+
job.completedAt = new Date(job.completedAt);
|
|
300
|
+
// Don't restore running processes - mark them as stopped
|
|
301
|
+
if (job.status === 'running') {
|
|
302
|
+
job.status = 'stopped';
|
|
303
|
+
}
|
|
304
|
+
await this.storage.save(job);
|
|
305
|
+
this.jobs.set(job.id, job);
|
|
306
|
+
}
|
|
307
|
+
this.logger.info(`Loaded ${persistedJobs.length} persisted jobs`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
this.logger.error('Failed to load persisted jobs', error);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async persistJobs() {
|
|
315
|
+
try {
|
|
316
|
+
const jobs = Array.from(this.jobs.values()).map(job => {
|
|
317
|
+
const { process: _process, timer: _timer, ...serializable } = job;
|
|
318
|
+
return serializable;
|
|
319
|
+
});
|
|
320
|
+
fs.writeFileSync(this.persistenceFile, JSON.stringify(jobs, null, 2));
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
this.logger.error('Failed to persist jobs', error);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
startScheduler() {
|
|
327
|
+
// Check for scheduled jobs every minute
|
|
328
|
+
this.schedulerInterval = setInterval(() => {
|
|
329
|
+
this.checkScheduledJobs();
|
|
330
|
+
}, 60000);
|
|
331
|
+
// Run immediately on startup
|
|
332
|
+
this.checkScheduledJobs();
|
|
333
|
+
}
|
|
334
|
+
async checkScheduledJobs() {
|
|
335
|
+
const jobs = await this.listJobs({ status: 'created' });
|
|
336
|
+
const now = new Date();
|
|
337
|
+
for (const job of jobs) {
|
|
338
|
+
if (job.schedule?.nextRun && job.schedule.nextRun <= now) {
|
|
339
|
+
this.logger.info(`Starting scheduled job: ${job.id}`);
|
|
340
|
+
try {
|
|
341
|
+
await this.startJob(job.id);
|
|
342
|
+
// Calculate next run time
|
|
343
|
+
if (job.schedule.interval) {
|
|
344
|
+
job.schedule.nextRun = new Date(now.getTime() + job.schedule.interval);
|
|
345
|
+
await this.updateJob(job.id, { schedule: job.schedule });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
this.logger.error(`Failed to start scheduled job ${job.id}`, error);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
setupCleanupHandlers() {
|
|
355
|
+
const cleanup = async () => {
|
|
356
|
+
this.logger.info('JobManager shutting down...');
|
|
357
|
+
if (this.schedulerInterval) {
|
|
358
|
+
clearInterval(this.schedulerInterval);
|
|
359
|
+
}
|
|
360
|
+
// Stop all running jobs
|
|
361
|
+
const jobs = await this.listJobs({ status: 'running' });
|
|
362
|
+
for (const job of jobs) {
|
|
363
|
+
try {
|
|
364
|
+
await this.stopJob(job.id);
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
this.logger.error(`Failed to stop job ${job.id}`, error);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
await this.persistJobs();
|
|
371
|
+
await this.cleanup();
|
|
372
|
+
};
|
|
373
|
+
process.on('SIGTERM', cleanup);
|
|
374
|
+
process.on('SIGINT', cleanup);
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Override cleanup to include scheduler
|
|
378
|
+
*/
|
|
379
|
+
async cleanup() {
|
|
380
|
+
if (this.schedulerInterval) {
|
|
381
|
+
clearInterval(this.schedulerInterval);
|
|
382
|
+
}
|
|
383
|
+
await super.cleanup();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
export default JobManager;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Job Storage
|
|
3
|
+
* Persistent storage for jobs and executions using DatabasePersistence
|
|
4
|
+
* Used by CronJobManager and other database-backed managers
|
|
5
|
+
*/
|
|
6
|
+
import DatabasePersistence from './database-persistence.js';
|
|
7
|
+
export class DatabaseJobStorage {
|
|
8
|
+
persistence;
|
|
9
|
+
userId;
|
|
10
|
+
constructor(userId) {
|
|
11
|
+
this.userId = userId;
|
|
12
|
+
this.persistence = new DatabasePersistence(userId);
|
|
13
|
+
}
|
|
14
|
+
async save(job) {
|
|
15
|
+
// Map BaseJobSpec to database format
|
|
16
|
+
const dbJob = {
|
|
17
|
+
job_id: job.id,
|
|
18
|
+
command: job.command,
|
|
19
|
+
started_at: job.startedAt?.toISOString(),
|
|
20
|
+
completed_at: job.completedAt?.toISOString(),
|
|
21
|
+
status: job.status,
|
|
22
|
+
exit_code: job.exitCode,
|
|
23
|
+
output: job.stdout,
|
|
24
|
+
error: job.stderr,
|
|
25
|
+
};
|
|
26
|
+
// Save using available method
|
|
27
|
+
await this.persistence.saveJob(dbJob);
|
|
28
|
+
}
|
|
29
|
+
async get(_jobId) {
|
|
30
|
+
// This would require adding a method to DatabasePersistence
|
|
31
|
+
// For now, return null and rely on list() filtering
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
async list(_filter) {
|
|
35
|
+
// Get active jobs from database
|
|
36
|
+
const dbJobs = await this.persistence.getActiveJobs();
|
|
37
|
+
// Convert to BaseJobSpec format
|
|
38
|
+
const jobs = dbJobs.map(dbJob => ({
|
|
39
|
+
id: dbJob.job_id,
|
|
40
|
+
name: dbJob.job_id, // Using job_id as name since name field doesn't exist
|
|
41
|
+
command: dbJob.command,
|
|
42
|
+
status: this.mapDbStatusToJobStatus(dbJob.status),
|
|
43
|
+
createdAt: new Date(dbJob.started_at),
|
|
44
|
+
startedAt: new Date(dbJob.started_at),
|
|
45
|
+
completedAt: dbJob.completed_at ? new Date(dbJob.completed_at) : undefined,
|
|
46
|
+
user: this.userId,
|
|
47
|
+
tags: [],
|
|
48
|
+
priority: 5,
|
|
49
|
+
maxRetries: 3,
|
|
50
|
+
retryCount: 0,
|
|
51
|
+
databaseSync: true,
|
|
52
|
+
exitCode: dbJob.exit_code,
|
|
53
|
+
stdout: dbJob.output,
|
|
54
|
+
stderr: dbJob.error,
|
|
55
|
+
}));
|
|
56
|
+
return jobs;
|
|
57
|
+
}
|
|
58
|
+
mapDbStatusToJobStatus(dbStatus) {
|
|
59
|
+
switch (dbStatus) {
|
|
60
|
+
case 'running':
|
|
61
|
+
return 'running';
|
|
62
|
+
case 'completed':
|
|
63
|
+
case 'success':
|
|
64
|
+
return 'completed';
|
|
65
|
+
case 'stopped':
|
|
66
|
+
return 'stopped';
|
|
67
|
+
case 'paused':
|
|
68
|
+
return 'paused';
|
|
69
|
+
case 'failed':
|
|
70
|
+
case 'timeout':
|
|
71
|
+
return 'failed';
|
|
72
|
+
case 'killed':
|
|
73
|
+
return 'killed';
|
|
74
|
+
default:
|
|
75
|
+
return 'created';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async update(jobId, updates) {
|
|
79
|
+
// Update by saving again (upsert behavior)
|
|
80
|
+
if (updates.command) {
|
|
81
|
+
const dbJob = {
|
|
82
|
+
job_id: jobId,
|
|
83
|
+
command: updates.command,
|
|
84
|
+
started_at: updates.startedAt?.toISOString(),
|
|
85
|
+
completed_at: updates.completedAt?.toISOString(),
|
|
86
|
+
status: updates.status || 'running',
|
|
87
|
+
exit_code: updates.exitCode,
|
|
88
|
+
output: updates.stdout,
|
|
89
|
+
error: updates.stderr,
|
|
90
|
+
};
|
|
91
|
+
await this.persistence.saveJob(dbJob);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async delete(jobId) {
|
|
95
|
+
// DatabasePersistence doesn't have a delete method yet
|
|
96
|
+
// This would need to be added
|
|
97
|
+
console.warn(`Delete not implemented for job ${jobId}`);
|
|
98
|
+
}
|
|
99
|
+
async saveExecution(execution) {
|
|
100
|
+
// Map to database format and save as job
|
|
101
|
+
const dbJob = {
|
|
102
|
+
job_id: execution.jobId,
|
|
103
|
+
command: execution.command,
|
|
104
|
+
started_at: execution.startTime.toISOString(),
|
|
105
|
+
completed_at: execution.endTime?.toISOString(),
|
|
106
|
+
status: execution.status,
|
|
107
|
+
exit_code: execution.exitCode,
|
|
108
|
+
output: execution.stdout,
|
|
109
|
+
error: execution.stderr || execution.errorMessage,
|
|
110
|
+
};
|
|
111
|
+
await this.persistence.saveJob(dbJob);
|
|
112
|
+
}
|
|
113
|
+
async getExecutions(jobId, limit = 50) {
|
|
114
|
+
// Get active jobs (no specific history method available yet)
|
|
115
|
+
const dbJobs = await this.persistence.getActiveJobs();
|
|
116
|
+
const jobExecutions = dbJobs.filter(job => job.job_id === jobId);
|
|
117
|
+
return jobExecutions.slice(0, limit).map(dbExec => ({
|
|
118
|
+
executionId: `exec_${dbExec.job_id}_${Date.now()}`,
|
|
119
|
+
jobId: dbExec.job_id,
|
|
120
|
+
jobName: dbExec.job_id,
|
|
121
|
+
command: dbExec.command,
|
|
122
|
+
startTime: new Date(dbExec.started_at),
|
|
123
|
+
endTime: dbExec.completed_at ? new Date(dbExec.completed_at) : undefined,
|
|
124
|
+
duration: dbExec.completed_at
|
|
125
|
+
? new Date(dbExec.completed_at).getTime() - new Date(dbExec.started_at).getTime()
|
|
126
|
+
: undefined,
|
|
127
|
+
status: this.mapDbStatus(dbExec.status),
|
|
128
|
+
exitCode: dbExec.exit_code,
|
|
129
|
+
stdout: dbExec.output,
|
|
130
|
+
stderr: dbExec.error,
|
|
131
|
+
errorMessage: dbExec.error,
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
mapDbStatus(dbStatus) {
|
|
135
|
+
switch (dbStatus) {
|
|
136
|
+
case 'running':
|
|
137
|
+
return 'running';
|
|
138
|
+
case 'completed':
|
|
139
|
+
case 'success':
|
|
140
|
+
return 'completed';
|
|
141
|
+
case 'failed':
|
|
142
|
+
return 'failed';
|
|
143
|
+
case 'killed':
|
|
144
|
+
return 'killed';
|
|
145
|
+
case 'timeout':
|
|
146
|
+
return 'timeout';
|
|
147
|
+
default:
|
|
148
|
+
return 'failed';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async cleanup() {
|
|
152
|
+
// DatabasePersistence maintains its own connections
|
|
153
|
+
// No cleanup needed here
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
export default DatabaseJobStorage;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-Memory Job Storage
|
|
3
|
+
* Fast, volatile storage for jobs and executions
|
|
4
|
+
* Used by JobManager for runtime job tracking
|
|
5
|
+
*/
|
|
6
|
+
export class MemoryJobStorage {
|
|
7
|
+
jobs = new Map();
|
|
8
|
+
executions = new Map();
|
|
9
|
+
maxExecutionsPerJob;
|
|
10
|
+
constructor(maxExecutionsPerJob = 100) {
|
|
11
|
+
this.maxExecutionsPerJob = maxExecutionsPerJob;
|
|
12
|
+
}
|
|
13
|
+
async save(job) {
|
|
14
|
+
this.jobs.set(job.id, { ...job });
|
|
15
|
+
}
|
|
16
|
+
async get(jobId) {
|
|
17
|
+
const job = this.jobs.get(jobId);
|
|
18
|
+
return job ? { ...job } : null;
|
|
19
|
+
}
|
|
20
|
+
async list(filter) {
|
|
21
|
+
let jobs = Array.from(this.jobs.values()).map(job => ({ ...job }));
|
|
22
|
+
// Basic filtering (additional filters applied by BaseJobManager)
|
|
23
|
+
if (filter?.status) {
|
|
24
|
+
const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
|
|
25
|
+
jobs = jobs.filter(job => statuses.includes(job.status));
|
|
26
|
+
}
|
|
27
|
+
return jobs;
|
|
28
|
+
}
|
|
29
|
+
async update(jobId, updates) {
|
|
30
|
+
const job = this.jobs.get(jobId);
|
|
31
|
+
if (!job) {
|
|
32
|
+
throw new Error(`Job ${jobId} not found`);
|
|
33
|
+
}
|
|
34
|
+
Object.assign(job, updates);
|
|
35
|
+
this.jobs.set(jobId, job);
|
|
36
|
+
}
|
|
37
|
+
async delete(jobId) {
|
|
38
|
+
this.jobs.delete(jobId);
|
|
39
|
+
this.executions.delete(jobId);
|
|
40
|
+
}
|
|
41
|
+
async saveExecution(execution) {
|
|
42
|
+
const jobExecutions = this.executions.get(execution.jobId) || [];
|
|
43
|
+
// Add new execution at the beginning
|
|
44
|
+
jobExecutions.unshift(execution);
|
|
45
|
+
// Limit number of executions stored
|
|
46
|
+
if (jobExecutions.length > this.maxExecutionsPerJob) {
|
|
47
|
+
jobExecutions.length = this.maxExecutionsPerJob;
|
|
48
|
+
}
|
|
49
|
+
this.executions.set(execution.jobId, jobExecutions);
|
|
50
|
+
}
|
|
51
|
+
async getExecutions(jobId, limit) {
|
|
52
|
+
const jobExecutions = this.executions.get(jobId) || [];
|
|
53
|
+
if (limit && limit < jobExecutions.length) {
|
|
54
|
+
return jobExecutions.slice(0, limit);
|
|
55
|
+
}
|
|
56
|
+
return jobExecutions.map(e => ({ ...e }));
|
|
57
|
+
}
|
|
58
|
+
async cleanup() {
|
|
59
|
+
this.jobs.clear();
|
|
60
|
+
this.executions.clear();
|
|
61
|
+
}
|
|
62
|
+
// Additional utility methods
|
|
63
|
+
getJobCount() {
|
|
64
|
+
return this.jobs.size;
|
|
65
|
+
}
|
|
66
|
+
getExecutionCount(jobId) {
|
|
67
|
+
if (jobId) {
|
|
68
|
+
return this.executions.get(jobId)?.length || 0;
|
|
69
|
+
}
|
|
70
|
+
return Array.from(this.executions.values()).reduce((sum, execs) => sum + execs.length, 0);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export default MemoryJobStorage;
|