systemd-ts 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +285 -24
- package/dist/index.d.mts +241 -6
- package/dist/index.mjs +582 -97
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
|
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { execFile } from "node:child_process";
|
|
5
|
-
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
//#region src/main/errors.ts
|
|
@@ -144,6 +144,30 @@ var NotifySendError = class extends SystemdTsError {
|
|
|
144
144
|
this.stdout = details.stdout;
|
|
145
145
|
}
|
|
146
146
|
};
|
|
147
|
+
var SystemctlCommandError = class extends SystemdTsError {
|
|
148
|
+
args;
|
|
149
|
+
command;
|
|
150
|
+
environmentReason;
|
|
151
|
+
exitCode;
|
|
152
|
+
operation;
|
|
153
|
+
reason;
|
|
154
|
+
stage;
|
|
155
|
+
stderr;
|
|
156
|
+
stdout;
|
|
157
|
+
constructor(message, options = {}) {
|
|
158
|
+
super(`SYSTEMD_TS_SYSTEMCTL`, message, options);
|
|
159
|
+
const details = extractCommandErrorDetails(options.cause);
|
|
160
|
+
this.args = options.args;
|
|
161
|
+
this.command = options.command;
|
|
162
|
+
this.environmentReason = options.environmentReason ?? classifyCommandEnvironmentReason(options.cause);
|
|
163
|
+
this.exitCode = details.exitCode;
|
|
164
|
+
this.operation = options.operation;
|
|
165
|
+
this.reason = options.reason;
|
|
166
|
+
this.stage = options.stage;
|
|
167
|
+
this.stderr = details.stderr;
|
|
168
|
+
this.stdout = details.stdout;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
147
171
|
function classifyCommandEnvironmentReason(cause) {
|
|
148
172
|
if (cause === null || typeof cause !== `object`) return;
|
|
149
173
|
const record = cause;
|
|
@@ -245,7 +269,7 @@ var Executable = class {
|
|
|
245
269
|
if (!commandParts.ok) return commandParts;
|
|
246
270
|
return {
|
|
247
271
|
ok: true,
|
|
248
|
-
value: commandParts.value.map(shellQuote
|
|
272
|
+
value: commandParts.value.map(shellQuote).join(` `)
|
|
249
273
|
};
|
|
250
274
|
}
|
|
251
275
|
};
|
|
@@ -295,7 +319,7 @@ function isMainModule(modulePath) {
|
|
|
295
319
|
if (mainArg === void 0) return false;
|
|
296
320
|
return normalizeFilePath(mainArg) === normalizeFilePath(modulePath);
|
|
297
321
|
}
|
|
298
|
-
function shellQuote
|
|
322
|
+
function shellQuote(value) {
|
|
299
323
|
return `'${value.replaceAll(`'`, `'\\''`)}'`;
|
|
300
324
|
}
|
|
301
325
|
function normalizeFilePath(path) {
|
|
@@ -311,6 +335,7 @@ var internal_exports = /* @__PURE__ */ __exportAll({
|
|
|
311
335
|
cloneUnitSection: () => cloneUnitSection,
|
|
312
336
|
defaultCommandExecutor: () => defaultCommandExecutor,
|
|
313
337
|
defaultUnitDirForScope: () => defaultUnitDirForScope,
|
|
338
|
+
extractCommandOutput: () => extractCommandOutput,
|
|
314
339
|
fileExists: () => fileExists,
|
|
315
340
|
freezeUnitOptions: () => freezeUnitOptions,
|
|
316
341
|
normalizeUnitName: () => normalizeUnitName,
|
|
@@ -318,7 +343,6 @@ var internal_exports = /* @__PURE__ */ __exportAll({
|
|
|
318
343
|
renderUnitFile: () => renderUnitFile,
|
|
319
344
|
resolveTimerTargetUnit: () => resolveTimerTargetUnit,
|
|
320
345
|
sendNotify: () => sendNotify,
|
|
321
|
-
shellQuote: () => shellQuote,
|
|
322
346
|
validateServiceSection: () => validateServiceSection
|
|
323
347
|
});
|
|
324
348
|
const EXEC_DIRECTIVE_KEYS = [
|
|
@@ -422,11 +446,11 @@ function renderUnitFile(sections) {
|
|
|
422
446
|
};
|
|
423
447
|
}
|
|
424
448
|
}
|
|
425
|
-
function
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
449
|
+
async function defaultCommandExecutor(command, args, options = {}) {
|
|
450
|
+
const result = await execFileAsync(command, [...args], options.env === void 0 ? {} : { env: {
|
|
451
|
+
...process.env,
|
|
452
|
+
...options.env
|
|
453
|
+
} });
|
|
430
454
|
return {
|
|
431
455
|
stderr: result.stderr,
|
|
432
456
|
stdout: result.stdout
|
|
@@ -441,18 +465,17 @@ async function fileExists(path) {
|
|
|
441
465
|
}
|
|
442
466
|
}
|
|
443
467
|
async function sendNotify(state, options) {
|
|
444
|
-
const args = [state];
|
|
468
|
+
const args = [`--no-block`, state];
|
|
445
469
|
if (options.pid !== void 0) args.push(`MAINPID=${options.pid}`);
|
|
446
470
|
if (options.status !== void 0) args.push(`STATUS=${options.status}`);
|
|
447
471
|
if (options.executor !== void 0) {
|
|
448
|
-
const command = buildNotifyShellCommand(args, options.socketPath);
|
|
449
472
|
try {
|
|
450
|
-
await options.executor(`
|
|
473
|
+
await options.executor(`systemd-notify`, args, options.socketPath === void 0 ? {} : { env: { NOTIFY_SOCKET: options.socketPath } });
|
|
451
474
|
} catch (cause) {
|
|
452
475
|
throw new NotifySendError(`Failed to send systemd notification through the configured executor`, {
|
|
453
|
-
args
|
|
476
|
+
args,
|
|
454
477
|
cause,
|
|
455
|
-
command: `
|
|
478
|
+
command: `systemd-notify`,
|
|
456
479
|
reason: `executor-failed`,
|
|
457
480
|
stage: `executor`
|
|
458
481
|
});
|
|
@@ -474,6 +497,17 @@ async function sendNotify(state, options) {
|
|
|
474
497
|
});
|
|
475
498
|
}
|
|
476
499
|
}
|
|
500
|
+
function extractCommandOutput(cause) {
|
|
501
|
+
if (cause === null || typeof cause !== `object`) return;
|
|
502
|
+
const record = cause;
|
|
503
|
+
const stdout = typeof record[`stdout`] === `string` ? record[`stdout`] : void 0;
|
|
504
|
+
const stderr = typeof record[`stderr`] === `string` ? record[`stderr`] : void 0;
|
|
505
|
+
if (stdout === void 0 && stderr === void 0) return;
|
|
506
|
+
return {
|
|
507
|
+
stderr: stderr ?? ``,
|
|
508
|
+
stdout: stdout ?? ``
|
|
509
|
+
};
|
|
510
|
+
}
|
|
477
511
|
function hasServiceSection(options) {
|
|
478
512
|
return `service` in options;
|
|
479
513
|
}
|
|
@@ -546,9 +580,6 @@ function isAbsoluteExecCommand(value) {
|
|
|
546
580
|
}
|
|
547
581
|
return value[index] === `/`;
|
|
548
582
|
}
|
|
549
|
-
function buildNotifyShellCommand(args, socketPath) {
|
|
550
|
-
return `${socketPath === void 0 ? `` : `NOTIFY_SOCKET=${shellQuote(socketPath)} `}${[`systemd-notify`, ...args].map(shellQuote).join(` `)}`;
|
|
551
|
-
}
|
|
552
583
|
//#endregion
|
|
553
584
|
//#region src/main/notify.ts
|
|
554
585
|
/**
|
|
@@ -730,6 +761,224 @@ var SystemdTimer = class {
|
|
|
730
761
|
}
|
|
731
762
|
};
|
|
732
763
|
//#endregion
|
|
764
|
+
//#region src/main/systemctl.ts
|
|
765
|
+
/**
|
|
766
|
+
* A TypeScript-first client for invoking `systemctl` with structured argument
|
|
767
|
+
* vectors instead of shell-composed command strings.
|
|
768
|
+
*
|
|
769
|
+
* This lower-level primitive is useful both on its own and as the subprocess
|
|
770
|
+
* transport underneath higher-level abstractions such as {@link Systemd}.
|
|
771
|
+
*/
|
|
772
|
+
var Systemctl = class {
|
|
773
|
+
/** Command executor used for `systemctl` subprocesses. */
|
|
774
|
+
executor;
|
|
775
|
+
/** The target manager scope, either `system` or `user`. */
|
|
776
|
+
scope;
|
|
777
|
+
constructor(options = {}) {
|
|
778
|
+
this.scope = options.scope ?? `system`;
|
|
779
|
+
this.executor = options.executor ?? defaultCommandExecutor;
|
|
780
|
+
Object.freeze(this);
|
|
781
|
+
}
|
|
782
|
+
/** Reloads the targeted systemd manager. */
|
|
783
|
+
async daemonReload() {
|
|
784
|
+
return this.run(`daemon-reload`);
|
|
785
|
+
}
|
|
786
|
+
/** Enables a unit by filename. */
|
|
787
|
+
async enable(unit) {
|
|
788
|
+
return this.run(`enable`, unit);
|
|
789
|
+
}
|
|
790
|
+
/** Disables a unit by filename. */
|
|
791
|
+
async disable(unit) {
|
|
792
|
+
return this.run(`disable`, unit);
|
|
793
|
+
}
|
|
794
|
+
/** Links a unit file path into the targeted manager. */
|
|
795
|
+
async link(path) {
|
|
796
|
+
return this.run(`link`, path);
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Checks the install-time enablement state of a unit file.
|
|
800
|
+
*
|
|
801
|
+
* Source:
|
|
802
|
+
* - systemd v260.1, `systemctl(1)`
|
|
803
|
+
*/
|
|
804
|
+
async isEnabled(unit, options = {}) {
|
|
805
|
+
const output = await this.run(`is-enabled`, ...options.full ? [`--full`] : [], ...options.quiet ? [`--quiet`] : [], unit);
|
|
806
|
+
if (!output.ok) return output;
|
|
807
|
+
return ok$1(firstNonEmptyOutputLine(output.value));
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Checks the runtime active state of a unit.
|
|
811
|
+
*
|
|
812
|
+
* Unless `quiet` is set, this returns the state string printed by
|
|
813
|
+
* `systemctl is-active`.
|
|
814
|
+
*/
|
|
815
|
+
async isActive(unit, options = {}) {
|
|
816
|
+
const output = await this.run(`is-active`, ...options.quiet ? [`--quiet`] : [], unit);
|
|
817
|
+
if (!output.ok) return output;
|
|
818
|
+
return ok$1(firstNonEmptyOutputLine(output.value));
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Checks whether a unit is in the failed state.
|
|
822
|
+
*
|
|
823
|
+
* Unless `quiet` is set, this returns the state string printed by
|
|
824
|
+
* `systemctl is-failed`.
|
|
825
|
+
*/
|
|
826
|
+
async isFailed(unit, options = {}) {
|
|
827
|
+
const output = await this.run(`is-failed`, ...options.quiet ? [`--quiet`] : [], unit);
|
|
828
|
+
if (!output.ok) return output;
|
|
829
|
+
return ok$1(firstNonEmptyOutputLine(output.value));
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Queries the overall manager state reported by `systemctl is-system-running`.
|
|
833
|
+
*
|
|
834
|
+
* Source:
|
|
835
|
+
* - systemd v260.1, `systemctl(1)`
|
|
836
|
+
*/
|
|
837
|
+
async isSystemRunning(options = {}) {
|
|
838
|
+
const output = await this.run(`is-system-running`, ...options.wait ? [`--wait`] : []);
|
|
839
|
+
if (!output.ok) return output;
|
|
840
|
+
return ok$1(firstNonEmptyOutputLine(output.value));
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Queries `systemctl show` for a unit.
|
|
844
|
+
*
|
|
845
|
+
* When `properties` are provided, only those properties are requested.
|
|
846
|
+
*/
|
|
847
|
+
async show(unit, options = {}) {
|
|
848
|
+
const propertyFlag = options.properties === void 0 || options.properties.length === 0 ? [] : [`--property=${options.properties.join(`,`)}`];
|
|
849
|
+
return this.run(`show`, unit, ...propertyFlag);
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Queries `systemctl show` and parses the result into a key-value object.
|
|
853
|
+
*
|
|
854
|
+
* Properties are returned exactly as printed by `systemctl show`.
|
|
855
|
+
*/
|
|
856
|
+
async showProperties(unit, options = {}) {
|
|
857
|
+
const output = await this.show(unit, options);
|
|
858
|
+
if (!output.ok) return output;
|
|
859
|
+
return ok$1(parseSystemctlShowOutput(output.value.stdout));
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Queries the standard status-related `show` properties for a unit and
|
|
863
|
+
* returns a parsed snapshot.
|
|
864
|
+
*/
|
|
865
|
+
async showStatus(unit) {
|
|
866
|
+
return this.showServiceStatus(unit);
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Queries the standard service-status `show` properties for a service unit
|
|
870
|
+
* and returns a parsed snapshot.
|
|
871
|
+
*/
|
|
872
|
+
async showServiceStatus(unit) {
|
|
873
|
+
const output = await this.show(unit, { properties: [
|
|
874
|
+
`Id`,
|
|
875
|
+
`ActiveState`,
|
|
876
|
+
`SubState`,
|
|
877
|
+
`Result`,
|
|
878
|
+
`ExecMainStatus`
|
|
879
|
+
] });
|
|
880
|
+
if (!output.ok) return output;
|
|
881
|
+
return ok$1(parseStartStatus(unit, output.value.stdout));
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Queries `systemctl list-timers` and returns the observed JSON payload
|
|
885
|
+
* shape directly, with raw integer timestamps left intact.
|
|
886
|
+
*/
|
|
887
|
+
async listTimers(options = {}) {
|
|
888
|
+
const args = [
|
|
889
|
+
`list-timers`,
|
|
890
|
+
...options.all ? [`--all`] : [],
|
|
891
|
+
...options.noPager ?? true ? [`--no-pager`] : [],
|
|
892
|
+
`--output=json`,
|
|
893
|
+
...options.patterns ?? []
|
|
894
|
+
];
|
|
895
|
+
const output = await this.run(...args);
|
|
896
|
+
if (!output.ok) return output;
|
|
897
|
+
try {
|
|
898
|
+
return ok$1(JSON.parse(output.value.stdout));
|
|
899
|
+
} catch (cause) {
|
|
900
|
+
return err$1(new SystemctlCommandError(`Failed to parse JSON output from systemctl list-timers`, {
|
|
901
|
+
args: [...this.scopeArgs(), ...args],
|
|
902
|
+
cause,
|
|
903
|
+
command: `systemctl`,
|
|
904
|
+
operation: `list-timers`,
|
|
905
|
+
reason: `invalid-json`
|
|
906
|
+
}));
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
/** Starts a unit by filename. */
|
|
910
|
+
async start(unit) {
|
|
911
|
+
return this.run(`start`, unit);
|
|
912
|
+
}
|
|
913
|
+
/** Stops a unit by filename. */
|
|
914
|
+
async stop(unit) {
|
|
915
|
+
return this.run(`stop`, unit);
|
|
916
|
+
}
|
|
917
|
+
/** Restarts a unit by filename. */
|
|
918
|
+
async restart(unit) {
|
|
919
|
+
return this.run(`restart`, unit);
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Resets the failed state for matching units, or for all units when no
|
|
923
|
+
* patterns are provided.
|
|
924
|
+
*/
|
|
925
|
+
async resetFailed(patterns = []) {
|
|
926
|
+
return this.run(`reset-failed`, ...patterns);
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Queries `systemctl status` for a unit.
|
|
930
|
+
*
|
|
931
|
+
* This is primarily a human-oriented command and is best paired with
|
|
932
|
+
* `show()` when structured state is needed.
|
|
933
|
+
*/
|
|
934
|
+
async status(unit, options = {}) {
|
|
935
|
+
const args = [`status`, unit];
|
|
936
|
+
if (options.noPager ?? true) args.push(`--no-pager`);
|
|
937
|
+
if (options.lines !== void 0) args.push(`--lines`, String(options.lines));
|
|
938
|
+
return this.run(...args);
|
|
939
|
+
}
|
|
940
|
+
/** Executes a raw `systemctl` invocation with scope-aware arguments. */
|
|
941
|
+
async run(...args) {
|
|
942
|
+
const scopedArgs = [...this.scopeArgs(), ...args];
|
|
943
|
+
try {
|
|
944
|
+
return ok$1(await this.executor(`systemctl`, scopedArgs));
|
|
945
|
+
} catch (cause) {
|
|
946
|
+
return err$1(new SystemctlCommandError(`Failed to run systemctl ${args[0] ?? `command`}`, {
|
|
947
|
+
args: scopedArgs,
|
|
948
|
+
cause,
|
|
949
|
+
command: `systemctl`,
|
|
950
|
+
operation: args[0],
|
|
951
|
+
reason: `executor-failed`
|
|
952
|
+
}));
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
scopeArgs() {
|
|
956
|
+
return this.scope === `user` ? [`--user`] : [];
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
function ok$1(value) {
|
|
960
|
+
return {
|
|
961
|
+
ok: true,
|
|
962
|
+
value
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
function err$1(error) {
|
|
966
|
+
return {
|
|
967
|
+
ok: false,
|
|
968
|
+
error
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
function parseSystemctlShowOutput(output) {
|
|
972
|
+
return Object.freeze(Object.fromEntries(output.split(`\n`).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => {
|
|
973
|
+
const separatorIndex = line.indexOf(`=`);
|
|
974
|
+
if (separatorIndex === -1) return [line, ``];
|
|
975
|
+
return [line.slice(0, separatorIndex), line.slice(separatorIndex + 1)];
|
|
976
|
+
})));
|
|
977
|
+
}
|
|
978
|
+
function firstNonEmptyOutputLine(output) {
|
|
979
|
+
return [output.stdout, output.stderr].flatMap((value) => value.split(`\n`)).map((value) => value.trim()).find((value) => value.length > 0) ?? ``;
|
|
980
|
+
}
|
|
981
|
+
//#endregion
|
|
733
982
|
//#region src/main/systemd.ts
|
|
734
983
|
/**
|
|
735
984
|
* A successful systemd unit materialization.
|
|
@@ -765,6 +1014,10 @@ var Systemd = class {
|
|
|
765
1014
|
linkUnits;
|
|
766
1015
|
/** The target manager scope, either `system` or `user`. */
|
|
767
1016
|
scope;
|
|
1017
|
+
/** The low-level `systemctl` client for this configured target. */
|
|
1018
|
+
systemctl;
|
|
1019
|
+
/** Higher-level `systemd-ts` upkeep helpers for application-owned units. */
|
|
1020
|
+
ts;
|
|
768
1021
|
/** The directory used when materializing unit files. */
|
|
769
1022
|
unitDir;
|
|
770
1023
|
/**
|
|
@@ -779,6 +1032,11 @@ var Systemd = class {
|
|
|
779
1032
|
this.unitDir = options.unitDir ?? defaultUnitDirForScope(this.scope);
|
|
780
1033
|
this.linkUnits = options.linkUnits ?? false;
|
|
781
1034
|
this.executor = options.executor ?? defaultCommandExecutor;
|
|
1035
|
+
this.systemctl = new Systemctl({
|
|
1036
|
+
executor: this.executor,
|
|
1037
|
+
scope: this.scope
|
|
1038
|
+
});
|
|
1039
|
+
this.ts = new SystemdTs(this);
|
|
782
1040
|
Object.freeze(this);
|
|
783
1041
|
}
|
|
784
1042
|
/**
|
|
@@ -816,16 +1074,17 @@ var Systemd = class {
|
|
|
816
1074
|
`enable`,
|
|
817
1075
|
unit.filename
|
|
818
1076
|
];
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
return err(new UnitEnableError(`Failed to enable ${unit.filename}`, {
|
|
1077
|
+
const enabled = await this.systemctl.enable(unit.filename);
|
|
1078
|
+
if (!enabled.ok) {
|
|
1079
|
+
const errorOptions = {
|
|
823
1080
|
args,
|
|
824
|
-
cause,
|
|
1081
|
+
cause: enabled.error,
|
|
825
1082
|
command: `systemctl`,
|
|
826
1083
|
stage: `enable`,
|
|
827
|
-
unitName: unit.filename
|
|
828
|
-
|
|
1084
|
+
unitName: unit.filename,
|
|
1085
|
+
...enabled.error.environmentReason === void 0 ? {} : { environmentReason: enabled.error.environmentReason }
|
|
1086
|
+
};
|
|
1087
|
+
return err(new UnitEnableError(`Failed to enable ${unit.filename}`, errorOptions));
|
|
829
1088
|
}
|
|
830
1089
|
}
|
|
831
1090
|
return ok(void 0);
|
|
@@ -855,38 +1114,38 @@ var Systemd = class {
|
|
|
855
1114
|
`start`,
|
|
856
1115
|
unit.filename
|
|
857
1116
|
];
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
return err(new UnitStartError(`Failed to start ${unit.filename}`, {
|
|
1117
|
+
const started = await this.systemctl.start(unit.filename);
|
|
1118
|
+
if (!started.ok) {
|
|
1119
|
+
const errorOptions = {
|
|
862
1120
|
args: startArgs,
|
|
863
|
-
cause,
|
|
1121
|
+
cause: started.error,
|
|
864
1122
|
command: `systemctl`,
|
|
865
1123
|
diagnostics: await this.collectStartDiagnostics(scopeArgs, unit.filename),
|
|
866
1124
|
stage: `start`,
|
|
867
|
-
unitName: unit.filename
|
|
868
|
-
|
|
1125
|
+
unitName: unit.filename,
|
|
1126
|
+
...started.error.environmentReason === void 0 ? {} : { environmentReason: started.error.environmentReason }
|
|
1127
|
+
};
|
|
1128
|
+
return err(new UnitStartError(`Failed to start ${unit.filename}`, errorOptions));
|
|
869
1129
|
}
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
return err(new UnitStartError(`Started ${unit.filename} but failed to query its status`, {
|
|
881
|
-
args: statusArgs,
|
|
882
|
-
cause,
|
|
1130
|
+
const status = await this.systemctl.showServiceStatus(unit.filename);
|
|
1131
|
+
if (!status.ok) {
|
|
1132
|
+
const errorOptions = {
|
|
1133
|
+
args: [
|
|
1134
|
+
...scopeArgs,
|
|
1135
|
+
`show`,
|
|
1136
|
+
unit.filename,
|
|
1137
|
+
`--property=Id,ActiveState,SubState,Result,ExecMainStatus`
|
|
1138
|
+
],
|
|
1139
|
+
cause: status.error,
|
|
883
1140
|
command: `systemctl`,
|
|
884
1141
|
diagnostics: await this.collectStartDiagnostics(scopeArgs, unit.filename),
|
|
885
1142
|
stage: `show-status`,
|
|
886
|
-
unitName: unit.filename
|
|
887
|
-
|
|
1143
|
+
unitName: unit.filename,
|
|
1144
|
+
...status.error.environmentReason === void 0 ? {} : { environmentReason: status.error.environmentReason }
|
|
1145
|
+
};
|
|
1146
|
+
return err(new UnitStartError(`Started ${unit.filename} but failed to query its status`, errorOptions));
|
|
888
1147
|
}
|
|
889
|
-
return ok(
|
|
1148
|
+
return ok(status.value);
|
|
890
1149
|
}
|
|
891
1150
|
/**
|
|
892
1151
|
* Reads recent output for a managed unit.
|
|
@@ -910,29 +1169,26 @@ var Systemd = class {
|
|
|
910
1169
|
}
|
|
911
1170
|
const scopeArgs = this.scopeArgs();
|
|
912
1171
|
const lines = options?.lines ?? 50;
|
|
913
|
-
const
|
|
914
|
-
|
|
1172
|
+
const logs = await this.systemctl.status(unit.filename, { lines });
|
|
1173
|
+
if (logs.ok) return ok(joinCommandOutput(logs.value));
|
|
1174
|
+
const errorOutput = extractCommandOutput(logs.error);
|
|
1175
|
+
if (errorOutput !== void 0) return ok(joinCommandOutput(errorOutput));
|
|
1176
|
+
const args = [
|
|
915
1177
|
...scopeArgs,
|
|
916
1178
|
`status`,
|
|
917
1179
|
unit.filename,
|
|
918
1180
|
`--no-pager`,
|
|
919
1181
|
`--lines`,
|
|
920
1182
|
String(lines)
|
|
921
|
-
]
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
logs
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
reason: `status-command-failed`,
|
|
931
|
-
stage: `status`,
|
|
932
|
-
unitName: unit.filename
|
|
933
|
-
}));
|
|
934
|
-
}
|
|
935
|
-
return ok([logs.stdout, logs.stderr].filter((value) => value.length > 0).join(`\n`));
|
|
1183
|
+
];
|
|
1184
|
+
return err(new UnitLogsReadError(`Failed to query logs for ${unit.filename} from systemctl status`, {
|
|
1185
|
+
args,
|
|
1186
|
+
cause: logs.error,
|
|
1187
|
+
command: `systemctl`,
|
|
1188
|
+
reason: `status-command-failed`,
|
|
1189
|
+
stage: `status`,
|
|
1190
|
+
unitName: unit.filename
|
|
1191
|
+
}));
|
|
936
1192
|
}
|
|
937
1193
|
/** Returns the on-disk unit-file path this instance uses for the given unit. */
|
|
938
1194
|
pathFor(unit) {
|
|
@@ -947,39 +1203,34 @@ var Systemd = class {
|
|
|
947
1203
|
`link`,
|
|
948
1204
|
path
|
|
949
1205
|
];
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
} catch (cause) {
|
|
1206
|
+
const linked = await this.systemctl.link(path);
|
|
1207
|
+
if (!linked.ok) {
|
|
953
1208
|
const linkedUnit = units.find((candidate) => this.pathFor(candidate) === path);
|
|
954
1209
|
const errorContext = {
|
|
955
1210
|
args,
|
|
956
|
-
cause,
|
|
1211
|
+
cause: linked.error,
|
|
957
1212
|
command: `systemctl`,
|
|
958
1213
|
stage: `link`,
|
|
959
|
-
|
|
960
|
-
|
|
1214
|
+
unitPath: path,
|
|
1215
|
+
...linked.error.environmentReason === void 0 ? {} : { environmentReason: linked.error.environmentReason },
|
|
1216
|
+
...linkedUnit === void 0 ? {} : { unitName: linkedUnit.filename }
|
|
961
1217
|
};
|
|
962
1218
|
throw operation === `enable` ? new UnitEnableError(`Failed to link ${path} before enable`, errorContext) : new UnitStartError(`Failed to link ${path} before start`, errorContext);
|
|
963
1219
|
}
|
|
964
1220
|
}
|
|
965
1221
|
}
|
|
966
1222
|
const args = [...scopeArgs, `daemon-reload`];
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
throw operation === `enable` ? new UnitEnableError(`Failed to reload systemd before enable`, {
|
|
971
|
-
args,
|
|
972
|
-
cause,
|
|
973
|
-
command: `systemctl`,
|
|
974
|
-
stage: `daemon-reload`,
|
|
975
|
-
unitName: units[0]?.filename
|
|
976
|
-
}) : new UnitStartError(`Failed to reload systemd before start`, {
|
|
1223
|
+
const reloaded = await this.systemctl.daemonReload();
|
|
1224
|
+
if (!reloaded.ok) {
|
|
1225
|
+
const errorContext = {
|
|
977
1226
|
args,
|
|
978
|
-
cause,
|
|
1227
|
+
cause: reloaded.error,
|
|
979
1228
|
command: `systemctl`,
|
|
980
1229
|
stage: `daemon-reload`,
|
|
981
|
-
|
|
982
|
-
|
|
1230
|
+
...reloaded.error.environmentReason === void 0 ? {} : { environmentReason: reloaded.error.environmentReason },
|
|
1231
|
+
...units[0]?.filename === void 0 ? {} : { unitName: units[0].filename }
|
|
1232
|
+
};
|
|
1233
|
+
throw operation === `enable` ? new UnitEnableError(`Failed to reload systemd before enable`, errorContext) : new UnitStartError(`Failed to reload systemd before start`, errorContext);
|
|
983
1234
|
}
|
|
984
1235
|
}
|
|
985
1236
|
scopeArgs() {
|
|
@@ -987,29 +1238,31 @@ var Systemd = class {
|
|
|
987
1238
|
}
|
|
988
1239
|
async collectStartDiagnostics(scopeArgs, unitName) {
|
|
989
1240
|
const diagnostics = {};
|
|
990
|
-
const
|
|
991
|
-
`systemctl`,
|
|
1241
|
+
const showArgs = [
|
|
992
1242
|
...scopeArgs,
|
|
993
1243
|
`show`,
|
|
994
1244
|
unitName,
|
|
995
1245
|
`--property=Id,ActiveState,SubState,Result,ExecMainStatus`
|
|
996
|
-
]
|
|
997
|
-
const show = await this.tryBestEffortCommand(`
|
|
998
|
-
if (show !== void 0
|
|
999
|
-
|
|
1000
|
-
diagnostics.
|
|
1246
|
+
];
|
|
1247
|
+
const show = await this.tryBestEffortCommand(`systemctl`, showArgs);
|
|
1248
|
+
if (show !== void 0) {
|
|
1249
|
+
const showOutput = joinCommandOutput(show);
|
|
1250
|
+
if (showOutput.length > 0) diagnostics.showOutput = showOutput;
|
|
1251
|
+
if (show.stdout.length > 0) diagnostics.showStatus = parseStartStatus(unitName, show.stdout);
|
|
1001
1252
|
}
|
|
1002
|
-
const
|
|
1003
|
-
`systemctl`,
|
|
1253
|
+
const statusArgs = [
|
|
1004
1254
|
...scopeArgs,
|
|
1005
1255
|
`status`,
|
|
1006
1256
|
unitName,
|
|
1007
1257
|
`--no-pager`,
|
|
1008
1258
|
`--lines`,
|
|
1009
1259
|
`20`
|
|
1010
|
-
]
|
|
1011
|
-
const status = await this.tryBestEffortCommand(`
|
|
1012
|
-
if (status !== void 0
|
|
1260
|
+
];
|
|
1261
|
+
const status = await this.tryBestEffortCommand(`systemctl`, statusArgs);
|
|
1262
|
+
if (status !== void 0) {
|
|
1263
|
+
const statusOutput = joinCommandOutput(status);
|
|
1264
|
+
if (statusOutput.length > 0) diagnostics.statusOutput = statusOutput;
|
|
1265
|
+
}
|
|
1013
1266
|
return diagnostics;
|
|
1014
1267
|
}
|
|
1015
1268
|
async collectLinkPaths(units) {
|
|
@@ -1063,11 +1316,240 @@ var Systemd = class {
|
|
|
1063
1316
|
}
|
|
1064
1317
|
async tryBestEffortCommand(command, args) {
|
|
1065
1318
|
try {
|
|
1066
|
-
return
|
|
1319
|
+
return await this.executor(command, args);
|
|
1320
|
+
} catch (cause) {
|
|
1321
|
+
const output = extractCommandOutput(cause);
|
|
1322
|
+
if (output !== void 0) return output;
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
/**
|
|
1328
|
+
* Opinionated `systemd-ts` upkeep helpers for application-owned units.
|
|
1329
|
+
*
|
|
1330
|
+
* This layer is intentionally analogous to a subset of `portablectl`'s
|
|
1331
|
+
* ownership workflow, but it operates on regular materialized units instead of
|
|
1332
|
+
* portable service images.
|
|
1333
|
+
*/
|
|
1334
|
+
var SystemdTs = class {
|
|
1335
|
+
systemd;
|
|
1336
|
+
constructor(systemd) {
|
|
1337
|
+
this.systemd = systemd;
|
|
1338
|
+
Object.freeze(this);
|
|
1339
|
+
}
|
|
1340
|
+
async attach(units, options) {
|
|
1341
|
+
if (units.length === 0) return err(new NoUnitsProvidedError(`Systemd.ts.attach()`));
|
|
1342
|
+
const described = await this.describeUnits(units);
|
|
1343
|
+
if (!described.ok) return described;
|
|
1344
|
+
const manifestPath = this.manifestPathFor(options.owner);
|
|
1345
|
+
const prior = await this.readManifest(options.owner);
|
|
1346
|
+
const diff = await this.diffManifest(prior, described.value);
|
|
1347
|
+
const materialized = await this.systemd.materialize(...units);
|
|
1348
|
+
if (!materialized.ok) return materialized;
|
|
1349
|
+
const enabled = [];
|
|
1350
|
+
if (options.enable ?? false) {
|
|
1351
|
+
const result = await this.systemd.enable(...units);
|
|
1352
|
+
if (!result.ok) return result;
|
|
1353
|
+
enabled.push(...units.map((unit) => unit.filename));
|
|
1354
|
+
}
|
|
1355
|
+
const started = [];
|
|
1356
|
+
if (options.start ?? false) for (const unit of units) {
|
|
1357
|
+
const result = await this.systemd.start(unit);
|
|
1358
|
+
if (!result.ok) return result;
|
|
1359
|
+
started.push(unit.filename);
|
|
1360
|
+
}
|
|
1361
|
+
await this.writeManifest(options.owner, described.value);
|
|
1362
|
+
return ok({
|
|
1363
|
+
added: diff.added,
|
|
1364
|
+
directory: this.systemd.unitDir,
|
|
1365
|
+
enabled,
|
|
1366
|
+
manifestPath,
|
|
1367
|
+
owner: options.owner,
|
|
1368
|
+
removed: diff.removed,
|
|
1369
|
+
restarted: [],
|
|
1370
|
+
started,
|
|
1371
|
+
unchanged: diff.unchanged,
|
|
1372
|
+
updated: diff.updated
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
async detach(selector, options = {}) {
|
|
1376
|
+
const manifest = await this.readManifest(selector.owner);
|
|
1377
|
+
if (manifest === void 0) return ok({
|
|
1378
|
+
deleted: [],
|
|
1379
|
+
detached: [],
|
|
1380
|
+
disabled: [],
|
|
1381
|
+
owner: selector.owner,
|
|
1382
|
+
stopped: []
|
|
1383
|
+
});
|
|
1384
|
+
const filenames = manifest.units.map((unit) => unit.filename);
|
|
1385
|
+
const deleted = [];
|
|
1386
|
+
const disabled = [];
|
|
1387
|
+
const stopped = [];
|
|
1388
|
+
if (options.stop ?? true) {
|
|
1389
|
+
await this.runSystemctl(`stop`, filenames);
|
|
1390
|
+
stopped.push(...filenames);
|
|
1391
|
+
}
|
|
1392
|
+
if (options.disable ?? true) {
|
|
1393
|
+
await this.runSystemctl(`disable`, filenames);
|
|
1394
|
+
disabled.push(...filenames);
|
|
1395
|
+
}
|
|
1396
|
+
if (options.deleteUnitFiles ?? true) {
|
|
1397
|
+
for (const unit of manifest.units) {
|
|
1398
|
+
await this.removeManagedUnitPath(unit.path);
|
|
1399
|
+
await rm(unit.path, { force: true });
|
|
1400
|
+
deleted.push(unit.filename);
|
|
1401
|
+
}
|
|
1402
|
+
await this.runSystemctl(`daemon-reload`);
|
|
1403
|
+
}
|
|
1404
|
+
await rm(this.manifestPathFor(selector.owner), { force: true });
|
|
1405
|
+
return ok({
|
|
1406
|
+
deleted,
|
|
1407
|
+
detached: filenames,
|
|
1408
|
+
disabled,
|
|
1409
|
+
owner: selector.owner,
|
|
1410
|
+
stopped
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
async reattach(units, options) {
|
|
1414
|
+
if (units.length === 0) return err(new NoUnitsProvidedError(`Systemd.ts.reattach()`));
|
|
1415
|
+
const described = await this.describeUnits(units);
|
|
1416
|
+
if (!described.ok) return described;
|
|
1417
|
+
const manifestPath = this.manifestPathFor(options.owner);
|
|
1418
|
+
const prior = await this.readManifest(options.owner);
|
|
1419
|
+
const diff = await this.diffManifest(prior, described.value);
|
|
1420
|
+
if ((options.stopRemoved ?? true) && diff.removed.length > 0) await this.runSystemctl(`stop`, diff.removed);
|
|
1421
|
+
if ((options.disableRemoved ?? true) && diff.removed.length > 0) await this.runSystemctl(`disable`, diff.removed);
|
|
1422
|
+
if ((options.prune ?? true) && prior !== void 0) {
|
|
1423
|
+
for (const unit of prior.units) {
|
|
1424
|
+
if (!diff.removed.includes(unit.filename)) continue;
|
|
1425
|
+
await this.removeManagedUnitPath(unit.path);
|
|
1426
|
+
await rm(unit.path, { force: true });
|
|
1427
|
+
}
|
|
1428
|
+
if (diff.removed.length > 0) await this.runSystemctl(`daemon-reload`);
|
|
1429
|
+
}
|
|
1430
|
+
const materialized = await this.systemd.materialize(...units);
|
|
1431
|
+
if (!materialized.ok) return materialized;
|
|
1432
|
+
const enabled = [];
|
|
1433
|
+
if (options.enable ?? false) {
|
|
1434
|
+
const result = await this.systemd.enable(...units);
|
|
1435
|
+
if (!result.ok) return result;
|
|
1436
|
+
enabled.push(...units.map((unit) => unit.filename));
|
|
1437
|
+
}
|
|
1438
|
+
const restarted = [];
|
|
1439
|
+
if (options.restartUpdated ?? false) {
|
|
1440
|
+
const restartable = units.filter((unit) => diff.updated.includes(unit.filename)).map((unit) => unit.filename);
|
|
1441
|
+
if (restartable.length > 0) {
|
|
1442
|
+
await this.runSystemctl(`restart`, restartable);
|
|
1443
|
+
restarted.push(...restartable);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
const started = [];
|
|
1447
|
+
if (options.start ?? false) for (const unit of units) {
|
|
1448
|
+
if (restarted.includes(unit.filename)) continue;
|
|
1449
|
+
const result = await this.systemd.start(unit);
|
|
1450
|
+
if (!result.ok) return result;
|
|
1451
|
+
started.push(unit.filename);
|
|
1452
|
+
}
|
|
1453
|
+
await this.writeManifest(options.owner, described.value);
|
|
1454
|
+
return ok({
|
|
1455
|
+
added: diff.added,
|
|
1456
|
+
directory: this.systemd.unitDir,
|
|
1457
|
+
enabled,
|
|
1458
|
+
manifestPath,
|
|
1459
|
+
owner: options.owner,
|
|
1460
|
+
removed: diff.removed,
|
|
1461
|
+
restarted,
|
|
1462
|
+
started,
|
|
1463
|
+
unchanged: diff.unchanged,
|
|
1464
|
+
updated: diff.updated
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
async describeUnits(units) {
|
|
1468
|
+
const described = [];
|
|
1469
|
+
for (const unit of units) {
|
|
1470
|
+
const rendered = unit.render();
|
|
1471
|
+
if (!rendered.ok) return rendered;
|
|
1472
|
+
described.push({
|
|
1473
|
+
filename: unit.filename,
|
|
1474
|
+
path: this.systemd.pathFor(unit),
|
|
1475
|
+
rendered: rendered.value
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
return ok(described);
|
|
1479
|
+
}
|
|
1480
|
+
async diffManifest(prior, desired) {
|
|
1481
|
+
const priorUnits = new Map(prior?.units.map((unit) => [unit.filename, unit.path]) ?? []);
|
|
1482
|
+
const desiredNames = new Set(desired.map((unit) => unit.filename));
|
|
1483
|
+
const added = [];
|
|
1484
|
+
const removed = [...priorUnits.keys()].filter((filename) => !desiredNames.has(filename));
|
|
1485
|
+
const unchanged = [];
|
|
1486
|
+
const updated = [];
|
|
1487
|
+
for (const unit of desired) {
|
|
1488
|
+
const priorPath = priorUnits.get(unit.filename);
|
|
1489
|
+
if (priorPath === void 0) {
|
|
1490
|
+
added.push(unit.filename);
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
try {
|
|
1494
|
+
if (await readFile(priorPath, `utf8`) === unit.rendered) {
|
|
1495
|
+
unchanged.push(unit.filename);
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
} catch {
|
|
1499
|
+
added.push(unit.filename);
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
updated.push(unit.filename);
|
|
1503
|
+
}
|
|
1504
|
+
return {
|
|
1505
|
+
added,
|
|
1506
|
+
removed,
|
|
1507
|
+
unchanged,
|
|
1508
|
+
updated
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
async manifestDir() {
|
|
1512
|
+
const directory = join(this.systemd.unitDir, `.systemd-ts`);
|
|
1513
|
+
await mkdir(directory, { recursive: true });
|
|
1514
|
+
return directory;
|
|
1515
|
+
}
|
|
1516
|
+
manifestPathFor(owner) {
|
|
1517
|
+
return join(this.systemd.unitDir, `.systemd-ts`, `${encodeURIComponent(owner)}.json`);
|
|
1518
|
+
}
|
|
1519
|
+
async readManifest(owner) {
|
|
1520
|
+
try {
|
|
1521
|
+
const content = await readFile(this.manifestPathFor(owner), `utf8`);
|
|
1522
|
+
return JSON.parse(content);
|
|
1067
1523
|
} catch {
|
|
1068
1524
|
return;
|
|
1069
1525
|
}
|
|
1070
1526
|
}
|
|
1527
|
+
async runSystemctl(action, units = []) {
|
|
1528
|
+
const args = action === `daemon-reload` ? [...this.scopeArgs(), action] : [
|
|
1529
|
+
...this.scopeArgs(),
|
|
1530
|
+
action,
|
|
1531
|
+
...units
|
|
1532
|
+
];
|
|
1533
|
+
await this.systemd.executor(`systemctl`, args);
|
|
1534
|
+
}
|
|
1535
|
+
async removeManagedUnitPath(path) {
|
|
1536
|
+
await this.systemd.executor(`rm`, [`-f`, path]);
|
|
1537
|
+
}
|
|
1538
|
+
scopeArgs() {
|
|
1539
|
+
return this.systemd.scope === `user` ? [`--user`] : [];
|
|
1540
|
+
}
|
|
1541
|
+
async writeManifest(owner, units) {
|
|
1542
|
+
await this.manifestDir();
|
|
1543
|
+
const manifest = {
|
|
1544
|
+
owner,
|
|
1545
|
+
units: units.map((unit) => ({
|
|
1546
|
+
filename: unit.filename,
|
|
1547
|
+
path: unit.path
|
|
1548
|
+
})),
|
|
1549
|
+
version: 1
|
|
1550
|
+
};
|
|
1551
|
+
await writeFile(this.manifestPathFor(owner), JSON.stringify(manifest, null, 2), `utf8`);
|
|
1552
|
+
}
|
|
1071
1553
|
};
|
|
1072
1554
|
function ok(value) {
|
|
1073
1555
|
return {
|
|
@@ -1081,6 +1563,9 @@ function err(error) {
|
|
|
1081
1563
|
error
|
|
1082
1564
|
};
|
|
1083
1565
|
}
|
|
1566
|
+
function joinCommandOutput(output) {
|
|
1567
|
+
return [output.stdout, output.stderr].filter((value) => value.length > 0).join(`\n`);
|
|
1568
|
+
}
|
|
1084
1569
|
let lazyDefaultSystemd;
|
|
1085
1570
|
/**
|
|
1086
1571
|
* Returns the lazily created default `Systemd` instance.
|
|
@@ -1122,4 +1607,4 @@ async function writeUnitDirectory(path) {
|
|
|
1122
1607
|
*/
|
|
1123
1608
|
const Internal = internal_exports;
|
|
1124
1609
|
//#endregion
|
|
1125
|
-
export { Executable, ExecutableInferenceError, Internal, InvalidExecDirectiveError, NoUnitsProvidedError, NotifySendError, Systemd, SystemdMaterialization, SystemdService, SystemdTimer, SystemdTsError, UnitEnableError, UnitLogsReadError, UnitMaterializationError, UnitStartError, defaultSystemd, defineExecutable, notify };
|
|
1610
|
+
export { Executable, ExecutableInferenceError, Internal, InvalidExecDirectiveError, NoUnitsProvidedError, NotifySendError, Systemctl, SystemctlCommandError, Systemd, SystemdMaterialization, SystemdService, SystemdTimer, SystemdTs, SystemdTsError, UnitEnableError, UnitLogsReadError, UnitMaterializationError, UnitStartError, defaultSystemd, defineExecutable, notify };
|