@push.rocks/smartdaemon 2.20.0 → 2.21.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.systemdservicedefinition.d.ts +16 -4
- package/dist_ts/classes.systemdservicedefinition.js +33 -7
- package/dist_ts/classes.systemdunit.d.ts +7 -0
- package/dist_ts/classes.systemdunit.js +11 -1
- package/dist_ts/classes.systemdunitfile.d.ts +26 -0
- package/dist_ts/classes.systemdunitfile.js +125 -9
- package/dist_ts/smartdaemon.classes.systemdmanager.d.ts +0 -1
- package/dist_ts/smartdaemon.classes.systemdmanager.js +2 -3
- package/dist_ts/smartdaemon.plugins.d.ts +1 -2
- package/dist_ts/smartdaemon.plugins.js +2 -3
- package/dist_ts/systemd.definition.d.ts +13 -0
- package/dist_ts/systemd.definition.js +1 -1
- package/dist_ts/systemd.managerversion.d.ts +2 -0
- package/dist_ts/systemd.managerversion.js +3 -1
- package/package.json +2 -3
- package/readme.md +84 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.systemdservicedefinition.ts +47 -10
- package/ts/classes.systemdunit.ts +11 -0
- package/ts/classes.systemdunitfile.ts +124 -7
- package/ts/smartdaemon.classes.systemdmanager.ts +1 -3
- package/ts/smartdaemon.plugins.ts +0 -2
- package/ts/systemd.definition.ts +12 -0
- package/ts/systemd.managerversion.ts +3 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import * as plugins from './smartdaemon.plugins.js';
|
|
2
2
|
import { environmentByteLimit, environmentVariableLimit, isCleanText, renderServiceRelations, systemdAddressFamilies,
|
|
3
3
|
systemdCapabilities, type TSystemdServiceStartup, type ISystemdServiceDependencies, type ISystemdServiceInstallTargets,
|
|
4
|
-
type ISystemdServiceCredential, type TSystemdCapability, type TSystemdAddressFamily
|
|
5
|
-
|
|
4
|
+
type ISystemdServiceCredential, type TSystemdCapability, type TSystemdAddressFamily,
|
|
5
|
+
type TSystemdServiceOutput } from './systemd.definition.js';
|
|
6
|
+
import { fileDescriptorStorePreserveMajor, outputAppendMajor } from './systemd.managerversion.js';
|
|
6
7
|
export type { ISystemdServiceStartup, ISystemdServiceNotifyStartup, TSystemdServiceStartup,
|
|
7
8
|
ISystemdServiceDependencies, ISystemdServiceInstallTargets, ISystemdServiceCredential, TSystemdCapability,
|
|
8
|
-
TSystemdAddressFamily } from './systemd.definition.js';
|
|
9
|
+
TSystemdAddressFamily, TSystemdServiceOutput } from './systemd.definition.js';
|
|
9
10
|
export { systemdCapabilities, systemdAddressFamilies } from './systemd.definition.js';
|
|
10
11
|
|
|
11
12
|
interface ISystemdServiceDefinitionBaseOptions {
|
|
@@ -78,6 +79,17 @@ interface ISystemdServiceDefinitionBaseOptions {
|
|
|
78
79
|
* 64 variables and 8192 UTF-8 bytes of `NAME=value` in total. They are taken literally: `%` is escaped, and `Environment=` never expands `$`.
|
|
79
80
|
*/
|
|
80
81
|
environment?: Readonly<Record<string, string>>;
|
|
82
|
+
/**
|
|
83
|
+
* Where the service's standard output goes (`StandardOutput=`): `'journal'` (the default),
|
|
84
|
+
* `'null'`, or `{ append: path }`, an absolute normalized path without a backslash, surrounding
|
|
85
|
+
* whitespace or trailing slash, whose file systemd appends to (`append:`, systemd 240 and later).
|
|
86
|
+
* An `append` output sets `requiredManagerMajor` to at least 240, so `SystemdUnitFile.install()`
|
|
87
|
+
* refuses an older manager, which would ignore the line. Omitted, the unit renders
|
|
88
|
+
* `StandardOutput=journal` as before.
|
|
89
|
+
*/
|
|
90
|
+
standardOutput?: TSystemdServiceOutput;
|
|
91
|
+
/** Where the service's standard error goes (`StandardError=`), as `standardOutput`; omitted, `journal`. */
|
|
92
|
+
standardError?: TSystemdServiceOutput;
|
|
81
93
|
/**
|
|
82
94
|
* System scope only. Runs the service as a transient user and group allocated at start
|
|
83
95
|
* (`DynamicUser=`), named by `user` and `group`, which then must not be `root` and have at most
|
|
@@ -177,8 +189,9 @@ export class SystemdServiceDefinition {
|
|
|
177
189
|
public readonly sha256: string;
|
|
178
190
|
/**
|
|
179
191
|
* The oldest systemd major whose manager reads every directive this definition renders, when it
|
|
180
|
-
* renders one an older manager would ignore: 254 with `fileDescriptorStorePreserve
|
|
181
|
-
* null. `SystemdUnitFile.install()` refuses a
|
|
192
|
+
* renders one an older manager would ignore: the highest of 254 with `fileDescriptorStorePreserve`
|
|
193
|
+
* and 240 with an `append` output; otherwise null. `SystemdUnitFile.install()` refuses a
|
|
194
|
+
* definition on an older manager.
|
|
182
195
|
*/
|
|
183
196
|
public readonly requiredManagerMajor: number | null;
|
|
184
197
|
|
|
@@ -217,7 +230,8 @@ export class SystemdServiceDefinition {
|
|
|
217
230
|
const optionalKeys: TSystemdServiceDefinitionOption[] = ['scope', 'startup', 'dependencies', 'install',
|
|
218
231
|
'restartSeconds', 'restartPreventExitStatus', 'successExitStatus', 'startLimitIntervalSeconds',
|
|
219
232
|
'startLimitBurst', 'delegate', 'limitNoFile', 'tasksMax', 'oomScoreAdjust', 'notifyAccess',
|
|
220
|
-
'fileDescriptorStoreMax', 'fileDescriptorStorePreserve', 'environment',
|
|
233
|
+
'fileDescriptorStoreMax', 'fileDescriptorStorePreserve', 'environment', 'standardOutput', 'standardError',
|
|
234
|
+
...systemOnlyOptions];
|
|
221
235
|
// Every own key is a known option held by an enumerable data property, so no accessor or trap runs.
|
|
222
236
|
for (const key of Reflect.ownKeys(options)) {
|
|
223
237
|
requireValue(typeof key === 'string' && [...keys, ...optionalKeys, 'user', 'group'].includes(key as never), 'options');
|
|
@@ -334,6 +348,25 @@ export class SystemdServiceDefinition {
|
|
|
334
348
|
service.push(...[...assignments.keys()].sort()
|
|
335
349
|
.map((name) => `Environment=${quote(assignments.get(name)!)}`));
|
|
336
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* An output stream's directive value, read once through the object's own data descriptor. An
|
|
353
|
+
* `append:` path is taken raw after specifier expansion, so only `%` is escaped.
|
|
354
|
+
*/
|
|
355
|
+
const output = (value: unknown, option: 'standardOutput' | 'standardError'): string => {
|
|
356
|
+
if (value === undefined || value === 'journal') return 'journal';
|
|
357
|
+
if (value === 'null') return 'null';
|
|
358
|
+
plainObject(value, option);
|
|
359
|
+
const entryKeys = Reflect.ownKeys(value as object);
|
|
360
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, 'append');
|
|
361
|
+
requireValue(entryKeys.length === 1 && entryKeys[0] === 'append' && descriptor?.enumerable &&
|
|
362
|
+
'value' in descriptor, option);
|
|
363
|
+
const file: unknown = descriptor!.value;
|
|
364
|
+
requireValue(absolute(file) && !file.endsWith('/') && file.trim() === file &&
|
|
365
|
+
!file.includes('\\'), option);
|
|
366
|
+
return `append:${(file as string).replaceAll('%', '%%')}`;
|
|
367
|
+
};
|
|
368
|
+
const standardOutput = output(options.standardOutput, 'standardOutput');
|
|
369
|
+
const standardError = output(options.standardError, 'standardError');
|
|
337
370
|
// Every system-only option is refused by name in a user manager's service.
|
|
338
371
|
for (const option of systemOnlyOptions) {
|
|
339
372
|
requireValue(scope === 'system' || options[option] === undefined, option);
|
|
@@ -508,8 +541,8 @@ export class SystemdServiceDefinition {
|
|
|
508
541
|
`TimeoutStopSec=${options.timeoutStopSeconds}s`,
|
|
509
542
|
...service,
|
|
510
543
|
'StandardInput=null',
|
|
511
|
-
|
|
512
|
-
|
|
544
|
+
`StandardOutput=${standardOutput}`,
|
|
545
|
+
`StandardError=${standardError}`,
|
|
513
546
|
'',
|
|
514
547
|
'[Install]',
|
|
515
548
|
...relations.install,
|
|
@@ -517,8 +550,12 @@ export class SystemdServiceDefinition {
|
|
|
517
550
|
].join('\n');
|
|
518
551
|
requireValue(Buffer.byteLength(this.content) <= 16384, 'options');
|
|
519
552
|
this.sha256 = plugins.crypto.createHash('sha256').update(this.content).digest('hex');
|
|
520
|
-
|
|
521
|
-
|
|
553
|
+
// An older manager ignores a directive it does not read, so the newest one any line needs counts.
|
|
554
|
+
const requiredMajors = [
|
|
555
|
+
...(options.fileDescriptorStorePreserve !== undefined ? [fileDescriptorStorePreserveMajor] : []),
|
|
556
|
+
...([standardOutput, standardError].some((value) => value.startsWith('append:')) ? [outputAppendMajor] : []),
|
|
557
|
+
];
|
|
558
|
+
this.requiredManagerMajor = requiredMajors.length > 0 ? Math.max(...requiredMajors) : null;
|
|
522
559
|
Object.freeze(this);
|
|
523
560
|
}
|
|
524
561
|
}
|
|
@@ -539,6 +539,17 @@ export class SystemdUnit {
|
|
|
539
539
|
return this.inspect();
|
|
540
540
|
}
|
|
541
541
|
|
|
542
|
+
/**
|
|
543
|
+
* Persistently disables the unit: removes the enablement links its `[Install]` section names
|
|
544
|
+
* (`systemctl disable`, which reloads the manager), then inspects. Never stops, masks, unmasks or
|
|
545
|
+
* removes the definition; a running unit keeps running. A failed or timed out command can leave
|
|
546
|
+
* partial enablement: inspect and retry the exact operation.
|
|
547
|
+
*/
|
|
548
|
+
public async disable(): Promise<ISystemdUnitState> {
|
|
549
|
+
await this.command(['disable', this.unitName]);
|
|
550
|
+
return this.inspect();
|
|
551
|
+
}
|
|
552
|
+
|
|
542
553
|
/** Loaded manager graph, including reverse edges. Enablement alone does not prove every required link. */
|
|
543
554
|
public async inspectRelationships(): Promise<ISystemdUnitRelationships> {
|
|
544
555
|
const keys = ['Id', 'Before', 'After', 'Requires', 'Wants', 'Conflicts', 'RequiredBy', 'WantedBy'];
|
|
@@ -157,15 +157,27 @@ const admittedFileStates: Readonly<Record<string, readonly string[]>> = {
|
|
|
157
157
|
masked: ['masked', 'masked-runtime'],
|
|
158
158
|
'not-found': [''],
|
|
159
159
|
};
|
|
160
|
-
/**
|
|
161
|
-
|
|
160
|
+
/**
|
|
161
|
+
* The unit-file states removal admits: a loaded unit only when disabled, so no enablement link is
|
|
162
|
+
* left naming a definition that no longer exists.
|
|
163
|
+
*/
|
|
164
|
+
const removableFileStates: Readonly<Record<string, readonly string[]>> = {
|
|
165
|
+
loaded: ['disabled'],
|
|
166
|
+
masked: ['masked', 'masked-runtime'],
|
|
167
|
+
'not-found': [''],
|
|
168
|
+
};
|
|
169
|
+
/** Admits a quiet unit whose load and unit-file states the operation may write behind. */
|
|
170
|
+
const requireAdmittedState = (
|
|
171
|
+
state: ISystemdUnitState,
|
|
172
|
+
fileStates: Readonly<Record<string, readonly string[]>>,
|
|
173
|
+
): void => {
|
|
162
174
|
const reason: TSystemdUnitFileUnsafeUnitReason | null = !idle(state)
|
|
163
175
|
? 'active'
|
|
164
176
|
: state.job !== ''
|
|
165
177
|
? 'job_pending'
|
|
166
|
-
: !Object.hasOwn(
|
|
178
|
+
: !Object.hasOwn(fileStates, state.loadState)
|
|
167
179
|
? 'unexpected_load_state'
|
|
168
|
-
: !
|
|
180
|
+
: !fileStates[state.loadState].includes(state.unitFileState)
|
|
169
181
|
? 'unexpected_file_state'
|
|
170
182
|
: null;
|
|
171
183
|
if (reason) throw new SystemdUnitFileUnsafeUnitError(reason, state, null);
|
|
@@ -310,7 +322,7 @@ export class SystemdUnitFile {
|
|
|
310
322
|
let temporaryInfo: plugins.Stats | undefined;
|
|
311
323
|
try {
|
|
312
324
|
const before = await this.unit.inspect();
|
|
313
|
-
const beforeConfiguration = await this.admit(before, maskDirectories);
|
|
325
|
+
const beforeConfiguration = await this.admit(before, maskDirectories, admittedFileStates);
|
|
314
326
|
await this.directories(true);
|
|
315
327
|
const parent = await plugins.fs.lstat(this.directory);
|
|
316
328
|
const previous = await this.read(this.path);
|
|
@@ -345,7 +357,7 @@ export class SystemdUnitFile {
|
|
|
345
357
|
);
|
|
346
358
|
const rechecked = await this.unit.inspect();
|
|
347
359
|
requireValue(JSON.stringify(rechecked) === JSON.stringify(before), 'changed');
|
|
348
|
-
const recheckedConfiguration = await this.admit(rechecked, maskDirectories);
|
|
360
|
+
const recheckedConfiguration = await this.admit(rechecked, maskDirectories, admittedFileStates);
|
|
349
361
|
requireValue(
|
|
350
362
|
recheckedConfiguration.fragmentPath === beforeConfiguration.fragmentPath,
|
|
351
363
|
'changed',
|
|
@@ -395,11 +407,116 @@ export class SystemdUnitFile {
|
|
|
395
407
|
}
|
|
396
408
|
}
|
|
397
409
|
|
|
410
|
+
/**
|
|
411
|
+
* Removes the definition `install()` wrote and the previous definitions it retained beside it
|
|
412
|
+
* (`<unit>.previous-<sha256>`), then reloads the manager. The expected SHA-256 of the installed
|
|
413
|
+
* definition is mandatory, so only the bytes the caller installed are removed; an absent unit
|
|
414
|
+
* file with no retained definition is idempotent (`removed: false`) and reloads nothing.
|
|
415
|
+
*
|
|
416
|
+
* Admits only a unit at rest with no job queued, no pending reload and no drop-ins, whose
|
|
417
|
+
* manager view still selects this file: a loaded unit must be disabled first
|
|
418
|
+
* (`SystemdUnit.disable()`), so no enablement link outlives its definition; a masked unit keeps
|
|
419
|
+
* its mask; a unit the manager does not find is admitted. Refusals are the `unsafe_unit` reasons
|
|
420
|
+
* of `install()`, `unexpected_file_state` for an enabled unit among them. Every file is checked
|
|
421
|
+
* like the installed one (regular, single link, mode 0644, owned by the expected uid) and each
|
|
422
|
+
* retained definition must hold the bytes its name digests, or nothing is removed (`changed`).
|
|
423
|
+
* After removal the manager must report the unit not found (or still masked) and at rest;
|
|
424
|
+
* otherwise `reload_failed`, with the files already removed. Never stops, disables or unmasks.
|
|
425
|
+
*/
|
|
426
|
+
public async uninstall(
|
|
427
|
+
expectedSha256: string,
|
|
428
|
+
): Promise<Readonly<{ removed: boolean; masked: boolean; removedPaths: readonly string[] }>> {
|
|
429
|
+
requireValue(typeof expectedSha256 === 'string' && /^[a-f0-9]{64}$/.test(expectedSha256),
|
|
430
|
+
'invalid_options');
|
|
431
|
+
const maskDirectories = this.maskDirectories();
|
|
432
|
+
if (maskDirectories === null) throw new SystemdUnitFileError('invalid_options');
|
|
433
|
+
requireValue(!this.busy, 'busy');
|
|
434
|
+
this.busy = true;
|
|
435
|
+
try {
|
|
436
|
+
const before = await this.unit.inspect();
|
|
437
|
+
const beforeConfiguration = await this.admit(before, maskDirectories, removableFileStates);
|
|
438
|
+
const unchanged = Object.freeze({ removed: false, masked: masked(before), removedPaths: Object.freeze([]) });
|
|
439
|
+
// A loaded unit selects this file, so its absence is drift, not an earlier removal.
|
|
440
|
+
if (!await this.directories(false)) {
|
|
441
|
+
requireValue(before.loadState !== 'loaded', 'changed');
|
|
442
|
+
return unchanged;
|
|
443
|
+
}
|
|
444
|
+
const parent = await plugins.fs.lstat(this.directory);
|
|
445
|
+
const current = await this.read(this.path);
|
|
446
|
+
if (current) requireValue(digest(current.bytes) === expectedSha256, 'changed');
|
|
447
|
+
else requireValue(before.loadState !== 'loaded', 'changed');
|
|
448
|
+
const targets = [
|
|
449
|
+
...(current ? [{ path: this.path, info: current.info }] : []),
|
|
450
|
+
...await this.retainedDefinitions(),
|
|
451
|
+
];
|
|
452
|
+
if (targets.length === 0) return unchanged;
|
|
453
|
+
const rechecked = await this.unit.inspect();
|
|
454
|
+
requireValue(JSON.stringify(rechecked) === JSON.stringify(before), 'changed');
|
|
455
|
+
const recheckedConfiguration = await this.admit(rechecked, maskDirectories, removableFileStates);
|
|
456
|
+
requireValue(recheckedConfiguration.fragmentPath === beforeConfiguration.fragmentPath, 'changed');
|
|
457
|
+
requireSafePath(sameFile(parent, await plugins.fs.lstat(this.directory)), this.directory, 'replaced');
|
|
458
|
+
// Every name must still hold the file that was checked before the first is removed, so a
|
|
459
|
+
// refusal never leaves a partial removal.
|
|
460
|
+
for (const target of targets) {
|
|
461
|
+
requireValue(sameFile(target.info, await plugins.fs.lstat(target.path)), 'changed');
|
|
462
|
+
}
|
|
463
|
+
for (const target of targets) await plugins.fs.unlink(target.path);
|
|
464
|
+
await this.flushDirectory(parent);
|
|
465
|
+
const removedPaths = Object.freeze(targets.map((target) => target.path));
|
|
466
|
+
try {
|
|
467
|
+
const reloadedConfiguration = await this.unit.reloadConfiguration();
|
|
468
|
+
const after = await this.unit.inspect();
|
|
469
|
+
requireValue(
|
|
470
|
+
quiet(after) &&
|
|
471
|
+
(masked(before)
|
|
472
|
+
? masked(after) && after.unitFileState === before.unitFileState
|
|
473
|
+
: after.loadState === 'not-found' && after.unitFileState === ''),
|
|
474
|
+
'changed',
|
|
475
|
+
);
|
|
476
|
+
const configuration = await this.unit.inspectConfiguration();
|
|
477
|
+
const expectedFragment = masked(after) ? beforeConfiguration.fragmentPath : '';
|
|
478
|
+
for (const snapshot of [reloadedConfiguration, configuration]) {
|
|
479
|
+
requireAdmittedConfiguration(after, snapshot, snapshot.fragmentPath === expectedFragment);
|
|
480
|
+
}
|
|
481
|
+
requireValue(await this.read(this.path) === null, 'changed');
|
|
482
|
+
return Object.freeze({ removed: current !== null, masked: masked(after), removedPaths });
|
|
483
|
+
} catch (error) {
|
|
484
|
+
// The files are already removed; the cause says why the reload was not accepted.
|
|
485
|
+
throw new SystemdUnitFileError('reload_failed', { cause: error });
|
|
486
|
+
}
|
|
487
|
+
} catch (error) {
|
|
488
|
+
if (error instanceof SystemdUnitFileError) throw error;
|
|
489
|
+
throw new SystemdUnitFileError('write_failed');
|
|
490
|
+
} finally {
|
|
491
|
+
this.busy = false;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* The previous definitions `install()` retained beside the unit, each checked like the unit file
|
|
497
|
+
* and holding exactly the bytes its name digests.
|
|
498
|
+
*/
|
|
499
|
+
private async retainedDefinitions(): Promise<Array<{ path: string; info: plugins.Stats }>> {
|
|
500
|
+
const prefix = `${this.name}.previous-`;
|
|
501
|
+
const retained: Array<{ path: string; info: plugins.Stats }> = [];
|
|
502
|
+
for (const entry of (await plugins.fs.readdir(this.directory)).sort()) {
|
|
503
|
+
if (!entry.startsWith(prefix)) continue;
|
|
504
|
+
const sha256 = entry.slice(prefix.length);
|
|
505
|
+
if (!/^[a-f0-9]{64}$/.test(sha256)) continue;
|
|
506
|
+
const path = plugins.path.join(this.directory, entry);
|
|
507
|
+
const file = await this.read(path);
|
|
508
|
+
requireValue(file !== null && digest(file.bytes) === sha256, 'changed');
|
|
509
|
+
retained.push({ path, info: file!.info });
|
|
510
|
+
}
|
|
511
|
+
return retained;
|
|
512
|
+
}
|
|
513
|
+
|
|
398
514
|
private async admit(
|
|
399
515
|
state: ISystemdUnitState,
|
|
400
516
|
maskDirectories: IMaskDirectories,
|
|
517
|
+
fileStates: Readonly<Record<string, readonly string[]>>,
|
|
401
518
|
): Promise<ISystemdUnitConfigurationState> {
|
|
402
|
-
requireAdmittedState(state);
|
|
519
|
+
requireAdmittedState(state, fileStates);
|
|
403
520
|
const configuration = await this.unit.inspectConfiguration();
|
|
404
521
|
const maskPaths = (state.unitFileState === 'masked-runtime'
|
|
405
522
|
? maskDirectories.runtime : maskDirectories.persistent)
|
|
@@ -28,7 +28,6 @@ export class SmartDaemonSystemdManager {
|
|
|
28
28
|
// INSTANCE
|
|
29
29
|
public smartdaemonRef: SmartDaemon;
|
|
30
30
|
public smartshellInstance: plugins.smartshell.Smartshell;
|
|
31
|
-
public smartsystem: plugins.smartsystem.Smartsystem;
|
|
32
31
|
public smartFs: plugins.smartfs.SmartFs;
|
|
33
32
|
|
|
34
33
|
public shouldExecute: boolean = false;
|
|
@@ -40,7 +39,6 @@ export class SmartDaemonSystemdManager {
|
|
|
40
39
|
this.smartshellInstance = new plugins.smartshell.Smartshell({
|
|
41
40
|
executor: 'bash',
|
|
42
41
|
});
|
|
43
|
-
this.smartsystem = new plugins.smartsystem.Smartsystem();
|
|
44
42
|
this.smartFs = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
|
|
45
43
|
this.sudoPassword = sudoPasswordArg;
|
|
46
44
|
|
|
@@ -59,7 +57,7 @@ export class SmartDaemonSystemdManager {
|
|
|
59
57
|
}
|
|
60
58
|
|
|
61
59
|
public async checkElegibility() {
|
|
62
|
-
if (
|
|
60
|
+
if (plugins.os.platform() === 'linux') {
|
|
63
61
|
this.shouldExecute = true;
|
|
64
62
|
} else {
|
|
65
63
|
console.log('Smartdaemon can only be used on Linux systems! Refusing to set up a service.');
|
|
@@ -18,7 +18,6 @@ import * as smartlogDestinationLocal from '@push.rocks/smartlog/destination-loca
|
|
|
18
18
|
import * as smartmarkdown from '@push.rocks/smartmarkdown/iso';
|
|
19
19
|
import * as smartpath from '@push.rocks/smartpath';
|
|
20
20
|
import * as smartshell from '@push.rocks/smartshell';
|
|
21
|
-
import * as smartsystem from '@push.rocks/smartsystem';
|
|
22
21
|
|
|
23
22
|
export {
|
|
24
23
|
lik,
|
|
@@ -28,7 +27,6 @@ export {
|
|
|
28
27
|
smartmarkdown,
|
|
29
28
|
smartpath,
|
|
30
29
|
smartshell,
|
|
31
|
-
smartsystem,
|
|
32
30
|
};
|
|
33
31
|
|
|
34
32
|
// third party
|
package/ts/systemd.definition.ts
CHANGED
|
@@ -28,6 +28,18 @@ export interface ISystemdServiceDependencies {
|
|
|
28
28
|
conflicts: readonly string[];
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Where one of the service's output streams goes (`StandardOutput=`, `StandardError=`): the
|
|
33
|
+
* journal, nowhere (`null`), or appended to a file (`append:`, systemd 240 and later). The
|
|
34
|
+
* service manager opens an `append` file with `O_APPEND` before it invokes the executable and
|
|
35
|
+
* before it drops to the service's `User=`, so with the manager's own privileges: root for the
|
|
36
|
+
* system manager, the user for a user manager. It creates a missing file owned by that user
|
|
37
|
+
* with mode 0666 masked by the unit's umask (0644 under the usual umask 0022); its directory must
|
|
38
|
+
* exist. A caller that needs another owner or a private mode creates the file first: an existing
|
|
39
|
+
* file keeps its owner and mode.
|
|
40
|
+
*/
|
|
41
|
+
export type TSystemdServiceOutput = 'journal' | 'null' | { readonly append: string };
|
|
42
|
+
|
|
31
43
|
/** Install metadata only; rendering and file installation do not enable these links. */
|
|
32
44
|
export interface ISystemdServiceInstallTargets {
|
|
33
45
|
wantedBy: readonly string[];
|
|
@@ -13,6 +13,9 @@ export interface ISystemdManagerVersion {
|
|
|
13
13
|
/** Internal: the systemd major that first reads `FileDescriptorStorePreserve=` and reports it. */
|
|
14
14
|
export const fileDescriptorStorePreserveMajor = 254;
|
|
15
15
|
|
|
16
|
+
/** Internal: the systemd major that first reads `append:` in `StandardOutput=` and `StandardError=`. */
|
|
17
|
+
export const outputAppendMajor = 240;
|
|
18
|
+
|
|
16
19
|
/** Internal: the systemd major whose `systemctl clean` first releases a file descriptor store (`--what=fdstore`). */
|
|
17
20
|
export const fileDescriptorStoreCleanMajor = 254;
|
|
18
21
|
|