lsh-framework 3.2.5 → 3.5.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/LICENSE +21 -0
- package/README.md +72 -34
- package/dist/commands/ipfs.js +7 -12
- package/dist/commands/sync.js +49 -38
- package/dist/constants/config.js +3 -0
- package/dist/lib/floating-point-arithmetic.js +2 -2
- package/dist/lib/ipfs-client-manager.js +51 -13
- package/dist/lib/ipfs-secrets-storage.js +21 -16
- package/dist/lib/ipfs-sync.js +88 -14
- package/dist/lib/secrets-manager.js +117 -47
- package/dist/lib/sync-key-store.js +87 -0
- package/dist/services/secrets/secrets.js +77 -39
- package/package.json +16 -16
- package/dist/__tests__/fixtures/job-fixtures.js +0 -204
- package/dist/__tests__/fixtures/supabase-mocks.js +0 -252
- package/dist/daemon/job-registry.js +0 -556
- package/dist/daemon/lshd.js +0 -968
- package/dist/daemon/saas-api-routes.js +0 -599
- package/dist/daemon/saas-api-server.js +0 -231
- package/dist/examples/supabase-integration.js +0 -106
- package/dist/lib/api-response.js +0 -226
- package/dist/lib/base-command-registrar.js +0 -287
- package/dist/lib/base-job-manager.js +0 -295
- package/dist/lib/cloud-config-manager.js +0 -348
- package/dist/lib/cron-job-manager.js +0 -368
- package/dist/lib/daemon-client-helper.js +0 -145
- package/dist/lib/daemon-client.js +0 -513
- package/dist/lib/database-persistence.js +0 -727
- package/dist/lib/database-schema.js +0 -259
- package/dist/lib/database-types.js +0 -90
- package/dist/lib/enhanced-history-system.js +0 -247
- package/dist/lib/history-system.js +0 -246
- package/dist/lib/job-manager.js +0 -436
- package/dist/lib/job-storage-database.js +0 -164
- package/dist/lib/job-storage-memory.js +0 -73
- package/dist/lib/local-storage-adapter.js +0 -507
- package/dist/lib/optimized-job-scheduler.js +0 -356
- package/dist/lib/saas-audit.js +0 -215
- package/dist/lib/saas-auth.js +0 -465
- package/dist/lib/saas-billing.js +0 -503
- package/dist/lib/saas-email.js +0 -403
- package/dist/lib/saas-encryption.js +0 -221
- package/dist/lib/saas-organizations.js +0 -662
- package/dist/lib/saas-secrets.js +0 -408
- package/dist/lib/saas-types.js +0 -165
- package/dist/lib/supabase-client.js +0 -125
- package/dist/lib/supabase-utils.js +0 -396
- package/dist/services/cron/cron-registrar.js +0 -240
- package/dist/services/cron/cron.js +0 -9
- package/dist/services/daemon/daemon-registrar.js +0 -585
- package/dist/services/daemon/daemon.js +0 -9
- package/dist/services/supabase/supabase-registrar.js +0 -375
- package/dist/services/supabase/supabase.js +0 -9
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Command History System Implementation
|
|
3
|
-
* Provides ZSH-compatible history functionality
|
|
4
|
-
*/
|
|
5
|
-
import * as fs from 'fs';
|
|
6
|
-
import * as path from 'path';
|
|
7
|
-
import * as os from 'os';
|
|
8
|
-
import { DEFAULTS } from '../constants/index.js';
|
|
9
|
-
export class HistorySystem {
|
|
10
|
-
entries = [];
|
|
11
|
-
currentIndex = -1;
|
|
12
|
-
config;
|
|
13
|
-
isEnabled = true;
|
|
14
|
-
constructor(config) {
|
|
15
|
-
this.config = {
|
|
16
|
-
maxSize: DEFAULTS.MAX_HISTORY_SIZE,
|
|
17
|
-
filePath: path.join(os.homedir(), '.lsh_history'),
|
|
18
|
-
shareHistory: false,
|
|
19
|
-
ignoreDups: true,
|
|
20
|
-
ignoreSpace: false,
|
|
21
|
-
expireDuplicatesFirst: true,
|
|
22
|
-
...config,
|
|
23
|
-
};
|
|
24
|
-
this.loadHistory();
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Add a command to history
|
|
28
|
-
*/
|
|
29
|
-
addCommand(command, exitCode) {
|
|
30
|
-
if (!this.isEnabled)
|
|
31
|
-
return;
|
|
32
|
-
// Skip empty commands
|
|
33
|
-
if (!command.trim())
|
|
34
|
-
return;
|
|
35
|
-
// Skip commands starting with space if ignoreSpace is enabled
|
|
36
|
-
if (this.config.ignoreSpace && command.startsWith(' '))
|
|
37
|
-
return;
|
|
38
|
-
// Remove duplicates if configured
|
|
39
|
-
if (this.config.ignoreDups) {
|
|
40
|
-
this.removeDuplicateCommand(command);
|
|
41
|
-
}
|
|
42
|
-
const entry = {
|
|
43
|
-
lineNumber: this.entries.length + 1,
|
|
44
|
-
command: command.trim(),
|
|
45
|
-
timestamp: Date.now(),
|
|
46
|
-
exitCode,
|
|
47
|
-
};
|
|
48
|
-
this.entries.push(entry);
|
|
49
|
-
// Trim history if it exceeds max size
|
|
50
|
-
if (this.entries.length > this.config.maxSize) {
|
|
51
|
-
this.entries = this.entries.slice(-this.config.maxSize);
|
|
52
|
-
this.renumberEntries();
|
|
53
|
-
}
|
|
54
|
-
this.currentIndex = this.entries.length - 1;
|
|
55
|
-
this.saveHistory();
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Get history entry by line number
|
|
59
|
-
*/
|
|
60
|
-
getEntry(lineNumber) {
|
|
61
|
-
return this.entries.find(entry => entry.lineNumber === lineNumber);
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Get history entry by command prefix
|
|
65
|
-
*/
|
|
66
|
-
getEntryByPrefix(prefix) {
|
|
67
|
-
return this.entries
|
|
68
|
-
.slice()
|
|
69
|
-
.reverse()
|
|
70
|
-
.find(entry => entry.command.startsWith(prefix));
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Get all history entries
|
|
74
|
-
*/
|
|
75
|
-
getAllEntries() {
|
|
76
|
-
return [...this.entries];
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Search history for commands matching pattern
|
|
80
|
-
*/
|
|
81
|
-
searchHistory(pattern) {
|
|
82
|
-
const regex = new RegExp(pattern, 'i');
|
|
83
|
-
return this.entries.filter(entry => regex.test(entry.command));
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Get previous command in history
|
|
87
|
-
*/
|
|
88
|
-
getPreviousCommand() {
|
|
89
|
-
if (this.currentIndex > 0) {
|
|
90
|
-
this.currentIndex--;
|
|
91
|
-
return this.entries[this.currentIndex].command;
|
|
92
|
-
}
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Get next command in history
|
|
97
|
-
*/
|
|
98
|
-
getNextCommand() {
|
|
99
|
-
if (this.currentIndex < this.entries.length - 1) {
|
|
100
|
-
this.currentIndex++;
|
|
101
|
-
return this.entries[this.currentIndex].command;
|
|
102
|
-
}
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Reset history navigation index
|
|
107
|
-
*/
|
|
108
|
-
resetIndex() {
|
|
109
|
-
this.currentIndex = this.entries.length - 1;
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Expand history references like !! !n !string
|
|
113
|
-
*/
|
|
114
|
-
expandHistory(command) {
|
|
115
|
-
let result = command;
|
|
116
|
-
// Handle !! (last command)
|
|
117
|
-
result = result.replace(/!!/g, () => {
|
|
118
|
-
const lastEntry = this.entries[this.entries.length - 1];
|
|
119
|
-
return lastEntry ? lastEntry.command : '!!';
|
|
120
|
-
});
|
|
121
|
-
// Handle !n (command number n)
|
|
122
|
-
result = result.replace(/!(\d+)/g, (match, numStr) => {
|
|
123
|
-
const num = parseInt(numStr, 10);
|
|
124
|
-
const entry = this.getEntry(num);
|
|
125
|
-
return entry ? entry.command : match;
|
|
126
|
-
});
|
|
127
|
-
// Handle !string (last command starting with string)
|
|
128
|
-
result = result.replace(/!([a-zA-Z0-9_]+)/g, (match, prefix) => {
|
|
129
|
-
const entry = this.getEntryByPrefix(prefix);
|
|
130
|
-
return entry ? entry.command : match;
|
|
131
|
-
});
|
|
132
|
-
// Handle ^old^new (quick substitution)
|
|
133
|
-
result = result.replace(/\^([^^]+)\^([^^]*)/g, (match, old, replacement) => {
|
|
134
|
-
const lastEntry = this.entries[this.entries.length - 1];
|
|
135
|
-
if (lastEntry) {
|
|
136
|
-
return lastEntry.command.replace(new RegExp(old, 'g'), replacement);
|
|
137
|
-
}
|
|
138
|
-
return match;
|
|
139
|
-
});
|
|
140
|
-
return result;
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Clear history
|
|
144
|
-
*/
|
|
145
|
-
clearHistory() {
|
|
146
|
-
this.entries = [];
|
|
147
|
-
this.currentIndex = -1;
|
|
148
|
-
this.saveHistory();
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* Get history statistics
|
|
152
|
-
*/
|
|
153
|
-
getStats() {
|
|
154
|
-
const unique = new Set(this.entries.map(e => e.command)).size;
|
|
155
|
-
const oldest = this.entries.length > 0 ? new Date(this.entries[0].timestamp) : null;
|
|
156
|
-
const newest = this.entries.length > 0 ? new Date(this.entries[this.entries.length - 1].timestamp) : null;
|
|
157
|
-
return {
|
|
158
|
-
total: this.entries.length,
|
|
159
|
-
unique,
|
|
160
|
-
oldest,
|
|
161
|
-
newest,
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Enable/disable history
|
|
166
|
-
*/
|
|
167
|
-
setEnabled(enabled) {
|
|
168
|
-
this.isEnabled = enabled;
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* Update configuration
|
|
172
|
-
*/
|
|
173
|
-
updateConfig(newConfig) {
|
|
174
|
-
this.config = { ...this.config, ...newConfig };
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Load history from file
|
|
178
|
-
*/
|
|
179
|
-
loadHistory() {
|
|
180
|
-
try {
|
|
181
|
-
if (fs.existsSync(this.config.filePath)) {
|
|
182
|
-
const content = fs.readFileSync(this.config.filePath, 'utf8');
|
|
183
|
-
const lines = content.split('\n').filter(line => line.trim());
|
|
184
|
-
this.entries = lines.map((line, index) => {
|
|
185
|
-
// Parse history line format: timestamp:command
|
|
186
|
-
const colonIndex = line.indexOf(':');
|
|
187
|
-
if (colonIndex > 0) {
|
|
188
|
-
const timestamp = parseInt(line.substring(0, colonIndex), 10);
|
|
189
|
-
const command = line.substring(colonIndex + 1);
|
|
190
|
-
return {
|
|
191
|
-
lineNumber: index + 1,
|
|
192
|
-
command,
|
|
193
|
-
timestamp: isNaN(timestamp) ? Date.now() : timestamp,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
else {
|
|
197
|
-
return {
|
|
198
|
-
lineNumber: index + 1,
|
|
199
|
-
command: line,
|
|
200
|
-
timestamp: Date.now(),
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
catch (_error) {
|
|
207
|
-
// If loading fails, start with empty history
|
|
208
|
-
this.entries = [];
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Save history to file
|
|
213
|
-
*/
|
|
214
|
-
saveHistory() {
|
|
215
|
-
try {
|
|
216
|
-
const content = this.entries
|
|
217
|
-
.map(entry => `${entry.timestamp}:${entry.command}`)
|
|
218
|
-
.join('\n');
|
|
219
|
-
// Ensure directory exists
|
|
220
|
-
const dir = path.dirname(this.config.filePath);
|
|
221
|
-
if (!fs.existsSync(dir)) {
|
|
222
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
223
|
-
}
|
|
224
|
-
fs.writeFileSync(this.config.filePath, content, 'utf8');
|
|
225
|
-
}
|
|
226
|
-
catch (_error) {
|
|
227
|
-
// Silently fail if we can't save history
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Remove duplicate command from history
|
|
232
|
-
*/
|
|
233
|
-
removeDuplicateCommand(command) {
|
|
234
|
-
const trimmedCommand = command.trim();
|
|
235
|
-
this.entries = this.entries.filter(entry => entry.command !== trimmedCommand);
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Renumber entries after trimming
|
|
239
|
-
*/
|
|
240
|
-
renumberEntries() {
|
|
241
|
-
this.entries.forEach((entry, index) => {
|
|
242
|
-
entry.lineNumber = index + 1;
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
export default HistorySystem;
|
package/dist/lib/job-manager.js
DELETED
|
@@ -1,436 +0,0 @@
|
|
|
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
|
-
initPromise;
|
|
18
|
-
constructor(persistenceFile = '/tmp/lsh-jobs.json') {
|
|
19
|
-
super(new MemoryJobStorage(), 'JobManager');
|
|
20
|
-
this.persistenceFile = persistenceFile;
|
|
21
|
-
this.initPromise = this.loadPersistedJobs();
|
|
22
|
-
this.startScheduler();
|
|
23
|
-
this.setupCleanupHandlers();
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Wait for initialization to complete
|
|
27
|
-
*/
|
|
28
|
-
async ready() {
|
|
29
|
-
await this.initPromise;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Create a job and persist to filesystem
|
|
33
|
-
*/
|
|
34
|
-
async createJob(spec) {
|
|
35
|
-
const job = await super.createJob(spec);
|
|
36
|
-
await this.persistJobs();
|
|
37
|
-
return job;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Update a job and persist to filesystem
|
|
41
|
-
*/
|
|
42
|
-
async updateJob(jobId, updates) {
|
|
43
|
-
const job = await super.updateJob(jobId, updates);
|
|
44
|
-
await this.persistJobs();
|
|
45
|
-
return job;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Remove a job and persist to filesystem
|
|
49
|
-
*/
|
|
50
|
-
async removeJob(jobId, force = false) {
|
|
51
|
-
const result = await super.removeJob(jobId, force);
|
|
52
|
-
if (result) {
|
|
53
|
-
await this.persistJobs();
|
|
54
|
-
}
|
|
55
|
-
return result;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Update job status and persist to filesystem
|
|
59
|
-
*/
|
|
60
|
-
async updateJobStatus(jobId, status, additionalUpdates) {
|
|
61
|
-
const job = await super.updateJobStatus(jobId, status, additionalUpdates);
|
|
62
|
-
await this.persistJobs();
|
|
63
|
-
return job;
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Start a job (execute it as a process)
|
|
67
|
-
*/
|
|
68
|
-
async startJob(jobId) {
|
|
69
|
-
const baseJob = await this.getJob(jobId);
|
|
70
|
-
if (!baseJob) {
|
|
71
|
-
throw new Error(`Job ${jobId} not found`);
|
|
72
|
-
}
|
|
73
|
-
const job = baseJob;
|
|
74
|
-
if (job.status === 'running') {
|
|
75
|
-
throw new Error(`Job ${jobId} is already running`);
|
|
76
|
-
}
|
|
77
|
-
try {
|
|
78
|
-
// Spawn the process
|
|
79
|
-
if (job.type === 'shell') {
|
|
80
|
-
job.process = spawn('sh', ['-c', job.command], {
|
|
81
|
-
cwd: job.cwd,
|
|
82
|
-
env: job.env,
|
|
83
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
const [cmd, ...args] = job.command.split(' ');
|
|
88
|
-
job.process = spawn(cmd, args.concat(job.args || []), {
|
|
89
|
-
cwd: job.cwd,
|
|
90
|
-
env: job.env,
|
|
91
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
job.pid = job.process.pid;
|
|
95
|
-
// Handle output
|
|
96
|
-
job.process.stdout?.on('data', (data) => {
|
|
97
|
-
job.stdout = (job.stdout || '') + data.toString();
|
|
98
|
-
if (job.logFile) {
|
|
99
|
-
fs.appendFileSync(job.logFile, data);
|
|
100
|
-
}
|
|
101
|
-
this.emit('jobOutput', job.id, 'stdout', data.toString());
|
|
102
|
-
});
|
|
103
|
-
job.process.stderr?.on('data', (data) => {
|
|
104
|
-
job.stderr = (job.stderr || '') + data.toString();
|
|
105
|
-
if (job.logFile) {
|
|
106
|
-
fs.appendFileSync(job.logFile, data);
|
|
107
|
-
}
|
|
108
|
-
this.emit('jobOutput', job.id, 'stderr', data.toString());
|
|
109
|
-
});
|
|
110
|
-
// Handle completion
|
|
111
|
-
job.process.on('exit', async (code, signal) => {
|
|
112
|
-
// Check if job still exists (might have been removed during cleanup)
|
|
113
|
-
const existingJob = await this.getJob(job.id);
|
|
114
|
-
if (!existingJob) {
|
|
115
|
-
this.logger.debug(`Job ${job.id} already removed, skipping status update`);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const status = code === 0 ? 'completed' : (signal === 'SIGKILL' ? 'killed' : 'failed');
|
|
119
|
-
await this.updateJobStatus(job.id, status, {
|
|
120
|
-
completedAt: new Date(),
|
|
121
|
-
exitCode: code || undefined,
|
|
122
|
-
});
|
|
123
|
-
this.emit('jobCompleted', job, code, signal);
|
|
124
|
-
});
|
|
125
|
-
// Set timeout if specified
|
|
126
|
-
if (job.timeout) {
|
|
127
|
-
job.timer = setTimeout(() => {
|
|
128
|
-
this.stopJob(job.id, 'SIGKILL');
|
|
129
|
-
}, job.timeout);
|
|
130
|
-
}
|
|
131
|
-
// Update status to running
|
|
132
|
-
const updatedJob = await this.updateJobStatus(job.id, 'running', {
|
|
133
|
-
startedAt: new Date(),
|
|
134
|
-
pid: job.pid,
|
|
135
|
-
});
|
|
136
|
-
return updatedJob;
|
|
137
|
-
}
|
|
138
|
-
catch (error) {
|
|
139
|
-
const err = error;
|
|
140
|
-
await this.updateJobStatus(job.id, 'failed', {
|
|
141
|
-
completedAt: new Date(),
|
|
142
|
-
stderr: err.message,
|
|
143
|
-
});
|
|
144
|
-
this.emit('jobFailed', job, error);
|
|
145
|
-
throw error;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Stop a running job
|
|
150
|
-
*/
|
|
151
|
-
async stopJob(jobId, signal = 'SIGTERM') {
|
|
152
|
-
const baseJob = await this.getJob(jobId);
|
|
153
|
-
if (!baseJob) {
|
|
154
|
-
throw new Error(`Job ${jobId} not found`);
|
|
155
|
-
}
|
|
156
|
-
const job = baseJob;
|
|
157
|
-
if (job.status !== 'running') {
|
|
158
|
-
throw new Error(`Job ${jobId} is not running`);
|
|
159
|
-
}
|
|
160
|
-
if (!job.process || !job.pid) {
|
|
161
|
-
throw new Error(`Job ${jobId} has no associated process`);
|
|
162
|
-
}
|
|
163
|
-
// Clear timeout if exists
|
|
164
|
-
if (job.timer) {
|
|
165
|
-
clearTimeout(job.timer);
|
|
166
|
-
job.timer = undefined;
|
|
167
|
-
}
|
|
168
|
-
// Kill the process
|
|
169
|
-
try {
|
|
170
|
-
job.process.kill(signal);
|
|
171
|
-
}
|
|
172
|
-
catch (error) {
|
|
173
|
-
this.logger.error(`Failed to kill job ${jobId}`, error);
|
|
174
|
-
}
|
|
175
|
-
// Update status
|
|
176
|
-
const updatedJob = await this.updateJobStatus(jobId, 'stopped', {
|
|
177
|
-
completedAt: new Date(),
|
|
178
|
-
});
|
|
179
|
-
return updatedJob;
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Create and immediately start a job
|
|
183
|
-
*/
|
|
184
|
-
async runJob(spec) {
|
|
185
|
-
const job = await this.createJob(spec);
|
|
186
|
-
return await this.startJob(job.id);
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* Pause a job (stop it but keep for later resumption)
|
|
190
|
-
*/
|
|
191
|
-
async pauseJob(jobId) {
|
|
192
|
-
await this.stopJob(jobId, 'SIGSTOP');
|
|
193
|
-
return await this.updateJobStatus(jobId, 'paused');
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Resume a paused job
|
|
197
|
-
*/
|
|
198
|
-
async resumeJob(jobId) {
|
|
199
|
-
const baseJob = await this.getJob(jobId);
|
|
200
|
-
if (!baseJob) {
|
|
201
|
-
throw new Error(`Job ${jobId} not found`);
|
|
202
|
-
}
|
|
203
|
-
const job = baseJob;
|
|
204
|
-
if (job.status !== 'paused') {
|
|
205
|
-
throw new Error(`Job ${jobId} is not paused`);
|
|
206
|
-
}
|
|
207
|
-
if (!job.process || !job.pid) {
|
|
208
|
-
throw new Error(`Job ${jobId} has no associated process`);
|
|
209
|
-
}
|
|
210
|
-
// Send SIGCONT to resume
|
|
211
|
-
try {
|
|
212
|
-
job.process.kill('SIGCONT');
|
|
213
|
-
return await this.updateJobStatus(jobId, 'running');
|
|
214
|
-
}
|
|
215
|
-
catch (error) {
|
|
216
|
-
throw new Error(`Failed to resume job ${jobId}: ${error}`);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Kill a job forcefully
|
|
221
|
-
*/
|
|
222
|
-
async killJob(jobId, signal = 'SIGKILL') {
|
|
223
|
-
return await this.stopJob(jobId, signal);
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Monitor a job's resource usage
|
|
227
|
-
*/
|
|
228
|
-
async monitorJob(jobId) {
|
|
229
|
-
const baseJob = await this.getJob(jobId);
|
|
230
|
-
if (!baseJob) {
|
|
231
|
-
throw new Error(`Job ${jobId} not found`);
|
|
232
|
-
}
|
|
233
|
-
const job = baseJob;
|
|
234
|
-
if (!job.pid) {
|
|
235
|
-
throw new Error(`Job ${jobId} is not running`);
|
|
236
|
-
}
|
|
237
|
-
try {
|
|
238
|
-
const { stdout } = await execAsync(`ps -p ${job.pid} -o pid,ppid,pcpu,pmem,etime,state`);
|
|
239
|
-
const lines = stdout.split('\n');
|
|
240
|
-
if (lines.length < 2) {
|
|
241
|
-
return null; // Process not found
|
|
242
|
-
}
|
|
243
|
-
const parts = lines[1].trim().split(/\s+/);
|
|
244
|
-
const monitoring = {
|
|
245
|
-
pid: parseInt(parts[0]),
|
|
246
|
-
ppid: parseInt(parts[1]),
|
|
247
|
-
cpu: parseFloat(parts[2]),
|
|
248
|
-
memory: parseFloat(parts[3]),
|
|
249
|
-
elapsed: parts[4],
|
|
250
|
-
state: parts[5],
|
|
251
|
-
timestamp: new Date(),
|
|
252
|
-
};
|
|
253
|
-
// Update job with current resource usage
|
|
254
|
-
job.cpuUsage = monitoring.cpu;
|
|
255
|
-
job.memoryUsage = monitoring.memory;
|
|
256
|
-
this.emit('jobMonitoring', job, monitoring);
|
|
257
|
-
return monitoring;
|
|
258
|
-
}
|
|
259
|
-
catch (_error) {
|
|
260
|
-
return null; // Process likely terminated
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Get system processes
|
|
265
|
-
*/
|
|
266
|
-
async getSystemProcesses() {
|
|
267
|
-
try {
|
|
268
|
-
const { stdout } = await execAsync('ps -eo pid,ppid,user,pcpu,pmem,lstart,comm,args');
|
|
269
|
-
const lines = stdout.split('\n').slice(1); // Skip header
|
|
270
|
-
return lines
|
|
271
|
-
.filter(line => line.trim())
|
|
272
|
-
.map(line => {
|
|
273
|
-
const parts = line.trim().split(/\s+/);
|
|
274
|
-
return {
|
|
275
|
-
pid: parseInt(parts[0]),
|
|
276
|
-
ppid: parseInt(parts[1]),
|
|
277
|
-
user: parts[2],
|
|
278
|
-
cpu: parseFloat(parts[3]),
|
|
279
|
-
memory: parseFloat(parts[4]),
|
|
280
|
-
startTime: new Date(parts.slice(5, 9).join(' ')),
|
|
281
|
-
name: parts[9],
|
|
282
|
-
command: parts.slice(10).join(' ') || parts[9],
|
|
283
|
-
status: 'running'
|
|
284
|
-
};
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
catch (error) {
|
|
288
|
-
this.logger.error('Failed to get system processes', error);
|
|
289
|
-
return [];
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* Get job statistics
|
|
294
|
-
*/
|
|
295
|
-
getJobStats() {
|
|
296
|
-
const jobs = Array.from(this.jobs.values());
|
|
297
|
-
const stats = {
|
|
298
|
-
total: jobs.length,
|
|
299
|
-
byStatus: {},
|
|
300
|
-
byType: {},
|
|
301
|
-
running: jobs.filter(j => j.status === 'running').length,
|
|
302
|
-
completed: jobs.filter(j => j.status === 'completed').length,
|
|
303
|
-
failed: jobs.filter(j => j.status === 'failed').length,
|
|
304
|
-
};
|
|
305
|
-
jobs.forEach(job => {
|
|
306
|
-
stats.byStatus[job.status] = (stats.byStatus[job.status] || 0) + 1;
|
|
307
|
-
stats.byType[job.type] = (stats.byType[job.type] || 0) + 1;
|
|
308
|
-
});
|
|
309
|
-
return stats;
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Clean up old jobs
|
|
313
|
-
*/
|
|
314
|
-
async cleanupJobs(olderThanHours = 24) {
|
|
315
|
-
const cutoff = new Date(Date.now() - olderThanHours * 60 * 60 * 1000);
|
|
316
|
-
const jobs = await this.listJobs();
|
|
317
|
-
let cleaned = 0;
|
|
318
|
-
for (const job of jobs) {
|
|
319
|
-
if (job.status === 'completed' || job.status === 'failed') {
|
|
320
|
-
if (job.completedAt && job.completedAt < cutoff) {
|
|
321
|
-
await this.removeJob(job.id, true);
|
|
322
|
-
cleaned++;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
this.logger.info(`Cleaned up ${cleaned} old jobs`);
|
|
327
|
-
return cleaned;
|
|
328
|
-
}
|
|
329
|
-
// ================================
|
|
330
|
-
// PRIVATE: Persistence & Scheduling
|
|
331
|
-
// ================================
|
|
332
|
-
async loadPersistedJobs() {
|
|
333
|
-
try {
|
|
334
|
-
if (fs.existsSync(this.persistenceFile)) {
|
|
335
|
-
const data = fs.readFileSync(this.persistenceFile, 'utf8');
|
|
336
|
-
// Handle empty file
|
|
337
|
-
if (!data || data.trim() === '') {
|
|
338
|
-
this.logger.info('Persistence file is empty, starting fresh');
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
const persistedJobs = JSON.parse(data);
|
|
342
|
-
for (const job of persistedJobs) {
|
|
343
|
-
// Convert date strings back to Date objects
|
|
344
|
-
job.createdAt = new Date(job.createdAt);
|
|
345
|
-
if (job.startedAt)
|
|
346
|
-
job.startedAt = new Date(job.startedAt);
|
|
347
|
-
if (job.completedAt)
|
|
348
|
-
job.completedAt = new Date(job.completedAt);
|
|
349
|
-
// Don't restore running processes - mark them as stopped
|
|
350
|
-
if (job.status === 'running') {
|
|
351
|
-
job.status = 'stopped';
|
|
352
|
-
}
|
|
353
|
-
await this.storage.save(job);
|
|
354
|
-
this.jobs.set(job.id, job);
|
|
355
|
-
}
|
|
356
|
-
this.logger.info(`Loaded ${persistedJobs.length} persisted jobs`);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
catch (error) {
|
|
360
|
-
this.logger.error('Failed to load persisted jobs', error);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
async persistJobs() {
|
|
364
|
-
try {
|
|
365
|
-
const jobs = Array.from(this.jobs.values()).map(job => {
|
|
366
|
-
const { process: _process, timer: _timer, ...serializable } = job;
|
|
367
|
-
return serializable;
|
|
368
|
-
});
|
|
369
|
-
// Write with secure permissions (mode 0o600 = rw-------)
|
|
370
|
-
fs.writeFileSync(this.persistenceFile, JSON.stringify(jobs, null, 2), { mode: 0o600 });
|
|
371
|
-
}
|
|
372
|
-
catch (error) {
|
|
373
|
-
this.logger.error('Failed to persist jobs', error);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
startScheduler() {
|
|
377
|
-
// Check for scheduled jobs every minute
|
|
378
|
-
this.schedulerInterval = setInterval(() => {
|
|
379
|
-
this.checkScheduledJobs();
|
|
380
|
-
}, 60000);
|
|
381
|
-
// Run immediately on startup
|
|
382
|
-
this.checkScheduledJobs();
|
|
383
|
-
}
|
|
384
|
-
async checkScheduledJobs() {
|
|
385
|
-
const jobs = await this.listJobs({ status: 'created' });
|
|
386
|
-
const now = new Date();
|
|
387
|
-
for (const job of jobs) {
|
|
388
|
-
if (job.schedule?.nextRun && job.schedule.nextRun <= now) {
|
|
389
|
-
this.logger.info(`Starting scheduled job: ${job.id}`);
|
|
390
|
-
try {
|
|
391
|
-
await this.startJob(job.id);
|
|
392
|
-
// Calculate next run time
|
|
393
|
-
if (job.schedule.interval) {
|
|
394
|
-
job.schedule.nextRun = new Date(now.getTime() + job.schedule.interval);
|
|
395
|
-
await this.updateJob(job.id, { schedule: job.schedule });
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
catch (error) {
|
|
399
|
-
this.logger.error(`Failed to start scheduled job ${job.id}`, error);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
setupCleanupHandlers() {
|
|
405
|
-
const cleanup = async () => {
|
|
406
|
-
this.logger.info('JobManager shutting down...');
|
|
407
|
-
if (this.schedulerInterval) {
|
|
408
|
-
clearInterval(this.schedulerInterval);
|
|
409
|
-
}
|
|
410
|
-
// Stop all running jobs
|
|
411
|
-
const jobs = await this.listJobs({ status: 'running' });
|
|
412
|
-
for (const job of jobs) {
|
|
413
|
-
try {
|
|
414
|
-
await this.stopJob(job.id);
|
|
415
|
-
}
|
|
416
|
-
catch (error) {
|
|
417
|
-
this.logger.error(`Failed to stop job ${job.id}`, error);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
await this.persistJobs();
|
|
421
|
-
await this.cleanup();
|
|
422
|
-
};
|
|
423
|
-
process.on('SIGTERM', cleanup);
|
|
424
|
-
process.on('SIGINT', cleanup);
|
|
425
|
-
}
|
|
426
|
-
/**
|
|
427
|
-
* Override cleanup to include scheduler
|
|
428
|
-
*/
|
|
429
|
-
async cleanup() {
|
|
430
|
-
if (this.schedulerInterval) {
|
|
431
|
-
clearInterval(this.schedulerInterval);
|
|
432
|
-
}
|
|
433
|
-
await super.cleanup();
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
export default JobManager;
|