@push.rocks/smartdaemon 2.2.0 → 2.4.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/.smartconfig.json +27 -8
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.systemdservicedefinition.d.ts +22 -0
- package/dist_ts/classes.systemdservicedefinition.js +93 -0
- package/dist_ts/classes.systemdunit.d.ts +49 -0
- package/dist_ts/classes.systemdunit.js +125 -0
- package/dist_ts/classes.systemdunitfile.d.ts +45 -0
- package/dist_ts/classes.systemdunitfile.js +242 -0
- package/dist_ts/index.d.ts +3 -0
- package/dist_ts/index.js +4 -1
- package/dist_ts/smartdaemon.plugins.d.ts +6 -1
- package/dist_ts/smartdaemon.plugins.js +6 -2
- package/package.json +4 -4
- package/readme.md +103 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.systemdservicedefinition.ts +134 -0
- package/ts/classes.systemdunit.ts +173 -0
- package/ts/classes.systemdunitfile.ts +311 -0
- package/ts/index.ts +3 -0
- package/ts/smartdaemon.plugins.ts +6 -1
- package/readme.hints.md +0 -1
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import * as plugins from './smartdaemon.plugins.js';
|
|
2
|
+
import { SystemdServiceDefinition } from './classes.systemdservicedefinition.js';
|
|
3
|
+
import { SystemdUnit, } from './classes.systemdunit.js';
|
|
4
|
+
export class SystemdUnitFileError extends Error {
|
|
5
|
+
constructor(code) {
|
|
6
|
+
super(`Systemd unit definition installation failed: ${code}.`);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.name = 'SystemdUnitFileError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const requireValue = (value, code) => {
|
|
12
|
+
if (!value)
|
|
13
|
+
throw new SystemdUnitFileError(code);
|
|
14
|
+
};
|
|
15
|
+
const digest = (bytes) => plugins.crypto.createHash('sha256').update(bytes).digest('hex');
|
|
16
|
+
const quiet = (state) => state.mainPid === 0 && state.controlPid === 0 &&
|
|
17
|
+
state.controlGroup === '' && state.job === '' &&
|
|
18
|
+
(state.activeState === 'inactive' && state.subState === 'dead' ||
|
|
19
|
+
state.activeState === 'failed' && state.subState === 'failed');
|
|
20
|
+
const masked = (state) => state.loadState === 'masked' &&
|
|
21
|
+
['masked', 'masked-runtime'].includes(state.unitFileState);
|
|
22
|
+
const sameFile = (left, right) => left.dev === right.dev && left.ino === right.ino;
|
|
23
|
+
/**
|
|
24
|
+
* Protected code/config installation, inside the caller's cross-process installation ownership scope.
|
|
25
|
+
* Other privileged writers must be serialized by that owner. Never changes enablement or masks.
|
|
26
|
+
*/
|
|
27
|
+
export class SystemdUnitFile {
|
|
28
|
+
constructor(options) {
|
|
29
|
+
this.busy = false;
|
|
30
|
+
this.unit = new SystemdUnit(options);
|
|
31
|
+
this.name = options.unitName;
|
|
32
|
+
this.directory = options.unitDirectory ?? '/usr/local/lib/systemd/system';
|
|
33
|
+
this.uid = options.expectedUid ?? 0;
|
|
34
|
+
requireValue(typeof this.directory === 'string' && this.directory !== '/' &&
|
|
35
|
+
plugins.path.isAbsolute(this.directory) &&
|
|
36
|
+
plugins.path.normalize(this.directory) === this.directory &&
|
|
37
|
+
!this.directory.endsWith('/') && !/[\x00-\x1f\x7f]/.test(this.directory) &&
|
|
38
|
+
this.directory.length <= 4000 && Number.isSafeInteger(this.uid) && this.uid >= 0, 'invalid_options');
|
|
39
|
+
this.path = plugins.path.join(this.directory, this.name);
|
|
40
|
+
}
|
|
41
|
+
/** Reads only the exact protected definition; no systemd or filesystem mutation. */
|
|
42
|
+
async inspect() {
|
|
43
|
+
if (!await this.directories(false))
|
|
44
|
+
return null;
|
|
45
|
+
const file = await this.read(this.path);
|
|
46
|
+
return file ? Object.freeze({ path: this.path, sha256: digest(file.bytes) }) : null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Expected previous digest is mandatory; null admits an absent file. Exact desired bytes are idempotent.
|
|
50
|
+
* Retains changed definitions beside the unit before replacement. A failed reload never rolls code back.
|
|
51
|
+
*/
|
|
52
|
+
async install(definition, expectedSha256) {
|
|
53
|
+
requireValue(definition instanceof SystemdServiceDefinition && definition.unitName === this.name &&
|
|
54
|
+
(expectedSha256 === null ||
|
|
55
|
+
typeof expectedSha256 === 'string' && /^[a-f0-9]{64}$/.test(expectedSha256)), 'invalid_options');
|
|
56
|
+
requireValue(!this.busy, 'busy');
|
|
57
|
+
this.busy = true;
|
|
58
|
+
let temporary;
|
|
59
|
+
let temporaryInfo;
|
|
60
|
+
try {
|
|
61
|
+
const before = await this.unit.inspect();
|
|
62
|
+
await this.admit(before);
|
|
63
|
+
await this.directories(true);
|
|
64
|
+
const parent = await plugins.fs.lstat(this.directory);
|
|
65
|
+
const previous = await this.read(this.path);
|
|
66
|
+
const previousSha = previous ? digest(previous.bytes) : null;
|
|
67
|
+
const changed = previousSha !== definition.sha256;
|
|
68
|
+
requireValue(!changed || previousSha === expectedSha256, 'changed');
|
|
69
|
+
if (changed) {
|
|
70
|
+
if (previous) {
|
|
71
|
+
const backup = `${this.path}.previous-${previousSha}`;
|
|
72
|
+
try {
|
|
73
|
+
await this.create(backup, previous.bytes);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error.code !== 'EEXIST')
|
|
77
|
+
throw error;
|
|
78
|
+
requireValue(digest((await this.read(backup)).bytes) === previousSha, 'changed');
|
|
79
|
+
}
|
|
80
|
+
await this.flushDirectory(parent);
|
|
81
|
+
}
|
|
82
|
+
temporary = plugins.path.join(this.directory, `.${this.name}.${plugins.crypto.randomUUID()}.tmp`);
|
|
83
|
+
await this.create(temporary, Buffer.from(definition.content));
|
|
84
|
+
temporaryInfo = await plugins.fs.lstat(temporary);
|
|
85
|
+
const current = await this.read(this.path);
|
|
86
|
+
requireValue((current ? digest(current.bytes) : null) === previousSha &&
|
|
87
|
+
(!current || previous && sameFile(current.info, previous.info)), 'changed');
|
|
88
|
+
requireValue(sameFile(parent, await plugins.fs.lstat(this.directory)), 'unsafe_path');
|
|
89
|
+
const rechecked = await this.unit.inspect();
|
|
90
|
+
requireValue(JSON.stringify(rechecked) === JSON.stringify(before), 'changed');
|
|
91
|
+
await this.admit(rechecked);
|
|
92
|
+
await plugins.fs.rename(temporary, this.path);
|
|
93
|
+
temporary = undefined;
|
|
94
|
+
await this.flushDirectory(parent);
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
await this.unit.reloadConfiguration();
|
|
98
|
+
const after = await this.unit.inspect();
|
|
99
|
+
requireValue(quiet(after) &&
|
|
100
|
+
(before.loadState === 'not-found'
|
|
101
|
+
? after.loadState === 'loaded' && after.unitFileState === 'disabled'
|
|
102
|
+
: after.loadState === before.loadState &&
|
|
103
|
+
after.unitFileState === before.unitFileState), 'changed');
|
|
104
|
+
const configuration = await this.unit.inspectConfiguration();
|
|
105
|
+
requireValue(!configuration.needsReload && configuration.dropInPaths === '' &&
|
|
106
|
+
configuration.fragmentPath === (masked(after) ? '/dev/null' : this.path), 'unsafe_unit');
|
|
107
|
+
const installed = await this.inspect();
|
|
108
|
+
requireValue(installed?.sha256 === definition.sha256, 'changed');
|
|
109
|
+
return Object.freeze({ changed, record: installed, masked: masked(after) });
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
throw new SystemdUnitFileError('reload_failed');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (error instanceof SystemdUnitFileError)
|
|
117
|
+
throw error;
|
|
118
|
+
throw new SystemdUnitFileError('write_failed');
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
try {
|
|
122
|
+
if (temporary && temporaryInfo) {
|
|
123
|
+
const named = await plugins.fs.lstat(temporary);
|
|
124
|
+
requireValue(sameFile(temporaryInfo, named), 'unsafe_path');
|
|
125
|
+
await plugins.fs.unlink(temporary);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
this.busy = false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async admit(state) {
|
|
134
|
+
requireValue(quiet(state) && (masked(state) ||
|
|
135
|
+
state.loadState === 'loaded' &&
|
|
136
|
+
['enabled', 'enabled-runtime', 'disabled'].includes(state.unitFileState) ||
|
|
137
|
+
state.loadState === 'not-found' && state.unitFileState === ''), 'unsafe_unit');
|
|
138
|
+
const configuration = await this.unit.inspectConfiguration();
|
|
139
|
+
requireValue(configuration.dropInPaths === '' && configuration.fragmentPath ===
|
|
140
|
+
(masked(state) ? '/dev/null' : state.loadState === 'not-found' ? '' : this.path), 'unsafe_unit');
|
|
141
|
+
}
|
|
142
|
+
async directories(create) {
|
|
143
|
+
requireValue(process.platform === 'linux' && process.getuid?.() === this.uid, 'unsafe_path');
|
|
144
|
+
let directory = '/';
|
|
145
|
+
for (const part of this.directory.split('/').filter(Boolean)) {
|
|
146
|
+
directory = plugins.path.join(directory, part);
|
|
147
|
+
let info;
|
|
148
|
+
try {
|
|
149
|
+
info = await plugins.fs.lstat(directory);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (error.code !== 'ENOENT')
|
|
153
|
+
throw error;
|
|
154
|
+
if (!create)
|
|
155
|
+
return false;
|
|
156
|
+
await plugins.fs.mkdir(directory, { mode: 0o755 });
|
|
157
|
+
const parent = await plugins.fs.open(plugins.path.dirname(directory), plugins.fsConstants.O_RDONLY | plugins.fsConstants.O_DIRECTORY |
|
|
158
|
+
plugins.fsConstants.O_NOFOLLOW);
|
|
159
|
+
try {
|
|
160
|
+
await parent.sync();
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
await parent.close();
|
|
164
|
+
}
|
|
165
|
+
info = await plugins.fs.lstat(directory);
|
|
166
|
+
}
|
|
167
|
+
requireValue(info.isDirectory() && !info.isSymbolicLink() && [0, this.uid].includes(info.uid) &&
|
|
168
|
+
((info.mode & 0o022) === 0 || directory === '/tmp' && info.uid === 0 &&
|
|
169
|
+
(info.mode & 0o7777) === 0o1777), 'unsafe_path');
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
async read(path) {
|
|
174
|
+
let file;
|
|
175
|
+
try {
|
|
176
|
+
file = await plugins.fs.open(path, plugins.fsConstants.O_RDONLY | plugins.fsConstants.O_NOFOLLOW |
|
|
177
|
+
plugins.fsConstants.O_NONBLOCK);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (error.code === 'ENOENT')
|
|
181
|
+
return null;
|
|
182
|
+
throw new SystemdUnitFileError('unsafe_path');
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const info = await file.stat();
|
|
186
|
+
requireValue(info.isFile() && info.uid === this.uid && info.nlink === 1 &&
|
|
187
|
+
(info.mode & 0o7777) === 0o644 && info.size <= 16384, 'unsafe_path');
|
|
188
|
+
const bytes = Buffer.alloc(16385);
|
|
189
|
+
let size = 0;
|
|
190
|
+
for (;;) {
|
|
191
|
+
const result = await file.read(bytes, size, bytes.length - size, size);
|
|
192
|
+
if (result.bytesRead === 0)
|
|
193
|
+
break;
|
|
194
|
+
size += result.bytesRead;
|
|
195
|
+
requireValue(size <= 16384, 'unsafe_path');
|
|
196
|
+
}
|
|
197
|
+
const after = await file.stat(), named = await plugins.fs.lstat(path);
|
|
198
|
+
requireValue(sameFile(info, named) && !named.isSymbolicLink() && after.size === size &&
|
|
199
|
+
info.size === after.size && info.mtimeMs === after.mtimeMs &&
|
|
200
|
+
info.ctimeMs === after.ctimeMs, 'changed');
|
|
201
|
+
return { bytes: bytes.subarray(0, size), info };
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
await file.close();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async create(path, bytes) {
|
|
208
|
+
const file = await plugins.fs.open(path, plugins.fsConstants.O_CREAT | plugins.fsConstants.O_EXCL |
|
|
209
|
+
plugins.fsConstants.O_WRONLY | plugins.fsConstants.O_NOFOLLOW, 0o644);
|
|
210
|
+
let created;
|
|
211
|
+
try {
|
|
212
|
+
try {
|
|
213
|
+
created = await file.stat();
|
|
214
|
+
await file.chmod(0o644);
|
|
215
|
+
await file.writeFile(bytes);
|
|
216
|
+
await file.sync();
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
await file.close();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (created) {
|
|
224
|
+
requireValue(sameFile(created, await plugins.fs.lstat(path)), 'unsafe_path');
|
|
225
|
+
await plugins.fs.unlink(path);
|
|
226
|
+
}
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async flushDirectory(expected) {
|
|
231
|
+
const directory = await plugins.fs.open(this.directory, plugins.fsConstants.O_RDONLY |
|
|
232
|
+
plugins.fsConstants.O_DIRECTORY | plugins.fsConstants.O_NOFOLLOW);
|
|
233
|
+
try {
|
|
234
|
+
requireValue(sameFile(expected, await directory.stat()), 'unsafe_path');
|
|
235
|
+
await directory.sync();
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
await directory.close();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5zeXN0ZW1kdW5pdGZpbGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9jbGFzc2VzLnN5c3RlbWR1bml0ZmlsZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLDBCQUEwQixDQUFDO0FBQ3BELE9BQU8sRUFBRSx3QkFBd0IsRUFBRSxNQUFNLHVDQUF1QyxDQUFDO0FBQ2pGLE9BQU8sRUFHTCxXQUFXLEdBQ1osTUFBTSwwQkFBMEIsQ0FBQztBQVlsQyxNQUFNLE9BQU8sb0JBQXFCLFNBQVEsS0FBSztJQUM3QyxZQUNrQixJQU9HO1FBRW5CLEtBQUssQ0FBQyxnREFBZ0QsSUFBSSxHQUFHLENBQUMsQ0FBQztRQVQvQyxTQUFJLEdBQUosSUFBSSxDQU9EO1FBR25CLElBQUksQ0FBQyxJQUFJLEdBQUcsc0JBQXNCLENBQUM7SUFDckMsQ0FBQztDQUNGO0FBQ0QsTUFBTSxZQUFZLEdBQUcsQ0FBQyxLQUFjLEVBQUUsSUFBa0MsRUFBUSxFQUFFO0lBQ2hGLElBQUksQ0FBQyxLQUFLO1FBQUUsTUFBTSxJQUFJLG9CQUFvQixDQUFDLElBQUksQ0FBQyxDQUFDO0FBQ25ELENBQUMsQ0FBQztBQUNGLE1BQU0sTUFBTSxHQUFHLENBQUMsS0FBaUIsRUFBRSxFQUFFLENBQ25DLE9BQU8sQ0FBQyxNQUFNLENBQUMsVUFBVSxDQUFDLFFBQVEsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUM7QUFDbEUsTUFBTSxLQUFLLEdBQUcsQ0FBQyxLQUF3QixFQUFFLEVBQUUsQ0FDekMsS0FBSyxDQUFDLE9BQU8sS0FBSyxDQUFDLElBQUksS0FBSyxDQUFDLFVBQVUsS0FBSyxDQUFDO0lBQzdDLEtBQUssQ0FBQyxZQUFZLEtBQUssRUFBRSxJQUFJLEtBQUssQ0FBQyxHQUFHLEtBQUssRUFBRTtJQUM3QyxDQUFDLEtBQUssQ0FBQyxXQUFXLEtBQUss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
|
package/dist_ts/index.d.ts
CHANGED
package/dist_ts/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
export * from './smartdaemon.classes.smartdaemon.js';
|
|
2
2
|
export * from './smartdaemon.classes.service.js';
|
|
3
|
-
|
|
3
|
+
export * from './classes.systemdunit.js';
|
|
4
|
+
export * from './classes.systemdservicedefinition.js';
|
|
5
|
+
export * from './classes.systemdunitfile.js';
|
|
6
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLHNDQUFzQyxDQUFDO0FBQ3JELGNBQWMsa0NBQWtDLENBQUM7QUFDakQsY0FBYywwQkFBMEIsQ0FBQztBQUN6QyxjQUFjLHVDQUF1QyxDQUFDO0FBQ3RELGNBQWMsOEJBQThCLENBQUMifQ==
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
|
-
|
|
2
|
+
import * as childProcess from 'node:child_process';
|
|
3
|
+
import * as fs from 'node:fs/promises';
|
|
4
|
+
import { constants as fsConstants } from 'node:fs';
|
|
5
|
+
import * as crypto from 'node:crypto';
|
|
6
|
+
export type { Stats } from 'node:fs';
|
|
7
|
+
export { path, childProcess, fs, fsConstants, crypto };
|
|
3
8
|
import * as lik from '@push.rocks/lik';
|
|
4
9
|
import * as smartfm from '@push.rocks/smartfm';
|
|
5
10
|
import * as smartfs from '@push.rocks/smartfs';
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// node native scope
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
|
|
3
|
+
import * as childProcess from 'node:child_process';
|
|
4
|
+
import * as fs from 'node:fs/promises';
|
|
5
|
+
import { constants as fsConstants } from 'node:fs';
|
|
6
|
+
import * as crypto from 'node:crypto';
|
|
7
|
+
export { path, childProcess, fs, fsConstants, crypto };
|
|
4
8
|
// @pushrocks scope
|
|
5
9
|
import * as lik from '@push.rocks/lik';
|
|
6
10
|
import * as smartfm from '@push.rocks/smartfm';
|
|
@@ -12,4 +16,4 @@ import * as smartshell from '@push.rocks/smartshell';
|
|
|
12
16
|
import * as smartsystem from '@push.rocks/smartsystem';
|
|
13
17
|
export { lik, smartfm, smartfs, smartlog, smartlogDestinationLocal, smartpath, smartshell, smartsystem, };
|
|
14
18
|
// third party
|
|
15
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
19
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRkYWVtb24ucGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3NtYXJ0ZGFlbW9uLnBsdWdpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsb0JBQW9CO0FBQ3BCLE9BQU8sS0FBSyxJQUFJLE1BQU0sTUFBTSxDQUFDO0FBQzdCLE9BQU8sS0FBSyxZQUFZLE1BQU0sb0JBQW9CLENBQUM7QUFDbkQsT0FBTyxLQUFLLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUN2QyxPQUFPLEVBQUUsU0FBUyxJQUFJLFdBQVcsRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUNuRCxPQUFPLEtBQUssTUFBTSxNQUFNLGFBQWEsQ0FBQztBQUd0QyxPQUFPLEVBQUUsSUFBSSxFQUFFLFlBQVksRUFBRSxFQUFFLEVBQUUsV0FBVyxFQUFFLE1BQU0sRUFBRSxDQUFDO0FBRXZELG1CQUFtQjtBQUNuQixPQUFPLEtBQUssR0FBRyxNQUFNLGlCQUFpQixDQUFDO0FBQ3ZDLE9BQU8sS0FBSyxPQUFPLE1BQU0scUJBQXFCLENBQUM7QUFDL0MsT0FBTyxLQUFLLE9BQU8sTUFBTSxxQkFBcUIsQ0FBQztBQUMvQyxPQUFPLEtBQUssUUFBUSxNQUFNLHNCQUFzQixDQUFDO0FBQ2pELE9BQU8sS0FBSyx3QkFBd0IsTUFBTSx3Q0FBd0MsQ0FBQztBQUNuRixPQUFPLEtBQUssU0FBUyxNQUFNLHVCQUF1QixDQUFDO0FBQ25ELE9BQU8sS0FBSyxVQUFVLE1BQU0sd0JBQXdCLENBQUM7QUFDckQsT0FBTyxLQUFLLFdBQVcsTUFBTSx5QkFBeUIsQ0FBQztBQUV2RCxPQUFPLEVBQ0wsR0FBRyxFQUNILE9BQU8sRUFDUCxPQUFPLEVBQ1AsUUFBUSxFQUNSLHdCQUF3QixFQUN4QixTQUFTLEVBQ1QsVUFBVSxFQUNWLFdBQVcsR0FDWixDQUFDO0FBRUYsY0FBYyJ9
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@push.rocks/smartdaemon",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Start scripts as long running daemons and manage them.",
|
|
6
6
|
"main": "dist_ts/index.js",
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"homepage": "https://code.foss.global/push.rocks/smartdaemon",
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@git.zone/tsbuild": "^4.4.2",
|
|
30
|
-
"@git.zone/tsrun": "^2.0.
|
|
31
|
-
"@git.zone/tstest": "^
|
|
32
|
-
"@types/node": "^
|
|
30
|
+
"@git.zone/tsrun": "^2.0.6",
|
|
31
|
+
"@git.zone/tstest": "^6.0.0",
|
|
32
|
+
"@types/node": "^26.4.1"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@push.rocks/lik": "^6.4.1",
|
package/readme.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# @push.rocks/smartdaemon 🚀
|
|
2
2
|
**Turn your Node.js scripts into production-ready system daemons**
|
|
3
3
|
|
|
4
|
+
## Issue Reporting and Security
|
|
5
|
+
|
|
6
|
+
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
|
7
|
+
|
|
4
8
|
[](https://www.npmjs.com/package/@push.rocks/smartdaemon)
|
|
5
9
|
[](https://opensource.org/licenses/MIT)
|
|
6
10
|
|
|
@@ -147,6 +151,104 @@ class SmartDaemon {
|
|
|
147
151
|
}
|
|
148
152
|
```
|
|
149
153
|
|
|
154
|
+
### Inspecting and controlling an existing systemd unit
|
|
155
|
+
|
|
156
|
+
`SystemdUnit` works with an exact canonical `.service` name. Construction performs
|
|
157
|
+
no I/O. It does not create or rewrite a unit, enable it, remove a mask, or invoke
|
|
158
|
+
sudo. The caller must already have permission for lifecycle commands.
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { SystemdUnit } from '@push.rocks/smartdaemon';
|
|
162
|
+
|
|
163
|
+
const unit = new SystemdUnit({ unitName: 'smartdaemon_my-api-server.service' });
|
|
164
|
+
const state = await unit.inspect();
|
|
165
|
+
console.log(state.loadState, state.activeState, state.unitFileState);
|
|
166
|
+
|
|
167
|
+
// Application code decides whether stopping or restarting is appropriate.
|
|
168
|
+
// await unit.stop();
|
|
169
|
+
// await unit.start();
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Snapshots are immutable and also include `subState`, `mainPid`, `controlPid`,
|
|
173
|
+
`controlGroup`, `killMode`, and `job`. An empty `job` means no systemd job was
|
|
174
|
+
reported. Missing and masked units remain distinct from inactive or disabled
|
|
175
|
+
units. Aliases resolving to a different canonical unit are rejected.
|
|
176
|
+
|
|
177
|
+
`start()` and `stop()` await the systemctl command and return a fresh snapshot.
|
|
178
|
+
They do not establish application readiness or prove database shutdown; callers
|
|
179
|
+
must apply the owning application's lifecycle contract. A command failure or
|
|
180
|
+
timeout rejects with `SystemdUnitError` and a local code, without returning raw
|
|
181
|
+
command diagnostics. The systemd job can continue after a client timeout, so a
|
|
182
|
+
failed command must not be treated as cancelled or successfully completed.
|
|
183
|
+
|
|
184
|
+
Calls use an absolute `systemctlPath` (default `/usr/bin/systemctl`), a clean
|
|
185
|
+
environment, no shell or password prompt, and bounded output. Trusted owning code
|
|
186
|
+
can choose another absolute executable path and `commandTimeoutMs` (default
|
|
187
|
+
120000, maximum 600000). This timeout bounds the client process only.
|
|
188
|
+
|
|
189
|
+
`inspectConfiguration()` reports the selected `fragmentPath`, opaque `dropInPaths`
|
|
190
|
+
list and `needsReload` flag. `reloadConfiguration()` explicitly reloads the manager's
|
|
191
|
+
unit definitions globally and returns a fresh configuration snapshot. Neither
|
|
192
|
+
operation changes enablement or masks.
|
|
193
|
+
|
|
194
|
+
### Installing a direct systemd service definition
|
|
195
|
+
|
|
196
|
+
`SystemdServiceDefinition` encodes a literal executable and argument list, with
|
|
197
|
+
explicit user, group, restart policy, kill mode and stop timeout. It uses `Type=exec`,
|
|
198
|
+
journal output and null stdin. It introduces no shell; percent specifiers and
|
|
199
|
+
argument environment expansion are escaped. Paths must be absolute and normalized;
|
|
200
|
+
control characters are rejected. Descriptions and working directories cannot have
|
|
201
|
+
backslashes or surrounding whitespace; executable paths also reject quotes and
|
|
202
|
+
backslashes. Arguments may contain spaces, quotes, backslashes and empty strings.
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
import { SystemdServiceDefinition, SystemdUnitFile } from '@push.rocks/smartdaemon';
|
|
206
|
+
|
|
207
|
+
const definition = new SystemdServiceDefinition({
|
|
208
|
+
unitName: 'smartdaemon_my-api-server.service',
|
|
209
|
+
description: 'My API server',
|
|
210
|
+
executable: '/opt/my-api/bin/server',
|
|
211
|
+
args: ['serve'],
|
|
212
|
+
workingDirectory: '/opt/my-api',
|
|
213
|
+
user: 'root',
|
|
214
|
+
group: 'root',
|
|
215
|
+
restart: 'no',
|
|
216
|
+
killMode: 'mixed',
|
|
217
|
+
timeoutStopSeconds: 360,
|
|
218
|
+
});
|
|
219
|
+
const file = new SystemdUnitFile({ unitName: definition.unitName });
|
|
220
|
+
// Inside the privileged installer's existing cross-process ownership scope:
|
|
221
|
+
const previous = await file.inspect();
|
|
222
|
+
const installed = await file.install(definition, previous?.sha256 ?? null);
|
|
223
|
+
console.log(installed.changed, installed.masked, installed.record.sha256);
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Installation requires Linux, root and a quiescent exact unit with no process,
|
|
227
|
+
cgroup or job. It rejects effective drop-ins and shadowing fragments. Definitions
|
|
228
|
+
default to `/usr/local/lib/systemd/system`, below administrator configuration in
|
|
229
|
+
the load path, so existing persistent/runtime masks remain intact. Existing
|
|
230
|
+
enablement is preserved; an absent unit becomes disabled. It never enables,
|
|
231
|
+
unmasks, starts or stops a service, or invokes sudo.
|
|
232
|
+
|
|
233
|
+
The caller must already serialize privileged installation across processes;
|
|
234
|
+
`SystemdUnitFile` only excludes simultaneous operations on the same instance.
|
|
235
|
+
The expected previous SHA-256 is mandatory (`null` requires an absent file),
|
|
236
|
+
while already matching bytes are idempotent. Installation checks protected
|
|
237
|
+
directories and regular single-link mode-0644 files, retains changed definitions
|
|
238
|
+
as `<unit>.previous-<sha256>`, flushes them before atomic replacement, reloads
|
|
239
|
+
the manager and checks its selected fragment and final bytes. A reload failure
|
|
240
|
+
leaves the new file and retained previous definition in place; recovery requires
|
|
241
|
+
inspection, never an automatic rollback. Filesystem and definition errors use
|
|
242
|
+
`SystemdUnitFileError` with a local code. Trusted qualification code can set
|
|
243
|
+
`unitDirectory`, `expectedUid` and the inherited `SystemdUnit` command options.
|
|
244
|
+
|
|
245
|
+
With `killMode: 'mixed'`, the main process owns graceful child shutdown and must
|
|
246
|
+
remain alive until its children have joined. Systemd sends SIGTERM to the main
|
|
247
|
+
process first and can send SIGKILL to the whole cgroup when the main process exits
|
|
248
|
+
or the stop deadline expires. `control-group` sends the initial signal to the
|
|
249
|
+
whole group. A successful systemd start still requires application readiness
|
|
250
|
+
verification, and a completed stop alone does not prove database drain.
|
|
251
|
+
|
|
150
252
|
### SmartDaemonService Class
|
|
151
253
|
|
|
152
254
|
```typescript
|
|
@@ -424,4 +526,4 @@ Registered at District court Bremen HRB 35230 HB, Germany
|
|
|
424
526
|
|
|
425
527
|
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
|
426
528
|
|
|
427
|
-
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
|
529
|
+
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import * as plugins from './smartdaemon.plugins.js';
|
|
2
|
+
|
|
3
|
+
export interface ISystemdServiceDefinitionOptions {
|
|
4
|
+
unitName: string;
|
|
5
|
+
description: string;
|
|
6
|
+
executable: string;
|
|
7
|
+
args: readonly string[];
|
|
8
|
+
workingDirectory: string;
|
|
9
|
+
user: string;
|
|
10
|
+
group: string;
|
|
11
|
+
restart: 'no' | 'on-failure' | 'always';
|
|
12
|
+
killMode: 'mixed' | 'control-group';
|
|
13
|
+
timeoutStopSeconds: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class SystemdServiceDefinitionError extends Error {
|
|
17
|
+
constructor() {
|
|
18
|
+
super('Invalid direct systemd service definition.');
|
|
19
|
+
this.name = 'SystemdServiceDefinitionError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Literal direct-execution definition; never introduces a shell or environment expansion. */
|
|
24
|
+
export class SystemdServiceDefinition {
|
|
25
|
+
public readonly unitName: string;
|
|
26
|
+
public readonly content: string;
|
|
27
|
+
public readonly sha256: string;
|
|
28
|
+
|
|
29
|
+
constructor(options: ISystemdServiceDefinitionOptions) {
|
|
30
|
+
const requireValue = (value: unknown): void => {
|
|
31
|
+
if (!value) throw new SystemdServiceDefinitionError();
|
|
32
|
+
};
|
|
33
|
+
const text = (value: unknown, maximum = 4096): value is string =>
|
|
34
|
+
typeof value === 'string' && value.length <= maximum && !/[\x00-\x1f\x7f]/.test(value) &&
|
|
35
|
+
!/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(value);
|
|
36
|
+
const absolute = (value: unknown) =>
|
|
37
|
+
text(value) && plugins.path.isAbsolute(value) &&
|
|
38
|
+
plugins.path.normalize(value) === value;
|
|
39
|
+
const keys = [
|
|
40
|
+
'unitName',
|
|
41
|
+
'description',
|
|
42
|
+
'executable',
|
|
43
|
+
'args',
|
|
44
|
+
'workingDirectory',
|
|
45
|
+
'user',
|
|
46
|
+
'group',
|
|
47
|
+
'restart',
|
|
48
|
+
'killMode',
|
|
49
|
+
'timeoutStopSeconds',
|
|
50
|
+
];
|
|
51
|
+
requireValue(
|
|
52
|
+
options && Object.getPrototypeOf(options) === Object.prototype &&
|
|
53
|
+
Reflect.ownKeys(options).length === keys.length && keys.every((key) => {
|
|
54
|
+
const descriptor = Object.getOwnPropertyDescriptor(options, key);
|
|
55
|
+
return descriptor && 'value' in descriptor;
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
requireValue(
|
|
59
|
+
typeof options.unitName === 'string' &&
|
|
60
|
+
/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,230}\.service$/.test(options.unitName),
|
|
61
|
+
);
|
|
62
|
+
requireValue(
|
|
63
|
+
text(options.description, 1024) && options.description.length > 0 &&
|
|
64
|
+
options.description.trim() === options.description && !options.description.includes('\\'),
|
|
65
|
+
);
|
|
66
|
+
requireValue(
|
|
67
|
+
absolute(options.executable) && options.executable !== '/' &&
|
|
68
|
+
!/[\\"']/.test(options.executable) && !options.executable.endsWith('/') &&
|
|
69
|
+
absolute(options.workingDirectory) &&
|
|
70
|
+
options.workingDirectory.trim() === options.workingDirectory &&
|
|
71
|
+
!options.workingDirectory.includes('\\'),
|
|
72
|
+
);
|
|
73
|
+
requireValue(
|
|
74
|
+
Array.isArray(options.args) && Object.getPrototypeOf(options.args) === Array.prototype &&
|
|
75
|
+
options.args.length <= 128 &&
|
|
76
|
+
Reflect.ownKeys(options.args).length === options.args.length + 1 &&
|
|
77
|
+
Array.from(
|
|
78
|
+
{ length: options.args.length },
|
|
79
|
+
(_, index) => Object.getOwnPropertyDescriptor(options.args, String(index)),
|
|
80
|
+
)
|
|
81
|
+
.every((descriptor) => descriptor && 'value' in descriptor && text(descriptor.value)),
|
|
82
|
+
);
|
|
83
|
+
requireValue(
|
|
84
|
+
[options.user, options.group].every((value) =>
|
|
85
|
+
typeof value === 'string' && /^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$/.test(value)
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
requireValue(
|
|
89
|
+
['no', 'on-failure', 'always'].includes(options.restart) &&
|
|
90
|
+
['mixed', 'control-group'].includes(options.killMode) &&
|
|
91
|
+
Number.isSafeInteger(options.timeoutStopSeconds) &&
|
|
92
|
+
options.timeoutStopSeconds >= 1 && options.timeoutStopSeconds <= 600,
|
|
93
|
+
);
|
|
94
|
+
// Quoting and percent/dollar escaping follow systemd.syntax and systemd.service.
|
|
95
|
+
const quote = (value: string, command = false) =>
|
|
96
|
+
'"' + value
|
|
97
|
+
.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('%', '%%')
|
|
98
|
+
.replaceAll('$', () => command ? '$$' : '$') +
|
|
99
|
+
'"';
|
|
100
|
+
this.unitName = options.unitName;
|
|
101
|
+
this.content = [
|
|
102
|
+
'[Unit]',
|
|
103
|
+
`Description=${options.description.replaceAll('%', '%%')}`,
|
|
104
|
+
'After=local-fs.target',
|
|
105
|
+
'',
|
|
106
|
+
'[Service]',
|
|
107
|
+
'Type=exec',
|
|
108
|
+
`User=${options.user}`,
|
|
109
|
+
`Group=${options.group}`,
|
|
110
|
+
`ExecStart=${
|
|
111
|
+
[quote(options.executable), ...options.args.map((value) => quote(value, true))].join(' ')
|
|
112
|
+
}`,
|
|
113
|
+
// WorkingDirectory is a raw path value, not an ExecStart word list.
|
|
114
|
+
`WorkingDirectory=${options.workingDirectory.replaceAll('%', '%%')}`,
|
|
115
|
+
`Restart=${options.restart}`,
|
|
116
|
+
`KillMode=${options.killMode}`,
|
|
117
|
+
'KillSignal=SIGTERM',
|
|
118
|
+
'FinalKillSignal=SIGKILL',
|
|
119
|
+
'SendSIGKILL=yes',
|
|
120
|
+
'SendSIGHUP=no',
|
|
121
|
+
`TimeoutStopSec=${options.timeoutStopSeconds}s`,
|
|
122
|
+
'StandardInput=null',
|
|
123
|
+
'StandardOutput=journal',
|
|
124
|
+
'StandardError=journal',
|
|
125
|
+
'',
|
|
126
|
+
'[Install]',
|
|
127
|
+
'WantedBy=multi-user.target',
|
|
128
|
+
'',
|
|
129
|
+
].join('\n');
|
|
130
|
+
requireValue(Buffer.byteLength(this.content) <= 16384);
|
|
131
|
+
this.sha256 = plugins.crypto.createHash('sha256').update(this.content).digest('hex');
|
|
132
|
+
Object.freeze(this);
|
|
133
|
+
}
|
|
134
|
+
}
|