unitup 0.0.8 → 0.0.10
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/README.md +398 -171
- package/docs/index.html +531 -0
- package/index.d.ts +204 -134
- package/package.json +4 -1
- package/src/cli.js +393 -90
- package/src/doctor.js +73 -14
- package/src/index.js +32 -1
- package/src/runtimes/bun.js +46 -0
- package/src/runtimes/common.js +73 -0
- package/src/runtimes/deno.js +46 -0
- package/src/runtimes/elixir.js +46 -0
- package/src/runtimes/go.js +46 -0
- package/src/runtimes/index.js +155 -0
- package/src/runtimes/native.js +38 -0
- package/src/runtimes/node.js +57 -0
- package/src/runtimes/php.js +46 -0
- package/src/runtimes/python.js +46 -0
- package/src/runtimes/ruby.js +46 -0
- package/src/runtimes/shell.js +46 -0
- package/src/systemd.js +349 -25
- package/src/unit.js +72 -22
- package/src/utils.js +220 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { findRuntimeExecutable, getBinaryVersion } from './common.js';
|
|
4
|
+
|
|
5
|
+
export async function createPhpAdapter(opts = {}) {
|
|
6
|
+
const customPath = opts.command;
|
|
7
|
+
let execPath = null;
|
|
8
|
+
|
|
9
|
+
if (customPath) {
|
|
10
|
+
const absPath = path.resolve(process.cwd(), customPath);
|
|
11
|
+
if (fs.existsSync(absPath)) {
|
|
12
|
+
try {
|
|
13
|
+
fs.accessSync(absPath, fs.constants.X_OK);
|
|
14
|
+
execPath = absPath;
|
|
15
|
+
} catch {}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!execPath) {
|
|
20
|
+
execPath = await findRuntimeExecutable(['php']);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!execPath) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
'PHP runtime could not be found.\n\nInstall PHP or specify its path:\n unitup add index.php --command /usr/bin/php'
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const version = await getBinaryVersion(execPath, '-v');
|
|
30
|
+
const scriptPath = opts.script ? path.resolve(process.cwd(), opts.script) : null;
|
|
31
|
+
const runtimeArgs = Array.isArray(opts.runtimeArgs) ? opts.runtimeArgs : [];
|
|
32
|
+
const scriptArgs = Array.isArray(opts.args) ? opts.args : [];
|
|
33
|
+
|
|
34
|
+
const args = [...runtimeArgs];
|
|
35
|
+
if (scriptPath) {
|
|
36
|
+
args.push(scriptPath);
|
|
37
|
+
}
|
|
38
|
+
args.push(...scriptArgs);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
command: execPath,
|
|
42
|
+
args,
|
|
43
|
+
runtime: 'php',
|
|
44
|
+
version
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { findRuntimeExecutable, getBinaryVersion } from './common.js';
|
|
4
|
+
|
|
5
|
+
export async function createPythonAdapter(opts = {}) {
|
|
6
|
+
const customPath = opts.command;
|
|
7
|
+
let execPath = null;
|
|
8
|
+
|
|
9
|
+
if (customPath) {
|
|
10
|
+
const absPath = path.resolve(process.cwd(), customPath);
|
|
11
|
+
if (fs.existsSync(absPath)) {
|
|
12
|
+
try {
|
|
13
|
+
fs.accessSync(absPath, fs.constants.X_OK);
|
|
14
|
+
execPath = absPath;
|
|
15
|
+
} catch {}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!execPath) {
|
|
20
|
+
execPath = await findRuntimeExecutable(['python3', 'python']);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!execPath) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
'Python runtime could not be found.\n\nInstall Python or specify its path:\n unitup add worker.py --command /usr/bin/python3'
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const version = await getBinaryVersion(execPath, '--version');
|
|
30
|
+
const scriptPath = opts.script ? path.resolve(process.cwd(), opts.script) : null;
|
|
31
|
+
const runtimeArgs = Array.isArray(opts.runtimeArgs) ? opts.runtimeArgs : [];
|
|
32
|
+
const scriptArgs = Array.isArray(opts.args) ? opts.args : [];
|
|
33
|
+
|
|
34
|
+
const args = [...runtimeArgs];
|
|
35
|
+
if (scriptPath) {
|
|
36
|
+
args.push(scriptPath);
|
|
37
|
+
}
|
|
38
|
+
args.push(...scriptArgs);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
command: execPath,
|
|
42
|
+
args,
|
|
43
|
+
runtime: 'python',
|
|
44
|
+
version
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { findRuntimeExecutable, getBinaryVersion } from './common.js';
|
|
4
|
+
|
|
5
|
+
export async function createRubyAdapter(opts = {}) {
|
|
6
|
+
const customPath = opts.command;
|
|
7
|
+
let execPath = null;
|
|
8
|
+
|
|
9
|
+
if (customPath) {
|
|
10
|
+
const absPath = path.resolve(process.cwd(), customPath);
|
|
11
|
+
if (fs.existsSync(absPath)) {
|
|
12
|
+
try {
|
|
13
|
+
fs.accessSync(absPath, fs.constants.X_OK);
|
|
14
|
+
execPath = absPath;
|
|
15
|
+
} catch {}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!execPath) {
|
|
20
|
+
execPath = await findRuntimeExecutable(['ruby']);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!execPath) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
'Ruby runtime could not be found.\n\nInstall Ruby or specify its path:\n unitup add app.rb --command /usr/bin/ruby'
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const version = await getBinaryVersion(execPath, '-v');
|
|
30
|
+
const scriptPath = opts.script ? path.resolve(process.cwd(), opts.script) : null;
|
|
31
|
+
const runtimeArgs = Array.isArray(opts.runtimeArgs) ? opts.runtimeArgs : [];
|
|
32
|
+
const scriptArgs = Array.isArray(opts.args) ? opts.args : [];
|
|
33
|
+
|
|
34
|
+
const args = [...runtimeArgs];
|
|
35
|
+
if (scriptPath) {
|
|
36
|
+
args.push(scriptPath);
|
|
37
|
+
}
|
|
38
|
+
args.push(...scriptArgs);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
command: execPath,
|
|
42
|
+
args,
|
|
43
|
+
runtime: 'ruby',
|
|
44
|
+
version
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { findRuntimeExecutable, getBinaryVersion } from './common.js';
|
|
4
|
+
|
|
5
|
+
export async function createShellAdapter(opts = {}) {
|
|
6
|
+
const customPath = opts.command;
|
|
7
|
+
let execPath = null;
|
|
8
|
+
|
|
9
|
+
if (customPath) {
|
|
10
|
+
const absPath = path.resolve(process.cwd(), customPath);
|
|
11
|
+
if (fs.existsSync(absPath)) {
|
|
12
|
+
try {
|
|
13
|
+
fs.accessSync(absPath, fs.constants.X_OK);
|
|
14
|
+
execPath = absPath;
|
|
15
|
+
} catch {}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!execPath) {
|
|
20
|
+
execPath = await findRuntimeExecutable(['bash', 'sh']);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!execPath) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
'Shell runtime could not be found.\n\nInstall bash/sh or specify its path:\n unitup add script.sh --command /bin/bash'
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const version = await getBinaryVersion(execPath, '--version');
|
|
30
|
+
const scriptPath = opts.script ? path.resolve(process.cwd(), opts.script) : null;
|
|
31
|
+
const runtimeArgs = Array.isArray(opts.runtimeArgs) ? opts.runtimeArgs : [];
|
|
32
|
+
const scriptArgs = Array.isArray(opts.args) ? opts.args : [];
|
|
33
|
+
|
|
34
|
+
const args = [...runtimeArgs];
|
|
35
|
+
if (scriptPath) {
|
|
36
|
+
args.push(scriptPath);
|
|
37
|
+
}
|
|
38
|
+
args.push(...scriptArgs);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
command: execPath,
|
|
42
|
+
args,
|
|
43
|
+
runtime: 'shell',
|
|
44
|
+
version
|
|
45
|
+
};
|
|
46
|
+
}
|
package/src/systemd.js
CHANGED
|
@@ -5,7 +5,8 @@ import path from 'node:path';
|
|
|
5
5
|
import {
|
|
6
6
|
sanitizeServiceName,
|
|
7
7
|
getUnitFilename,
|
|
8
|
-
formatRelativeTime
|
|
8
|
+
formatRelativeTime,
|
|
9
|
+
readAppMetadata
|
|
9
10
|
} from './utils.js';
|
|
10
11
|
import {
|
|
11
12
|
getUserUnitDir,
|
|
@@ -263,6 +264,8 @@ export async function checkUserLinger() {
|
|
|
263
264
|
// Systemd Operations
|
|
264
265
|
// ---------------------------------------------------------------------------
|
|
265
266
|
|
|
267
|
+
import { resolveRuntimeConfig } from './runtimes/index.js';
|
|
268
|
+
|
|
266
269
|
export async function daemonReload() {
|
|
267
270
|
const res = await runCommand('systemctl', ['--user', 'daemon-reload']);
|
|
268
271
|
if (res.code !== 0) {
|
|
@@ -277,7 +280,16 @@ export async function resetFailed() {
|
|
|
277
280
|
|
|
278
281
|
export async function addService(opts) {
|
|
279
282
|
const safeName = sanitizeServiceName(opts.name);
|
|
280
|
-
const
|
|
283
|
+
const runtimeConfig = await resolveRuntimeConfig({ ...opts, name: safeName });
|
|
284
|
+
|
|
285
|
+
const { path: unitPath } = writeUnitFile({
|
|
286
|
+
...opts,
|
|
287
|
+
name: safeName,
|
|
288
|
+
runtime: runtimeConfig.runtime,
|
|
289
|
+
command: runtimeConfig.command,
|
|
290
|
+
args: runtimeConfig.args,
|
|
291
|
+
cwd: opts.cwd
|
|
292
|
+
});
|
|
281
293
|
|
|
282
294
|
await daemonReload();
|
|
283
295
|
|
|
@@ -289,6 +301,17 @@ export async function addService(opts) {
|
|
|
289
301
|
}
|
|
290
302
|
|
|
291
303
|
export async function startService(name, enable = false) {
|
|
304
|
+
if (name && typeof name === 'string' && name.startsWith('@')) {
|
|
305
|
+
const list = await getServicesByGroup(name);
|
|
306
|
+
if (list.length === 0) {
|
|
307
|
+
throw new Error(`No services found in group "${name}".`);
|
|
308
|
+
}
|
|
309
|
+
for (const s of list) {
|
|
310
|
+
await startService(s, enable);
|
|
311
|
+
}
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
|
|
292
315
|
const safeName = sanitizeServiceName(name);
|
|
293
316
|
if (!unitFileExists(safeName)) {
|
|
294
317
|
throw new Error(`Service "${safeName}" does not exist.\nCreate it with: unitup add <script> --name ${safeName}`);
|
|
@@ -306,6 +329,17 @@ export async function startService(name, enable = false) {
|
|
|
306
329
|
}
|
|
307
330
|
|
|
308
331
|
export async function stopService(name) {
|
|
332
|
+
if (name && typeof name === 'string' && name.startsWith('@')) {
|
|
333
|
+
const list = await getServicesByGroup(name);
|
|
334
|
+
if (list.length === 0) {
|
|
335
|
+
throw new Error(`No services found in group "${name}".`);
|
|
336
|
+
}
|
|
337
|
+
for (const s of list) {
|
|
338
|
+
await stopService(s);
|
|
339
|
+
}
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
|
|
309
343
|
const safeName = sanitizeServiceName(name);
|
|
310
344
|
if (!unitFileExists(safeName)) {
|
|
311
345
|
throw new Error(`Service "${safeName}" does not exist.`);
|
|
@@ -319,6 +353,17 @@ export async function stopService(name) {
|
|
|
319
353
|
}
|
|
320
354
|
|
|
321
355
|
export async function restartService(name) {
|
|
356
|
+
if (name && typeof name === 'string' && name.startsWith('@')) {
|
|
357
|
+
const list = await getServicesByGroup(name);
|
|
358
|
+
if (list.length === 0) {
|
|
359
|
+
throw new Error(`No services found in group "${name}".`);
|
|
360
|
+
}
|
|
361
|
+
for (const s of list) {
|
|
362
|
+
await restartService(s);
|
|
363
|
+
}
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
|
|
322
367
|
const safeName = sanitizeServiceName(name);
|
|
323
368
|
if (!unitFileExists(safeName)) {
|
|
324
369
|
throw new Error(`Service "${safeName}" does not exist.`);
|
|
@@ -332,6 +377,17 @@ export async function restartService(name) {
|
|
|
332
377
|
}
|
|
333
378
|
|
|
334
379
|
export async function removeService(name) {
|
|
380
|
+
if (name && typeof name === 'string' && name.startsWith('@')) {
|
|
381
|
+
const list = await getServicesByGroup(name);
|
|
382
|
+
if (list.length === 0) {
|
|
383
|
+
throw new Error(`No services found in group "${name}".`);
|
|
384
|
+
}
|
|
385
|
+
for (const s of list) {
|
|
386
|
+
await removeService(s);
|
|
387
|
+
}
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
|
|
335
391
|
const safeName = sanitizeServiceName(name);
|
|
336
392
|
if (!unitFileExists(safeName)) {
|
|
337
393
|
throw new Error(`Service "${safeName}" does not exist.`);
|
|
@@ -386,11 +442,12 @@ export async function getServiceStatus(name) {
|
|
|
386
442
|
// Read unit file content for script and working dir
|
|
387
443
|
let script = 'unknown';
|
|
388
444
|
let cwd = 'unknown';
|
|
445
|
+
let parsed = {};
|
|
389
446
|
|
|
390
447
|
const unitPath = getUnitPath(safeName);
|
|
391
448
|
try {
|
|
392
449
|
const content = fs.readFileSync(unitPath, 'utf8');
|
|
393
|
-
|
|
450
|
+
parsed = parseUnitContent(content);
|
|
394
451
|
if (parsed.script) script = parsed.script;
|
|
395
452
|
if (parsed.cwd) cwd = parsed.cwd;
|
|
396
453
|
} catch {
|
|
@@ -420,6 +477,17 @@ export async function getServiceStatus(name) {
|
|
|
420
477
|
? formatRelativeTime(startedRaw)
|
|
421
478
|
: 'never';
|
|
422
479
|
|
|
480
|
+
const meta = readAppMetadata(safeName);
|
|
481
|
+
const command = meta?.command || parsed.command || meta?.node || process.execPath;
|
|
482
|
+
const argsList = meta?.args || (parsed.script ? [parsed.script] : []);
|
|
483
|
+
|
|
484
|
+
const { formatMemoryBytes } = await import('./utils.js');
|
|
485
|
+
const memoryCurrent = formatMemoryBytes(show.MemoryCurrent);
|
|
486
|
+
const memoryPeak = formatMemoryBytes(show.MemoryPeak);
|
|
487
|
+
const memoryHigh = formatMemoryBytes(show.MemoryHigh || meta?.resources?.memoryHigh || parsed.memoryHigh);
|
|
488
|
+
const memoryMax = formatMemoryBytes(show.MemoryMax || meta?.resources?.memoryMax || parsed.memoryMax);
|
|
489
|
+
const memorySwapMax = formatMemoryBytes(show.MemorySwapMax || meta?.resources?.memorySwapMax || parsed.memorySwapMax);
|
|
490
|
+
|
|
423
491
|
return {
|
|
424
492
|
name: safeName,
|
|
425
493
|
unitFile: getUnitFilename(safeName),
|
|
@@ -430,16 +498,66 @@ export async function getServiceStatus(name) {
|
|
|
430
498
|
restarts,
|
|
431
499
|
started,
|
|
432
500
|
startedRaw,
|
|
501
|
+
command,
|
|
502
|
+
arguments: argsList.join(' '),
|
|
503
|
+
args: argsList,
|
|
433
504
|
script,
|
|
434
|
-
cwd
|
|
505
|
+
cwd,
|
|
506
|
+
memory: memoryCurrent,
|
|
507
|
+
memoryPeak,
|
|
508
|
+
memoryHigh,
|
|
509
|
+
memoryMax,
|
|
510
|
+
memorySwapMax
|
|
435
511
|
};
|
|
436
512
|
}
|
|
437
513
|
|
|
438
|
-
export async function
|
|
514
|
+
export async function getServicesByGroup(groupName) {
|
|
515
|
+
const cleanGroup = groupName.startsWith('@') ? groupName.slice(1) : groupName;
|
|
516
|
+
const units = listUnitFiles();
|
|
517
|
+
const matched = [];
|
|
518
|
+
for (const unit of units) {
|
|
519
|
+
const meta = readAppMetadata(unit.name);
|
|
520
|
+
const itemGroup = meta?.group || 'default';
|
|
521
|
+
if (itemGroup.toLowerCase() === cleanGroup.toLowerCase()) {
|
|
522
|
+
matched.push(unit.name);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return matched;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export async function listServices(filterOpts = {}) {
|
|
439
529
|
const units = listUnitFiles();
|
|
440
530
|
const result = [];
|
|
531
|
+
const targetGroup = filterOpts.group
|
|
532
|
+
? (filterOpts.group.startsWith('@') ? filterOpts.group.slice(1) : filterOpts.group).toLowerCase()
|
|
533
|
+
: null;
|
|
441
534
|
|
|
442
535
|
for (const unit of units) {
|
|
536
|
+
const meta = readAppMetadata(unit.name);
|
|
537
|
+
const group = meta?.group || 'default';
|
|
538
|
+
const runtime = meta?.runtime || 'node';
|
|
539
|
+
|
|
540
|
+
if (targetGroup && group.toLowerCase() !== targetGroup) {
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
let commandSummary = '';
|
|
545
|
+
if (meta?.command) {
|
|
546
|
+
if (runtime === 'native') {
|
|
547
|
+
commandSummary = './' + path.basename(meta.command);
|
|
548
|
+
} else {
|
|
549
|
+
const cmdBase = path.basename(meta.command);
|
|
550
|
+
const argsStr = (meta.args || [])
|
|
551
|
+
.map(a => (a.startsWith('/') || a.startsWith('./') ? path.basename(a) : a))
|
|
552
|
+
.join(' ');
|
|
553
|
+
commandSummary = (cmdBase + (argsStr ? ' ' + argsStr : '')).trim();
|
|
554
|
+
}
|
|
555
|
+
} else if (meta?.script) {
|
|
556
|
+
commandSummary = `node ${path.basename(meta.script)}`;
|
|
557
|
+
} else {
|
|
558
|
+
commandSummary = 'unknown';
|
|
559
|
+
}
|
|
560
|
+
|
|
443
561
|
try {
|
|
444
562
|
const show = await getServiceShow(unit.name);
|
|
445
563
|
const activeState = show.ActiveState || 'inactive';
|
|
@@ -458,17 +576,35 @@ export async function listServices() {
|
|
|
458
576
|
}
|
|
459
577
|
|
|
460
578
|
const enabled = unitFileState.startsWith('enabled') ? 'yes' : 'no';
|
|
579
|
+
const pid = show.MainPID && show.MainPID !== '0' ? show.MainPID : '-';
|
|
580
|
+
const restarts = show.NRestarts ?? '0';
|
|
581
|
+
const startedRaw = show.ActiveEnterTimestamp;
|
|
582
|
+
const uptime = startedRaw && startedRaw !== '0' && startedRaw !== 'n/a'
|
|
583
|
+
? formatRelativeTime(startedRaw)
|
|
584
|
+
: 'never';
|
|
461
585
|
|
|
462
586
|
result.push({
|
|
463
587
|
name: unit.name,
|
|
588
|
+
runtime,
|
|
589
|
+
group,
|
|
464
590
|
status,
|
|
465
|
-
enabled
|
|
591
|
+
enabled,
|
|
592
|
+
pid,
|
|
593
|
+
command: commandSummary,
|
|
594
|
+
uptime,
|
|
595
|
+
restarts
|
|
466
596
|
});
|
|
467
597
|
} catch {
|
|
468
598
|
result.push({
|
|
469
599
|
name: unit.name,
|
|
600
|
+
runtime,
|
|
601
|
+
group,
|
|
470
602
|
status: 'unknown',
|
|
471
|
-
enabled: 'unknown'
|
|
603
|
+
enabled: 'unknown',
|
|
604
|
+
pid: '-',
|
|
605
|
+
command: commandSummary,
|
|
606
|
+
uptime: 'never',
|
|
607
|
+
restarts: '0'
|
|
472
608
|
});
|
|
473
609
|
}
|
|
474
610
|
}
|
|
@@ -476,9 +612,79 @@ export async function listServices() {
|
|
|
476
612
|
return result;
|
|
477
613
|
}
|
|
478
614
|
|
|
615
|
+
export async function inspectService(name) {
|
|
616
|
+
const safeName = sanitizeServiceName(name);
|
|
617
|
+
if (!unitFileExists(safeName)) {
|
|
618
|
+
throw new Error(`Service "${safeName}" does not exist.`);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const meta = readAppMetadata(safeName);
|
|
622
|
+
const statusObj = await getServiceStatus(safeName);
|
|
623
|
+
|
|
624
|
+
const command = meta?.command || statusObj.command || statusObj.node || process.execPath;
|
|
625
|
+
const argsList = meta?.args || (statusObj.script ? [statusObj.script] : []);
|
|
626
|
+
|
|
627
|
+
return {
|
|
628
|
+
name: safeName,
|
|
629
|
+
runtime: meta?.runtime || 'node',
|
|
630
|
+
status: statusObj.status,
|
|
631
|
+
activeState: statusObj.activeState,
|
|
632
|
+
subState: statusObj.subState,
|
|
633
|
+
command,
|
|
634
|
+
arguments: argsList.join(' '),
|
|
635
|
+
args: argsList,
|
|
636
|
+
cwd: meta?.cwd || statusObj.cwd,
|
|
637
|
+
unit: getUnitFilename(safeName),
|
|
638
|
+
unitPath: getUnitPath(safeName),
|
|
639
|
+
group: meta?.group || 'default',
|
|
640
|
+
pid: statusObj.pid,
|
|
641
|
+
restarts: statusObj.restarts,
|
|
642
|
+
started: statusObj.started,
|
|
643
|
+
memory: statusObj.memory,
|
|
644
|
+
memoryPeak: statusObj.memoryPeak,
|
|
645
|
+
memoryHigh: statusObj.memoryHigh,
|
|
646
|
+
memoryMax: statusObj.memoryMax,
|
|
647
|
+
memorySwapMax: statusObj.memorySwapMax,
|
|
648
|
+
// Legacy fields for backward compatibility
|
|
649
|
+
script: meta?.script || statusObj.script,
|
|
650
|
+
node: meta?.node || command
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export async function getServiceFailures() {
|
|
655
|
+
const allServices = await listServices();
|
|
656
|
+
const failures = [];
|
|
657
|
+
|
|
658
|
+
for (const item of allServices) {
|
|
659
|
+
if (item.status === 'failed') {
|
|
660
|
+
const show = await getServiceShow(item.name);
|
|
661
|
+
const exitCode = show.ExecMainStatus || show.ExecMainCode || '1';
|
|
662
|
+
failures.push({
|
|
663
|
+
name: item.name,
|
|
664
|
+
group: item.group,
|
|
665
|
+
status: 'failed',
|
|
666
|
+
exitCode: String(exitCode),
|
|
667
|
+
restarts: item.restarts,
|
|
668
|
+
uptime: item.uptime
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return failures;
|
|
674
|
+
}
|
|
675
|
+
|
|
479
676
|
export async function runJournalctlLogs(name, opts = {}) {
|
|
480
677
|
const safeName = sanitizeServiceName(name);
|
|
481
678
|
const unitFilename = getUnitFilename(safeName);
|
|
679
|
+
|
|
680
|
+
if (opts.diskUsage) {
|
|
681
|
+
const res = await runCommand('journalctl', ['--disk-usage']);
|
|
682
|
+
if (res.code !== 0 && (res.stderr.toLowerCase().includes('permission') || res.stderr.toLowerCase().includes('root'))) {
|
|
683
|
+
throw new Error('Journal maintenance requires additional privileges on this system.\nRun the command manually with the appropriate permissions.');
|
|
684
|
+
}
|
|
685
|
+
return res.stdout || res.stderr;
|
|
686
|
+
}
|
|
687
|
+
|
|
482
688
|
const syslogId = `unitup-${safeName}`;
|
|
483
689
|
|
|
484
690
|
const candidateArgSets = [
|
|
@@ -509,19 +715,27 @@ export async function runJournalctlLogs(name, opts = {}) {
|
|
|
509
715
|
}
|
|
510
716
|
|
|
511
717
|
const workingCandidate = chosenCandidate || ['--user', '-u', unitFilename];
|
|
718
|
+
const filterArgs = [...workingCandidate];
|
|
719
|
+
|
|
720
|
+
if (opts.since) filterArgs.push(`--since=${opts.since}`);
|
|
721
|
+
if (opts.until) filterArgs.push(`--until=${opts.until}`);
|
|
722
|
+
if (opts.priority) filterArgs.push(`--priority=${opts.priority}`);
|
|
723
|
+
if (opts.grep) filterArgs.push(`--grep=${opts.grep}`);
|
|
724
|
+
if (opts.boot) filterArgs.push('--boot');
|
|
725
|
+
|
|
726
|
+
if (opts.json) {
|
|
727
|
+
filterArgs.push('-o', 'json');
|
|
728
|
+
} else if (opts.cat || opts.output === 'cat') {
|
|
729
|
+
filterArgs.push('-o', 'cat');
|
|
730
|
+
} else if (opts.output) {
|
|
731
|
+
filterArgs.push('-o', opts.output);
|
|
732
|
+
}
|
|
512
733
|
|
|
513
734
|
if (opts.follow) {
|
|
514
|
-
|
|
515
|
-
if (opts.lines)
|
|
516
|
-
followArgs.push('-n', String(opts.lines));
|
|
517
|
-
}
|
|
518
|
-
if (opts.cat || opts.output === 'cat') {
|
|
519
|
-
followArgs.push('-o', 'cat');
|
|
520
|
-
} else if (opts.output) {
|
|
521
|
-
followArgs.push('-o', opts.output);
|
|
522
|
-
}
|
|
735
|
+
filterArgs.push('-f');
|
|
736
|
+
if (opts.lines) filterArgs.push('-n', String(opts.lines));
|
|
523
737
|
try {
|
|
524
|
-
const child = spawn('journalctl',
|
|
738
|
+
const child = spawn('journalctl', filterArgs, { stdio: 'inherit', shell: false });
|
|
525
739
|
child.on('error', () => {});
|
|
526
740
|
return child;
|
|
527
741
|
} catch {
|
|
@@ -529,15 +743,125 @@ export async function runJournalctlLogs(name, opts = {}) {
|
|
|
529
743
|
}
|
|
530
744
|
}
|
|
531
745
|
|
|
532
|
-
|
|
533
|
-
if (opts.cat || opts.output === 'cat') {
|
|
534
|
-
staticArgs.push('-o', 'cat');
|
|
535
|
-
} else if (opts.output) {
|
|
536
|
-
staticArgs.push('-o', opts.output);
|
|
537
|
-
}
|
|
746
|
+
filterArgs.push('--no-pager');
|
|
538
747
|
const lines = opts.lines ? String(opts.lines) : '100';
|
|
539
|
-
|
|
748
|
+
filterArgs.push('-n', lines);
|
|
540
749
|
|
|
541
|
-
const res = await runCommand('journalctl',
|
|
750
|
+
const res = await runCommand('journalctl', filterArgs);
|
|
542
751
|
return res.stdout || res.stderr || 'No logs found.';
|
|
543
752
|
}
|
|
753
|
+
|
|
754
|
+
export async function setServiceLimits(name, options = {}) {
|
|
755
|
+
const safeName = sanitizeServiceName(name);
|
|
756
|
+
if (!unitFileExists(safeName)) {
|
|
757
|
+
throw new Error(`Service "${safeName}" does not exist.`);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const meta = readAppMetadata(safeName) || {};
|
|
761
|
+
const { validateMemorySize } = await import('./utils.js');
|
|
762
|
+
|
|
763
|
+
let memoryHigh = options.memoryHigh;
|
|
764
|
+
let memoryMax = options.memoryMax;
|
|
765
|
+
let memorySwapMax = options.memorySwapMax;
|
|
766
|
+
|
|
767
|
+
const resources = { ...(meta.resources || {}) };
|
|
768
|
+
|
|
769
|
+
if (options.resetMemory) {
|
|
770
|
+
delete resources.memoryHigh;
|
|
771
|
+
delete resources.memoryMax;
|
|
772
|
+
delete resources.memorySwapMax;
|
|
773
|
+
memoryHigh = undefined;
|
|
774
|
+
memoryMax = undefined;
|
|
775
|
+
memorySwapMax = undefined;
|
|
776
|
+
} else {
|
|
777
|
+
if (memoryHigh !== undefined) {
|
|
778
|
+
memoryHigh = validateMemorySize(memoryHigh, 'MemoryHigh');
|
|
779
|
+
resources.memoryHigh = memoryHigh;
|
|
780
|
+
}
|
|
781
|
+
if (memoryMax !== undefined) {
|
|
782
|
+
memoryMax = validateMemorySize(memoryMax, 'MemoryMax');
|
|
783
|
+
resources.memoryMax = memoryMax;
|
|
784
|
+
}
|
|
785
|
+
if (memorySwapMax !== undefined) {
|
|
786
|
+
memorySwapMax = validateMemorySize(memorySwapMax, 'MemorySwapMax');
|
|
787
|
+
resources.memorySwapMax = memorySwapMax;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const updatedMeta = {
|
|
792
|
+
...meta,
|
|
793
|
+
name: safeName,
|
|
794
|
+
resources: Object.keys(resources).length > 0 ? resources : undefined,
|
|
795
|
+
memoryHigh: resources.memoryHigh,
|
|
796
|
+
memoryMax: resources.memoryMax,
|
|
797
|
+
memorySwapMax: resources.memorySwapMax
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
writeUnitFile(updatedMeta);
|
|
801
|
+
|
|
802
|
+
await runCommand('systemctl', ['--user', 'daemon-reload']);
|
|
803
|
+
|
|
804
|
+
const status = await getServiceStatus(safeName);
|
|
805
|
+
if (status.status === 'running') {
|
|
806
|
+
const setPropsArgs = ['--user', 'set-property', getUnitFilename(safeName)];
|
|
807
|
+
if (options.resetMemory) {
|
|
808
|
+
setPropsArgs.push('MemoryHigh=infinity', 'MemoryMax=infinity', 'MemorySwapMax=infinity');
|
|
809
|
+
} else {
|
|
810
|
+
if (resources.memoryHigh) setPropsArgs.push(`MemoryHigh=${resources.memoryHigh}`);
|
|
811
|
+
if (resources.memoryMax) setPropsArgs.push(`MemoryMax=${resources.memoryMax}`);
|
|
812
|
+
if (resources.memorySwapMax) setPropsArgs.push(`MemorySwapMax=${resources.memorySwapMax}`);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (setPropsArgs.length > 3) {
|
|
816
|
+
try {
|
|
817
|
+
await runCommand('systemctl', setPropsArgs);
|
|
818
|
+
} catch {
|
|
819
|
+
// Graceful fallback if runtime property setting fails
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return await inspectService(safeName);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
export async function executeJournalctlMaintenance(action, opts = {}) {
|
|
828
|
+
const args = [];
|
|
829
|
+
if (action === 'disk-usage') {
|
|
830
|
+
args.push('--disk-usage');
|
|
831
|
+
} else if (action === 'rotate') {
|
|
832
|
+
args.push('--rotate');
|
|
833
|
+
} else if (action === 'vacuum') {
|
|
834
|
+
const { validateMemorySize } = await import('./utils.js');
|
|
835
|
+
if (opts.size) {
|
|
836
|
+
const validSize = validateMemorySize(opts.size, 'Vacuum size');
|
|
837
|
+
args.push(`--vacuum-size=${validSize}`);
|
|
838
|
+
} else if (opts.time) {
|
|
839
|
+
const timeStr = String(opts.time).trim();
|
|
840
|
+
if (/[;&|$`"'\n\r\t ]/.test(timeStr)) {
|
|
841
|
+
throw new Error(`Vacuum time contains invalid characters or shell injection: "${timeStr}".`);
|
|
842
|
+
}
|
|
843
|
+
args.push(`--vacuum-time=${timeStr}`);
|
|
844
|
+
} else if (opts.files) {
|
|
845
|
+
const filesNum = parseInt(opts.files, 10);
|
|
846
|
+
if (Number.isNaN(filesNum) || filesNum <= 0) {
|
|
847
|
+
throw new Error(`Invalid vacuum files count: "${opts.files}". Must be a positive integer.`);
|
|
848
|
+
}
|
|
849
|
+
args.push(`--vacuum-files=${filesNum}`);
|
|
850
|
+
} else {
|
|
851
|
+
throw new Error('Vacuum action requires one of --size, --time, or --files.');
|
|
852
|
+
}
|
|
853
|
+
} else {
|
|
854
|
+
throw new Error(`Unknown journal maintenance action: "${action}".`);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (opts.dryRun) {
|
|
858
|
+
return `[dry-run] Would execute: journalctl ${args.join(' ')}`;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const res = await runCommand('journalctl', args);
|
|
862
|
+
if (res.code !== 0 && (res.stderr.toLowerCase().includes('permission') || res.stderr.toLowerCase().includes('root') || res.stderr.toLowerCase().includes('access denied'))) {
|
|
863
|
+
throw new Error('Journal maintenance requires additional privileges on this system.\nRun the command manually with the appropriate permissions.');
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
return (res.stdout || res.stderr || 'Journal maintenance completed successfully.').trim();
|
|
867
|
+
}
|