unitup 0.1.5 → 0.2.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/CHANGELOG.md +5 -3
- package/README.md +89 -8
- package/biome.json +0 -1
- package/docs/index.html +188 -234
- package/index.d.ts +336 -445
- package/package.json +9 -3
- package/src/cli.js +187 -101
- package/src/doctor.js +123 -56
- package/src/errors.js +131 -0
- package/src/index.js +132 -59
- package/src/logs.js +148 -0
- package/src/platform/base.js +211 -0
- package/src/platform/index.js +43 -0
- package/src/platform/linux.js +155 -0
- package/src/platform/macos.js +723 -0
- package/src/platform/windows-host.js +246 -0
- package/src/platform/windows.js +495 -0
- package/src/runtimes/bun.js +1 -1
- package/src/runtimes/common.js +24 -6
- package/src/runtimes/deno.js +1 -1
- package/src/runtimes/elixir.js +1 -1
- package/src/runtimes/go.js +1 -1
- package/src/runtimes/index.js +7 -7
- package/src/runtimes/native.js +2 -4
- package/src/runtimes/node.js +1 -1
- package/src/runtimes/php.js +1 -1
- package/src/runtimes/python.js +1 -1
- package/src/runtimes/ruby.js +1 -1
- package/src/runtimes/shell.js +1 -1
- package/src/schedule.js +31 -24
- package/src/service/config.js +55 -0
- package/src/service/manager.js +233 -0
- package/src/service/normalize.js +216 -0
- package/src/systemd.js +84 -38
- package/src/unit.js +12 -18
- package/src/utils.js +32 -18
- package/tsconfig.json +1 -1
- package/types-test/index.ts +158 -0
package/src/logs.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Reads lines from one or more log files with tailing, filtering, and follow support.
|
|
6
|
+
*
|
|
7
|
+
* @param {string|string[]} filePaths
|
|
8
|
+
* @param {object} [options]
|
|
9
|
+
* @param {number} [options.lines=100]
|
|
10
|
+
* @param {boolean} [options.follow=false]
|
|
11
|
+
* @param {string} [options.grep]
|
|
12
|
+
* @param {string} [options.since]
|
|
13
|
+
* @param {boolean} [options.json=false]
|
|
14
|
+
* @returns {Promise<string|EventEmitter>}
|
|
15
|
+
*/
|
|
16
|
+
export async function readServiceLogs(filePaths, options = {}) {
|
|
17
|
+
const allCandidatePaths = (Array.isArray(filePaths) ? filePaths : [filePaths]).filter(Boolean);
|
|
18
|
+
const existingPaths = allCandidatePaths.filter((p) => fs.existsSync(p));
|
|
19
|
+
existingPaths.sort((a, b) => {
|
|
20
|
+
try {
|
|
21
|
+
return fs.statSync(a).mtimeMs - fs.statSync(b).mtimeMs;
|
|
22
|
+
} catch {
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
const maxLines = typeof options.lines === 'number' && options.lines > 0 ? options.lines : 100;
|
|
27
|
+
const grepPattern = options.grep ? new RegExp(options.grep, 'i') : null;
|
|
28
|
+
|
|
29
|
+
// Collect all lines from currently existing files
|
|
30
|
+
const allEntries = [];
|
|
31
|
+
for (const filePath of existingPaths) {
|
|
32
|
+
try {
|
|
33
|
+
const stat = fs.statSync(filePath);
|
|
34
|
+
let content = '';
|
|
35
|
+
|
|
36
|
+
// If file is larger than 1MB, read only the tail chunk
|
|
37
|
+
if (stat.size > 1024 * 1024) {
|
|
38
|
+
const chunkSize = Math.min(stat.size, Math.max(1024 * 1024, maxLines * 2048));
|
|
39
|
+
const start = Math.max(0, stat.size - chunkSize);
|
|
40
|
+
const buffer = Buffer.alloc(stat.size - start);
|
|
41
|
+
const fd = fs.openSync(filePath, 'r');
|
|
42
|
+
fs.readSync(fd, buffer, 0, buffer.length, start);
|
|
43
|
+
fs.closeSync(fd);
|
|
44
|
+
content = buffer.toString('utf8');
|
|
45
|
+
} else {
|
|
46
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const lines = content.split('\n');
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
if (!line && line.trim() === '') continue;
|
|
52
|
+
if (grepPattern && !grepPattern.test(line)) continue;
|
|
53
|
+
allEntries.push(line);
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// ignore
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Slice tail lines
|
|
61
|
+
const sliced = allEntries.slice(-maxLines);
|
|
62
|
+
|
|
63
|
+
if (options.follow) {
|
|
64
|
+
const emitter = new EventEmitter();
|
|
65
|
+
const filePositions = new Map();
|
|
66
|
+
|
|
67
|
+
for (const p of allCandidatePaths) {
|
|
68
|
+
try {
|
|
69
|
+
if (fs.existsSync(p)) {
|
|
70
|
+
const stat = fs.statSync(p);
|
|
71
|
+
filePositions.set(p, stat.size);
|
|
72
|
+
} else {
|
|
73
|
+
filePositions.set(p, 0);
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
filePositions.set(p, 0);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Print initial existing lines
|
|
81
|
+
for (const l of sliced) {
|
|
82
|
+
console.log(l);
|
|
83
|
+
emitter.emit('line', l);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let isPolling = false;
|
|
87
|
+
const interval = setInterval(() => {
|
|
88
|
+
if (isPolling) return;
|
|
89
|
+
isPolling = true;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
for (const p of allCandidatePaths) {
|
|
93
|
+
try {
|
|
94
|
+
if (!fs.existsSync(p)) continue;
|
|
95
|
+
const stat = fs.statSync(p);
|
|
96
|
+
const lastPos = filePositions.get(p) || 0;
|
|
97
|
+
|
|
98
|
+
if (stat.size > lastPos) {
|
|
99
|
+
const stream = fs.createReadStream(p, { start: lastPos, end: stat.size, encoding: 'utf8' });
|
|
100
|
+
let buffer = '';
|
|
101
|
+
stream.on('data', (chunk) => {
|
|
102
|
+
buffer += chunk;
|
|
103
|
+
});
|
|
104
|
+
stream.on('end', () => {
|
|
105
|
+
filePositions.set(p, stat.size);
|
|
106
|
+
const newLines = buffer.split('\n');
|
|
107
|
+
for (const line of newLines) {
|
|
108
|
+
if (!line || !line.trim()) continue;
|
|
109
|
+
if (grepPattern && !grepPattern.test(line)) continue;
|
|
110
|
+
console.log(line);
|
|
111
|
+
emitter.emit('line', line);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
} else if (stat.size < lastPos) {
|
|
115
|
+
// File truncated or rotated
|
|
116
|
+
filePositions.set(p, 0);
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
// ignore
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} finally {
|
|
123
|
+
isPolling = false;
|
|
124
|
+
}
|
|
125
|
+
}, 250);
|
|
126
|
+
|
|
127
|
+
emitter.stop = () => {
|
|
128
|
+
clearInterval(interval);
|
|
129
|
+
emitter.emit('close');
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return emitter;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (options.json) {
|
|
136
|
+
return JSON.stringify(
|
|
137
|
+
sliced.map((line) => ({ message: line })),
|
|
138
|
+
null,
|
|
139
|
+
2
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (sliced.length === 0) {
|
|
144
|
+
return 'No logs found.';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return sliced.join('\n');
|
|
148
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { UnsupportedPlatformError } from '../errors.js';
|
|
2
|
+
import { runCommand } from '../systemd.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Base ServiceAdapter class defining the cross-platform contract for service management.
|
|
6
|
+
*/
|
|
7
|
+
export class ServiceAdapter {
|
|
8
|
+
/**
|
|
9
|
+
* @param {object} [options]
|
|
10
|
+
*/
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Platform identifier name.
|
|
17
|
+
* @type {string}
|
|
18
|
+
*/
|
|
19
|
+
get name() {
|
|
20
|
+
return 'base';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Platform capabilities metadata.
|
|
25
|
+
* @type {object}
|
|
26
|
+
*/
|
|
27
|
+
get capabilities() {
|
|
28
|
+
return {
|
|
29
|
+
serviceManager: 'none',
|
|
30
|
+
supports: {
|
|
31
|
+
install: false,
|
|
32
|
+
uninstall: false,
|
|
33
|
+
start: false,
|
|
34
|
+
stop: false,
|
|
35
|
+
restart: false,
|
|
36
|
+
enable: false,
|
|
37
|
+
disable: false,
|
|
38
|
+
status: false,
|
|
39
|
+
logs: false,
|
|
40
|
+
restartPolicy: false,
|
|
41
|
+
userServices: false,
|
|
42
|
+
systemServices: false,
|
|
43
|
+
memoryLimits: false,
|
|
44
|
+
schedule: false
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Executes a command using the configured command runner.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} cmd
|
|
53
|
+
* @param {string[]} args
|
|
54
|
+
* @param {object} [opts]
|
|
55
|
+
* @returns {Promise<{ stdout: string, stderr: string, code: number }>}
|
|
56
|
+
*/
|
|
57
|
+
async run(cmd, args, opts) {
|
|
58
|
+
return runCommand(cmd, args, opts);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Generates the service definition (unit file, plist, etc.) from normalized config.
|
|
63
|
+
*
|
|
64
|
+
* @param {object} config - NormalizedServiceConfig
|
|
65
|
+
* @returns {string|object}
|
|
66
|
+
*/
|
|
67
|
+
generateService(config) {
|
|
68
|
+
throw new UnsupportedPlatformError(this.name);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Installs a service from normalized config.
|
|
73
|
+
*
|
|
74
|
+
* @param {object} config - NormalizedServiceConfig
|
|
75
|
+
* @param {object} [options]
|
|
76
|
+
* @returns {Promise<object>}
|
|
77
|
+
*/
|
|
78
|
+
async install(config, options = {}) {
|
|
79
|
+
throw new UnsupportedPlatformError(this.name);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Uninstalls a service by name.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} name
|
|
86
|
+
* @param {object} [options]
|
|
87
|
+
* @returns {Promise<boolean>}
|
|
88
|
+
*/
|
|
89
|
+
async uninstall(name, options = {}) {
|
|
90
|
+
throw new UnsupportedPlatformError(this.name);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Starts a service by name.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} name
|
|
97
|
+
* @param {object} [options]
|
|
98
|
+
* @returns {Promise<boolean>}
|
|
99
|
+
*/
|
|
100
|
+
async start(name, options = {}) {
|
|
101
|
+
throw new UnsupportedPlatformError(this.name);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Stops a service by name.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} name
|
|
108
|
+
* @param {object} [options]
|
|
109
|
+
* @returns {Promise<boolean>}
|
|
110
|
+
*/
|
|
111
|
+
async stop(name, options = {}) {
|
|
112
|
+
throw new UnsupportedPlatformError(this.name);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Restarts a service by name.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} name
|
|
119
|
+
* @param {object} [options]
|
|
120
|
+
* @returns {Promise<boolean>}
|
|
121
|
+
*/
|
|
122
|
+
async restart(name, options = {}) {
|
|
123
|
+
throw new UnsupportedPlatformError(this.name);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Enables automatic startup for a service.
|
|
128
|
+
*
|
|
129
|
+
* @param {string} name
|
|
130
|
+
* @param {object} [options]
|
|
131
|
+
* @returns {Promise<boolean>}
|
|
132
|
+
*/
|
|
133
|
+
async enable(name, options = {}) {
|
|
134
|
+
throw new UnsupportedPlatformError(this.name);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Disables automatic startup for a service.
|
|
139
|
+
*
|
|
140
|
+
* @param {string} name
|
|
141
|
+
* @param {object} [options]
|
|
142
|
+
* @returns {Promise<boolean>}
|
|
143
|
+
*/
|
|
144
|
+
async disable(name, options = {}) {
|
|
145
|
+
throw new UnsupportedPlatformError(this.name);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Gets normalized status for a service.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} name
|
|
152
|
+
* @param {object} [options]
|
|
153
|
+
* @returns {Promise<object>}
|
|
154
|
+
*/
|
|
155
|
+
async status(name, options = {}) {
|
|
156
|
+
throw new UnsupportedPlatformError(this.name);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Inspects detailed configuration and runtime info for a service.
|
|
161
|
+
*
|
|
162
|
+
* @param {string} name
|
|
163
|
+
* @param {object} [options]
|
|
164
|
+
* @returns {Promise<object>}
|
|
165
|
+
*/
|
|
166
|
+
async inspect(name, options = {}) {
|
|
167
|
+
throw new UnsupportedPlatformError(this.name);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Retrieves or streams logs for a service.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} name
|
|
174
|
+
* @param {object} [options]
|
|
175
|
+
* @returns {Promise<any>}
|
|
176
|
+
*/
|
|
177
|
+
async logs(name, options = {}) {
|
|
178
|
+
throw new UnsupportedPlatformError(this.name);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Lists all services.
|
|
183
|
+
*
|
|
184
|
+
* @param {object} [options]
|
|
185
|
+
* @returns {Promise<Array<object>>}
|
|
186
|
+
*/
|
|
187
|
+
async list(options = {}) {
|
|
188
|
+
throw new UnsupportedPlatformError(this.name);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Checks if a service is installed.
|
|
193
|
+
*
|
|
194
|
+
* @param {string} name
|
|
195
|
+
* @param {object} [options]
|
|
196
|
+
* @returns {Promise<boolean>|boolean}
|
|
197
|
+
*/
|
|
198
|
+
isInstalled(name, options = {}) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Lists failed services.
|
|
204
|
+
*
|
|
205
|
+
* @param {object} [options]
|
|
206
|
+
* @returns {Promise<Array<object>>}
|
|
207
|
+
*/
|
|
208
|
+
async failures(options = {}) {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { UnsupportedPlatformError } from '../errors.js';
|
|
2
|
+
import { ServiceAdapter } from './base.js';
|
|
3
|
+
import { LinuxAdapter } from './linux.js';
|
|
4
|
+
import { MacOSAdapter } from './macos.js';
|
|
5
|
+
import { WindowsAdapter } from './windows.js';
|
|
6
|
+
|
|
7
|
+
export { ServiceAdapter, LinuxAdapter, MacOSAdapter, WindowsAdapter };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns the appropriate platform service adapter for the given OS.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} [platform]
|
|
13
|
+
* @param {object} [options]
|
|
14
|
+
* @returns {ServiceAdapter}
|
|
15
|
+
*/
|
|
16
|
+
export function getAdapter(platform, options = {}) {
|
|
17
|
+
const targetPlatform =
|
|
18
|
+
platform || process.env.UNITUP_PLATFORM || (process.env.XDG_CONFIG_HOME ? 'linux' : process.platform);
|
|
19
|
+
|
|
20
|
+
switch (targetPlatform) {
|
|
21
|
+
case 'linux':
|
|
22
|
+
return new LinuxAdapter(options);
|
|
23
|
+
case 'darwin':
|
|
24
|
+
case 'macos':
|
|
25
|
+
return new MacOSAdapter(options);
|
|
26
|
+
case 'win32':
|
|
27
|
+
case 'windows':
|
|
28
|
+
return new WindowsAdapter(options);
|
|
29
|
+
default:
|
|
30
|
+
throw new UnsupportedPlatformError(targetPlatform);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns platform capabilities for the target or current OS.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} [platform]
|
|
38
|
+
* @returns {object}
|
|
39
|
+
*/
|
|
40
|
+
export function getPlatformCapabilities(platform) {
|
|
41
|
+
const adapter = getAdapter(platform);
|
|
42
|
+
return adapter.capabilities;
|
|
43
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addService,
|
|
3
|
+
getServiceFailures,
|
|
4
|
+
getServiceShow,
|
|
5
|
+
getServiceStatus,
|
|
6
|
+
inspectService,
|
|
7
|
+
listServices,
|
|
8
|
+
removeService,
|
|
9
|
+
restartService,
|
|
10
|
+
runJournalctlLogs,
|
|
11
|
+
startService,
|
|
12
|
+
stopService
|
|
13
|
+
} from '../systemd.js';
|
|
14
|
+
import { generateUnitContent, getUnitPath, unitFileExists } from '../unit.js';
|
|
15
|
+
import { getUnitFilename, sanitizeServiceName } from '../utils.js';
|
|
16
|
+
import { ServiceAdapter } from './base.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Linux platform service adapter using systemd and journald.
|
|
20
|
+
*/
|
|
21
|
+
export class LinuxAdapter extends ServiceAdapter {
|
|
22
|
+
get name() {
|
|
23
|
+
return 'linux';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get capabilities() {
|
|
27
|
+
return {
|
|
28
|
+
serviceManager: 'systemd',
|
|
29
|
+
supports: {
|
|
30
|
+
install: true,
|
|
31
|
+
uninstall: true,
|
|
32
|
+
start: true,
|
|
33
|
+
stop: true,
|
|
34
|
+
restart: true,
|
|
35
|
+
enable: true,
|
|
36
|
+
disable: true,
|
|
37
|
+
status: true,
|
|
38
|
+
logs: true,
|
|
39
|
+
restartPolicy: true,
|
|
40
|
+
userServices: true,
|
|
41
|
+
systemServices: true,
|
|
42
|
+
memoryLimits: true,
|
|
43
|
+
schedule: true
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
generateService(config) {
|
|
49
|
+
return generateUnitContent({
|
|
50
|
+
name: config.name,
|
|
51
|
+
command: config.command,
|
|
52
|
+
args: config.args,
|
|
53
|
+
script: config.script,
|
|
54
|
+
cwd: config.cwd,
|
|
55
|
+
env: config.env,
|
|
56
|
+
envFile: config.envFile,
|
|
57
|
+
restart: config.restart?.policy || 'on-failure',
|
|
58
|
+
memoryHigh: config.resources?.memoryHigh,
|
|
59
|
+
memoryMax: config.resources?.memoryMax,
|
|
60
|
+
memorySwapMax: config.resources?.memorySwapMax
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async install(config, options = {}) {
|
|
65
|
+
return addService({
|
|
66
|
+
...config,
|
|
67
|
+
...options,
|
|
68
|
+
name: config.name,
|
|
69
|
+
command: config.command,
|
|
70
|
+
args: config.args,
|
|
71
|
+
cwd: config.cwd,
|
|
72
|
+
env: config.env,
|
|
73
|
+
envFile: config.envFile,
|
|
74
|
+
restart: config.restart?.policy || 'on-failure',
|
|
75
|
+
memoryHigh: config.resources?.memoryHigh,
|
|
76
|
+
memoryMax: config.resources?.memoryMax,
|
|
77
|
+
memorySwapMax: config.resources?.memorySwapMax,
|
|
78
|
+
force: options.force || config.raw?.force
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async uninstall(name, options = {}) {
|
|
83
|
+
return removeService(name, options);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async start(name, options = {}) {
|
|
87
|
+
return startService(name, options.enable || false);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async stop(name, options = {}) {
|
|
91
|
+
return stopService(name);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async restart(name, options = {}) {
|
|
95
|
+
return restartService(name);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async enable(name, options = {}) {
|
|
99
|
+
const safeName = sanitizeServiceName(name);
|
|
100
|
+
const unitFilename = getUnitFilename(safeName);
|
|
101
|
+
const res = await this.run('systemctl', ['--user', 'enable', unitFilename]);
|
|
102
|
+
if (res.code !== 0) {
|
|
103
|
+
throw new Error(`Failed to enable service "${safeName}": ${res.stderr || res.stdout}`);
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async disable(name, options = {}) {
|
|
109
|
+
const safeName = sanitizeServiceName(name);
|
|
110
|
+
const unitFilename = getUnitFilename(safeName);
|
|
111
|
+
const res = await this.run('systemctl', ['--user', 'disable', unitFilename]);
|
|
112
|
+
if (res.code !== 0) {
|
|
113
|
+
throw new Error(`Failed to disable service "${safeName}": ${res.stderr || res.stdout}`);
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async status(name, options = {}) {
|
|
119
|
+
const statusObj = await getServiceStatus(name);
|
|
120
|
+
return {
|
|
121
|
+
...statusObj,
|
|
122
|
+
installed: true,
|
|
123
|
+
state: statusObj.status,
|
|
124
|
+
enabled: statusObj.enabled !== undefined ? statusObj.enabled : true,
|
|
125
|
+
platform: 'linux',
|
|
126
|
+
manager: 'systemd'
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async inspect(name, options = {}) {
|
|
131
|
+
return inspectService(name);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async logs(name, options = {}) {
|
|
135
|
+
return runJournalctlLogs(name, options);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async list(options = {}) {
|
|
139
|
+
const list = await listServices(options);
|
|
140
|
+
return list.map((item) => ({
|
|
141
|
+
...item,
|
|
142
|
+
platform: 'linux',
|
|
143
|
+
manager: 'systemd'
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
isInstalled(name, options = {}) {
|
|
148
|
+
const safeName = sanitizeServiceName(name);
|
|
149
|
+
return unitFileExists(safeName);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async failures(options = {}) {
|
|
153
|
+
return getServiceFailures();
|
|
154
|
+
}
|
|
155
|
+
}
|