@push.rocks/smartdaemon 2.7.2 → 2.9.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.
@@ -3,6 +3,16 @@ export interface ISystemdServiceStartup {
3
3
  remainAfterExit: boolean;
4
4
  timeoutStartSeconds: number;
5
5
  }
6
+ /**
7
+ * Ready when a process admitted by `notifyAccess` sends `READY=1`. `NotifyAccess=none` is not
8
+ * offered: systemd would discard every notification and the start could only time out.
9
+ */
10
+ export interface ISystemdServiceNotifyStartup {
11
+ type: 'notify';
12
+ notifyAccess: 'main' | 'exec' | 'all';
13
+ timeoutStartSeconds: number;
14
+ }
15
+ export type TSystemdServiceStartup = ISystemdServiceStartup | ISystemdServiceNotifyStartup;
6
16
  /** Complete literal relations. System services always retain After=local-fs.target. */
7
17
  export interface ISystemdServiceDependencies {
8
18
  defaultDependencies: boolean;
@@ -19,7 +29,7 @@ export interface ISystemdServiceInstallTargets {
19
29
  }
20
30
  /** Internal validated renderer; no systemd/process/filesystem effects. */
21
31
  export declare const renderServiceRelations: (options: {
22
- startup?: ISystemdServiceStartup;
32
+ startup?: TSystemdServiceStartup;
23
33
  dependencies?: ISystemdServiceDependencies;
24
34
  install?: ISystemdServiceInstallTargets;
25
35
  }, unitName: string, restart: string, scope: "system" | "user", fail: () => never) => {
@@ -3,9 +3,10 @@ import * as plugins from './smartdaemon.plugins.js';
3
3
  export const renderServiceRelations = (options, unitName, restart, scope, fail) => {
4
4
  const requireValue = (value) => { if (!value)
5
5
  fail(); };
6
+ const plain = (value) => requireValue(value !== null && typeof value === 'object'
7
+ && !plugins.util.types.isProxy(value) && Object.getPrototypeOf(value) === Object.prototype);
6
8
  const record = (value, keys) => {
7
- requireValue(value !== null && typeof value === 'object' && !plugins.util.types.isProxy(value)
8
- && Object.getPrototypeOf(value) === Object.prototype);
9
+ plain(value);
9
10
  const actual = Reflect.ownKeys(value);
10
11
  requireValue(actual.length === keys.length && actual.every(key => typeof key === 'string' && keys.includes(key)
11
12
  && Object.getOwnPropertyDescriptor(value, key)?.enumerable && 'value' in Object.getOwnPropertyDescriptor(value, key)));
@@ -34,10 +35,20 @@ export const renderServiceRelations = (options, unitName, restart, scope, fail)
34
35
  };
35
36
  if (options.startup !== undefined) {
36
37
  const value = options.startup;
37
- record(value, ['type', 'remainAfterExit', 'timeoutStartSeconds']);
38
- requireValue(value.type === 'oneshot' && typeof value.remainAfterExit === 'boolean' && restart !== 'always'
39
- && Number.isSafeInteger(value.timeoutStartSeconds) && value.timeoutStartSeconds >= 1 && value.timeoutStartSeconds <= 600);
40
- result.service = ['Type=oneshot', `RemainAfterExit=${value.remainAfterExit ? 'yes' : 'no'}`, `TimeoutStartSec=${value.timeoutStartSeconds}s`];
38
+ plain(value);
39
+ // Select the shape from the data descriptor so no accessor runs before record() refuses it.
40
+ const kind = Object.getOwnPropertyDescriptor(value, 'type');
41
+ record(value, kind !== undefined && 'value' in kind && kind.value === 'notify'
42
+ ? ['type', 'notifyAccess', 'timeoutStartSeconds'] : ['type', 'remainAfterExit', 'timeoutStartSeconds']);
43
+ requireValue(Number.isSafeInteger(value.timeoutStartSeconds) && value.timeoutStartSeconds >= 1 && value.timeoutStartSeconds <= 600);
44
+ if (value.type === 'notify') {
45
+ requireValue(['main', 'exec', 'all'].includes(value.notifyAccess));
46
+ result.service = ['Type=notify', `NotifyAccess=${value.notifyAccess}`, `TimeoutStartSec=${value.timeoutStartSeconds}s`];
47
+ }
48
+ else {
49
+ requireValue(value.type === 'oneshot' && typeof value.remainAfterExit === 'boolean' && restart !== 'always');
50
+ result.service = ['Type=oneshot', `RemainAfterExit=${value.remainAfterExit ? 'yes' : 'no'}`, `TimeoutStartSec=${value.timeoutStartSeconds}s`];
51
+ }
41
52
  }
42
53
  if (options.dependencies !== undefined) {
43
54
  const value = options.dependencies;
@@ -58,4 +69,4 @@ export const renderServiceRelations = (options, unitName, restart, scope, fail)
58
69
  }
59
70
  return result;
60
71
  };
61
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3lzdGVtZC5kZWZpbml0aW9uLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvc3lzdGVtZC5kZWZpbml0aW9uLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sMEJBQTBCLENBQUM7QUF3QnBELDBFQUEwRTtBQUMxRSxNQUFNLENBQUMsTUFBTSxzQkFBc0IsR0FBRyxDQUFDLE9BSXRDLEVBQUUsUUFBZ0IsRUFBRSxPQUFlLEVBQUUsS0FBd0IsRUFBRSxJQUFpQixFQUUvRSxFQUFFO0lBQ0YsTUFBTSxZQUFZLEdBQUcsQ0FBQyxLQUFjLEVBQVEsRUFBRSxHQUFHLElBQUksQ0FBQyxLQUFLO1FBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDdkUsTUFBTSxNQUFNLEdBQUcsQ0FBQyxLQUFjLEVBQUUsSUFBdUIsRUFBUSxFQUFFO1FBQy9ELFlBQVksQ0FBQyxLQUFLLEtBQUssSUFBSSxJQUFJLE9BQU8sS0FBSyxLQUFLLFFBQVEsSUFBSSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUM7ZUFDekYsTUFBTSxDQUFDLGNBQWMsQ0FBQyxLQUFLLENBQUMsS0FBSyxNQUFNLENBQUMsU0FBUyxDQUFDLENBQUM7UUFDeEQsTUFBTSxNQUFNLEdBQUcsT0FBTyxDQUFDLE9BQU8sQ0FBQyxLQUFlLENBQUMsQ0FBQztRQUNoRCxZQUFZLENBQUMsTUFBTSxDQUFDLE1BQU0sS0FBSyxJQUFJLENBQUMsTUFBTSxJQUFJLE1BQU0sQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQyxPQUFPLEdBQUcsS0FBSyxRQUFRLElBQUksSUFBSSxDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUM7ZUFDMUcsTUFBTSxDQUFDLHdCQUF3QixDQUFDLEtBQUssRUFBRSxHQUFHLENBQUMsRUFBRSxVQUFVLElBQUksT0FBTyxJQUFJLE1BQU0sQ0FBQyx3QkFBd0Is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
72
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3lzdGVtZC5kZWZpbml0aW9uLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvc3lzdGVtZC5kZWZpbml0aW9uLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sMEJBQTBCLENBQUM7QUFvQ3BELDBFQUEwRTtBQUMxRSxNQUFNLENBQUMsTUFBTSxzQkFBc0IsR0FBRyxDQUFDLE9BSXRDLEVBQUUsUUFBZ0IsRUFBRSxPQUFlLEVBQUUsS0FBd0IsRUFBRSxJQUFpQixFQUUvRSxFQUFFO0lBQ0YsTUFBTSxZQUFZLEdBQUcsQ0FBQyxLQUFjLEVBQVEsRUFBRSxHQUFHLElBQUksQ0FBQyxLQUFLO1FBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDdkUsTUFBTSxLQUFLLEdBQUcsQ0FBQyxLQUFjLEVBQVEsRUFBRSxDQUFDLFlBQVksQ0FBQyxLQUFLLEtBQUssSUFBSSxJQUFJLE9BQU8sS0FBSyxLQUFLLFFBQVE7V0FDM0YsQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDLElBQUksTUFBTSxDQUFDLGNBQWMsQ0FBQyxLQUFLLENBQUMsS0FBSyxNQUFNLENBQUMsU0FBUyxDQUFDLENBQUM7SUFDOUYsTUFBTSxNQUFNLEdBQUcsQ0FBQyxLQUFjLEVBQUUsSUFBdUIsRUFBUSxFQUFFO1FBQy9ELEtBQUssQ0FBQyxLQUFLLENBQUMsQ0FBQztRQUNiLE1BQU0sTUFBTSxHQUFHLE9BQU8sQ0FBQyxPQUFPLENBQUMsS0FBZSxDQUFDLENBQUM7UUFDaEQsWUFBWSxDQUFDLE1BQU0sQ0FBQyxNQUFNLEtBQUssSUFBSSxDQUFDLE1BQU0sSUFBSSxNQUFNLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUMsT0FBTyxHQUFHLEtBQUssUUFBUSxJQUFJLElBQUksQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDO2VBQzFHLE1BQU0sQ0FBQyx3QkFBd0IsQ0FBQyxLQUFLLEVBQUUsR0FBRyxDQUFDLEVBQUUsVUFBVSxJQUFJLE9BQU8sSUFBSSxNQUFNLENBQUMsd0JBQXdCLENBQUMsS0FBSyxFQUFFLEdBQUcsQ0FBRSxDQUFDLENBQUMsQ0FBQztJQUM1SCxDQUFDLENBQUM7SUFDRixNQUFNLEtBQUssR0FBRyxDQUFDLEtBQXdCLEVBQVksRUFBRTtRQUNuRCxZQUFZLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUM7ZUFDbEUsTUFBTSxDQUFDLGNBQWMsQ0FBQyxLQUFLLENBQUMsS0FBSyxLQUFLLENBQUMsU0FBUyxJQUFJLEtBQUssQ0FBQyxNQUFNLElBQUksRUFBRTtlQUN0RSxPQUFPLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFDLE1BQU0sS0FBSyxLQUFLLENBQUMsTUFBTSxHQUFHLENBQUMsQ0FBQyxDQUFDO1FBQ3pELE1BQU0sTUFBTSxHQUFhLEVBQUUsQ0FBQztRQUM1QixLQUFLLElBQUksS0FBSyxHQUFHLENBQUMsRUFBRSxLQUFLLEdBQUcsS0FBSyxDQUFDLE1BQU0sRUFBRSxLQUFLLEVBQUUsRUFBRSxDQUFDO1lBQ2xELE1BQU0sVUFBVSxHQUFHLE1BQU0sQ0FBQyx3QkFBd0IsQ0FBQyxLQUFLLEVBQUUsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUM7WUFDekUsWUFBWSxDQUFDLFVBQVUsRUFBRSxVQUFVLElBQUksT0FBTyxJQUFJLFVBQVUsQ0FBQyxDQUFDO1lBQzlELE1BQU0sSUFBSSxHQUFHLFVBQVcsQ0FBQyxLQUFLLENBQUM7WUFDL0IsWUFBWSxDQUFDLE9BQU8sSUFBSSxLQUFLLFFBQVEsSUFBSSxJQUFJLEtBQUssUUFBUSxJQUFJLElBQUksQ0FBQyxNQUFNLElBQUksR0FBRzttQkFDM0UsZ0hBQWdILENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUM7WUFDbEksTUFBTSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUNwQixDQUFDO1FBQ0QsWUFBWSxDQUFDLElBQUksR0FBRyxDQUFDLE1BQU0sQ0FBQyxDQUFDLElBQUksS0FBSyxNQUFNLENBQUMsTUFBTSxDQUFDLENBQUM7UUFDckQsT0FBTyxNQUFNLENBQUMsSUFBSSxFQUFFLENBQUM7SUFDdkIsQ0FBQyxDQUFDO0lBQ0YsTUFBTSxJQUFJLEdBQUcsQ0FBQyxHQUFXLEVBQUUsS0FBd0IsRUFBWSxFQUFFLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsQ0FBQyxHQUFHLEdBQUcsSUFBSSxLQUFLLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO0lBQ3BILE1BQU0sTUFBTSxHQUFHO1FBQ2IsSUFBSSxFQUFFLEtBQUssS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDLENBQUMsdUJBQXVCLENBQUMsQ0FBQyxDQUFDLENBQUMsRUFBYztRQUNyRSxPQUFPLEVBQUUsQ0FBQyxXQUFXLENBQUM7UUFDdEIsT0FBTyxFQUFFLENBQUMsS0FBSyxLQUFLLFFBQVEsQ0FBQyxDQUFDLENBQUMsNEJBQTRCLENBQUMsQ0FBQyxDQUFDLHlCQUF5QixDQUFDO0tBQ3pGLENBQUM7SUFDRixJQUFJLE9BQU8sQ0FBQyxPQUFPLEtBQUssU0FBUyxFQUFFLENBQUM7UUFDbEMsTUFBTSxLQUFLLEdBQUcsT0FBTyxDQUFDLE9BQU8sQ0FBQztRQUM5QixLQUFLLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDYiw0RkFBNEY7UUFDNUYsTUFBTSxJQUFJLEdBQUcsTUFBTSxDQUFDLHdCQUF3QixDQUFDLEtBQUssRUFBRSxNQUFNLENBQUMsQ0FBQztRQUM1RCxNQUFNLENBQUMsS0FBSyxFQUFFLElBQUksS0FBSyxTQUFTLElBQUksT0FBTyxJQUFJLElBQUksSUFBSSxJQUFJLENBQUMsS0FBSyxLQUFLLFFBQVE7WUFDNUUsQ0FBQyxDQUFDLENBQUMsTUFBTSxFQUFFLGNBQWMsRUFBRSxxQkFBcUIsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLE1BQU0sRUFBRSxpQkFBaUIsRUFBRSxxQkFBcUIsQ0FBQyxDQUFDLENBQUM7UUFDMUcsWUFBWSxDQUFDLE1BQU0sQ0FBQyxhQUFhLENBQUMsS0FBSyxDQUFDLG1CQUFtQixDQUFDLElBQUksS0FBSyxDQUFDLG1CQUFtQixJQUFJLENBQUMsSUFBSSxLQUFLLENBQUMsbUJBQW1CLElBQUksR0FBRyxDQUFDLENBQUM7UUFDcEksSUFBSSxLQUFLLENBQUMsSUFBSSxLQUFLLFFBQVEsRUFBRSxDQUFDO1lBQzVCLFlBQVksQ0FBQyxDQUFDLE1BQU0sRUFBRSxNQUFNLEVBQUUsS0FBSyxDQUFDLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxZQUFZLENBQUMsQ0FBQyxDQUFDO1lBQ25FLE1BQU0sQ0FBQyxPQUFPLEdBQUcsQ0FBQyxhQUFhLEVBQUUsZ0JBQWdCLEtBQUssQ0FBQyxZQUFZLEVBQUUsRUFBRSxtQkFBbUIsS0FBSyxDQUFDLG1CQUFtQixHQUFHLENBQUMsQ0FBQztRQUMxSCxDQUFDO2FBQU0sQ0FBQztZQUNOLFlBQVksQ0FBQyxLQUFLLENBQUMsSUFBSSxLQUFLLFNBQVMsSUFBSSxPQUFPLEtBQUssQ0FBQyxlQUFlLEtBQUssU0FBUyxJQUFJLE9BQU8sS0FBSyxRQUFRLENBQUMsQ0FBQztZQUM3RyxNQUFNLENBQUMsT0FBTyxHQUFHLENBQUMsY0FBYyxFQUFFLG1CQUFtQixLQUFLLENBQUMsZUFBZSxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLElBQUksRUFBRSxFQUFFLG1CQUFtQixLQUFLLENBQUMsbUJBQW1CLEdBQUcsQ0FBQyxDQUFDO1FBQ2hKLENBQUM7SUFDSCxDQUFDO0lBQ0QsSUFBSSxPQUFPLENBQUMsWUFBWSxLQUFLLFNBQVMsRUFBRSxDQUFDO1FBQ3ZDLE1BQU0sS0FBSyxHQUFHLE9BQU8sQ0FBQyxZQUFZLENBQUM7UUFDbkMsTUFBTSxDQUFDLEtBQUssRUFBRSxDQUFDLHFCQUFxQixFQUFFLE9BQU8sRUFBRSxRQUFRLEVBQUUsVUFBVSxFQUFFLE9BQU8sRUFBRSxXQUFXLENBQUMsQ0FBQyxDQUFDO1FBQzVGLFlBQVksQ0FBQyxPQUFPLEtBQUssQ0FBQyxtQkFBbUIsS0FBSyxTQUFTLENBQUMsQ0FBQztRQUM3RCxNQUFNLEtBQUssR0FBRyxDQUFDLEdBQUcsSUFBSSxHQUFHLENBQUMsQ0FBQyxHQUFHLENBQUMsS0FBSyxLQUFLLFFBQVEsQ0FBQyxDQUFDLENBQUMsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsRUFBRSxHQUFHLEtBQUssQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxFQUFFLENBQUM7UUFDL0csTUFBTSxNQUFNLEdBQUcsS0FBSyxDQUFDLEtBQUssQ0FBQyxNQUFNLENBQUMsRUFBRSxRQUFRLEdBQUcsS0FBSyxDQUFDLEtBQUssQ0FBQyxRQUFRLENBQUMsRUFBRSxLQUFLLEdBQUcsS0FBSyxDQUFDLEtBQUssQ0FBQyxLQUFLLENBQUMsRUFBRSxTQUFTLEdBQUcsS0FBSyxDQUFDLEtBQUssQ0FBQyxTQUFTLENBQUMsQ0FBQztRQUNySSxZQUFZLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLENBQUMsS0FBSyxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsQ0FBQyxJQUFJLFNBQVMsQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxRQUFRLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQztRQUN4SSxNQUFNLENBQUMsSUFBSSxHQUFHLENBQUMsdUJBQXVCLEtBQUssQ0FBQyxtQkFBbUIsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxJQUFJLEVBQUUsRUFBRSxHQUFHLElBQUksQ0FBQyxPQUFPLEVBQUUsS0FBSyxDQUFDO1lBQ3ZHLEdBQUcsSUFBSSxDQUFDLFFBQVEsRUFBRSxNQUFNLENBQUMsRUFBRSxHQUFHLElBQUksQ0FBQyxVQUFVLEVBQUUsUUFBUSxDQUFDLEVBQUUsR0FBRyxJQUFJLENBQUMsT0FBTyxFQUFFLEtBQUssQ0FBQyxFQUFFLEdBQUcsSUFBSSxDQUFDLFdBQVcsRUFBRSxTQUFTLENBQUMsQ0FBQyxDQUFDO0lBQ3hILENBQUM7SUFDRCxJQUFJLE9BQU8sQ0FBQyxPQUFPLEtBQUssU0FBUyxFQUFFLENBQUM7UUFDbEMsTUFBTSxLQUFLLEdBQUcsT0FBTyxDQUFDLE9BQU8sQ0FBQztRQUM5QixNQUFNLENBQUMsS0FBSyxFQUFFLENBQUMsVUFBVSxFQUFFLFlBQVksQ0FBQyxDQUFDLENBQUM7UUFDMUMsTUFBTSxNQUFNLEdBQUcsS0FBSyxDQUFDLEtBQUssQ0FBQyxRQUFRLENBQUMsRUFBRSxRQUFRLEdBQUcsS0FBSyxDQUFDLEtBQUssQ0FBQyxVQUFVLENBQUMsQ0FBQztRQUN6RSxZQUFZLENBQUMsTUFBTSxDQUFDLE1BQU0sR0FBRyxRQUFRLENBQUMsTUFBTSxHQUFHLENBQUMsSUFBSSxNQUFNLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsQ0FBQyxRQUFRLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQztRQUNwRyxNQUFNLENBQUMsT0FBTyxHQUFHLENBQUMsR0FBRyxJQUFJLENBQUMsVUFBVSxFQUFFLE1BQU0sQ0FBQyxFQUFFLEdBQUcsSUFBSSxDQUFDLFlBQVksRUFBRSxRQUFRLENBQUMsQ0FBQyxDQUFDO0lBQ2xGLENBQUM7SUFDRCxPQUFPLE1BQU0sQ0FBQztBQUNoQixDQUFDLENBQUMifQ==
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartdaemon",
3
- "version": "2.7.2",
3
+ "version": "2.9.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": "^5.0.0",
30
- "@git.zone/tsrun": "^3.0.0",
31
- "@git.zone/tstest": "^6.2.1",
32
- "@types/node": "^26.6.1"
30
+ "@git.zone/tsrun": "^3.0.1",
31
+ "@git.zone/tstest": "^6.3.2",
32
+ "@types/node": "^26.6.2"
33
33
  },
34
34
  "dependencies": {
35
35
  "@push.rocks/lik": "^6.4.1",
package/readme.md CHANGED
@@ -212,6 +212,23 @@ list and `needsReload` flag. `reloadConfiguration()` explicitly reloads the mana
212
212
  unit definitions globally and returns a fresh configuration snapshot. Neither
213
213
  operation changes enablement or masks.
214
214
 
215
+ `inspectServiceSettings()` reports the loaded service settings a definition renders, drop-ins
216
+ included: `type`, `notifyAccess`, `restart`, `killMode` (systemd's own words; `type` is empty
217
+ for a unit systemd could not load), `delegate`, `restartMicroseconds` and
218
+ `timeoutStopMicroseconds` (exact integers or `'infinity'`), `limitNoFile` and `limitNoFileSoft`
219
+ (hard and soft), `tasksMax` (integers or `'infinity'`) and `oomScoreAdjust`. A missing, repeated,
220
+ foreign or malformed property rejects with `invalid_state`, so a manager too old to report one of
221
+ them fails closed. Reload changed files first; the snapshot describes what the manager loaded.
222
+
223
+ ```typescript
224
+ const settings = await new SystemdUnit({ unitName: 'pallet-containerd.service' }).inspectServiceSettings();
225
+ const carriesContract = settings.type === 'notify' && settings.notifyAccess === 'all' &&
226
+ settings.killMode === 'process' && settings.delegate && settings.limitNoFile === 'infinity' &&
227
+ settings.limitNoFileSoft === 'infinity' && settings.tasksMax === 'infinity' &&
228
+ settings.oomScoreAdjust === -999 && settings.restart === 'always' &&
229
+ settings.restartMicroseconds === 2_000_000 && settings.timeoutStopMicroseconds === 60_000_000;
230
+ ```
231
+
215
232
  `enable()` persistently enables the existing unit definition and returns a fresh
216
233
  state snapshot. It does not rewrite the definition, start the unit, unmask it or
217
234
  force conflicting links. Run it inside the owning installer's cross-process
@@ -315,12 +332,100 @@ a login session:
315
332
  const installed = await new SystemdUnitFile({ scope: 'user', unitName: 'authswitch.service' }).inspect();
316
333
  ```
317
334
 
335
+ `inspect()` and `install()` check every component of the unit directory from `/`
336
+ down, then the unit file itself. A refusal is a `SystemdUnitFileUnsafePathError`, a
337
+ `SystemdUnitFileError` with code `unsafe_path` that names the offending `path` and a
338
+ `reason` of type `TSystemdUnitFileUnsafePathReason`, so the owner knows exactly what
339
+ to fix:
340
+
341
+ | `reason` | `path` | Refused because |
342
+ | --- | --- | --- |
343
+ | `unsupported_platform` | unit directory | the process does not run on Linux |
344
+ | `foreign_process` | unit directory | the process does not run as the expected uid |
345
+ | `symlink` | directory component or unit file | it is a symbolic link |
346
+ | `not_directory` | directory component | it is not a directory |
347
+ | `foreign_owner` | directory component or unit file | a directory is owned by neither root nor the expected uid, or the file not by the expected uid |
348
+ | `group_or_world_writable` | directory component | it is group- or world-writable (only a root-owned mode-1777 `/tmp` is admitted) |
349
+ | `unreadable` | unit file | it cannot be opened; the operating-system error is the `cause` |
350
+ | `not_regular_file` | unit file | it is not a regular file |
351
+ | `hard_linked` | unit file | it has more than one hard link |
352
+ | `wrong_mode` | unit file | its mode is not exactly 0644 |
353
+ | `too_large` | unit file | it is larger than 16384 bytes |
354
+ | `replaced` | unit directory or a file being written | it changed identity between two checks during `install()` |
355
+
356
+ ```typescript
357
+ import { SystemdUnitFileUnsafePathError } from '@push.rocks/smartdaemon';
358
+
359
+ try {
360
+ await file.inspect();
361
+ } catch (error) {
362
+ if (error instanceof SystemdUnitFileUnsafePathError) {
363
+ // e.g. 'group_or_world_writable' '/home/user/.local' after `chmod 0775 ~/.local`
364
+ console.error(error.reason, error.path);
365
+ }
366
+ throw error;
367
+ }
368
+ ```
369
+
318
370
  For a boot prerequisite that must finish before dependent services start, supply
319
371
  `startup: { type: 'oneshot', remainAfterExit: true, timeoutStartSeconds: 120 }`.
320
372
  Systemd waits for successful process exit; `RemainAfterExit` keeps the completed
321
373
  unit active. The start timeout is an integer from 1 to 600 seconds. Oneshot
322
374
  services reject `restart: 'always'`; the default exec definition remains unchanged.
323
375
 
376
+ A long-running service that reports its own readiness uses
377
+ `startup: { type: 'notify', notifyAccess: 'all', timeoutStartSeconds: 120 }`, rendered as
378
+ `Type=notify`, `NotifyAccess=` and `TimeoutStartSec=`. Systemd counts the start complete when a
379
+ process admitted by `notifyAccess` sends `READY=1`: `'main'` admits only the main process,
380
+ `'exec'` also the processes of the unit's `Exec*=` commands, `'all'` every process in the unit's
381
+ cgroup, as a ready notification from a child of the main process needs. `NotifyAccess=none` is
382
+ not offered because it discards every notification. Notify services may use `restart: 'always'`.
383
+
384
+ These optional settings render one directive each, after `TimeoutStopSec=` in this order, and
385
+ nothing when omitted, so existing definitions keep their exact bytes:
386
+
387
+ | Option | Directive | Accepted values |
388
+ | --- | --- | --- |
389
+ | `restartSeconds` | `RestartSec=` (after `Restart=`) | integer 1–600; refused with `restart: 'no'` |
390
+ | `delegate` | `Delegate=` | boolean |
391
+ | `limitNoFile` | `LimitNOFILE=` (soft and hard) | positive integer or `'infinity'` |
392
+ | `tasksMax` | `TasksMax=` | positive integer or `'infinity'` |
393
+ | `oomScoreAdjust` | `OOMScoreAdjust=` | integer −1000–1000 |
394
+
395
+ ```typescript
396
+ const containerd = new SystemdServiceDefinition({
397
+ unitName: 'pallet-containerd.service',
398
+ description: 'Pallet containerd',
399
+ executable: '/opt/pallet/current/pallet-control',
400
+ args: ['containerd-serve'],
401
+ workingDirectory: '/',
402
+ user: 'root',
403
+ group: 'root',
404
+ restart: 'always',
405
+ restartSeconds: 2,
406
+ killMode: 'process',
407
+ timeoutStopSeconds: 60,
408
+ startup: { type: 'notify', notifyAccess: 'all', timeoutStartSeconds: 120 },
409
+ dependencies: {
410
+ defaultDependencies: true,
411
+ after: ['pallet-guard.service'],
412
+ before: ['pallet-node.service'],
413
+ requires: ['pallet-guard.service'],
414
+ wants: [],
415
+ conflicts: [],
416
+ },
417
+ delegate: true,
418
+ limitNoFile: 'infinity',
419
+ tasksMax: 'infinity',
420
+ oomScoreAdjust: -999,
421
+ });
422
+ ```
423
+
424
+ Systemd applies these within what the manager itself may grant. Under systemd 255 in an
425
+ unprivileged container, this unit started with the manager's own open-file hard limit for
426
+ `LimitNOFILE=infinity` and without the lowered OOM score, and still reported `active`.
427
+ `inspectServiceSettings()` reports the configured values, not the running process's limits.
428
+
324
429
  Optional `dependencies` describes the complete literal runtime relationships:
325
430
 
326
431
  ```typescript
@@ -407,7 +512,8 @@ as `<unit>.previous-<sha256>`, flushes them before atomic replacement, reloads
407
512
  the manager and checks its selected fragment and final bytes. A reload failure
408
513
  leaves the new file and retained previous definition in place; recovery requires
409
514
  inspection, never an automatic rollback. Filesystem and definition errors use
410
- `SystemdUnitFileError` with a local code. Trusted qualification code can set
515
+ `SystemdUnitFileError` with a local code; every `unsafe_path` refusal is a
516
+ `SystemdUnitFileUnsafePathError` naming the offending `path` and its `reason`. Trusted qualification code can set
411
517
  `unitDirectory`, `expectedUid` and the inherited `SystemdUnit` command options;
412
518
  user scope requires `expectedUid` to match the caller's actual uid. The manager's
413
519
  selected fragment must match the installed file after reload, so an unrecognized
@@ -417,7 +523,9 @@ With `killMode: 'mixed'`, the main process owns graceful child shutdown and must
417
523
  remain alive until its children have joined. Systemd sends SIGTERM to the main
418
524
  process first and can send SIGKILL to the whole cgroup when the main process exits
419
525
  or the stop deadline expires. `control-group` sends the initial signal to the
420
- whole group. A successful systemd start still requires application readiness
526
+ whole group. `process` signals only the main process, on stop and on a stop timeout; every
527
+ other process in the unit survives, so the main process must own their shutdown or their
528
+ deliberate retention (as with `Delegate=yes` workloads). A successful systemd start still requires application readiness
421
529
  verification, and a completed stop alone does not prove database drain.
422
530
 
423
531
  ### SmartDaemonService Class
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartdaemon',
6
- version: '2.7.2',
6
+ version: '2.9.0',
7
7
  description: 'Start scripts as long running daemons and manage them.'
8
8
  }
@@ -1,7 +1,8 @@
1
1
  import * as plugins from './smartdaemon.plugins.js';
2
- import { renderServiceRelations, type ISystemdServiceStartup, type ISystemdServiceDependencies,
2
+ import { renderServiceRelations, type TSystemdServiceStartup, type ISystemdServiceDependencies,
3
3
  type ISystemdServiceInstallTargets } from './systemd.definition.js';
4
- export type { ISystemdServiceStartup, ISystemdServiceDependencies, ISystemdServiceInstallTargets } from './systemd.definition.js';
4
+ export type { ISystemdServiceStartup, ISystemdServiceNotifyStartup, TSystemdServiceStartup,
5
+ ISystemdServiceDependencies, ISystemdServiceInstallTargets } from './systemd.definition.js';
5
6
 
6
7
  interface ISystemdServiceDefinitionBaseOptions {
7
8
  unitName: string;
@@ -10,11 +11,21 @@ interface ISystemdServiceDefinitionBaseOptions {
10
11
  args: readonly string[];
11
12
  workingDirectory: string;
12
13
  restart: 'no' | 'on-failure' | 'always';
13
- killMode: 'mixed' | 'control-group';
14
+ killMode: 'mixed' | 'control-group' | 'process';
14
15
  timeoutStopSeconds: number;
15
- startup?: ISystemdServiceStartup;
16
+ startup?: TSystemdServiceStartup;
16
17
  dependencies?: ISystemdServiceDependencies;
17
18
  install?: ISystemdServiceInstallTargets;
19
+ /** Integer seconds from 1 to 600 before a restart (`RestartSec=`). Refused with `restart: 'no'`. */
20
+ restartSeconds?: number;
21
+ /** Leaves the cgroups of the processes this service starts to them (`Delegate=`). */
22
+ delegate?: boolean;
23
+ /** Soft and hard open-file limit (`LimitNOFILE=`): a positive integer or `'infinity'`. */
24
+ limitNoFile?: number | 'infinity';
25
+ /** Task limit of the unit's cgroup (`TasksMax=`): a positive integer or `'infinity'`. */
26
+ tasksMax?: number | 'infinity';
27
+ /** Integer from -1000 to 1000 (`OOMScoreAdjust=`). */
28
+ oomScoreAdjust?: number;
18
29
  }
19
30
 
20
31
  export type ISystemdServiceDefinitionOptions = ISystemdServiceDefinitionBaseOptions & (
@@ -67,8 +78,10 @@ export class SystemdServiceDefinition {
67
78
  'timeoutStopSeconds',
68
79
  ...(scope === 'system' ? ['user', 'group'] : []),
69
80
  ];
81
+ const optionalKeys = ['scope', 'startup', 'dependencies', 'install', 'restartSeconds', 'delegate',
82
+ 'limitNoFile', 'tasksMax', 'oomScoreAdjust'];
70
83
  requireValue(
71
- Reflect.ownKeys(options).every(key => typeof key === 'string' && [...keys, 'scope', 'startup', 'dependencies', 'install'].includes(key)
84
+ Reflect.ownKeys(options).every(key => typeof key === 'string' && [...keys, ...optionalKeys].includes(key)
72
85
  && Object.getOwnPropertyDescriptor(options, key)?.enumerable && 'value' in Object.getOwnPropertyDescriptor(options, key)!) && keys.every((key) => {
73
86
  const descriptor = Object.getOwnPropertyDescriptor(options, key);
74
87
  return descriptor && 'value' in descriptor;
@@ -108,10 +121,35 @@ export class SystemdServiceDefinition {
108
121
  }
109
122
  requireValue(
110
123
  ['no', 'on-failure', 'always'].includes(options.restart) &&
111
- ['mixed', 'control-group'].includes(options.killMode) &&
124
+ ['mixed', 'control-group', 'process'].includes(options.killMode) &&
112
125
  Number.isSafeInteger(options.timeoutStopSeconds) &&
113
126
  options.timeoutStopSeconds >= 1 && options.timeoutStopSeconds <= 600,
114
127
  );
128
+ const integer = (value: unknown, minimum: number, maximum: number): value is number =>
129
+ Number.isSafeInteger(value) && (value as number) >= minimum && (value as number) <= maximum;
130
+ const limit = (value: unknown): value is number | 'infinity' =>
131
+ value === 'infinity' || integer(value, 1, Number.MAX_SAFE_INTEGER);
132
+ // Each optional directive is absent unless supplied, so existing definitions keep their bytes.
133
+ const service: string[] = [];
134
+ if (options.restartSeconds !== undefined) {
135
+ requireValue(integer(options.restartSeconds, 1, 600) && options.restart !== 'no');
136
+ }
137
+ if (options.delegate !== undefined) {
138
+ requireValue(typeof options.delegate === 'boolean');
139
+ service.push(`Delegate=${options.delegate ? 'yes' : 'no'}`);
140
+ }
141
+ if (options.limitNoFile !== undefined) {
142
+ requireValue(limit(options.limitNoFile));
143
+ service.push(`LimitNOFILE=${options.limitNoFile}`);
144
+ }
145
+ if (options.tasksMax !== undefined) {
146
+ requireValue(limit(options.tasksMax));
147
+ service.push(`TasksMax=${options.tasksMax}`);
148
+ }
149
+ if (options.oomScoreAdjust !== undefined) {
150
+ requireValue(integer(options.oomScoreAdjust, -1000, 1000));
151
+ service.push(`OOMScoreAdjust=${options.oomScoreAdjust}`);
152
+ }
115
153
  // Quoting and percent/dollar escaping follow systemd.syntax and systemd.service.
116
154
  const quote = (value: string, command = false) =>
117
155
  '"' + value
@@ -135,12 +173,14 @@ export class SystemdServiceDefinition {
135
173
  // WorkingDirectory is a raw path value, not an ExecStart word list.
136
174
  `WorkingDirectory=${options.workingDirectory.replaceAll('%', '%%')}`,
137
175
  `Restart=${options.restart}`,
176
+ ...(options.restartSeconds !== undefined ? [`RestartSec=${options.restartSeconds}s`] : []),
138
177
  `KillMode=${options.killMode}`,
139
178
  'KillSignal=SIGTERM',
140
179
  'FinalKillSignal=SIGKILL',
141
180
  'SendSIGKILL=yes',
142
181
  'SendSIGHUP=no',
143
182
  `TimeoutStopSec=${options.timeoutStopSeconds}s`,
183
+ ...service,
144
184
  'StandardInput=null',
145
185
  'StandardOutput=journal',
146
186
  'StandardError=journal',
@@ -56,6 +56,29 @@ export interface ISystemdUnitRelationships {
56
56
  readonly wantedBy: readonly string[];
57
57
  }
58
58
 
59
+ /**
60
+ * Service settings as the manager loaded them, drop-ins included. Timespans are exact integer
61
+ * microseconds; a limit systemd reports as unbounded is `'infinity'`. Mode strings are systemd's
62
+ * own vocabulary, so a value this package does not render is still reported; `type` is empty for
63
+ * a unit systemd could not load.
64
+ */
65
+ export interface ISystemdServiceSettings {
66
+ readonly unitName: string;
67
+ readonly type: string;
68
+ readonly notifyAccess: string;
69
+ readonly restart: string;
70
+ readonly restartMicroseconds: number | 'infinity';
71
+ readonly killMode: string;
72
+ readonly timeoutStopMicroseconds: number | 'infinity';
73
+ readonly delegate: boolean;
74
+ /** Hard open-file limit (`LimitNOFILE`). */
75
+ readonly limitNoFile: number | 'infinity';
76
+ /** Soft open-file limit (`LimitNOFILESoft`). */
77
+ readonly limitNoFileSoft: number | 'infinity';
78
+ readonly tasksMax: number | 'infinity';
79
+ readonly oomScoreAdjust: number;
80
+ }
81
+
59
82
  export class SystemdUnitError extends Error {
60
83
  constructor(public readonly code: 'invalid_options' | 'command_failed' | 'invalid_state') {
61
84
  super(`Systemd unit operation failed: ${code}.`);
@@ -63,6 +86,37 @@ export class SystemdUnitError extends Error {
63
86
  }
64
87
  }
65
88
 
89
+ // systemd's format_timespan() units, largest first; a year and a month are 365.25 and 30.44 days.
90
+ const timespanUnits = new Map<string, bigint>([
91
+ ['y', 31557600000000n], ['month', 2629800000000n], ['w', 604800000000n], ['d', 86400000000n],
92
+ ['h', 3600000000n], ['min', 60000000n], ['s', 1000000n], ['ms', 1000n], ['us', 1n],
93
+ ]);
94
+
95
+ /** Parses `systemctl show` timespans such as `0`, `100ms`, `1min 30s`, `1.500000s` or `infinity`. */
96
+ const parseTimespan = (text: string): number | 'infinity' | undefined => {
97
+ if (text === 'infinity') return 'infinity';
98
+ if (text === '0') return 0;
99
+ const names = [...timespanUnits.keys()];
100
+ let total = 0n;
101
+ let previous = -1;
102
+ for (const part of text.split(' ')) {
103
+ const match = /^(0|[1-9]\d{0,19})(?:\.(\d{1,6}))?([a-z]+)$/.exec(part);
104
+ const index = match ? names.indexOf(match[3]) : -1;
105
+ if (!match || index <= previous) return undefined;
106
+ previous = index;
107
+ const scale = timespanUnits.get(match[3])!;
108
+ const fraction = match[2] === undefined ? 0n : BigInt(match[2].padEnd(6, '0')) * scale;
109
+ if (fraction % 1000000n !== 0n) return undefined;
110
+ total += BigInt(match[1]) * scale + fraction / 1000000n;
111
+ }
112
+ return total > 0n && total <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(total) : undefined;
113
+ };
114
+
115
+ /** `infinity` or a decimal unsigned integer within the safe integer range. */
116
+ const parseLimit = (text: string): number | 'infinity' | undefined =>
117
+ text === 'infinity' ? 'infinity'
118
+ : /^(0|[1-9]\d*)$/.test(text) && Number.isSafeInteger(Number(text)) ? Number(text) : undefined;
119
+
66
120
  const properties = [
67
121
  'Id',
68
122
  'LoadState',
@@ -217,6 +271,50 @@ export class SystemdUnit {
217
271
  });
218
272
  }
219
273
 
274
+ /**
275
+ * Reports the loaded service settings that a definition renders, so an owner can verify an
276
+ * installed unit carries them. Reads only; the manager must have reloaded changed files first.
277
+ */
278
+ public async inspectServiceSettings(): Promise<ISystemdServiceSettings> {
279
+ const keys = ['Id', 'Type', 'NotifyAccess', 'Restart', 'RestartUSec', 'KillMode', 'TimeoutStopUSec',
280
+ 'Delegate', 'LimitNOFILE', 'LimitNOFILESoft', 'TasksMax', 'OOMScoreAdjust'];
281
+ const stdout = await this.command(['show', this.unitName, `--property=${keys.join(',')}`, '--all']);
282
+ const fields = new Map<string, string>();
283
+ for (const line of (stdout.endsWith('\n') ? stdout.slice(0, -1) : stdout).split('\n')) {
284
+ const separator = line.indexOf('='), key = line.slice(0, separator);
285
+ if (separator < 1 || !keys.includes(key) || fields.has(key)) throw new SystemdUnitError('invalid_state');
286
+ fields.set(key, line.slice(separator + 1));
287
+ }
288
+ // An older manager omits a property it does not know; the count check refuses that snapshot.
289
+ if (fields.size !== keys.length || fields.get('Id') !== this.unitName) throw new SystemdUnitError('invalid_state');
290
+ const mode = (key: string, empty = false): string => {
291
+ const value = fields.get(key)!;
292
+ if (!(empty && value === '') && !/^[a-z][a-z-]{0,31}$/.test(value)) throw new SystemdUnitError('invalid_state');
293
+ return value;
294
+ };
295
+ const parsed = <T>(key: string, parse: (text: string) => T | undefined): T => {
296
+ const value = parse(fields.get(key)!);
297
+ if (value === undefined) throw new SystemdUnitError('invalid_state');
298
+ return value;
299
+ };
300
+ const oomScoreAdjust = parsed('OOMScoreAdjust', (text) =>
301
+ /^(0|-?[1-9]\d{0,3})$/.test(text) && Math.abs(Number(text)) <= 1000 ? Number(text) : undefined);
302
+ return Object.freeze({
303
+ unitName: this.unitName,
304
+ type: mode('Type', true),
305
+ notifyAccess: mode('NotifyAccess'),
306
+ restart: mode('Restart'),
307
+ restartMicroseconds: parsed('RestartUSec', parseTimespan),
308
+ killMode: mode('KillMode'),
309
+ timeoutStopMicroseconds: parsed('TimeoutStopUSec', parseTimespan),
310
+ delegate: parsed('Delegate', (text) => text === 'yes' ? true : text === 'no' ? false : undefined),
311
+ limitNoFile: parsed('LimitNOFILE', parseLimit),
312
+ limitNoFileSoft: parsed('LimitNOFILESoft', parseLimit),
313
+ tasksMax: parsed('TasksMax', parseLimit),
314
+ oomScoreAdjust,
315
+ });
316
+ }
317
+
220
318
  /** Reloads unit definitions globally; does not enable, unmask, start or stop a unit. */
221
319
  public async reloadConfiguration(): Promise<ISystemdUnitConfigurationState> {
222
320
  await this.command(['daemon-reload']);