@push.rocks/smartdaemon 2.3.1 → 2.4.1

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 CHANGED
@@ -20,7 +20,6 @@
20
20
  },
21
21
  "release": {
22
22
  "preflight": {
23
- "requireCleanTree": true,
24
23
  "test": true,
25
24
  "build": true
26
25
  },
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartdaemon',
6
- version: '2.3.1',
6
+ version: '2.4.1',
7
7
  description: 'Start scripts as long running daemons and manage them.'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSx5QkFBeUI7SUFDL0IsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLHdEQUF3RDtDQUN0RSxDQUFBIn0=
@@ -0,0 +1,22 @@
1
+ export interface ISystemdServiceDefinitionOptions {
2
+ unitName: string;
3
+ description: string;
4
+ executable: string;
5
+ args: readonly string[];
6
+ workingDirectory: string;
7
+ user: string;
8
+ group: string;
9
+ restart: 'no' | 'on-failure' | 'always';
10
+ killMode: 'mixed' | 'control-group';
11
+ timeoutStopSeconds: number;
12
+ }
13
+ export declare class SystemdServiceDefinitionError extends Error {
14
+ constructor();
15
+ }
16
+ /** Literal direct-execution definition; never introduces a shell or environment expansion. */
17
+ export declare class SystemdServiceDefinition {
18
+ readonly unitName: string;
19
+ readonly content: string;
20
+ readonly sha256: string;
21
+ constructor(options: ISystemdServiceDefinitionOptions);
22
+ }
@@ -0,0 +1,93 @@
1
+ import * as plugins from './smartdaemon.plugins.js';
2
+ export class SystemdServiceDefinitionError extends Error {
3
+ constructor() {
4
+ super('Invalid direct systemd service definition.');
5
+ this.name = 'SystemdServiceDefinitionError';
6
+ }
7
+ }
8
+ /** Literal direct-execution definition; never introduces a shell or environment expansion. */
9
+ export class SystemdServiceDefinition {
10
+ constructor(options) {
11
+ const requireValue = (value) => {
12
+ if (!value)
13
+ throw new SystemdServiceDefinitionError();
14
+ };
15
+ const text = (value, maximum = 4096) => typeof value === 'string' && value.length <= maximum && !/[\x00-\x1f\x7f]/.test(value) &&
16
+ !/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(value);
17
+ const absolute = (value) => text(value) && plugins.path.isAbsolute(value) &&
18
+ plugins.path.normalize(value) === value;
19
+ const keys = [
20
+ 'unitName',
21
+ 'description',
22
+ 'executable',
23
+ 'args',
24
+ 'workingDirectory',
25
+ 'user',
26
+ 'group',
27
+ 'restart',
28
+ 'killMode',
29
+ 'timeoutStopSeconds',
30
+ ];
31
+ requireValue(options && Object.getPrototypeOf(options) === Object.prototype &&
32
+ Reflect.ownKeys(options).length === keys.length && keys.every((key) => {
33
+ const descriptor = Object.getOwnPropertyDescriptor(options, key);
34
+ return descriptor && 'value' in descriptor;
35
+ }));
36
+ requireValue(typeof options.unitName === 'string' &&
37
+ /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,230}\.service$/.test(options.unitName));
38
+ requireValue(text(options.description, 1024) && options.description.length > 0 &&
39
+ options.description.trim() === options.description && !options.description.includes('\\'));
40
+ requireValue(absolute(options.executable) && options.executable !== '/' &&
41
+ !/[\\"']/.test(options.executable) && !options.executable.endsWith('/') &&
42
+ absolute(options.workingDirectory) &&
43
+ options.workingDirectory.trim() === options.workingDirectory &&
44
+ !options.workingDirectory.includes('\\'));
45
+ requireValue(Array.isArray(options.args) && Object.getPrototypeOf(options.args) === Array.prototype &&
46
+ options.args.length <= 128 &&
47
+ Reflect.ownKeys(options.args).length === options.args.length + 1 &&
48
+ Array.from({ length: options.args.length }, (_, index) => Object.getOwnPropertyDescriptor(options.args, String(index)))
49
+ .every((descriptor) => descriptor && 'value' in descriptor && text(descriptor.value)));
50
+ requireValue([options.user, options.group].every((value) => typeof value === 'string' && /^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$/.test(value)));
51
+ requireValue(['no', 'on-failure', 'always'].includes(options.restart) &&
52
+ ['mixed', 'control-group'].includes(options.killMode) &&
53
+ Number.isSafeInteger(options.timeoutStopSeconds) &&
54
+ options.timeoutStopSeconds >= 1 && options.timeoutStopSeconds <= 600);
55
+ // Quoting and percent/dollar escaping follow systemd.syntax and systemd.service.
56
+ const quote = (value, command = false) => '"' + value
57
+ .replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('%', '%%')
58
+ .replaceAll('$', () => command ? '$$' : '$') +
59
+ '"';
60
+ this.unitName = options.unitName;
61
+ this.content = [
62
+ '[Unit]',
63
+ `Description=${options.description.replaceAll('%', '%%')}`,
64
+ 'After=local-fs.target',
65
+ '',
66
+ '[Service]',
67
+ 'Type=exec',
68
+ `User=${options.user}`,
69
+ `Group=${options.group}`,
70
+ `ExecStart=${[quote(options.executable), ...options.args.map((value) => quote(value, true))].join(' ')}`,
71
+ // WorkingDirectory is a raw path value, not an ExecStart word list.
72
+ `WorkingDirectory=${options.workingDirectory.replaceAll('%', '%%')}`,
73
+ `Restart=${options.restart}`,
74
+ `KillMode=${options.killMode}`,
75
+ 'KillSignal=SIGTERM',
76
+ 'FinalKillSignal=SIGKILL',
77
+ 'SendSIGKILL=yes',
78
+ 'SendSIGHUP=no',
79
+ `TimeoutStopSec=${options.timeoutStopSeconds}s`,
80
+ 'StandardInput=null',
81
+ 'StandardOutput=journal',
82
+ 'StandardError=journal',
83
+ '',
84
+ '[Install]',
85
+ 'WantedBy=multi-user.target',
86
+ '',
87
+ ].join('\n');
88
+ requireValue(Buffer.byteLength(this.content) <= 16384);
89
+ this.sha256 = plugins.crypto.createHash('sha256').update(this.content).digest('hex');
90
+ Object.freeze(this);
91
+ }
92
+ }
93
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5zeXN0ZW1kc2VydmljZWRlZmluaXRpb24uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9jbGFzc2VzLnN5c3RlbWRzZXJ2aWNlZGVmaW5pdGlvbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLDBCQUEwQixDQUFDO0FBZXBELE1BQU0sT0FBTyw2QkFBOEIsU0FBUSxLQUFLO0lBQ3REO1FBQ0UsS0FBSyxDQUFDLDRDQUE0QyxDQUFDLENBQUM7UUFDcEQsSUFBSSxDQUFDLElBQUksR0FBRywrQkFBK0IsQ0FBQztJQUM5QyxDQUFDO0NBQ0Y7QUFFRCw4RkFBOEY7QUFDOUYsTUFBTSxPQUFPLHdCQUF3QjtJQUtuQyxZQUFZLE9BQXlDO1FBQ25ELE1BQU0sWUFBWSxHQUFHLENBQUMsS0FBYyxFQUFRLEVBQUU7WUFDNUMsSUFBSSxDQUFDLEtBQUs7Z0JBQUUsTUFBTSxJQUFJLDZCQUE2QixFQUFFLENBQUM7UUFDeEQsQ0FBQyxDQUFDO1FBQ0YsTUFBTSxJQUFJLEdBQUcsQ0FBQyxLQUFjLEVBQUUsT0FBTyxHQUFHLElBQUksRUFBbUIsRUFBRSxDQUMvRCxPQUFPLEtBQUssS0FBSyxRQUFRLElBQUksS0FBSyxDQUFDLE1BQU0sSUFBSSxPQUFPLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDO1lBQ3RGLENBQUMseUVBQXlFLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQ3pGLE1BQU0sUUFBUSxHQUFHLENBQUMsS0FBYyxFQUFFLEVBQUUsQ0FDbEMsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLE9BQU8sQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLEtBQUssQ0FBQztZQUM3QyxPQUFPLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxLQUFLLENBQUMsS0FBSyxLQUFLLENBQUM7UUFDMUMsTUFBTSxJQUFJLEdBQUc7WUFDWCxVQUFVO1lBQ1YsYUFBYTtZ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
@@ -18,6 +18,13 @@ export interface ISystemdUnitState {
18
18
  /** Empty when no systemd job is pending; otherwise systemctl's job description. */
19
19
  readonly job: string;
20
20
  }
21
+ export interface ISystemdUnitConfigurationState {
22
+ readonly unitName: string;
23
+ readonly fragmentPath: string;
24
+ /** Opaque systemctl path list. An empty value proves no effective drop-ins. */
25
+ readonly dropInPaths: string;
26
+ readonly needsReload: boolean;
27
+ }
21
28
  export declare class SystemdUnitError extends Error {
22
29
  readonly code: 'invalid_options' | 'command_failed' | 'invalid_state';
23
30
  constructor(code: 'invalid_options' | 'command_failed' | 'invalid_state');
@@ -34,5 +41,9 @@ export declare class SystemdUnit {
34
41
  start(): Promise<ISystemdUnitState>;
35
42
  /** Await the systemd job, then inspect. The snapshot alone is not an application data-drain proof. */
36
43
  stop(): Promise<ISystemdUnitState>;
44
+ /** Reports the manager's selected definition without reading or changing its contents. */
45
+ inspectConfiguration(): Promise<ISystemdUnitConfigurationState>;
46
+ /** Reloads unit definitions globally; does not enable, unmask, start or stop a unit. */
47
+ reloadConfiguration(): Promise<ISystemdUnitConfigurationState>;
37
48
  private command;
38
49
  }
@@ -75,6 +75,34 @@ export class SystemdUnit {
75
75
  await this.command(['stop', this.unitName]);
76
76
  return this.inspect();
77
77
  }
78
+ /** Reports the manager's selected definition without reading or changing its contents. */
79
+ async inspectConfiguration() {
80
+ const keys = ['Id', 'FragmentPath', 'DropInPaths', 'NeedDaemonReload'];
81
+ const stdout = await this.command(['show', this.unitName, `--property=${keys.join(',')}`, '--all']);
82
+ const fields = new Map();
83
+ for (const line of stdout.trimEnd().split('\n')) {
84
+ const separator = line.indexOf('='), key = line.slice(0, separator);
85
+ if (separator < 1 || !keys.includes(key) || fields.has(key)) {
86
+ throw new SystemdUnitError('invalid_state');
87
+ }
88
+ fields.set(key, line.slice(separator + 1));
89
+ }
90
+ if (fields.size !== keys.length || fields.get('Id') !== this.unitName ||
91
+ !['yes', 'no'].includes(fields.get('NeedDaemonReload'))) {
92
+ throw new SystemdUnitError('invalid_state');
93
+ }
94
+ return Object.freeze({
95
+ unitName: this.unitName,
96
+ fragmentPath: fields.get('FragmentPath'),
97
+ dropInPaths: fields.get('DropInPaths'),
98
+ needsReload: fields.get('NeedDaemonReload') === 'yes',
99
+ });
100
+ }
101
+ /** Reloads unit definitions globally; does not enable, unmask, start or stop a unit. */
102
+ async reloadConfiguration() {
103
+ await this.command(['daemon-reload']);
104
+ return this.inspectConfiguration();
105
+ }
78
106
  command(argsArg) {
79
107
  return new Promise((resolve, reject) => {
80
108
  plugins.childProcess.execFile(this.executable, ['--system', '--no-ask-password', '--no-pager', ...argsArg], {
@@ -94,4 +122,4 @@ export class SystemdUnit {
94
122
  });
95
123
  }
96
124
  }
97
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5zeXN0ZW1kdW5pdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL2NsYXNzZXMuc3lzdGVtZHVuaXQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSwwQkFBMEIsQ0FBQztBQXdCcEQsTUFBTSxPQUFPLGdCQUFpQixTQUFRLEtBQUs7SUFDekMsWUFBNEIsSUFBNEQ7UUFDdEYsS0FBSyxDQUFDLGtDQUFrQyxJQUFJLEdBQUcsQ0FBQyxDQUFDO1FBRHZCLFNBQUksR0FBSixJQUFJLENBQXdEO1FBRXRGLElBQUksQ0FBQyxJQUFJLEdBQUcsa0JBQWtCLENBQUM7SUFDakMsQ0FBQztDQUNGO0FBRUQsTUFBTSxVQUFVLEdBQUc7SUFDakIsSUFBSTtJQUNKLFdBQVc7SUFDWCxhQUFhO0lBQ2IsVUFBVTtJQUNWLGVBQWU7SUFDZixTQUFTO0lBQ1QsWUFBWTtJQUNaLGNBQWM7SUFDZCxVQUFVO0lBQ1YsS0FBSztDQUNHLENBQUM7QUFFWCw2R0FBNkc7QUFDN0csTUFBTSxPQUFPLFdBQVc7SUFLdEIsWUFBWSxVQUErQjtRQUN6QyxJQUFJLENBQUMsUUFBUSxHQUFHLFVBQVUsRUFBRSxRQUFRLENBQUM7UUFDckMsSUFBSSxDQUFDLFVBQVUsR0FBRyxVQUFVLEVBQUUsYUFBYSxJQUFJLG9CQUFvQixDQUFDO1FBQ3BFLElBQUksQ0FBQyxPQUFPLEdBQUcsVUFBVSxFQUFFLGdCQUFnQixJQUFJLE1BQU0sQ0FBQztRQUN0RCxJQUNFLE9BQU8sSUFBSSxDQUFDLFFBQVEsS0FBSyxRQUFRO1lBQ2pDLENBQUMsK0NBQStDLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUM7WUFDcEUsT0FBTyxJQUFJLENBQUMsVUFBVSxLQUFLLFFBQVEsSUFBSSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUM7WUFDaEYsSUFBSSxDQUFDLFVBQVUsQ0FBQyxRQUFRLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsYUFBYSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUM7WUFDckUsSUFBSSxDQUFDLE9BQU8sR0FBRyxDQUFDLElBQUksSUFBSSxDQUFDLE9BQU8sR0FBRyxNQUFNO1lBQ3pDLE1BQU0sSUFBSSxnQkFBZ0IsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0lBQ2xELENBQUM7SUFFRCxvRkFBb0Y7SUFDN0UsS0FBSyxDQUFDLE9BQU87UUFDbEIsTUFBTSxNQUFNLEdBQUcsTUFBTSxJQUFJLENBQUMsT0FBTyxDQUFDO1lBQ2hDLE1BQU07WUFDTixJQUFJLENBQUMsUUFBUTtZQUNiLGNBQWMsVUFBVSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsRUFBRTtZQUNwQyxPQUFPO1NBQ1IsQ0FBQyxDQUFDO1FBQ0gsTUFBTSxNQUFNLEdBQUcsSUFBSSxHQUFHLEVBQWtCLENBQUM7UUFDekMsS0FBSyxNQUFNLElBQUksSUFBSSxNQUFNLENBQUMsT0FBTyxFQUFFLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUM7WUFDaEQsTUFBTSxTQUFTLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQztZQUNwQyxNQUFNLEdBQUcsR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxTQUFTLENBQUMsQ0FBQztZQUNyQyxJQUFJLFNBQVMsR0FBRyxDQUFDLElBQUksQ0FBRSxVQUFnQyxDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUMsSUFBSSxNQUFNLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUM7Z0JBQ3pGLE1BQU0sSUFBSSxnQkFBZ0IsQ0FBQyxlQUFlLENBQUMsQ0FBQztZQUM5QyxDQUFDO1lBQ0QsTUFBTSxDQUFDLEdBQUcsQ0FBQyxHQUFHLEVBQUUsSUFBSSxDQUFDLEtBQUssQ0FBQyxTQUFTLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQztRQUM3QyxDQUFDO1FBQ0QsSUFDRSxNQUFNLENBQUMsSUFBSSxLQUFLLFVBQVUsQ0FBQyxNQUFNLElBQUksTUFBTSxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsS0FBSyxJQUFJLENBQUMsUUFBUTtZQUN2RSxDQUFDLFdBQVcsRUFBRSxhQUFhLEVBQUUsVUFBVSxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsR0FBRyxFQUFFLEVBQUUsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLENBQUM7WUFDeEUsQ0FBQyxTQUFTLEVBQUUsWUFBWSxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsR0FBRyxFQUFFLEVBQUUsQ0FDckMsQ0FBQyxnQkFBZ0IsQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUUsQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLGFBQWEsQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQzNGO1lBQ0QsTUFBTSxJQUFJLGdCQUFnQixDQUFDLGVBQWUsQ0FBQyxDQUFDO1FBQzlDLE9BQU8sTUFBTSxDQUFDLE1BQU0sQ0FBQztZQUNuQixRQUFRLEVBQUUsSUFBSSxDQUFDLFFBQVE7WUFDdkIsU0FBUyxFQUFFLE1BQU0sQ0FBQyxHQUFHLENBQUMsV0FBVyxDQUFFO1lBQ25DLFdBQVcsRUFBRSxNQUFNLENBQUMsR0FBRyxDQUFDLGFBQWEsQ0FBRTtZQUN2QyxRQUFRLEVBQUUsTUFBTSxDQUFDLEdBQUcsQ0FBQyxVQUFVLENBQUU7WUFDakMsYUFBYSxFQUFFLE1BQU0sQ0FBQyxHQUFHLENBQUMsZUFBZSxDQUFFO1lBQzNDLE9BQU8sRUFBRSxNQUFNLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxTQUFTLENBQUMsQ0FBQztZQUN0QyxVQUFVLEVBQUUsTUFBTSxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsWUFBWSxDQUFDLENBQUM7WUFDNUMsWUFBWSxFQUFFLE1BQU0sQ0FBQyxHQUFHLENBQUMsY0FBYyxDQUFFO1lBQ3pDLFFBQVEsRUFBRSxNQUFNLENBQUMsR0FBRyxDQUFDLFVBQVUsQ0FBRTtZQUNqQyxHQUFHLEVBQUUsTUFBTSxDQUFDLEdBQUcsQ0FBQyxLQUFLLENBQUU7U0FDeEIsQ0FBQyxDQUFDO0lBQ0wsQ0FBQztJQUVELG9GQUFvRjtJQUM3RSxLQUFLLENBQUMsS0FBSztRQUNoQixNQUFNLElBQUksQ0FBQyxPQUFPLENBQUMsQ0FBQyxPQUFPLEVBQUUsSUFBSSxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUM7UUFDN0MsT0FBTyxJQUFJLENBQUMsT0FBTyxFQUFFLENBQUM7SUFDeEIsQ0FBQztJQUVELHNHQUFzRztJQUMvRixLQUFLLENBQUMsSUFBSTtRQUNmLE1BQU0sSUFBSSxDQUFDLE9BQU8sQ0FBQyxDQUFDLE1BQU0sRUFBRSxJQUFJLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQztRQUM1QyxPQUFPLElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQztJQUN4QixDQUFDO0lBRU8sT0FBTyxDQUFDLE9BQWlCO1FBQy9CLE9BQU8sSUFBSSxPQUFPLENBQUMsQ0FBQyxPQUFPLEVBQUUsTUFBTSxFQUFFLEVBQUU7WUFDckMsT0FBTyxDQUFDLFlBQVksQ0FBQyxRQUFRLENBQzNCLElBQUksQ0FBQyxVQUFVLEVBQ2YsQ0FBQyxVQUFVLEVBQUUsbUJBQW1CLEVBQUUsWUFBWSxFQUFFLEdBQUcsT0FBTyxDQUFDLEVBQzNEO2dCQUNFLFFBQVEsRUFBRSxNQUFNO2dCQUNoQixPQUFPLEVBQUUsSUFBSSxDQUFDLE9BQU87Z0JBQ3JCLFVBQVUsRUFBRSxTQUFTO2dCQUNyQixTQUFTLEVBQUUsS0FBSztnQkFDaEIsV0FBVyxFQUFFLElBQUk7Z0JBQ2pCLEdBQUcsRUFBRSxFQUFFLElBQUksRUFBRSxlQUFlLEVBQUUsSUFBSSxFQUFFLEdBQUcsRUFBRSxNQUFNLEVBQUUsR0FBRyxFQUFFO2FBQ3ZELEVBQ0QsQ0FBQyxLQUFLLEVBQUUsTUFBTSxFQUFFLEVBQUU7Z0JBQ2hCLDBGQUEwRjtnQkFDMUYsSUFBSSxLQUFLO29CQUFFLE1BQU0sQ0FBQyxJQUFJLGdCQUFnQixDQUFDLGdCQUFnQixDQUFDLENBQUMsQ0FBQzs7b0JBQ3JELE9BQU8sQ0FBQyxNQUFNLENBQUMsQ0FBQztZQUN2QixDQUFDLENBQ0YsQ0FBQztRQUNKLENBQUMsQ0FBQyxDQUFDO0lBQ0wsQ0FBQztDQUNGIn0=
125
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5zeXN0ZW1kdW5pdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL2NsYXNzZXMuc3lzdGVtZHVuaXQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSwwQkFBMEIsQ0FBQztBQWdDcEQsTUFBTSxPQUFPLGdCQUFpQixTQUFRLEtBQUs7SUFDekMsWUFBNEIsSUFBNEQ7UUFDdEYsS0FBSyxDQUFDLGtDQUFrQyxJQUFJLEdBQUcsQ0FBQyxDQUFDO1FBRHZCLFNBQUksR0FBSixJQUFJLENBQXdEO1FBRXRGLElBQUksQ0FBQyxJQUFJLEdBQUcsa0JBQWtCLENBQUM7SUFDakMsQ0FBQztDQUNGO0FBRUQsTUFBTSxVQUFVLEdBQUc7SUFDakIsSUFBSTtJQUNKLFdBQVc7SUFDWCxhQUFhO0lBQ2IsVUFBVTtJQUNWLGVBQWU7SUFDZixTQUFTO0lBQ1QsWUFBWTtJQUNaLGNBQWM7SUFDZCxVQUFVO0lBQ1YsS0FBSztDQUNHLENBQUM7QUFFWCw2R0FBNkc7QUFDN0csTUFBTSxPQUFPLFdBQVc7SUFLdEIsWUFBWSxVQUErQjtRQUN6QyxJQUFJLENBQUMsUUFBUSxHQUFHLFVBQVUsRUFBRSxRQUFRLENBQUM7UUFDckMsSUFBSSxDQUFDLFVBQVUsR0FBRyxVQUFVLEVBQUUsYUFBYSxJQUFJLG9CQUFvQixDQUFDO1FBQ3BFLElBQUksQ0FBQyxPQUFPLEdBQUcsVUFBVSxFQUFFLGdCQUFnQixJQUFJLE1BQU0sQ0FBQztRQUN0RCxJQUNFLE9BQU8sSUFBSSxDQUFDLFFBQVEsS0FBSyxRQUFRO1lBQ2pDLENBQUMsK0NBQStDLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUM7WUFDcEUsT0FBTyxJQUFJLENBQUMsVUFBVSxLQUFLLFFBQVEsSUFBSSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUM7WUFDaEYsSUFBSSxDQUFDLFVBQVUsQ0FBQyxR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
@@ -0,0 +1,45 @@
1
+ import { SystemdServiceDefinition } from './classes.systemdservicedefinition.js';
2
+ import { type ISystemdUnitOptions } from './classes.systemdunit.js';
3
+ export interface ISystemdUnitFileOptions extends ISystemdUnitOptions {
4
+ /** Local vendor definitions can be installed without removing an administrator's mask. */
5
+ unitDirectory?: string;
6
+ /** Trusted owning-code qualification only. Defaults to root. */
7
+ expectedUid?: number;
8
+ }
9
+ export interface ISystemdUnitFileRecord {
10
+ readonly path: string;
11
+ readonly sha256: string;
12
+ }
13
+ export declare class SystemdUnitFileError extends Error {
14
+ readonly code: 'invalid_options' | 'unsafe_path' | 'busy' | 'changed' | 'unsafe_unit' | 'write_failed' | 'reload_failed';
15
+ constructor(code: 'invalid_options' | 'unsafe_path' | 'busy' | 'changed' | 'unsafe_unit' | 'write_failed' | 'reload_failed');
16
+ }
17
+ /**
18
+ * Protected code/config installation, inside the caller's cross-process installation ownership scope.
19
+ * Other privileged writers must be serialized by that owner. Never changes enablement or masks.
20
+ */
21
+ export declare class SystemdUnitFile {
22
+ readonly path: string;
23
+ private readonly directory;
24
+ private readonly uid;
25
+ private readonly unit;
26
+ private readonly name;
27
+ private busy;
28
+ constructor(options: ISystemdUnitFileOptions);
29
+ /** Reads only the exact protected definition; no systemd or filesystem mutation. */
30
+ inspect(): Promise<ISystemdUnitFileRecord | null>;
31
+ /**
32
+ * Expected previous digest is mandatory; null admits an absent file. Exact desired bytes are idempotent.
33
+ * Retains changed definitions beside the unit before replacement. A failed reload never rolls code back.
34
+ */
35
+ install(definition: SystemdServiceDefinition, expectedSha256: string | null): Promise<Readonly<{
36
+ changed: boolean;
37
+ record: ISystemdUnitFileRecord;
38
+ masked: boolean;
39
+ }>>;
40
+ private admit;
41
+ private directories;
42
+ private read;
43
+ private create;
44
+ private flushDirectory;
45
+ }
@@ -0,0 +1,249 @@
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
+ const beforeConfiguration = 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
+ const recheckedConfiguration = await this.admit(rechecked);
92
+ requireValue(recheckedConfiguration.fragmentPath === beforeConfiguration.fragmentPath, 'changed');
93
+ await plugins.fs.rename(temporary, this.path);
94
+ temporary = undefined;
95
+ await this.flushDirectory(parent);
96
+ }
97
+ try {
98
+ const reloadedConfiguration = await this.unit.reloadConfiguration();
99
+ const after = await this.unit.inspect();
100
+ requireValue(quiet(after) &&
101
+ (before.loadState === 'not-found'
102
+ ? after.loadState === 'loaded' && after.unitFileState === 'disabled'
103
+ : after.loadState === before.loadState &&
104
+ after.unitFileState === before.unitFileState), 'changed');
105
+ const configuration = await this.unit.inspectConfiguration();
106
+ requireValue([reloadedConfiguration, configuration].every((snapshot) => !snapshot.needsReload && snapshot.dropInPaths === '' &&
107
+ snapshot.fragmentPath ===
108
+ (masked(after) ? beforeConfiguration.fragmentPath : this.path)), 'unsafe_unit');
109
+ const installed = await this.inspect();
110
+ requireValue(installed?.sha256 === definition.sha256, 'changed');
111
+ return Object.freeze({ changed, record: installed, masked: masked(after) });
112
+ }
113
+ catch {
114
+ throw new SystemdUnitFileError('reload_failed');
115
+ }
116
+ }
117
+ catch (error) {
118
+ if (error instanceof SystemdUnitFileError)
119
+ throw error;
120
+ throw new SystemdUnitFileError('write_failed');
121
+ }
122
+ finally {
123
+ try {
124
+ if (temporary && temporaryInfo) {
125
+ const named = await plugins.fs.lstat(temporary);
126
+ requireValue(sameFile(temporaryInfo, named), 'unsafe_path');
127
+ await plugins.fs.unlink(temporary);
128
+ }
129
+ }
130
+ finally {
131
+ this.busy = false;
132
+ }
133
+ }
134
+ }
135
+ async admit(state) {
136
+ requireValue(quiet(state) && (masked(state) ||
137
+ state.loadState === 'loaded' &&
138
+ ['enabled', 'enabled-runtime', 'disabled'].includes(state.unitFileState) ||
139
+ state.loadState === 'not-found' && state.unitFileState === ''), 'unsafe_unit');
140
+ const configuration = await this.unit.inspectConfiguration();
141
+ const maskPath = plugins.path.join(state.unitFileState === 'masked-runtime' ? '/run/systemd/system' : '/etc/systemd/system', this.name);
142
+ requireValue(!configuration.needsReload && configuration.dropInPaths === '' &&
143
+ (masked(state)
144
+ ? configuration.fragmentPath !== this.path &&
145
+ ['/dev/null', maskPath].includes(configuration.fragmentPath)
146
+ : configuration.fragmentPath === (state.loadState === 'not-found' ? '' : this.path)), 'unsafe_unit');
147
+ return configuration;
148
+ }
149
+ async directories(create) {
150
+ requireValue(process.platform === 'linux' && process.getuid?.() === this.uid, 'unsafe_path');
151
+ let directory = '/';
152
+ for (const part of this.directory.split('/').filter(Boolean)) {
153
+ directory = plugins.path.join(directory, part);
154
+ let info;
155
+ try {
156
+ info = await plugins.fs.lstat(directory);
157
+ }
158
+ catch (error) {
159
+ if (error.code !== 'ENOENT')
160
+ throw error;
161
+ if (!create)
162
+ return false;
163
+ await plugins.fs.mkdir(directory, { mode: 0o755 });
164
+ const parent = await plugins.fs.open(plugins.path.dirname(directory), plugins.fsConstants.O_RDONLY | plugins.fsConstants.O_DIRECTORY |
165
+ plugins.fsConstants.O_NOFOLLOW);
166
+ try {
167
+ await parent.sync();
168
+ }
169
+ finally {
170
+ await parent.close();
171
+ }
172
+ info = await plugins.fs.lstat(directory);
173
+ }
174
+ requireValue(info.isDirectory() && !info.isSymbolicLink() && [0, this.uid].includes(info.uid) &&
175
+ ((info.mode & 0o022) === 0 || directory === '/tmp' && info.uid === 0 &&
176
+ (info.mode & 0o7777) === 0o1777), 'unsafe_path');
177
+ }
178
+ return true;
179
+ }
180
+ async read(path) {
181
+ let file;
182
+ try {
183
+ file = await plugins.fs.open(path, plugins.fsConstants.O_RDONLY | plugins.fsConstants.O_NOFOLLOW |
184
+ plugins.fsConstants.O_NONBLOCK);
185
+ }
186
+ catch (error) {
187
+ if (error.code === 'ENOENT')
188
+ return null;
189
+ throw new SystemdUnitFileError('unsafe_path');
190
+ }
191
+ try {
192
+ const info = await file.stat();
193
+ requireValue(info.isFile() && info.uid === this.uid && info.nlink === 1 &&
194
+ (info.mode & 0o7777) === 0o644 && info.size <= 16384, 'unsafe_path');
195
+ const bytes = Buffer.alloc(16385);
196
+ let size = 0;
197
+ for (;;) {
198
+ const result = await file.read(bytes, size, bytes.length - size, size);
199
+ if (result.bytesRead === 0)
200
+ break;
201
+ size += result.bytesRead;
202
+ requireValue(size <= 16384, 'unsafe_path');
203
+ }
204
+ const after = await file.stat(), named = await plugins.fs.lstat(path);
205
+ requireValue(sameFile(info, named) && !named.isSymbolicLink() && after.size === size &&
206
+ info.size === after.size && info.mtimeMs === after.mtimeMs &&
207
+ info.ctimeMs === after.ctimeMs, 'changed');
208
+ return { bytes: bytes.subarray(0, size), info };
209
+ }
210
+ finally {
211
+ await file.close();
212
+ }
213
+ }
214
+ async create(path, bytes) {
215
+ const file = await plugins.fs.open(path, plugins.fsConstants.O_CREAT | plugins.fsConstants.O_EXCL |
216
+ plugins.fsConstants.O_WRONLY | plugins.fsConstants.O_NOFOLLOW, 0o644);
217
+ let created;
218
+ try {
219
+ try {
220
+ created = await file.stat();
221
+ await file.chmod(0o644);
222
+ await file.writeFile(bytes);
223
+ await file.sync();
224
+ }
225
+ finally {
226
+ await file.close();
227
+ }
228
+ }
229
+ catch (error) {
230
+ if (created) {
231
+ requireValue(sameFile(created, await plugins.fs.lstat(path)), 'unsafe_path');
232
+ await plugins.fs.unlink(path);
233
+ }
234
+ throw error;
235
+ }
236
+ }
237
+ async flushDirectory(expected) {
238
+ const directory = await plugins.fs.open(this.directory, plugins.fsConstants.O_RDONLY |
239
+ plugins.fsConstants.O_DIRECTORY | plugins.fsConstants.O_NOFOLLOW);
240
+ try {
241
+ requireValue(sameFile(expected, await directory.stat()), 'unsafe_path');
242
+ await directory.sync();
243
+ }
244
+ finally {
245
+ await directory.close();
246
+ }
247
+ }
248
+ }
249
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5zeXN0ZW1kdW5pdGZpbGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9jbGFzc2VzLnN5c3RlbWR1bml0ZmlsZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLDBCQUEwQixDQUFDO0FBQ3BELE9BQU8sRUFBRSx3QkFBd0IsRUFBRSxNQUFNLHVDQUF1QyxDQUFDO0FBQ2pGLE9BQU8sRUFJTCxXQUFXLEdBQ1osTUFBTSwwQkFBMEIsQ0FBQztBQVlsQyxNQUFNLE9BQU8sb0JBQXFCLFNBQVEsS0FBSztJQUM3QyxZQUNrQixJQU9HO1FBRW5CLEtBQUssQ0FBQyxnREFBZ0QsSUFBSSxHQUFHLENBQUMsQ0FBQztRQVQvQyxTQUFJLEdBQUosSUFBSSxDQU9EO1FBR25CLElBQUksQ0FBQyxJQUFJLEdBQUcsc0JBQXNCLENBQUM7SUFDckMsQ0FBQztDQUNGO0FBQ0QsTUFBTSxZQUFZLEdBQUcsQ0FBQyxLQUFjLEVBQUUsSUFBa0MsRUFBUSxFQUFFO0lBQ2hGLElBQUksQ0FBQyxLQUFLO1FBQUUsTUFBTSxJQUFJLG9CQUFvQixDQUFDLElBQUksQ0FBQyxDQUFDO0FBQ25ELENBQUMsQ0FBQztBQUNGLE1BQU0sTUFBTSxHQUFHLENBQUMsS0FBaUIsRUFBRSxFQUFFLENBQ25DLE9BQU8sQ0FBQyxNQUFNLENBQUMsVUFBVSxDQUFDLFFBQVEsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUM7QUFDbEUsTUFBTSxLQUFLLEdBQUcsQ0FBQyxLQUF3QixFQUFFLEVBQUUsQ0FDekMsS0FBSyxDQUFDLE9BQU8sS0FBSyxDQUFDLElBQUksS0FBSyxDQUFDLFVBQVUsS0FBSyxDQUFDO0lBQzdDLEtBQUssQ0FBQyxZQUFZLEtBQUssRUFBRSxJQUFJLEtBQUssQ0FBQyxHQUFHLEtBQUssRUFBRTtJQUM3QyxDQUFDLEtBQUssQ0FBQyxXQUFXLEtBQUssVUFBVSxJQUFJLEtBQUssQ0FBQyxRQUFRLEtBQUssTUFBTTtRQUM1RCxLQUFLLENBQUMsV0FBVyxLQUFLLFFBQVEsSUFBSSxLQUFLLENBQUMsUUFBUSxLQUFLLFFBQVEsQ0FBQyxDQUFDO0FBQ25FLE1BQU0sTUFBTSxHQUFHLENBQUMsS0FBd0IsRUFBRSxFQUFFLENBQzFDLEtBQUssQ0FBQyxTQUFTLEtBQUssUUFBUTtJQUM1QixDQUFDLFFBQVEsRUFBRSxnQkFBZ0IsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxLQUFLLENBQUMsYUFBYSxDQUFDLENBQUM7QUFDN0QsTUFBTSxRQUFRLEdBQUcsQ0FBQyxJQUFrQyxFQUFFLEtBQW1DLEVBQUUsRUFBRSxDQUMzRixJQUFJLENBQUMsR0FBRyxLQUFLLEtBQUssQ0FBQyxHQUFHLElBQUksSUFBSSxDQUFDLEdBQUcsS0FBSyxLQUFLLENBQUMsR0FBRyxDQUFDO0FBRW5EOzs7R0FHRztBQUNILE1BQU0sT0FBTyxlQUFlO0lBUTFCLFlBQVksT0FBZ0M7UUFGcEMsU0FBSSxHQUFHLEtBQUssQ0FBQztRQUduQixJQUFJLENBQUMsSUFBSSxHQUFHLElBQUksV0FBVyxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBQ3JDLElBQUksQ0FBQyxJQUFJLEdBQUcsT0FBTyxDQUFDLFFBQVEsQ0FBQztRQUM3QixJQUFJLENBQUMsU0FBUyxHQUFHLE9BQU8sQ0FBQyxhQUFhLElBQUksK0JBQStCLENBQUM7UUFDMUUsSUFBSSxDQUFDLEdBQUcsR0FBRyxPQUFPLENBQUMsV0FBVyxJQUFJLENBQUMsQ0FBQztRQUNwQyxZQUFZLENBQ1YsT0FBTyxJQUFJLENBQUMsU0FBUyxLQUFLLFFBQVEsSUFBSSxJQUFJLENBQUMsU0FBUyxLQUFLLEdBQUc7WUFDMUQsT0FBTyxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQztZQUN2QyxPQUFPLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLEtBQUssSUFBSSxDQUFDLFNBQVM7WUFDekQsQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLGlCQUFpQixDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDO1lBQ3hFLElBQUksQ0FBQyxTQUFTLENBQUMsTUFBTSxJQUFJLElBQUksSUFBSSxNQUFNLENBQUMsYUFBYSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsSUFBSSxJQUFJLENBQUMsR0FBRyxJQUFJLENBQUMsRUFDbEYsaUJBQWlCLENBQ2xCLENBQUM7UUFDRixJQUFJLENBQUMsSUFBSSxHQUFHLE9BQU8sQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxTQUFTLEVBQUUsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO0lBQzNELENBQUM7SUFFRCxvRkFBb0Y7SUFDN0UsS0FBSyxDQUFDLE9BQU87UUFDbEIsSUFBSSxDQUFDLE1BQU0sSUFBSSxDQUFDLFdBQVcsQ0FBQyxLQUFLLENBQUM7WUFBRSxPQUFPLElBQUksQ0FBQztRQUNoRCxNQUFNLElBQUksR0FBRyxNQUFNLElBQUksQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO1FBQ3hDLE9BQU8sSUFBSSxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLEVBQUUsSUFBSSxFQUFFLElBQUksQ0FBQyxJQUFJLEVBQUUsTUFBTSxFQUFFLE1BQU0sQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUM7SUFDdEYsQ0FBQztJQUVEOzs7T0FHRztJQUNJLEtBQUssQ0FBQyxPQUFPLENBQ2xCLFVBQW9DLEVBQ3BDLGNBQTZCO1FBRTdCLFlBQVksQ0FDVixVQUFVLFlBQVksd0JBQXdCLElBQUksVUFBVSxDQUFDLFFBQVEsS0FBSyxJQUFJLENBQUMsSUFBSTtZQUNqRixDQUFDLGNBQWMsS0FBSyxJQUFJO2dCQUN0QixPQUFPLGNBQWMsS0FBSyxRQUFRLElBQUksZ0JBQWdCLENBQUMsSUFBSSxDQUFDLGNBQWMsQ0FBQyxDQUFDLEVBQ2hGLGlCQUFpQixDQUNsQixDQUFDO1FBQ0YsWUFBWSxDQUFDLENBQUMsSUFBSSxDQUFDLElBQUksRUFBRSxNQUFNLENBQUMsQ0FBQztRQUNqQyxJQUFJLENBQUMsSUFBSSxHQUFHLElBQUksQ0FBQztRQUNqQixJQUFJLFNBQTZCLENBQUM7UUFDbEMsSUFBSSxhQUF3QyxDQUFDO1FBQzdDLElBQUksQ0FBQztZQUNILE1BQU0sTUFBTSxHQUFHLE1BQU0sSUFBSSxDQUFDLElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQztZQUN6QyxNQUFNLG1CQUFtQixHQUFHLE1BQU0sSUFBSSxDQUFDLEtBQUssQ0FBQyxNQUFNLENBQUMsQ0FBQztZQUNyRCxNQUFNLElBQUksQ0FBQyxXQUFXLENBQUMsSUFBSSxDQUFDLENBQUM7WUFDN0IsTUFBTSxNQUFNLEdBQUcsTUFBTSxPQUFPLENBQUMsRUFBRSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLENBQUM7WUFDdEQsTUFBTSxRQUFRLEdBQUcsTUFBTSxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztZQUM1QyxNQUFNLFdBQVcsR0FBRyxRQUFRLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQztZQUM3RCxNQUFNLE9BQU8sR0FBRyxXQUFXLEtBQUssVUFBVSxDQUFDLE1BQU0sQ0FBQztZQUNsRCxZQUFZLENBQUMsQ0FBQyxPQUFPLElBQUksV0FBVyxLQUFLLGNBQWMsRUFBRSxTQUFTLENBQUMsQ0FBQztZQUNwRSxJQUFJLE9BQU8sRUFBRSxDQUFDO2dCQUNaLElBQUksUUFBUSxFQUFFLENBQUM7b0JBQ2IsTUFBTSxNQUFNLEdBQUcsR0FBRyxJQUFJLENBQUMsSUFBSSxhQUFhLFdBQVcsRUFBRSxDQUFDO29CQUN0RCxJQUFJLENBQUM7d0JBQ0gsTUFBTSxJQUFJLENBQUMsTUFBTSxDQUFDLE1BQU0sRUFBRSxRQUFRLENBQUMsS0FBSyxDQUFDLENBQUM7b0JBQzVDLENBQUM7b0JBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQzt3QkFDZixJQUFLLEtBQStCLENBQUMsSUFBSSxLQUFLLFFBQVE7NEJBQUUsTUFBTSxLQUFLLENBQUM7d0JBQ3BFLFlBQVksQ0FBQyxNQUFNLENBQUMsQ0FBQyxNQUFNLElBQUksQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLENBQUUsQ0FBQyxLQUFLLENBQUMsS0FBSyxXQUFXLEVBQUUsU0FBUyxDQUFDLENBQUM7b0JBQ3BGLENBQUM7b0JBQ0QsTUFBTSxJQUFJLENBQUMsY0FBYyxDQUFDLE1BQU0sQ0FBQyxDQUFDO2dCQUNwQyxDQUFDO2dCQUNELFNBQVMsR0FBRyxPQUFPLENBQUMsSUFBSSxDQUFDLElBQUksQ0FDM0IsSUFBSSxDQUFDLFNBQVMsRUFDZCxJQUFJLElBQUksQ0FBQyxJQUFJLElBQUksT0FBTyxDQUFDLE1BQU0sQ0FBQyxVQUFVLEVBQUUsTUFBTSxDQUNuRCxDQUFDO2dCQUNGLE1BQU0sSUFBSSxDQUFDLE1BQU0sQ0FBQyxTQUFTLEVBQUUsTUFBTSxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQztnQkFDOUQsYUFBYSxHQUFHLE1BQU0sT0FBTyxDQUFDLEVBQUUsQ0FBQyxLQUFLLENBQUMsU0FBUyxDQUFDLENBQUM7Z0JBQ2xELE1BQU0sT0FBTyxHQUFHLE1BQU0sSUFBSSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7Z0JBQzNDLFlBQVksQ0FDVixDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLEtBQUssV0FBVztvQkFDdEQsQ0FBQyxDQUFDLE9BQU8sSUFBSSxRQUFRLElBQUksUUFBUSxDQUFDLE9BQU8sQ0FBQyxJQUFJLEVBQUUsUUFBUSxDQUFDLElBQUksQ0FBQyxDQUFDLEVBQ2pFLFNBQVMsQ0FDVixDQUFDO2dCQUNGLFlBQVksQ0FBQyxRQUFRLENBQUMsTUFBTSxFQUFFLE1BQU0sT0FBTyxDQUFDLEVBQUUsQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxDQUFDLEVBQUUsYUFBYSxDQUFDLENBQUM7Z0JBQ3RGLE1BQU0sU0FBUyxHQUFHLE1BQU0sSUFBSSxDQUFDLElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQztnQkFDNUMsWUFBWSxDQUFDLElBQUksQ0FBQyxTQUFTLENBQUMsU0FBUyxDQUFDLEtBQUssSUFBSSxDQUFDLFNBQVMsQ0FBQyxNQUFNLENBQUMsRUFBRSxTQUFTLENBQUMsQ0FBQztnQkFDOUUsTUFBTSxzQkFBc0IsR0FBRyxNQUFNLElBQUksQ0FBQyxLQUFLLENBQUMsU0FBUyxDQUFDLENBQUM7Z0JBQzNELFlBQVksQ0FDVixzQkFBc0IsQ0FBQyxZQUFZLEtBQUssbUJBQW1CLENBQUMsWUFBWSxFQUN4RSxTQUFTLENBQ1YsQ0FBQztnQkFDRixNQUFNLE9BQU8sQ0FBQyxFQUFFLENBQUMsTUFBTSxDQUFDLFNBQVMsRUFBRSxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7Z0JBQzlDLFNBQVMsR0FBRyxTQUFTLENBQUM7Z0JBQ3RCLE1BQU0sSUFBSSxDQUFDLGNBQWMsQ0FBQyxNQUFNLENBQUMsQ0FBQztZQUNwQyxDQUFDO1lBQ0QsSUFBSSxDQUFDO2dCQUNILE1BQU0scUJBQXFCLEdBQUcsTUFBTSxJQUFJLENBQUMsSUFBSSxDQUFDLG1CQUFtQixFQUFFLENBQUM7Z0JBQ3BFLE1BQU0sS0FBSyxHQUFHLE1BQU0sSUFBSSxDQUFDLElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQztnQkFDeEMsWUFBWSxDQUNWLEtBQUssQ0FBQyxLQUFLLENBQUM7b0JBQ1YsQ0FBQyxNQUFNLENBQUMsU0FBUyxLQUFLLFdBQVc7d0JBQy9CLENBQUMsQ0FBQyxLQUFLLENBQUMsU0FBUyxLQUFLLFFBQVEsSUFBSSxLQUFLLENBQUMsYUFBYSxLQUFLLFVBQVU7d0JBQ3BFLENBQUMsQ0FBQyxLQUFLLENBQUMsU0FBUyxLQUFLLE1BQU0sQ0FBQyxTQUFTOzRCQUNwQyxLQUFLLENBQUMsYUFBYSxLQUFLLE1BQU0sQ0FBQyxhQUFhLENBQUMsRUFDbkQsU0FBUyxDQUNWLENBQUM7Z0JBQ0YsTUFBTSxhQUFhLEdBQUcsTUFBTSxJQUFJLENBQUMsSUFBSSxDQUFDLG9CQUFvQixFQUFFLENBQUM7Z0JBQzdELFlBQVksQ0FDVixDQUFDLHFCQUFxQixFQUFFLGFBQWEsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLFFBQVEsRUFBRSxFQUFFLENBQ3hELENBQUMsUUFBUSxDQUFDLFdBQVcsSUFBSSxRQUFRLENBQUMsV0FBVyxLQUFLLEVBQUU7b0JBQ3BELFFBQVEsQ0FBQyxZQUFZO3dCQUNuQixDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsbUJBQW1CLENBQUMsWUFBWSxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQ2pFLEVBQ0QsYUFBYSxDQUNkLENBQUM7Z0JBQ0YsTUFBTSxTQUFTLEdBQUcsTUFBTSxJQUFJLENBQUMsT0FBTyxFQUFFLENBQUM7Z0JBQ3ZDLFlBQVksQ0FBQyxTQUFTLEVBQUUsTUFBTSxLQUFLLFVBQVUsQ0FBQyxNQUFNLEVBQUUsU0FBUyxDQUFDLENBQUM7Z0JBQ2pFLE9BQU8sTUFBTSxDQUFDLE1BQU0sQ0FBQyxFQUFFLE9BQU8sRUFBRSxNQUFNLEVBQUUsU0FBVSxFQUFFLE1BQU0sRUFBRSxNQUFNLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQyxDQUFDO1lBQy9FLENBQUM7WUFBQyxNQUFNLENBQUM7Z0JBQ1AsTUFBTSxJQUFJLG9CQUFvQixDQUFDLGVBQWUsQ0FBQyxDQUFDO1lBQ2xELENBQUM7UUFDSCxDQUFDO1FBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztZQUNmLElBQUksS0FBSyxZQUFZLG9CQUFvQjtnQkFBRSxNQUFNLEtBQUssQ0FBQztZQUN2RCxNQUFNLElBQUksb0JBQW9CLENBQUMsY0FBYyxDQUFDLENBQUM7UUFDakQsQ0FBQztnQkFBUyxDQUFDO1lBQ1QsSUFBSSxDQUFDO2dCQUNILElBQUksU0FBUyxJQUFJLGFBQWEsRUFBRSxDQUFDO29CQUMvQixNQUFNLEtBQUssR0FBRyxNQUFNLE9BQU8sQ0FBQyxFQUFFLENBQUMsS0FBSyxDQUFDLFNBQVMsQ0FBQyxDQUFDO29CQUNoRCxZQUFZLENBQUMsUUFBUSxDQUFDLGFBQWEsRUFBRSxLQUFLLENBQUMsRUFBRSxhQUFhLENBQUMsQ0FBQztvQkFDNUQsTUFBTSxPQUFPLENBQUMsRUFBRSxDQUFDLE1BQU0sQ0FBQyxTQUFTLENBQUMsQ0FBQztnQkFDckMsQ0FBQztZQUNILENBQUM7b0JBQVMsQ0FBQztnQkFDVCxJQUFJLENBQUMsSUFBSSxHQUFHLEtBQUssQ0FBQztZQUNwQixDQUFDO1FBQ0gsQ0FBQztJQUNILENBQUM7SUFFTyxLQUFLLENBQUMsS0FBSyxDQUFDLEtBQXdCO1FBQzFDLFlBQVksQ0FDVixLQUFLLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDO1lBQzVCLEtBQUssQ0FBQyxTQUFTLEtBQUssUUFBUTtnQkFDMUIsQ0FBQyxTQUFTLEVBQUUsaUJBQWlCLEVBQUUsVUFBVSxDQUFDLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxhQUFhLENBQUM7WUFDMUUsS0FBSyxDQUFDLFNBQVMsS0FBSyxXQUFXLElBQUksS0FBSyxDQUFDLGFBQWEsS0FBSyxFQUFFLENBQUMsRUFDaEUsYUFBYSxDQUNkLENBQUM7UUFDRixNQUFNLGFBQWEsR0FBRyxNQUFNLElBQUksQ0FBQyxJQUFJLENBQUMsb0JBQW9CLEVBQUUsQ0FBQztRQUM3RCxNQUFNLFFBQVEsR0FBRyxPQUFPLENBQUMsSUFBSSxDQUFDLElBQUksQ0FDaEMsS0FBSyxDQUFDLGFBQWEsS0FBSyxnQkFBZ0IsQ0FBQyxDQUFDLENBQUMscUJBQXFCLENBQUMsQ0FBQyxDQUFDLHFCQUFxQixFQUN4RixJQUFJLENBQUMsSUFBSSxDQUNWLENBQUM7UUFDRixZQUFZLENBQ1YsQ0FBQyxhQUFhLENBQUMsV0FBVyxJQUFJLGFBQWEsQ0FBQyxXQUFXLEtBQUssRUFBRTtZQUM1RCxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUM7Z0JBQ1osQ0FBQyxDQUFDLGFBQWEsQ0FBQyxZQUFZLEtBQUssSUFBSSxDQUFDLElBQUk7b0JBQ3hDLENBQUMsV0FBVyxFQUFFLFFBQVEsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxhQUFhLENBQUMsWUFBWSxDQUFDO2dCQUM5RCxDQUFDLENBQUMsYUFBYSxDQUFDLFlBQVksS0FBSyxDQUFDLEtBQUssQ0FBQyxTQUFTLEtBQUssV0FBVyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQyxFQUN4RixhQUFhLENBQ2QsQ0FBQztRQUNGLE9BQU8sYUFBYSxDQUFDO0lBQ3ZCLENBQUM7SUFFTyxLQUFLLENBQUMsV0FBVyxDQUFDLE1BQWU7UUFDdkMsWUFBWSxDQUFDLE9BQU8sQ0FBQyxRQUFRLEtBQUssT0FBTyxJQUFJLE9BQU8sQ0FBQyxNQUFNLEVBQUUsRUFBRSxLQUFLLElBQUksQ0FBQyxHQUFHLEVBQUUsYUFBYSxDQUFDLENBQUM7UUFDN0YsSUFBSSxTQUFTLEdBQUcsR0FBRyxDQUFDO1FBQ3BCLEtBQUssTUFBTSxJQUFJLElBQUksSUFBSSxDQUFDLFNBQVMsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsTUFBTSxDQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUM7WUFDN0QsU0FBUyxHQUFHLE9BQU8sQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLFNBQVMsRUFBRSxJQUFJLENBQUMsQ0FBQztZQUMvQyxJQUFJLElBQW1CLENBQUM7WUFDeEIsSUFBSSxDQUFDO2dCQUNILElBQUksR0FBRyxNQUFNLE9BQU8sQ0FBQyxFQUFFLENBQUMsS0FBSyxDQUFDLFNBQVMsQ0FBQyxDQUFDO1lBQzNDLENBQUM7WUFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO2dCQUNmLElBQUssS0FBK0IsQ0FBQyxJQUFJLEtBQUssUUFBUTtvQkFBRSxNQUFNLEtBQUssQ0FBQztnQkFDcEUsSUFBSSxDQUFDLE1BQU07b0JBQUUsT0FBTyxLQUFLLENBQUM7Z0JBQzFCLE1BQU0sT0FBTyxDQUFDLEVBQUUsQ0FBQyxLQUFLLENBQUMsU0FBUyxFQUFFLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUM7Z0JBQ25ELE1BQU0sTUFBTSxHQUFHLE1BQU0sT0FBTyxDQUFDLEVBQUUsQ0FBQyxJQUFJLENBQ2xDLE9BQU8sQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLFNBQVMsQ0FBQyxFQUMvQixPQUFPLENBQUMsV0FBVyxDQUFDLFFBQVEsR0FBRyxPQUFPLENBQUMsV0FBVyxDQUFDLFdBQVc7b0JBQzVELE9BQU8sQ0FBQyxXQUFXLENBQUMsVUFBVSxDQUNqQyxDQUFDO2dCQUNGLElBQUksQ0FBQztvQkFDSCxNQUFNLE1BQU0sQ0FBQyxJQUFJLEVBQUUsQ0FBQztnQkFDdEIsQ0FBQzt3QkFBUyxDQUFDO29CQUNULE1BQU0sTUFBTSxDQUFDLEtBQUssRUFBRSxDQUFDO2dCQUN2QixDQUFDO2dCQUNELElBQUksR0FBRyxNQUFNLE9BQU8sQ0FBQyxFQUFFLENBQUMsS0FBSyxDQUFDLFNBQVMsQ0FBQyxDQUFDO1lBQzNDLENBQUM7WUFDRCxZQUFZLENBQ1YsSUFBSSxDQUFDLFdBQVcsRUFBRSxJQUFJLENBQUMsSUFBSSxDQUFDLGNBQWMsRUFBRSxJQUFJLENBQUMsQ0FBQyxFQUFFLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxRQUFRLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQztnQkFDOUUsQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLEdBQUcsS0FBSyxDQUFDLEtBQUssQ0FBQyxJQUFJLFNBQVMsS0FBSyxNQUFNLElBQUksSUFBSSxDQUFDLEdBQUcsS0FBSyxDQUFDO29CQUNoRSxDQUFDLElBQUksQ0FBQyxJQUFJLEdBQUcsTUFBTSxDQUFDLEtBQUssTUFBTSxDQUFDLEVBQ3RDLGFBQWEsQ0FDZCxDQUFDO1FBQ0osQ0FBQztRQUNELE9BQU8sSUFBSSxDQUFDO0lBQ2QsQ0FBQztJQUVPLEtBQUssQ0FBQyxJQUFJLENBQUMsSUFBWTtRQUM3QixJQUFJLElBQWlELENBQUM7UUFDdEQsSUFBSSxDQUFDO1lBQ0gsSUFBSSxHQUFHLE1BQU0sT0FBTyxDQUFDLEVBQUUsQ0FBQyxJQUFJLENBQzFCLElBQUksRUFDSixPQUFPLENBQUMsV0FBVyxDQUFDLFFBQVEsR0FBRyxPQUFPLENBQUMsV0FBVyxDQUFDLFVBQVU7Z0JBQzNELE9BQU8sQ0FBQyxXQUFXLENBQUMsVUFBVSxDQUNqQyxDQUFDO1FBQ0osQ0FBQztRQUFDLE9BQU8sS0FBSyxFQUFFLENBQUM7WUFDZixJQUFLLEtBQStCLENBQUMsSUFBSSxLQUFLLFFBQVE7Z0JBQUUsT0FBTyxJQUFJLENBQUM7WUFDcEUsTUFBTSxJQUFJLG9CQUFvQixDQUFDLGFBQWEsQ0FBQyxDQUFDO1FBQ2hELENBQUM7UUFDRCxJQUFJLENBQUM7WUFDSCxNQUFNLElBQUksR0FBRyxNQUFNLElBQUksQ0FBQyxJQUFJLEVBQUUsQ0FBQztZQUMvQixZQUFZLENBQ1YsSUFBSSxDQUFDLE1BQU0sRUFBRSxJQUFJLElBQUksQ0FBQyxHQUFHLEtBQUssSUFBSSxDQUFDLEdBQUcsSUFBSSxJQUFJLENBQUMsS0FBSyxLQUFLLENBQUM7Z0JBQ3hELENBQUMsSUFBSSxDQUFDLElBQUksR0FBRyxNQUFNLENBQUMsS0FBSyxLQUFLLElBQUksSUFBSSxDQUFDLElBQUksSUFBSSxLQUFLLEVBQ3RELGFBQWEsQ0FDZCxDQUFDO1lBQ0YsTUFBTSxLQUFLLEdBQUcsTUFBTSxDQUFDLEtBQUssQ0FBQyxLQUFLLENBQUMsQ0FBQztZQUNsQyxJQUFJLElBQUksR0FBRyxDQUFDLENBQUM7WUFDYixTQUFTLENBQUM7Z0JBQ1IsTUFBTSxNQUFNLEdBQUcsTUFBTSxJQUFJLENBQUMsSUFBSSxDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsS0FBSyxDQUFDLE1BQU0sR0FBRyxJQUFJLEVBQUUsSUFBSSxDQUFDLENBQUM7Z0JBQ3ZFLElBQUksTUFBTSxDQUFDLFNBQVMsS0FBSyxDQUFDO29CQUFFLE1BQU07Z0JBQ2xDLElBQUksSUFBSSxNQUFNLENBQUMsU0FBUyxDQUFDO2dCQUN6QixZQUFZLENBQUMsSUFBSSxJQUFJLEtBQUssRUFBRSxhQUFhLENBQUMsQ0FBQztZQUM3QyxDQUFDO1lBQ0QsTUFBTSxLQUFLLEdBQUcsTUFBTSxJQUFJLENBQUMsSUFBSSxFQUFFLEVBQUUsS0FBSyxHQUFHLE1BQU0sT0FBTyxDQUFDLEVBQUUsQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLENBQUM7WUFDdEUsWUFBWSxDQUNWLFFBQVEsQ0FBQyxJQUFJLEVBQUUsS0FBSyxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsY0FBYyxFQUFFLElBQUksS0FBSyxDQUFDLElBQUksS0FBSyxJQUFJO2dCQUNyRSxJQUFJLENBQUMsSUFBSSxLQUFLLEtBQUssQ0FBQyxJQUFJLElBQUksSUFBSSxDQUFDLE9BQU8sS0FBSyxLQUFLLENBQUMsT0FBTztnQkFDMUQsSUFBSSxDQUFDLE9BQU8sS0FBSyxLQUFLLENBQUMsT0FBTyxFQUNoQyxTQUFTLENBQ1YsQ0FBQztZQUNGLE9BQU8sRUFBRSxLQUFLLEVBQUUsS0FBSyxDQUFDLFFBQVEsQ0FBQyxDQUFDLEVBQUUsSUFBSSxDQUFDLEVBQUUsSUFBSSxFQUFFLENBQUM7UUFDbEQsQ0FBQztnQkFBUyxDQUFDO1lBQ1QsTUFBTSxJQUFJLENBQUMsS0FBSyxFQUFFLENBQUM7UUFDckIsQ0FBQztJQUNILENBQUM7SUFFTyxLQUFLLENBQUMsTUFBTSxDQUFDLElBQVksRUFBRSxLQUFpQjtRQUNsRCxNQUFNLElBQUksR0FBRyxNQUFNLE9BQU8sQ0FBQyxFQUFFLENBQUMsSUFBSSxDQUNoQyxJQUFJLEVBQ0osT0FBTyxDQUFDLFdBQVcsQ0FBQyxPQUFPLEdBQUcsT0FBTyxDQUFDLFdBQVcsQ0FBQyxNQUFNO1lBQ3RELE9BQU8sQ0FBQyxXQUFXLENBQUMsUUFBUSxHQUFHLE9BQU8sQ0FBQyxXQUFXLENBQUMsVUFBVSxFQUMvRCxLQUFLLENBQ04sQ0FBQztRQUNGLElBQUksT0FBa0MsQ0FBQztRQUN2QyxJQUFJLENBQUM7WUFDSCxJQUFJLENBQUM7Z0JBQ0gsT0FBTyxHQUFHLE1BQU0sSUFBSSxDQUFDLElBQUksRUFBRSxDQUFDO2dCQUM1QixNQUFNLElBQUksQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLENBQUM7Z0JBQ3hCLE1BQU0sSUFBSSxDQUFDLFNBQVMsQ0FBQyxLQUFLLENBQUMsQ0FBQztnQkFDNUIsTUFBTSxJQUFJLENBQUMsSUFBSSxFQUFFLENBQUM7WUFDcEIsQ0FBQztvQkFBUyxDQUFDO2dCQUNULE1BQU0sSUFBSSxDQUFDLEtBQUssRUFBRSxDQUFDO1lBQ3JCLENBQUM7UUFDSCxDQUFDO1FBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztZQUNmLElBQUksT0FBTyxFQUFFLENBQUM7Z0JBQ1osWUFBWSxDQUFDLFFBQVEsQ0FBQyxPQUFPLEVBQUUsTUFBTSxPQUFPLENBQUMsRUFBRSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsQ0FBQyxFQUFFLGFBQWEsQ0FBQyxDQUFDO2dCQUM3RSxNQUFNLE9BQU8sQ0FBQyxFQUFFLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxDQUFDO1lBQ2hDLENBQUM7WUFDRCxNQUFNLEtBQUssQ0FBQztRQUNkLENBQUM7SUFDSCxDQUFDO0lBRU8sS0FBSyxDQUFDLGNBQWMsQ0FBQyxRQUF1QjtRQUNsRCxNQUFNLFNBQVMsR0FBRyxNQUFNLE9BQU8sQ0FBQyxFQUFFLENBQUMsSUFBSSxDQUNyQyxJQUFJLENBQUMsU0FBUyxFQUNkLE9BQU8sQ0FBQyxXQUFXLENBQUMsUUFBUTtZQUMxQixPQUFPLENBQUMsV0FBVyxDQUFDLFdBQVcsR0FBRyxPQUFPLENBQUMsV0FBVyxDQUFDLFVBQVUsQ0FDbkUsQ0FBQztRQUNGLElBQUksQ0FBQztZQUNILFlBQVksQ0FBQyxRQUFRLENBQUMsUUFBUSxFQUFFLE1BQU0sU0FBUyxDQUFDLElBQUksRUFBRSxDQUFDLEVBQUUsYUFBYSxDQUFDLENBQUM7WUFDeEUsTUFBTSxTQUFTLENBQUMsSUFBSSxFQUFFLENBQUM7UUFDekIsQ0FBQztnQkFBUyxDQUFDO1lBQ1QsTUFBTSxTQUFTLENBQUMsS0FBSyxFQUFFLENBQUM7UUFDMUIsQ0FBQztJQUNILENBQUM7Q0FDRiJ9
@@ -1,3 +1,5 @@
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';
package/dist_ts/index.js CHANGED
@@ -1,4 +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
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLHNDQUFzQyxDQUFDO0FBQ3JELGNBQWMsa0NBQWtDLENBQUM7QUFDakQsY0FBYywwQkFBMEIsQ0FBQyJ9
4
+ export * from './classes.systemdservicedefinition.js';
5
+ export * from './classes.systemdunitfile.js';
6
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLHNDQUFzQyxDQUFDO0FBQ3JELGNBQWMsa0NBQWtDLENBQUM7QUFDakQsY0FBYywwQkFBMEIsQ0FBQztBQUN6QyxjQUFjLHVDQUF1QyxDQUFDO0FBQ3RELGNBQWMsOEJBQThCLENBQUMifQ==
@@ -1,6 +1,10 @@
1
1
  import * as path from 'path';
2
2
  import * as childProcess from 'node:child_process';
3
- export { path, childProcess };
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 };
4
8
  import * as lik from '@push.rocks/lik';
5
9
  import * as smartfm from '@push.rocks/smartfm';
6
10
  import * as smartfs from '@push.rocks/smartfs';
@@ -1,7 +1,10 @@
1
1
  // node native scope
2
2
  import * as path from 'path';
3
3
  import * as childProcess from 'node:child_process';
4
- export { path, childProcess };
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 };
5
8
  // @pushrocks scope
6
9
  import * as lik from '@push.rocks/lik';
7
10
  import * as smartfm from '@push.rocks/smartfm';
@@ -13,4 +16,4 @@ import * as smartshell from '@push.rocks/smartshell';
13
16
  import * as smartsystem from '@push.rocks/smartsystem';
14
17
  export { lik, smartfm, smartfs, smartlog, smartlogDestinationLocal, smartpath, smartshell, smartsystem, };
15
18
  // third party
16
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRkYWVtb24ucGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3NtYXJ0ZGFlbW9uLnBsdWdpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsb0JBQW9CO0FBQ3BCLE9BQU8sS0FBSyxJQUFJLE1BQU0sTUFBTSxDQUFDO0FBQzdCLE9BQU8sS0FBSyxZQUFZLE1BQU0sb0JBQW9CLENBQUM7QUFFbkQsT0FBTyxFQUFFLElBQUksRUFBRSxZQUFZLEVBQUUsQ0FBQztBQUU5QixtQkFBbUI7QUFDbkIsT0FBTyxLQUFLLEdBQUcsTUFBTSxpQkFBaUIsQ0FBQztBQUN2QyxPQUFPLEtBQUssT0FBTyxNQUFNLHFCQUFxQixDQUFDO0FBQy9DLE9BQU8sS0FBSyxPQUFPLE1BQU0scUJBQXFCLENBQUM7QUFDL0MsT0FBTyxLQUFLLFFBQVEsTUFBTSxzQkFBc0IsQ0FBQztBQUNqRCxPQUFPLEtBQUssd0JBQXdCLE1BQU0sd0NBQXdDLENBQUM7QUFDbkYsT0FBTyxLQUFLLFNBQVMsTUFBTSx1QkFBdUIsQ0FBQztBQUNuRCxPQUFPLEtBQUssVUFBVSxNQUFNLHdCQUF3QixDQUFDO0FBQ3JELE9BQU8sS0FBSyxXQUFXLE1BQU0seUJBQXlCLENBQUM7QUFFdkQsT0FBTyxFQUNMLEdBQUcsRUFDSCxPQUFPLEVBQ1AsT0FBTyxFQUNQLFFBQVEsRUFDUix3QkFBd0IsRUFDeEIsU0FBUyxFQUNULFVBQVUsRUFDVixXQUFXLEdBQ1osQ0FBQztBQUVGLGNBQWMifQ==
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.1",
3
+ "version": "2.4.1",
4
4
  "private": false,
5
5
  "description": "Start scripts as long running daemons and manage them.",
6
6
  "main": "dist_ts/index.js",
@@ -28,7 +28,7 @@
28
28
  "devDependencies": {
29
29
  "@git.zone/tsbuild": "^4.4.2",
30
30
  "@git.zone/tsrun": "^2.0.6",
31
- "@git.zone/tstest": "^4.0.0",
31
+ "@git.zone/tstest": "^6.0.0",
32
32
  "@types/node": "^26.4.1"
33
33
  },
34
34
  "dependencies": {
package/readme.md CHANGED
@@ -186,6 +186,76 @@ environment, no shell or password prompt, and bounded output. Trusted owning cod
186
186
  can choose another absolute executable path and `commandTimeoutMs` (default
187
187
  120000, maximum 600000). This timeout bounds the client process only.
188
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
+ Masked units may report `/dev/null` or their exact administrator mask path:
234
+ `/etc/systemd/system/<unit>` for a persistent mask, `/run/systemd/system/<unit>`
235
+ for a runtime mask. Installation preserves that exact manager-selected fragment
236
+ through the write and reload, and never replaces the masking fragment itself.
237
+ Pending manager reloads are rejected before writing; inspect and explicitly
238
+ reload an existing changed configuration before retrying installation.
239
+
240
+ The caller must already serialize privileged installation across processes;
241
+ `SystemdUnitFile` only excludes simultaneous operations on the same instance.
242
+ The expected previous SHA-256 is mandatory (`null` requires an absent file),
243
+ while already matching bytes are idempotent. Installation checks protected
244
+ directories and regular single-link mode-0644 files, retains changed definitions
245
+ as `<unit>.previous-<sha256>`, flushes them before atomic replacement, reloads
246
+ the manager and checks its selected fragment and final bytes. A reload failure
247
+ leaves the new file and retained previous definition in place; recovery requires
248
+ inspection, never an automatic rollback. Filesystem and definition errors use
249
+ `SystemdUnitFileError` with a local code. Trusted qualification code can set
250
+ `unitDirectory`, `expectedUid` and the inherited `SystemdUnit` command options.
251
+
252
+ With `killMode: 'mixed'`, the main process owns graceful child shutdown and must
253
+ remain alive until its children have joined. Systemd sends SIGTERM to the main
254
+ process first and can send SIGKILL to the whole cgroup when the main process exits
255
+ or the stop deadline expires. `control-group` sends the initial signal to the
256
+ whole group. A successful systemd start still requires application readiness
257
+ verification, and a completed stop alone does not prove database drain.
258
+
189
259
  ### SmartDaemonService Class
190
260
 
191
261
  ```typescript
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartdaemon',
6
- version: '2.3.1',
6
+ version: '2.4.1',
7
7
  description: 'Start scripts as long running daemons and manage them.'
8
8
  }
@@ -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
+ }
@@ -22,6 +22,14 @@ export interface ISystemdUnitState {
22
22
  readonly job: string;
23
23
  }
24
24
 
25
+ export interface ISystemdUnitConfigurationState {
26
+ readonly unitName: string;
27
+ readonly fragmentPath: string;
28
+ /** Opaque systemctl path list. An empty value proves no effective drop-ins. */
29
+ readonly dropInPaths: string;
30
+ readonly needsReload: boolean;
31
+ }
32
+
25
33
  export class SystemdUnitError extends Error {
26
34
  constructor(public readonly code: 'invalid_options' | 'command_failed' | 'invalid_state') {
27
35
  super(`Systemd unit operation failed: ${code}.`);
@@ -111,6 +119,36 @@ export class SystemdUnit {
111
119
  return this.inspect();
112
120
  }
113
121
 
122
+ /** Reports the manager's selected definition without reading or changing its contents. */
123
+ public async inspectConfiguration(): Promise<ISystemdUnitConfigurationState> {
124
+ const keys = ['Id', 'FragmentPath', 'DropInPaths', 'NeedDaemonReload'];
125
+ const stdout = await this.command(['show', this.unitName, `--property=${keys.join(',')}`, '--all']);
126
+ const fields = new Map<string, string>();
127
+ for (const line of stdout.trimEnd().split('\n')) {
128
+ const separator = line.indexOf('='), key = line.slice(0, separator);
129
+ if (separator < 1 || !keys.includes(key) || fields.has(key)) {
130
+ throw new SystemdUnitError('invalid_state');
131
+ }
132
+ fields.set(key, line.slice(separator + 1));
133
+ }
134
+ if (fields.size !== keys.length || fields.get('Id') !== this.unitName ||
135
+ !['yes', 'no'].includes(fields.get('NeedDaemonReload')!)) {
136
+ throw new SystemdUnitError('invalid_state');
137
+ }
138
+ return Object.freeze({
139
+ unitName: this.unitName,
140
+ fragmentPath: fields.get('FragmentPath')!,
141
+ dropInPaths: fields.get('DropInPaths')!,
142
+ needsReload: fields.get('NeedDaemonReload') === 'yes',
143
+ });
144
+ }
145
+
146
+ /** Reloads unit definitions globally; does not enable, unmask, start or stop a unit. */
147
+ public async reloadConfiguration(): Promise<ISystemdUnitConfigurationState> {
148
+ await this.command(['daemon-reload']);
149
+ return this.inspectConfiguration();
150
+ }
151
+
114
152
  private command(argsArg: string[]): Promise<string> {
115
153
  return new Promise((resolve, reject) => {
116
154
  plugins.childProcess.execFile(
@@ -0,0 +1,327 @@
1
+ import * as plugins from './smartdaemon.plugins.js';
2
+ import { SystemdServiceDefinition } from './classes.systemdservicedefinition.js';
3
+ import {
4
+ type ISystemdUnitOptions,
5
+ type ISystemdUnitConfigurationState,
6
+ type ISystemdUnitState,
7
+ SystemdUnit,
8
+ } from './classes.systemdunit.js';
9
+
10
+ export interface ISystemdUnitFileOptions extends ISystemdUnitOptions {
11
+ /** Local vendor definitions can be installed without removing an administrator's mask. */
12
+ unitDirectory?: string;
13
+ /** Trusted owning-code qualification only. Defaults to root. */
14
+ expectedUid?: number;
15
+ }
16
+ export interface ISystemdUnitFileRecord {
17
+ readonly path: string;
18
+ readonly sha256: string;
19
+ }
20
+ export class SystemdUnitFileError extends Error {
21
+ constructor(
22
+ public readonly code:
23
+ | 'invalid_options'
24
+ | 'unsafe_path'
25
+ | 'busy'
26
+ | 'changed'
27
+ | 'unsafe_unit'
28
+ | 'write_failed'
29
+ | 'reload_failed',
30
+ ) {
31
+ super(`Systemd unit definition installation failed: ${code}.`);
32
+ this.name = 'SystemdUnitFileError';
33
+ }
34
+ }
35
+ const requireValue = (value: unknown, code: SystemdUnitFileError['code']): void => {
36
+ if (!value) throw new SystemdUnitFileError(code);
37
+ };
38
+ const digest = (bytes: Uint8Array) =>
39
+ plugins.crypto.createHash('sha256').update(bytes).digest('hex');
40
+ const quiet = (state: ISystemdUnitState) =>
41
+ state.mainPid === 0 && state.controlPid === 0 &&
42
+ state.controlGroup === '' && state.job === '' &&
43
+ (state.activeState === 'inactive' && state.subState === 'dead' ||
44
+ state.activeState === 'failed' && state.subState === 'failed');
45
+ const masked = (state: ISystemdUnitState) =>
46
+ state.loadState === 'masked' &&
47
+ ['masked', 'masked-runtime'].includes(state.unitFileState);
48
+ const sameFile = (left: { dev: number; ino: number }, right: { dev: number; ino: number }) =>
49
+ left.dev === right.dev && left.ino === right.ino;
50
+
51
+ /**
52
+ * Protected code/config installation, inside the caller's cross-process installation ownership scope.
53
+ * Other privileged writers must be serialized by that owner. Never changes enablement or masks.
54
+ */
55
+ export class SystemdUnitFile {
56
+ public readonly path: string;
57
+ private readonly directory: string;
58
+ private readonly uid: number;
59
+ private readonly unit: SystemdUnit;
60
+ private readonly name: string;
61
+ private busy = false;
62
+
63
+ constructor(options: ISystemdUnitFileOptions) {
64
+ this.unit = new SystemdUnit(options);
65
+ this.name = options.unitName;
66
+ this.directory = options.unitDirectory ?? '/usr/local/lib/systemd/system';
67
+ this.uid = options.expectedUid ?? 0;
68
+ requireValue(
69
+ typeof this.directory === 'string' && this.directory !== '/' &&
70
+ plugins.path.isAbsolute(this.directory) &&
71
+ plugins.path.normalize(this.directory) === this.directory &&
72
+ !this.directory.endsWith('/') && !/[\x00-\x1f\x7f]/.test(this.directory) &&
73
+ this.directory.length <= 4000 && Number.isSafeInteger(this.uid) && this.uid >= 0,
74
+ 'invalid_options',
75
+ );
76
+ this.path = plugins.path.join(this.directory, this.name);
77
+ }
78
+
79
+ /** Reads only the exact protected definition; no systemd or filesystem mutation. */
80
+ public async inspect(): Promise<ISystemdUnitFileRecord | null> {
81
+ if (!await this.directories(false)) return null;
82
+ const file = await this.read(this.path);
83
+ return file ? Object.freeze({ path: this.path, sha256: digest(file.bytes) }) : null;
84
+ }
85
+
86
+ /**
87
+ * Expected previous digest is mandatory; null admits an absent file. Exact desired bytes are idempotent.
88
+ * Retains changed definitions beside the unit before replacement. A failed reload never rolls code back.
89
+ */
90
+ public async install(
91
+ definition: SystemdServiceDefinition,
92
+ expectedSha256: string | null,
93
+ ): Promise<Readonly<{ changed: boolean; record: ISystemdUnitFileRecord; masked: boolean }>> {
94
+ requireValue(
95
+ definition instanceof SystemdServiceDefinition && definition.unitName === this.name &&
96
+ (expectedSha256 === null ||
97
+ typeof expectedSha256 === 'string' && /^[a-f0-9]{64}$/.test(expectedSha256)),
98
+ 'invalid_options',
99
+ );
100
+ requireValue(!this.busy, 'busy');
101
+ this.busy = true;
102
+ let temporary: string | undefined;
103
+ let temporaryInfo: plugins.Stats | undefined;
104
+ try {
105
+ const before = await this.unit.inspect();
106
+ const beforeConfiguration = await this.admit(before);
107
+ await this.directories(true);
108
+ const parent = await plugins.fs.lstat(this.directory);
109
+ const previous = await this.read(this.path);
110
+ const previousSha = previous ? digest(previous.bytes) : null;
111
+ const changed = previousSha !== definition.sha256;
112
+ requireValue(!changed || previousSha === expectedSha256, 'changed');
113
+ if (changed) {
114
+ if (previous) {
115
+ const backup = `${this.path}.previous-${previousSha}`;
116
+ try {
117
+ await this.create(backup, previous.bytes);
118
+ } catch (error) {
119
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
120
+ requireValue(digest((await this.read(backup))!.bytes) === previousSha, 'changed');
121
+ }
122
+ await this.flushDirectory(parent);
123
+ }
124
+ temporary = plugins.path.join(
125
+ this.directory,
126
+ `.${this.name}.${plugins.crypto.randomUUID()}.tmp`,
127
+ );
128
+ await this.create(temporary, Buffer.from(definition.content));
129
+ temporaryInfo = await plugins.fs.lstat(temporary);
130
+ const current = await this.read(this.path);
131
+ requireValue(
132
+ (current ? digest(current.bytes) : null) === previousSha &&
133
+ (!current || previous && sameFile(current.info, previous.info)),
134
+ 'changed',
135
+ );
136
+ requireValue(sameFile(parent, await plugins.fs.lstat(this.directory)), 'unsafe_path');
137
+ const rechecked = await this.unit.inspect();
138
+ requireValue(JSON.stringify(rechecked) === JSON.stringify(before), 'changed');
139
+ const recheckedConfiguration = await this.admit(rechecked);
140
+ requireValue(
141
+ recheckedConfiguration.fragmentPath === beforeConfiguration.fragmentPath,
142
+ 'changed',
143
+ );
144
+ await plugins.fs.rename(temporary, this.path);
145
+ temporary = undefined;
146
+ await this.flushDirectory(parent);
147
+ }
148
+ try {
149
+ const reloadedConfiguration = await this.unit.reloadConfiguration();
150
+ const after = await this.unit.inspect();
151
+ requireValue(
152
+ quiet(after) &&
153
+ (before.loadState === 'not-found'
154
+ ? after.loadState === 'loaded' && after.unitFileState === 'disabled'
155
+ : after.loadState === before.loadState &&
156
+ after.unitFileState === before.unitFileState),
157
+ 'changed',
158
+ );
159
+ const configuration = await this.unit.inspectConfiguration();
160
+ requireValue(
161
+ [reloadedConfiguration, configuration].every((snapshot) =>
162
+ !snapshot.needsReload && snapshot.dropInPaths === '' &&
163
+ snapshot.fragmentPath ===
164
+ (masked(after) ? beforeConfiguration.fragmentPath : this.path)
165
+ ),
166
+ 'unsafe_unit',
167
+ );
168
+ const installed = await this.inspect();
169
+ requireValue(installed?.sha256 === definition.sha256, 'changed');
170
+ return Object.freeze({ changed, record: installed!, masked: masked(after) });
171
+ } catch {
172
+ throw new SystemdUnitFileError('reload_failed');
173
+ }
174
+ } catch (error) {
175
+ if (error instanceof SystemdUnitFileError) throw error;
176
+ throw new SystemdUnitFileError('write_failed');
177
+ } finally {
178
+ try {
179
+ if (temporary && temporaryInfo) {
180
+ const named = await plugins.fs.lstat(temporary);
181
+ requireValue(sameFile(temporaryInfo, named), 'unsafe_path');
182
+ await plugins.fs.unlink(temporary);
183
+ }
184
+ } finally {
185
+ this.busy = false;
186
+ }
187
+ }
188
+ }
189
+
190
+ private async admit(state: ISystemdUnitState): Promise<ISystemdUnitConfigurationState> {
191
+ requireValue(
192
+ quiet(state) && (masked(state) ||
193
+ state.loadState === 'loaded' &&
194
+ ['enabled', 'enabled-runtime', 'disabled'].includes(state.unitFileState) ||
195
+ state.loadState === 'not-found' && state.unitFileState === ''),
196
+ 'unsafe_unit',
197
+ );
198
+ const configuration = await this.unit.inspectConfiguration();
199
+ const maskPath = plugins.path.join(
200
+ state.unitFileState === 'masked-runtime' ? '/run/systemd/system' : '/etc/systemd/system',
201
+ this.name,
202
+ );
203
+ requireValue(
204
+ !configuration.needsReload && configuration.dropInPaths === '' &&
205
+ (masked(state)
206
+ ? configuration.fragmentPath !== this.path &&
207
+ ['/dev/null', maskPath].includes(configuration.fragmentPath)
208
+ : configuration.fragmentPath === (state.loadState === 'not-found' ? '' : this.path)),
209
+ 'unsafe_unit',
210
+ );
211
+ return configuration;
212
+ }
213
+
214
+ private async directories(create: boolean): Promise<boolean> {
215
+ requireValue(process.platform === 'linux' && process.getuid?.() === this.uid, 'unsafe_path');
216
+ let directory = '/';
217
+ for (const part of this.directory.split('/').filter(Boolean)) {
218
+ directory = plugins.path.join(directory, part);
219
+ let info: plugins.Stats;
220
+ try {
221
+ info = await plugins.fs.lstat(directory);
222
+ } catch (error) {
223
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
224
+ if (!create) return false;
225
+ await plugins.fs.mkdir(directory, { mode: 0o755 });
226
+ const parent = await plugins.fs.open(
227
+ plugins.path.dirname(directory),
228
+ plugins.fsConstants.O_RDONLY | plugins.fsConstants.O_DIRECTORY |
229
+ plugins.fsConstants.O_NOFOLLOW,
230
+ );
231
+ try {
232
+ await parent.sync();
233
+ } finally {
234
+ await parent.close();
235
+ }
236
+ info = await plugins.fs.lstat(directory);
237
+ }
238
+ requireValue(
239
+ info.isDirectory() && !info.isSymbolicLink() && [0, this.uid].includes(info.uid) &&
240
+ ((info.mode & 0o022) === 0 || directory === '/tmp' && info.uid === 0 &&
241
+ (info.mode & 0o7777) === 0o1777),
242
+ 'unsafe_path',
243
+ );
244
+ }
245
+ return true;
246
+ }
247
+
248
+ private async read(path: string): Promise<{ bytes: Buffer; info: plugins.Stats } | null> {
249
+ let file: Awaited<ReturnType<typeof plugins.fs.open>>;
250
+ try {
251
+ file = await plugins.fs.open(
252
+ path,
253
+ plugins.fsConstants.O_RDONLY | plugins.fsConstants.O_NOFOLLOW |
254
+ plugins.fsConstants.O_NONBLOCK,
255
+ );
256
+ } catch (error) {
257
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
258
+ throw new SystemdUnitFileError('unsafe_path');
259
+ }
260
+ try {
261
+ const info = await file.stat();
262
+ requireValue(
263
+ info.isFile() && info.uid === this.uid && info.nlink === 1 &&
264
+ (info.mode & 0o7777) === 0o644 && info.size <= 16384,
265
+ 'unsafe_path',
266
+ );
267
+ const bytes = Buffer.alloc(16385);
268
+ let size = 0;
269
+ for (;;) {
270
+ const result = await file.read(bytes, size, bytes.length - size, size);
271
+ if (result.bytesRead === 0) break;
272
+ size += result.bytesRead;
273
+ requireValue(size <= 16384, 'unsafe_path');
274
+ }
275
+ const after = await file.stat(), named = await plugins.fs.lstat(path);
276
+ requireValue(
277
+ sameFile(info, named) && !named.isSymbolicLink() && after.size === size &&
278
+ info.size === after.size && info.mtimeMs === after.mtimeMs &&
279
+ info.ctimeMs === after.ctimeMs,
280
+ 'changed',
281
+ );
282
+ return { bytes: bytes.subarray(0, size), info };
283
+ } finally {
284
+ await file.close();
285
+ }
286
+ }
287
+
288
+ private async create(path: string, bytes: Uint8Array): Promise<void> {
289
+ const file = await plugins.fs.open(
290
+ path,
291
+ plugins.fsConstants.O_CREAT | plugins.fsConstants.O_EXCL |
292
+ plugins.fsConstants.O_WRONLY | plugins.fsConstants.O_NOFOLLOW,
293
+ 0o644,
294
+ );
295
+ let created: plugins.Stats | undefined;
296
+ try {
297
+ try {
298
+ created = await file.stat();
299
+ await file.chmod(0o644);
300
+ await file.writeFile(bytes);
301
+ await file.sync();
302
+ } finally {
303
+ await file.close();
304
+ }
305
+ } catch (error) {
306
+ if (created) {
307
+ requireValue(sameFile(created, await plugins.fs.lstat(path)), 'unsafe_path');
308
+ await plugins.fs.unlink(path);
309
+ }
310
+ throw error;
311
+ }
312
+ }
313
+
314
+ private async flushDirectory(expected: plugins.Stats): Promise<void> {
315
+ const directory = await plugins.fs.open(
316
+ this.directory,
317
+ plugins.fsConstants.O_RDONLY |
318
+ plugins.fsConstants.O_DIRECTORY | plugins.fsConstants.O_NOFOLLOW,
319
+ );
320
+ try {
321
+ requireValue(sameFile(expected, await directory.stat()), 'unsafe_path');
322
+ await directory.sync();
323
+ } finally {
324
+ await directory.close();
325
+ }
326
+ }
327
+ }
package/ts/index.ts CHANGED
@@ -1,3 +1,5 @@
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';
@@ -1,8 +1,12 @@
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 type { Stats } from 'node:fs';
4
8
 
5
- export { path, childProcess };
9
+ export { path, childProcess, fs, fsConstants, crypto };
6
10
 
7
11
  // @pushrocks scope
8
12
  import * as lik from '@push.rocks/lik';