@push.rocks/smartdaemon 2.2.0 → 2.3.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
@@ -19,18 +19,38 @@
19
19
  ]
20
20
  },
21
21
  "release": {
22
- "registries": [
23
- "https://verdaccio.lossless.digital",
24
- "https://registry.npmjs.org"
25
- ],
26
- "accessLevel": "public"
27
- }
22
+ "preflight": {
23
+ "requireCleanTree": true,
24
+ "test": true,
25
+ "build": true
26
+ },
27
+ "targets": {
28
+ "git": {
29
+ "enabled": true,
30
+ "remote": "origin",
31
+ "pushBranch": true,
32
+ "pushTags": true
33
+ },
34
+ "npm": {
35
+ "enabled": true,
36
+ "registries": [
37
+ "https://verdaccio.lossless.digital",
38
+ "https://registry.npmjs.org"
39
+ ],
40
+ "accessLevel": "public",
41
+ "alreadyPublished": "success"
42
+ },
43
+ "docker": {
44
+ "enabled": false
45
+ }
46
+ }
47
+ },
48
+ "schemaVersion": 2
28
49
  },
29
50
  "@git.zone/tsdoc": {
30
51
  "legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
31
52
  },
32
53
  "@ship.zone/szci": {
33
- "npmGlobalTools": [],
34
- "npmRegistryUrl": "registry.npmjs.org"
54
+ "npmGlobalTools": []
35
55
  }
36
56
  }
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartdaemon',
6
- version: '2.2.0',
6
+ version: '2.3.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,38 @@
1
+ export interface ISystemdUnitOptions {
2
+ unitName: string;
3
+ /** Absolute executable path supplied by trusted owning code. */
4
+ systemctlPath?: string;
5
+ /** Bounds the systemctl client, not the server-side job. Defaults to 120 seconds. */
6
+ commandTimeoutMs?: number;
7
+ }
8
+ export interface ISystemdUnitState {
9
+ readonly unitName: string;
10
+ readonly loadState: string;
11
+ readonly activeState: string;
12
+ readonly subState: string;
13
+ readonly unitFileState: string;
14
+ readonly mainPid: number;
15
+ readonly controlPid: number;
16
+ readonly controlGroup: string;
17
+ readonly killMode: string;
18
+ /** Empty when no systemd job is pending; otherwise systemctl's job description. */
19
+ readonly job: string;
20
+ }
21
+ export declare class SystemdUnitError extends Error {
22
+ readonly code: 'invalid_options' | 'command_failed' | 'invalid_state';
23
+ constructor(code: 'invalid_options' | 'command_failed' | 'invalid_state');
24
+ }
25
+ /** Controls an existing system unit without creating files, changing enablement or escalating privileges. */
26
+ export declare class SystemdUnit {
27
+ private readonly unitName;
28
+ private readonly executable;
29
+ private readonly timeout;
30
+ constructor(optionsArg: ISystemdUnitOptions);
31
+ /** Snapshot only. Callers own application-specific admission and restart policy. */
32
+ inspect(): Promise<ISystemdUnitState>;
33
+ /** Await the systemd job, then inspect. This does not enable or unmask the unit. */
34
+ start(): Promise<ISystemdUnitState>;
35
+ /** Await the systemd job, then inspect. The snapshot alone is not an application data-drain proof. */
36
+ stop(): Promise<ISystemdUnitState>;
37
+ private command;
38
+ }
@@ -0,0 +1,97 @@
1
+ import * as plugins from './smartdaemon.plugins.js';
2
+ export class SystemdUnitError extends Error {
3
+ constructor(code) {
4
+ super(`Systemd unit operation failed: ${code}.`);
5
+ this.code = code;
6
+ this.name = 'SystemdUnitError';
7
+ }
8
+ }
9
+ const properties = [
10
+ 'Id',
11
+ 'LoadState',
12
+ 'ActiveState',
13
+ 'SubState',
14
+ 'UnitFileState',
15
+ 'MainPID',
16
+ 'ControlPID',
17
+ 'ControlGroup',
18
+ 'KillMode',
19
+ 'Job',
20
+ ];
21
+ /** Controls an existing system unit without creating files, changing enablement or escalating privileges. */
22
+ export class SystemdUnit {
23
+ constructor(optionsArg) {
24
+ this.unitName = optionsArg?.unitName;
25
+ this.executable = optionsArg?.systemctlPath ?? '/usr/bin/systemctl';
26
+ this.timeout = optionsArg?.commandTimeoutMs ?? 120000;
27
+ if (typeof this.unitName !== 'string' ||
28
+ !/^[a-zA-Z0-9][a-zA-Z0-9_.:@-]{0,230}\.service$/.test(this.unitName) ||
29
+ typeof this.executable !== 'string' || !plugins.path.isAbsolute(this.executable) ||
30
+ this.executable.includes('\0') || !Number.isSafeInteger(this.timeout) ||
31
+ this.timeout < 1 || this.timeout > 600000)
32
+ throw new SystemdUnitError('invalid_options');
33
+ }
34
+ /** Snapshot only. Callers own application-specific admission and restart policy. */
35
+ async inspect() {
36
+ const stdout = await this.command([
37
+ 'show',
38
+ this.unitName,
39
+ `--property=${properties.join(',')}`,
40
+ '--all',
41
+ ]);
42
+ const fields = new Map();
43
+ for (const line of stdout.trimEnd().split('\n')) {
44
+ const separator = line.indexOf('=');
45
+ const key = line.slice(0, separator);
46
+ if (separator < 1 || !properties.includes(key) || fields.has(key)) {
47
+ throw new SystemdUnitError('invalid_state');
48
+ }
49
+ fields.set(key, line.slice(separator + 1));
50
+ }
51
+ if (fields.size !== properties.length || fields.get('Id') !== this.unitName ||
52
+ ['LoadState', 'ActiveState', 'SubState'].some((key) => !fields.get(key)) ||
53
+ ['MainPID', 'ControlPID'].some((key) => !/^(0|[1-9]\d*)$/.test(fields.get(key)) || !Number.isSafeInteger(Number(fields.get(key)))))
54
+ throw new SystemdUnitError('invalid_state');
55
+ return Object.freeze({
56
+ unitName: this.unitName,
57
+ loadState: fields.get('LoadState'),
58
+ activeState: fields.get('ActiveState'),
59
+ subState: fields.get('SubState'),
60
+ unitFileState: fields.get('UnitFileState'),
61
+ mainPid: Number(fields.get('MainPID')),
62
+ controlPid: Number(fields.get('ControlPID')),
63
+ controlGroup: fields.get('ControlGroup'),
64
+ killMode: fields.get('KillMode'),
65
+ job: fields.get('Job'),
66
+ });
67
+ }
68
+ /** Await the systemd job, then inspect. This does not enable or unmask the unit. */
69
+ async start() {
70
+ await this.command(['start', this.unitName]);
71
+ return this.inspect();
72
+ }
73
+ /** Await the systemd job, then inspect. The snapshot alone is not an application data-drain proof. */
74
+ async stop() {
75
+ await this.command(['stop', this.unitName]);
76
+ return this.inspect();
77
+ }
78
+ command(argsArg) {
79
+ return new Promise((resolve, reject) => {
80
+ plugins.childProcess.execFile(this.executable, ['--system', '--no-ask-password', '--no-pager', ...argsArg], {
81
+ encoding: 'utf8',
82
+ timeout: this.timeout,
83
+ killSignal: 'SIGKILL',
84
+ maxBuffer: 16384,
85
+ windowsHide: true,
86
+ env: { PATH: '/usr/bin:/bin', LANG: 'C', LC_ALL: 'C' },
87
+ }, (error, stdout) => {
88
+ // execFile settles after the client closes. A timed-out client's remote job may continue.
89
+ if (error)
90
+ reject(new SystemdUnitError('command_failed'));
91
+ else
92
+ resolve(stdout);
93
+ });
94
+ });
95
+ }
96
+ }
97
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5zeXN0ZW1kdW5pdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL2NsYXNzZXMuc3lzdGVtZHVuaXQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSwwQkFBMEIsQ0FBQztBQXdCcEQsTUFBTSxPQUFPLGdCQUFpQixTQUFRLEtBQUs7SUFDekMsWUFBNEIsSUFBNEQ7UUFDdEYsS0FBSyxDQUFDLGtDQUFrQyxJQUFJLEdBQUcsQ0FBQyxDQUFDO1FBRHZCLFNBQUksR0FBSixJQUFJLENBQXdEO1FBRXRGLElBQUksQ0FBQyxJQUFJLEdBQUcsa0JBQWtCLENBQUM7SUFDakMsQ0FBQztDQUNGO0FBRUQsTUFBTSxVQUFVLEdBQUc7SUFDakIsSUFBSTtJQUNKLFdBQVc7SUFDWCxhQUFhO0lBQ2IsVUFBVTtJQUNWLGVBQWU7SUFDZixTQUFTO0lBQ1QsWUFBWTtJQUNaLGNBQWM7SUFDZCxVQUFVO0lBQ1YsS0FBSztDQUNHLENBQUM7QUFFWCw2R0FBNkc7QUFDN0csTUFBTSxPQUFPLFdBQVc7SUFLdEIsWUFBWSxVQUErQjtRQUN6QyxJQUFJLENBQUMsUUFBUSxHQUFHLFVBQVUsRUFBRSxRQUFRLENBQUM7UUFDckMsSUFBSSxDQUFDLFVBQVUsR0FBRyxVQUFVLEVBQUUsYUFBYSxJQUFJLG9CQUFvQixDQUFDO1FBQ3BFLElBQUksQ0FBQyxPQUFPLEdBQUcsVUFBVSxFQUFFLGdCQUFnQixJQUFJLE1BQU0sQ0FBQztRQUN0RCxJQUNFLE9BQU8sSUFBSSxDQUFDLFFBQVEsS0FBSyxRQUFRO1lBQ2pDLENBQUMsK0NBQStDLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUM7WUFDcEUsT0FBTyxJQUFJLENBQUMsVUFBVSxLQUFLLFFBQVEsSUFBSSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUM7WUFDaEYsSUFBSSxDQUFDLFVBQVUsQ0FBQyxR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
@@ -1,2 +1,3 @@
1
1
  export * from './smartdaemon.classes.smartdaemon.js';
2
2
  export * from './smartdaemon.classes.service.js';
3
+ export * from './classes.systemdunit.js';
package/dist_ts/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './smartdaemon.classes.smartdaemon.js';
2
2
  export * from './smartdaemon.classes.service.js';
3
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLHNDQUFzQyxDQUFDO0FBQ3JELGNBQWMsa0NBQWtDLENBQUMifQ==
3
+ export * from './classes.systemdunit.js';
4
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLHNDQUFzQyxDQUFDO0FBQ3JELGNBQWMsa0NBQWtDLENBQUM7QUFDakQsY0FBYywwQkFBMEIsQ0FBQyJ9
@@ -1,5 +1,6 @@
1
1
  import * as path from 'path';
2
- export { path };
2
+ import * as childProcess from 'node:child_process';
3
+ export { path, childProcess };
3
4
  import * as lik from '@push.rocks/lik';
4
5
  import * as smartfm from '@push.rocks/smartfm';
5
6
  import * as smartfs from '@push.rocks/smartfs';
@@ -1,6 +1,7 @@
1
1
  // node native scope
2
2
  import * as path from 'path';
3
- export { path };
3
+ import * as childProcess from 'node:child_process';
4
+ export { path, childProcess };
4
5
  // @pushrocks scope
5
6
  import * as lik from '@push.rocks/lik';
6
7
  import * as smartfm from '@push.rocks/smartfm';
@@ -12,4 +13,4 @@ import * as smartshell from '@push.rocks/smartshell';
12
13
  import * as smartsystem from '@push.rocks/smartsystem';
13
14
  export { lik, smartfm, smartfs, smartlog, smartlogDestinationLocal, smartpath, smartshell, smartsystem, };
14
15
  // third party
15
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRkYWVtb24ucGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3NtYXJ0ZGFlbW9uLnBsdWdpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsb0JBQW9CO0FBQ3BCLE9BQU8sS0FBSyxJQUFJLE1BQU0sTUFBTSxDQUFDO0FBRTdCLE9BQU8sRUFBRSxJQUFJLEVBQUUsQ0FBQztBQUVoQixtQkFBbUI7QUFDbkIsT0FBTyxLQUFLLEdBQUcsTUFBTSxpQkFBaUIsQ0FBQztBQUN2QyxPQUFPLEtBQUssT0FBTyxNQUFNLHFCQUFxQixDQUFDO0FBQy9DLE9BQU8sS0FBSyxPQUFPLE1BQU0scUJBQXFCLENBQUM7QUFDL0MsT0FBTyxLQUFLLFFBQVEsTUFBTSxzQkFBc0IsQ0FBQztBQUNqRCxPQUFPLEtBQUssd0JBQXdCLE1BQU0sd0NBQXdDLENBQUM7QUFDbkYsT0FBTyxLQUFLLFNBQVMsTUFBTSx1QkFBdUIsQ0FBQztBQUNuRCxPQUFPLEtBQUssVUFBVSxNQUFNLHdCQUF3QixDQUFDO0FBQ3JELE9BQU8sS0FBSyxXQUFXLE1BQU0seUJBQXlCLENBQUM7QUFFdkQsT0FBTyxFQUNMLEdBQUcsRUFDSCxPQUFPLEVBQ1AsT0FBTyxFQUNQLFFBQVEsRUFDUix3QkFBd0IsRUFDeEIsU0FBUyxFQUNULFVBQVUsRUFDVixXQUFXLEdBQ1osQ0FBQztBQUVGLGNBQWMifQ==
16
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRkYWVtb24ucGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3NtYXJ0ZGFlbW9uLnBsdWdpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsb0JBQW9CO0FBQ3BCLE9BQU8sS0FBSyxJQUFJLE1BQU0sTUFBTSxDQUFDO0FBQzdCLE9BQU8sS0FBSyxZQUFZLE1BQU0sb0JBQW9CLENBQUM7QUFFbkQsT0FBTyxFQUFFLElBQUksRUFBRSxZQUFZLEVBQUUsQ0FBQztBQUU5QixtQkFBbUI7QUFDbkIsT0FBTyxLQUFLLEdBQUcsTUFBTSxpQkFBaUIsQ0FBQztBQUN2QyxPQUFPLEtBQUssT0FBTyxNQUFNLHFCQUFxQixDQUFDO0FBQy9DLE9BQU8sS0FBSyxPQUFPLE1BQU0scUJBQXFCLENBQUM7QUFDL0MsT0FBTyxLQUFLLFFBQVEsTUFBTSxzQkFBc0IsQ0FBQztBQUNqRCxPQUFPLEtBQUssd0JBQXdCLE1BQU0sd0NBQXdDLENBQUM7QUFDbkYsT0FBTyxLQUFLLFNBQVMsTUFBTSx1QkFBdUIsQ0FBQztBQUNuRCxPQUFPLEtBQUssVUFBVSxNQUFNLHdCQUF3QixDQUFDO0FBQ3JELE9BQU8sS0FBSyxXQUFXLE1BQU0seUJBQXlCLENBQUM7QUFFdkQsT0FBTyxFQUNMLEdBQUcsRUFDSCxPQUFPLEVBQ1AsT0FBTyxFQUNQLFFBQVEsRUFDUix3QkFBd0IsRUFDeEIsU0FBUyxFQUNULFVBQVUsRUFDVixXQUFXLEdBQ1osQ0FBQztBQUVGLGNBQWMifQ==
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartdaemon",
3
- "version": "2.2.0",
3
+ "version": "2.3.1",
4
4
  "private": false,
5
5
  "description": "Start scripts as long running daemons and manage them.",
6
6
  "main": "dist_ts/index.js",
@@ -27,9 +27,9 @@
27
27
  "homepage": "https://code.foss.global/push.rocks/smartdaemon",
28
28
  "devDependencies": {
29
29
  "@git.zone/tsbuild": "^4.4.2",
30
- "@git.zone/tsrun": "^2.0.4",
31
- "@git.zone/tstest": "^3.6.6",
32
- "@types/node": "^25.9.1"
30
+ "@git.zone/tsrun": "^2.0.6",
31
+ "@git.zone/tstest": "^4.0.0",
32
+ "@types/node": "^26.4.1"
33
33
  },
34
34
  "dependencies": {
35
35
  "@push.rocks/lik": "^6.4.1",
package/readme.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # @push.rocks/smartdaemon 🚀
2
2
  **Turn your Node.js scripts into production-ready system daemons**
3
3
 
4
+ ## Issue Reporting and Security
5
+
6
+ For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
7
+
4
8
  [![npm version](https://img.shields.io/npm/v/@push.rocks/smartdaemon.svg?style=flat-square)](https://www.npmjs.com/package/@push.rocks/smartdaemon)
5
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
10
 
@@ -147,6 +151,41 @@ class SmartDaemon {
147
151
  }
148
152
  ```
149
153
 
154
+ ### Inspecting and controlling an existing systemd unit
155
+
156
+ `SystemdUnit` works with an exact canonical `.service` name. Construction performs
157
+ no I/O. It does not create or rewrite a unit, enable it, remove a mask, or invoke
158
+ sudo. The caller must already have permission for lifecycle commands.
159
+
160
+ ```typescript
161
+ import { SystemdUnit } from '@push.rocks/smartdaemon';
162
+
163
+ const unit = new SystemdUnit({ unitName: 'smartdaemon_my-api-server.service' });
164
+ const state = await unit.inspect();
165
+ console.log(state.loadState, state.activeState, state.unitFileState);
166
+
167
+ // Application code decides whether stopping or restarting is appropriate.
168
+ // await unit.stop();
169
+ // await unit.start();
170
+ ```
171
+
172
+ Snapshots are immutable and also include `subState`, `mainPid`, `controlPid`,
173
+ `controlGroup`, `killMode`, and `job`. An empty `job` means no systemd job was
174
+ reported. Missing and masked units remain distinct from inactive or disabled
175
+ units. Aliases resolving to a different canonical unit are rejected.
176
+
177
+ `start()` and `stop()` await the systemctl command and return a fresh snapshot.
178
+ They do not establish application readiness or prove database shutdown; callers
179
+ must apply the owning application's lifecycle contract. A command failure or
180
+ timeout rejects with `SystemdUnitError` and a local code, without returning raw
181
+ command diagnostics. The systemd job can continue after a client timeout, so a
182
+ failed command must not be treated as cancelled or successfully completed.
183
+
184
+ Calls use an absolute `systemctlPath` (default `/usr/bin/systemctl`), a clean
185
+ environment, no shell or password prompt, and bounded output. Trusted owning code
186
+ can choose another absolute executable path and `commandTimeoutMs` (default
187
+ 120000, maximum 600000). This timeout bounds the client process only.
188
+
150
189
  ### SmartDaemonService Class
151
190
 
152
191
  ```typescript
@@ -424,4 +463,4 @@ Registered at District court Bremen HRB 35230 HB, Germany
424
463
 
425
464
  For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
426
465
 
427
- By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
466
+ By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartdaemon',
6
- version: '2.2.0',
6
+ version: '2.3.1',
7
7
  description: 'Start scripts as long running daemons and manage them.'
8
8
  }
@@ -0,0 +1,135 @@
1
+ import * as plugins from './smartdaemon.plugins.js';
2
+
3
+ export interface ISystemdUnitOptions {
4
+ unitName: string;
5
+ /** Absolute executable path supplied by trusted owning code. */
6
+ systemctlPath?: string;
7
+ /** Bounds the systemctl client, not the server-side job. Defaults to 120 seconds. */
8
+ commandTimeoutMs?: number;
9
+ }
10
+
11
+ export interface ISystemdUnitState {
12
+ readonly unitName: string;
13
+ readonly loadState: string;
14
+ readonly activeState: string;
15
+ readonly subState: string;
16
+ readonly unitFileState: string;
17
+ readonly mainPid: number;
18
+ readonly controlPid: number;
19
+ readonly controlGroup: string;
20
+ readonly killMode: string;
21
+ /** Empty when no systemd job is pending; otherwise systemctl's job description. */
22
+ readonly job: string;
23
+ }
24
+
25
+ export class SystemdUnitError extends Error {
26
+ constructor(public readonly code: 'invalid_options' | 'command_failed' | 'invalid_state') {
27
+ super(`Systemd unit operation failed: ${code}.`);
28
+ this.name = 'SystemdUnitError';
29
+ }
30
+ }
31
+
32
+ const properties = [
33
+ 'Id',
34
+ 'LoadState',
35
+ 'ActiveState',
36
+ 'SubState',
37
+ 'UnitFileState',
38
+ 'MainPID',
39
+ 'ControlPID',
40
+ 'ControlGroup',
41
+ 'KillMode',
42
+ 'Job',
43
+ ] as const;
44
+
45
+ /** Controls an existing system unit without creating files, changing enablement or escalating privileges. */
46
+ export class SystemdUnit {
47
+ private readonly unitName: string;
48
+ private readonly executable: string;
49
+ private readonly timeout: number;
50
+
51
+ constructor(optionsArg: ISystemdUnitOptions) {
52
+ this.unitName = optionsArg?.unitName;
53
+ this.executable = optionsArg?.systemctlPath ?? '/usr/bin/systemctl';
54
+ this.timeout = optionsArg?.commandTimeoutMs ?? 120000;
55
+ if (
56
+ typeof this.unitName !== 'string' ||
57
+ !/^[a-zA-Z0-9][a-zA-Z0-9_.:@-]{0,230}\.service$/.test(this.unitName) ||
58
+ typeof this.executable !== 'string' || !plugins.path.isAbsolute(this.executable) ||
59
+ this.executable.includes('\0') || !Number.isSafeInteger(this.timeout) ||
60
+ this.timeout < 1 || this.timeout > 600000
61
+ ) throw new SystemdUnitError('invalid_options');
62
+ }
63
+
64
+ /** Snapshot only. Callers own application-specific admission and restart policy. */
65
+ public async inspect(): Promise<ISystemdUnitState> {
66
+ const stdout = await this.command([
67
+ 'show',
68
+ this.unitName,
69
+ `--property=${properties.join(',')}`,
70
+ '--all',
71
+ ]);
72
+ const fields = new Map<string, string>();
73
+ for (const line of stdout.trimEnd().split('\n')) {
74
+ const separator = line.indexOf('=');
75
+ const key = line.slice(0, separator);
76
+ if (separator < 1 || !(properties as readonly string[]).includes(key) || fields.has(key)) {
77
+ throw new SystemdUnitError('invalid_state');
78
+ }
79
+ fields.set(key, line.slice(separator + 1));
80
+ }
81
+ if (
82
+ fields.size !== properties.length || fields.get('Id') !== this.unitName ||
83
+ ['LoadState', 'ActiveState', 'SubState'].some((key) => !fields.get(key)) ||
84
+ ['MainPID', 'ControlPID'].some((key) =>
85
+ !/^(0|[1-9]\d*)$/.test(fields.get(key)!) || !Number.isSafeInteger(Number(fields.get(key)))
86
+ )
87
+ ) throw new SystemdUnitError('invalid_state');
88
+ return Object.freeze({
89
+ unitName: this.unitName,
90
+ loadState: fields.get('LoadState')!,
91
+ activeState: fields.get('ActiveState')!,
92
+ subState: fields.get('SubState')!,
93
+ unitFileState: fields.get('UnitFileState')!,
94
+ mainPid: Number(fields.get('MainPID')),
95
+ controlPid: Number(fields.get('ControlPID')),
96
+ controlGroup: fields.get('ControlGroup')!,
97
+ killMode: fields.get('KillMode')!,
98
+ job: fields.get('Job')!,
99
+ });
100
+ }
101
+
102
+ /** Await the systemd job, then inspect. This does not enable or unmask the unit. */
103
+ public async start(): Promise<ISystemdUnitState> {
104
+ await this.command(['start', this.unitName]);
105
+ return this.inspect();
106
+ }
107
+
108
+ /** Await the systemd job, then inspect. The snapshot alone is not an application data-drain proof. */
109
+ public async stop(): Promise<ISystemdUnitState> {
110
+ await this.command(['stop', this.unitName]);
111
+ return this.inspect();
112
+ }
113
+
114
+ private command(argsArg: string[]): Promise<string> {
115
+ return new Promise((resolve, reject) => {
116
+ plugins.childProcess.execFile(
117
+ this.executable,
118
+ ['--system', '--no-ask-password', '--no-pager', ...argsArg],
119
+ {
120
+ encoding: 'utf8',
121
+ timeout: this.timeout,
122
+ killSignal: 'SIGKILL',
123
+ maxBuffer: 16384,
124
+ windowsHide: true,
125
+ env: { PATH: '/usr/bin:/bin', LANG: 'C', LC_ALL: 'C' },
126
+ },
127
+ (error, stdout) => {
128
+ // execFile settles after the client closes. A timed-out client's remote job may continue.
129
+ if (error) reject(new SystemdUnitError('command_failed'));
130
+ else resolve(stdout);
131
+ },
132
+ );
133
+ });
134
+ }
135
+ }
package/ts/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './smartdaemon.classes.smartdaemon.js';
2
2
  export * from './smartdaemon.classes.service.js';
3
+ export * from './classes.systemdunit.js';
@@ -1,7 +1,8 @@
1
1
  // node native scope
2
2
  import * as path from 'path';
3
+ import * as childProcess from 'node:child_process';
3
4
 
4
- export { path };
5
+ export { path, childProcess };
5
6
 
6
7
  // @pushrocks scope
7
8
  import * as lik from '@push.rocks/lik';
package/readme.hints.md DELETED
@@ -1 +0,0 @@
1
-