@phreshos/cli 0.1.7 → 0.1.9
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 +9 -6
- package/dist/prompts.js +3 -3
- package/dist/system/command.js +6 -2
- package/dist/system/lifecycle.js +3 -1
- package/dist/system/service/background.js +139 -0
- package/dist/system/service/index.js +1 -1
- package/dist/system/service/linux.js +17 -78
- package/dist/system/service/macos.js +1 -0
- package/dist/system/service/systemd.js +101 -0
- package/dist/template/package.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,12 +57,15 @@ native per-user service. The selected release and the service entry therefore
|
|
|
57
57
|
cannot become two competing sources of truth if installation is interrupted.
|
|
58
58
|
It never reads a source checkout and never requires Bun or TypeScript.
|
|
59
59
|
|
|
60
|
-
The System runs under `launchd` on macOS and `systemd --user` on
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
The System runs under `launchd` on macOS and a real `systemd --user` manager on
|
|
61
|
+
Linux. In Linux containers with no init manager, it runs as a detached
|
|
62
|
+
user-owned background process that survives the terminal but ends with the
|
|
63
|
+
container. Automatic startup is unavailable there rather than being reported
|
|
64
|
+
as enabled. `start` and `stop` change current execution only; where a native
|
|
65
|
+
manager exists, `enable` and `disable` change automatic startup only. Successful
|
|
66
|
+
installation and startup show the desktop address. `status` reports that same
|
|
67
|
+
address with the installed version, service readiness, and automatic startup
|
|
68
|
+
without changing them; `version` reports only the installed System release.
|
|
66
69
|
|
|
67
70
|
Installation files and persistent System state have separate homes. Removing
|
|
68
71
|
the System unregisters its service and removes its release files while keeping
|
package/dist/prompts.js
CHANGED
|
@@ -64,7 +64,7 @@ export default function prompts() {
|
|
|
64
64
|
async function ask(explanation, question, fallback) {
|
|
65
65
|
if (!interactive)
|
|
66
66
|
throw new Error(`${question} Supply the corresponding option when no terminal is attached`);
|
|
67
|
-
log.message(colors.dim(explanation), { spacing:
|
|
67
|
+
log.message(colors.dim(explanation), { spacing: 1 });
|
|
68
68
|
const value = await text({
|
|
69
69
|
message: question,
|
|
70
70
|
placeholder: fallback,
|
|
@@ -77,7 +77,7 @@ export default function prompts() {
|
|
|
77
77
|
async function yes(explanation, question, fallback) {
|
|
78
78
|
if (!interactive)
|
|
79
79
|
throw new Error(`${question} Supply the corresponding option when no terminal is attached`);
|
|
80
|
-
log.message(colors.dim(explanation), { spacing:
|
|
80
|
+
log.message(colors.dim(explanation), { spacing: 1 });
|
|
81
81
|
const value = await confirm({ message: question, initialValue: fallback });
|
|
82
82
|
if (isCancel(value))
|
|
83
83
|
stop();
|
|
@@ -86,7 +86,7 @@ export default function prompts() {
|
|
|
86
86
|
async function choose(explanation, question, values, fallback) {
|
|
87
87
|
if (!interactive)
|
|
88
88
|
throw new Error(`${question} Supply the corresponding option when no terminal is attached`);
|
|
89
|
-
log.message(colors.dim(explanation), { spacing:
|
|
89
|
+
log.message(colors.dim(explanation), { spacing: 1 });
|
|
90
90
|
const value = await select({
|
|
91
91
|
message: question,
|
|
92
92
|
options: values.map(value => ({ value, label: value })),
|
package/dist/system/command.js
CHANGED
|
@@ -12,6 +12,7 @@ export default function systemCommands(program, provided) {
|
|
|
12
12
|
const interaction = prompts();
|
|
13
13
|
interaction.begin("Install System", "official stable release");
|
|
14
14
|
const status = await interaction.progress("Installing PhreshOS", "PhreshOS installed", () => current().install());
|
|
15
|
+
interaction.detail("desktop", accent(status.desktop));
|
|
15
16
|
interaction.finish(`PhreshOS ${accent(status.installed?.version ?? "")} installed`);
|
|
16
17
|
});
|
|
17
18
|
system.command("uninstall")
|
|
@@ -54,14 +55,17 @@ function action(system, name, description, current, work) {
|
|
|
54
55
|
await installed(lifecycle);
|
|
55
56
|
const interaction = prompts();
|
|
56
57
|
interaction.begin(`System ${title(name)}`);
|
|
57
|
-
await interaction.progress(`${title(name)}ing PhreshOS`, `PhreshOS ${past(name)}`, () => work(lifecycle));
|
|
58
|
+
const status = await interaction.progress(`${title(name)}ing PhreshOS`, `PhreshOS ${past(name)}`, () => work(lifecycle));
|
|
59
|
+
if (name === "start")
|
|
60
|
+
interaction.detail("desktop", accent(status.desktop));
|
|
58
61
|
interaction.finish(`System ${past(name)}`);
|
|
59
62
|
});
|
|
60
63
|
}
|
|
61
64
|
function report(interaction, status) {
|
|
62
65
|
interaction.detail("version", accent(status.installed?.version ?? "unknown"));
|
|
66
|
+
interaction.detail("desktop", accent(status.desktop));
|
|
63
67
|
interaction.detail("service", service(status));
|
|
64
|
-
interaction.detail("startup", status.enabled ? positive("enabled") : dim("disabled"));
|
|
68
|
+
interaction.detail("startup", status.automaticStartup ? status.enabled ? positive("enabled") : dim("disabled") : dim("unavailable"));
|
|
65
69
|
}
|
|
66
70
|
async function installed(lifecycle) {
|
|
67
71
|
const status = await lifecycle.status();
|
package/dist/system/lifecycle.js
CHANGED
|
@@ -35,7 +35,8 @@ export default class SystemLifecycle {
|
|
|
35
35
|
const activation = await this.activate(prepared, previous, previousService);
|
|
36
36
|
try {
|
|
37
37
|
await service.register(definition(installation, executable));
|
|
38
|
-
await service.
|
|
38
|
+
if ((await service.inspect()).automaticStartup)
|
|
39
|
+
await service.enable();
|
|
39
40
|
await service.start();
|
|
40
41
|
await this.waitUntilReady();
|
|
41
42
|
await activation.commit();
|
|
@@ -100,6 +101,7 @@ export default class SystemLifecycle {
|
|
|
100
101
|
const ready = state.running && await this.dependencies.ready(installation.paths.intake);
|
|
101
102
|
return {
|
|
102
103
|
...(installed ? { installed } : {}),
|
|
104
|
+
desktop: "http://localhost:4300",
|
|
103
105
|
...state,
|
|
104
106
|
ready,
|
|
105
107
|
root: installation.paths.root,
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
/**
|
|
7
|
+
* Owns the System for the lifetime of a Linux container with no init manager.
|
|
8
|
+
* Registration and the pid are user files; execution is a detached Node child.
|
|
9
|
+
*/
|
|
10
|
+
export default class BackgroundSystemService {
|
|
11
|
+
definition;
|
|
12
|
+
pid;
|
|
13
|
+
constructor(userHome) {
|
|
14
|
+
this.definition = join(userHome, ".config", "phreshos", "system-service.json");
|
|
15
|
+
this.pid = join(userHome, ".local", "state", "phreshos", "system.pid");
|
|
16
|
+
}
|
|
17
|
+
async inspect() {
|
|
18
|
+
const registered = existsSync(this.definition);
|
|
19
|
+
const pid = await this.readPid();
|
|
20
|
+
const running = pid !== undefined && await this.owns(pid);
|
|
21
|
+
if (pid !== undefined && !running)
|
|
22
|
+
await rm(this.pid, { force: true });
|
|
23
|
+
return {
|
|
24
|
+
registered,
|
|
25
|
+
automaticStartup: false,
|
|
26
|
+
enabled: false,
|
|
27
|
+
running,
|
|
28
|
+
...(running ? { pid } : {})
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
async register(definition) {
|
|
32
|
+
await this.stop();
|
|
33
|
+
await atomic(this.definition, JSON.stringify(definition), 0o600);
|
|
34
|
+
}
|
|
35
|
+
async unregister() {
|
|
36
|
+
await this.stop();
|
|
37
|
+
await rm(this.definition, { force: true });
|
|
38
|
+
}
|
|
39
|
+
async start() {
|
|
40
|
+
const definition = await this.readDefinition();
|
|
41
|
+
if ((await this.inspect()).running)
|
|
42
|
+
return;
|
|
43
|
+
await mkdir(dirname(definition.output), { recursive: true });
|
|
44
|
+
const output = await open(definition.output, "a", 0o600);
|
|
45
|
+
try {
|
|
46
|
+
const child = spawn(definition.executable, [definition.entry], {
|
|
47
|
+
cwd: definition.directory,
|
|
48
|
+
detached: true,
|
|
49
|
+
stdio: ["ignore", output.fd, output.fd]
|
|
50
|
+
});
|
|
51
|
+
await new Promise(function (settle, refuse) {
|
|
52
|
+
child.once("spawn", settle);
|
|
53
|
+
child.once("error", refuse);
|
|
54
|
+
});
|
|
55
|
+
if (child.pid === undefined)
|
|
56
|
+
throw new Error("The PhreshOS System background process has no pid");
|
|
57
|
+
await atomic(this.pid, String(child.pid), 0o600);
|
|
58
|
+
child.unref();
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
await output.close();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async stop() {
|
|
65
|
+
const pid = await this.readPid();
|
|
66
|
+
if (pid === undefined)
|
|
67
|
+
return;
|
|
68
|
+
if (!await this.owns(pid)) {
|
|
69
|
+
await rm(this.pid, { force: true });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
process.kill(pid, "SIGTERM");
|
|
73
|
+
const until = Date.now() + 5_000;
|
|
74
|
+
while (Date.now() < until && await this.owns(pid))
|
|
75
|
+
await new Promise(settle => setTimeout(settle, 50));
|
|
76
|
+
if (await this.owns(pid))
|
|
77
|
+
process.kill(pid, "SIGKILL");
|
|
78
|
+
await rm(this.pid, { force: true });
|
|
79
|
+
}
|
|
80
|
+
async enable() {
|
|
81
|
+
throw new Error("Automatic System startup is unavailable because this Linux environment has no service manager");
|
|
82
|
+
}
|
|
83
|
+
async disable() {
|
|
84
|
+
throw new Error("Automatic System startup is unavailable because this Linux environment has no service manager");
|
|
85
|
+
}
|
|
86
|
+
async readDefinition() {
|
|
87
|
+
let value;
|
|
88
|
+
try {
|
|
89
|
+
value = JSON.parse(await readFile(this.definition, "utf8"));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
throw new Error("The PhreshOS System service is not registered");
|
|
93
|
+
}
|
|
94
|
+
if (!definition(value))
|
|
95
|
+
throw new Error("The PhreshOS System service definition is invalid");
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
async readPid() {
|
|
99
|
+
try {
|
|
100
|
+
const value = (await readFile(this.pid, "utf8")).trim();
|
|
101
|
+
return /^[1-9][0-9]*$/.test(value) ? Number(value) : undefined;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async owns(pid) {
|
|
108
|
+
try {
|
|
109
|
+
process.kill(pid, 0);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
if (process.platform !== "linux")
|
|
115
|
+
return true;
|
|
116
|
+
const definition = await this.readDefinition().catch(() => undefined);
|
|
117
|
+
if (!definition)
|
|
118
|
+
return false;
|
|
119
|
+
const command = await readFile(`/proc/${pid}/cmdline`, "utf8").catch(() => "");
|
|
120
|
+
return command.split("\0").includes(definition.entry);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function atomic(path, content, mode) {
|
|
124
|
+
await mkdir(dirname(path), { recursive: true });
|
|
125
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
126
|
+
try {
|
|
127
|
+
await writeFile(temporary, content, { mode });
|
|
128
|
+
await rename(temporary, path);
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
await rm(temporary, { force: true });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function definition(value) {
|
|
135
|
+
if (!value || typeof value !== "object")
|
|
136
|
+
return false;
|
|
137
|
+
const candidate = value;
|
|
138
|
+
return ["executable", "entry", "directory", "output"].every(name => typeof candidate[name] === "string" && candidate[name] !== "");
|
|
139
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import LinuxSystemService from "./linux.js";
|
|
3
3
|
import MacOSSystemService from "./macos.js";
|
|
4
|
-
/** Select the
|
|
4
|
+
/** Select the per-user service implementation available in this environment. */
|
|
5
5
|
export default function systemService(platform = process.platform, userHome = homedir()) {
|
|
6
6
|
if (platform === "darwin")
|
|
7
7
|
return new MacOSSystemService(userHome);
|
|
@@ -1,99 +1,38 @@
|
|
|
1
1
|
import { execute } from "../process.js";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
5
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
+
import BackgroundSystemService from "./background.js";
|
|
3
|
+
import SystemdSystemService from "./systemd.js";
|
|
6
4
|
const command = "systemctl";
|
|
7
|
-
|
|
5
|
+
/** Selects systemd only after proving that its user manager is real. */
|
|
8
6
|
export default class LinuxSystemService {
|
|
9
|
-
|
|
10
|
-
file;
|
|
7
|
+
selected;
|
|
11
8
|
constructor(userHome, run = execute) {
|
|
12
|
-
this.
|
|
13
|
-
this.file = join(userHome, ".config", "systemd", "user", unit);
|
|
9
|
+
this.selected = select(userHome, run);
|
|
14
10
|
}
|
|
15
11
|
async inspect() {
|
|
16
|
-
|
|
17
|
-
const active = await this.run(command, ["--user", "is-active", "--quiet", unit]);
|
|
18
|
-
const enabled = await this.run(command, ["--user", "is-enabled", "--quiet", unit]);
|
|
19
|
-
const pid = active.code === 0 ? await this.run(command, ["--user", "show", unit, "--property", "MainPID", "--value"]) : undefined;
|
|
20
|
-
const value = pid && /^[0-9]+$/.test(pid.stdout.trim()) ? Number(pid.stdout.trim()) : undefined;
|
|
21
|
-
return {
|
|
22
|
-
registered,
|
|
23
|
-
enabled: registered && enabled.code === 0,
|
|
24
|
-
running: registered && active.code === 0,
|
|
25
|
-
...(value ? { pid: value } : {})
|
|
26
|
-
};
|
|
12
|
+
return await (await this.selected).inspect();
|
|
27
13
|
}
|
|
28
14
|
async register(definition) {
|
|
29
|
-
await this.
|
|
30
|
-
await mkdir(dirname(this.file), { recursive: true });
|
|
31
|
-
await mkdir(dirname(definition.output), { recursive: true });
|
|
32
|
-
const temporary = `${this.file}.${randomUUID()}.tmp`;
|
|
33
|
-
try {
|
|
34
|
-
await writeFile(temporary, service(definition), { mode: 0o600 });
|
|
35
|
-
await rename(temporary, this.file);
|
|
36
|
-
}
|
|
37
|
-
finally {
|
|
38
|
-
await rm(temporary, { force: true });
|
|
39
|
-
}
|
|
40
|
-
await this.require(["--user", "daemon-reload"]);
|
|
15
|
+
await (await this.selected).register(definition);
|
|
41
16
|
}
|
|
42
17
|
async unregister() {
|
|
43
|
-
await this.
|
|
44
|
-
await this.run(command, ["--user", "disable", unit]);
|
|
45
|
-
await rm(this.file, { force: true });
|
|
46
|
-
await this.require(["--user", "daemon-reload"]);
|
|
47
|
-
await this.run(command, ["--user", "reset-failed", unit]);
|
|
18
|
+
await (await this.selected).unregister();
|
|
48
19
|
}
|
|
49
20
|
async start() {
|
|
50
|
-
|
|
51
|
-
throw new Error("The PhreshOS System service is not registered");
|
|
52
|
-
await this.require(["--user", "start", unit]);
|
|
21
|
+
await (await this.selected).start();
|
|
53
22
|
}
|
|
54
23
|
async stop() {
|
|
55
|
-
|
|
56
|
-
if (state.code === 0)
|
|
57
|
-
await this.require(["--user", "stop", unit]);
|
|
24
|
+
await (await this.selected).stop();
|
|
58
25
|
}
|
|
59
26
|
async enable() {
|
|
60
|
-
|
|
61
|
-
throw new Error("The PhreshOS System service is not registered");
|
|
62
|
-
await this.require(["--user", "enable", unit]);
|
|
27
|
+
await (await this.selected).enable();
|
|
63
28
|
}
|
|
64
29
|
async disable() {
|
|
65
|
-
|
|
66
|
-
throw new Error("The PhreshOS System service is not registered");
|
|
67
|
-
await this.require(["--user", "disable", unit]);
|
|
30
|
+
await (await this.selected).disable();
|
|
68
31
|
}
|
|
69
|
-
async require(args) {
|
|
70
|
-
const result = await this.run(command, args);
|
|
71
|
-
if (result.code !== 0)
|
|
72
|
-
throw new Error(result.stderr.trim() || result.stdout.trim() || `${command} exited with code ${result.code}`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
function service(definition) {
|
|
76
|
-
return `[Unit]
|
|
77
|
-
Description=PhreshOS System
|
|
78
|
-
|
|
79
|
-
[Service]
|
|
80
|
-
Type=simple
|
|
81
|
-
ExecStart=${quote(definition.executable)} ${quote(definition.entry)}
|
|
82
|
-
WorkingDirectory=${setting(definition.directory)}
|
|
83
|
-
Restart=on-failure
|
|
84
|
-
RestartSec=2
|
|
85
|
-
StandardOutput=append:${setting(definition.output)}
|
|
86
|
-
StandardError=append:${setting(definition.output)}
|
|
87
|
-
|
|
88
|
-
[Install]
|
|
89
|
-
WantedBy=default.target
|
|
90
|
-
`;
|
|
91
|
-
}
|
|
92
|
-
function quote(value) {
|
|
93
|
-
return JSON.stringify(value);
|
|
94
32
|
}
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
33
|
+
async function select(userHome, run) {
|
|
34
|
+
const result = await run(command, ["--user", "show-environment"]).catch(() => undefined);
|
|
35
|
+
const lines = result?.stdout.trim().split("\n").filter(Boolean) ?? [];
|
|
36
|
+
const systemd = result?.code === 0 && lines.length > 0 && lines.every(line => /^[a-zA-Z_][a-zA-Z0-9_]*=/.test(line));
|
|
37
|
+
return systemd ? new SystemdSystemService(userHome, run) : new BackgroundSystemService(userHome);
|
|
99
38
|
}
|
|
@@ -29,6 +29,7 @@ export default class MacOSSystemService {
|
|
|
29
29
|
const pid = /\bpid\s*=\s*(\d+)/.exec(service.stdout)?.[1];
|
|
30
30
|
return {
|
|
31
31
|
registered,
|
|
32
|
+
automaticStartup: true,
|
|
32
33
|
enabled: registered && !explicitlyDisabled,
|
|
33
34
|
running: service.code === 0 && /\bstate\s*=\s*running\b/.test(service.stdout),
|
|
34
35
|
...(pid ? { pid: Number(pid) } : {})
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { execute } from "../process.js";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
const command = "systemctl";
|
|
7
|
+
const unit = "phreshos.service";
|
|
8
|
+
/** A real systemd user manager, after Linux selection has proved it exists. */
|
|
9
|
+
export default class SystemdSystemService {
|
|
10
|
+
run;
|
|
11
|
+
file;
|
|
12
|
+
constructor(userHome, run = execute) {
|
|
13
|
+
this.run = run;
|
|
14
|
+
this.file = join(userHome, ".config", "systemd", "user", unit);
|
|
15
|
+
}
|
|
16
|
+
async inspect() {
|
|
17
|
+
const registered = existsSync(this.file);
|
|
18
|
+
const active = await this.run(command, ["--user", "is-active", "--quiet", unit]);
|
|
19
|
+
const enabled = await this.run(command, ["--user", "is-enabled", "--quiet", unit]);
|
|
20
|
+
const pid = active.code === 0 ? await this.run(command, ["--user", "show", unit, "--property", "MainPID", "--value"]) : undefined;
|
|
21
|
+
const value = pid && /^[0-9]+$/.test(pid.stdout.trim()) ? Number(pid.stdout.trim()) : undefined;
|
|
22
|
+
return {
|
|
23
|
+
registered,
|
|
24
|
+
automaticStartup: true,
|
|
25
|
+
enabled: registered && enabled.code === 0,
|
|
26
|
+
running: registered && active.code === 0,
|
|
27
|
+
...(value ? { pid: value } : {})
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async register(definition) {
|
|
31
|
+
await this.stop();
|
|
32
|
+
await mkdir(dirname(this.file), { recursive: true });
|
|
33
|
+
await mkdir(dirname(definition.output), { recursive: true });
|
|
34
|
+
const temporary = `${this.file}.${randomUUID()}.tmp`;
|
|
35
|
+
try {
|
|
36
|
+
await writeFile(temporary, service(definition), { mode: 0o600 });
|
|
37
|
+
await rename(temporary, this.file);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
await rm(temporary, { force: true });
|
|
41
|
+
}
|
|
42
|
+
await this.require(["--user", "daemon-reload"]);
|
|
43
|
+
}
|
|
44
|
+
async unregister() {
|
|
45
|
+
await this.stop();
|
|
46
|
+
await this.run(command, ["--user", "disable", unit]);
|
|
47
|
+
await rm(this.file, { force: true });
|
|
48
|
+
await this.require(["--user", "daemon-reload"]);
|
|
49
|
+
await this.run(command, ["--user", "reset-failed", unit]);
|
|
50
|
+
}
|
|
51
|
+
async start() {
|
|
52
|
+
if (!existsSync(this.file))
|
|
53
|
+
throw new Error("The PhreshOS System service is not registered");
|
|
54
|
+
await this.require(["--user", "start", unit]);
|
|
55
|
+
}
|
|
56
|
+
async stop() {
|
|
57
|
+
const state = await this.run(command, ["--user", "is-active", "--quiet", unit]);
|
|
58
|
+
if (state.code === 0)
|
|
59
|
+
await this.require(["--user", "stop", unit]);
|
|
60
|
+
}
|
|
61
|
+
async enable() {
|
|
62
|
+
if (!existsSync(this.file))
|
|
63
|
+
throw new Error("The PhreshOS System service is not registered");
|
|
64
|
+
await this.require(["--user", "enable", unit]);
|
|
65
|
+
}
|
|
66
|
+
async disable() {
|
|
67
|
+
if (!existsSync(this.file))
|
|
68
|
+
throw new Error("The PhreshOS System service is not registered");
|
|
69
|
+
await this.require(["--user", "disable", unit]);
|
|
70
|
+
}
|
|
71
|
+
async require(args) {
|
|
72
|
+
const result = await this.run(command, args);
|
|
73
|
+
if (result.code !== 0)
|
|
74
|
+
throw new Error(result.stderr.trim() || result.stdout.trim() || `${command} exited with code ${result.code}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function service(definition) {
|
|
78
|
+
return `[Unit]
|
|
79
|
+
Description=PhreshOS System
|
|
80
|
+
|
|
81
|
+
[Service]
|
|
82
|
+
Type=simple
|
|
83
|
+
ExecStart=${quote(definition.executable)} ${quote(definition.entry)}
|
|
84
|
+
WorkingDirectory=${setting(definition.directory)}
|
|
85
|
+
Restart=on-failure
|
|
86
|
+
RestartSec=2
|
|
87
|
+
StandardOutput=append:${setting(definition.output)}
|
|
88
|
+
StandardError=append:${setting(definition.output)}
|
|
89
|
+
|
|
90
|
+
[Install]
|
|
91
|
+
WantedBy=default.target
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
function quote(value) {
|
|
95
|
+
return JSON.stringify(value);
|
|
96
|
+
}
|
|
97
|
+
function setting(value) {
|
|
98
|
+
if (value.includes("\n") || value.includes("\r"))
|
|
99
|
+
throw new Error("A systemd service path cannot contain a line break");
|
|
100
|
+
return value.replaceAll("%", "%%");
|
|
101
|
+
}
|