1import { spawn } from "bun";
2import P from "parsimmon";
3import { err, ok, type Result } from "./utils";
4
5interface SystemctlShowOutput {
6 [key: string]: string;
7}
8
9// Parsimmon parsers for systemctl output
10const newline = P.string("\n");
11const equals = P.string("=");
12
13// Key: anything except = and newline
14const key = P.regexp(/[^=\n]+/).map((s) => s.trim());
15
16// Single line value: everything until newline (or end of input)
17const singleLineValue = P.regexp(/[^\n]*/);
18
19// Continuation line: newline followed by whitespace and content
20const continuationLine = P.seq(
21 newline,
22 P.regexp(/[ \t]*/), // optional whitespace
23 P.regexp(/[^\n]*/), // content
24).map(([, , content]) => "\n" + content);
25
26// Multi-line value: first line + any continuation lines
27const multiLineValue = P.seq(singleLineValue, continuationLine.many()).map(
28 ([first, continuations]) => (first + continuations.join("")).trim(),
29);
30
31// Key-value pair: key = value
32const keyValuePair = P.seq(key, equals, multiLineValue).map(([k, , v]) => ({
33 key: k,
34 value: v,
35}));
36
37// Empty line (just whitespace)
38const emptyLine = P.regexp(/[ \t]*/).result(null);
39
40// A line is either a key-value pair or empty line
41const line = P.alt(keyValuePair, emptyLine);
42
43// Complete systemctl output: lines separated by newlines, ending with optional newline
44const systemctlOutput = P.seq(line.sepBy(newline), P.alt(newline, P.eof)).map(
45 ([lines]) =>
46 lines.filter((l): l is { key: string; value: string } => l !== null),
47);
48
49const parseSystemctlOutput = (
50 output: string,
51): Result<SystemctlShowOutput, string> => {
52 const result = systemctlOutput.parse(output);
53
54 if (!result.status) {
55 return err(
56 `Parse error at position ${result.index.offset}: ${result.expected.join(", ")}`,
57 );
58 }
59
60 const kvMap: SystemctlShowOutput = {};
61
62 for (const { key, value } of result.value) {
63 if (value.length > 0) {
64 kvMap[key.toLowerCase()] = value;
65 }
66 }
67
68 return ok(kvMap);
69};
70
71export const systemctlShow = async (
72 serviceName: string,
73): Promise<Result<SystemctlShowOutput, string>> => {
74 try {
75 const proc = spawn(["systemctl", "show", `${serviceName}.service`], {
76 stdout: "pipe",
77 stderr: "pipe",
78 });
79
80 const output = await new Response(proc.stdout).text();
81 const exitCode = await proc.exited;
82
83 if (exitCode !== 0) {
84 const error = await new Response(proc.stderr).text();
85 return err(`systemctl show failed with exit code ${exitCode}: ${error}`);
86 }
87
88 return parseSystemctlOutput(output);
89 } catch (error) {
90 return err(`failed to execute systemctl show: ${error}`);
91 }
92};