@patiom/daemon 0.0.4 → 0.0.5
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/dist/index.js +167 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14,8 +14,9 @@ import "adm-zip";
|
|
|
14
14
|
import getPort, { portNumbers } from "get-port";
|
|
15
15
|
import { parse, stringify } from "smol-toml";
|
|
16
16
|
import dotenv from "dotenv";
|
|
17
|
+
import { spawn } from "node:child_process";
|
|
17
18
|
//#region package.json
|
|
18
|
-
var version = "0.0.
|
|
19
|
+
var version = "0.0.5";
|
|
19
20
|
//#endregion
|
|
20
21
|
//#region src/config.ts
|
|
21
22
|
const PATIOM_ROOT = "/var/lib/patiom";
|
|
@@ -178,6 +179,19 @@ const swapCurrentSymlink = async (appName, releaseId, log) => {
|
|
|
178
179
|
await fs.rename(tempSymlink, currentSymlink);
|
|
179
180
|
log("Current symlink swapped successfully");
|
|
180
181
|
};
|
|
182
|
+
const getCurrentRelease = async (appName) => {
|
|
183
|
+
const currentSymlink = getCurrentSymlink(appName);
|
|
184
|
+
try {
|
|
185
|
+
const target = await fs.readlink(currentSymlink);
|
|
186
|
+
return {
|
|
187
|
+
id: path.basename(target),
|
|
188
|
+
path: target,
|
|
189
|
+
createdAt: (await fs.stat(target)).birthtime
|
|
190
|
+
};
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
181
195
|
//#endregion
|
|
182
196
|
//#region src/core/pnpm.ts
|
|
183
197
|
const hasLockfile = async (releaseDir) => {
|
|
@@ -222,6 +236,10 @@ const enable = (name) => execa("systemctl", ["enable", name]);
|
|
|
222
236
|
const start = (name) => execa("systemctl", ["start", name]);
|
|
223
237
|
const stop = (name) => execa("systemctl", ["stop", name]);
|
|
224
238
|
const restart = (name) => execa("systemctl", ["restart", name]);
|
|
239
|
+
const parseUnits = (stdout, appName) => stdout.split("\n").filter((line) => line.includes(`${appName}@`)).map((line) => {
|
|
240
|
+
const match = line.match(`${appName}@([0-9]+)\\.service`);
|
|
241
|
+
return match ? match[1] : null;
|
|
242
|
+
}).filter((port) => port !== null);
|
|
225
243
|
const listRunningInstances = async (appName) => {
|
|
226
244
|
try {
|
|
227
245
|
const { stdout } = await execa("systemctl", [
|
|
@@ -232,10 +250,22 @@ const listRunningInstances = async (appName) => {
|
|
|
232
250
|
"--plain",
|
|
233
251
|
`${appName}@*`
|
|
234
252
|
]);
|
|
235
|
-
return stdout
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
253
|
+
return parseUnits(stdout, appName);
|
|
254
|
+
} catch {
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
const listAllInstances = async (appName) => {
|
|
259
|
+
try {
|
|
260
|
+
const { stdout } = await execa("systemctl", [
|
|
261
|
+
"list-units",
|
|
262
|
+
"--type=service",
|
|
263
|
+
"--all",
|
|
264
|
+
"--no-pager",
|
|
265
|
+
"--plain",
|
|
266
|
+
`${appName}@*`
|
|
267
|
+
]);
|
|
268
|
+
return parseUnits(stdout, appName);
|
|
239
269
|
} catch {
|
|
240
270
|
return [];
|
|
241
271
|
}
|
|
@@ -374,7 +404,7 @@ After=network.target
|
|
|
374
404
|
[Service]
|
|
375
405
|
Type=exec
|
|
376
406
|
WorkingDirectory=${PATIOM_ROOT}/apps/%p/current
|
|
377
|
-
ExecStart
|
|
407
|
+
ExecStart=/usr/local/bin/pnpm run ${startScript}
|
|
378
408
|
Restart=always
|
|
379
409
|
EnvironmentFile=${PATIOM_ROOT}/apps/%p/shared/.env
|
|
380
410
|
Environment=PORT=%i
|
|
@@ -461,9 +491,18 @@ const requireScope = (scope) => {
|
|
|
461
491
|
//#region src/core/validation.ts
|
|
462
492
|
const SAFE_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/u;
|
|
463
493
|
const ULID_PATTERN = /^[0-9A-HJKMNP-TV-Z]{26}$/u;
|
|
494
|
+
const RESERVED_NAMES = new Set([
|
|
495
|
+
"rpxy",
|
|
496
|
+
"daemon",
|
|
497
|
+
"patiom",
|
|
498
|
+
"patiom-server",
|
|
499
|
+
"system",
|
|
500
|
+
"status"
|
|
501
|
+
]);
|
|
464
502
|
const validateAppName = (name) => {
|
|
465
503
|
if (!name || !SAFE_NAME_PATTERN.test(name)) throw new Error("Invalid app name. Use only letters, numbers, hyphens, underscores, and dots.");
|
|
466
504
|
if (name.includes("..") || name.includes("/") || name.includes("\\")) throw new Error("Invalid app name. Path traversal characters are not allowed.");
|
|
505
|
+
if (RESERVED_NAMES.has(name.toLowerCase())) throw new Error(`'${name}' is a reserved name and cannot be used as an app name.`);
|
|
467
506
|
};
|
|
468
507
|
const validateReleaseId = (releaseId) => {
|
|
469
508
|
if (!releaseId || !ULID_PATTERN.test(releaseId)) throw new Error("Invalid release ID. Must be a 26-character ULID.");
|
|
@@ -790,6 +829,38 @@ dbRoute.delete("/:name", requireScope("rw"), async (c) => {
|
|
|
790
829
|
});
|
|
791
830
|
});
|
|
792
831
|
//#endregion
|
|
832
|
+
//#region src/core/diagnostics.ts
|
|
833
|
+
const getServiceState = async (name) => {
|
|
834
|
+
try {
|
|
835
|
+
const { stdout } = await execa("systemctl", ["is-active", name]);
|
|
836
|
+
return stdout.trim();
|
|
837
|
+
} catch {
|
|
838
|
+
return "unknown";
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
const getServiceLogs = async (name, lines = 20) => {
|
|
842
|
+
try {
|
|
843
|
+
const { stdout } = await execa("journalctl", [
|
|
844
|
+
"-u",
|
|
845
|
+
name,
|
|
846
|
+
"--no-pager",
|
|
847
|
+
"-n",
|
|
848
|
+
String(lines)
|
|
849
|
+
]);
|
|
850
|
+
return stdout.split("\n").filter(Boolean);
|
|
851
|
+
} catch {
|
|
852
|
+
return [];
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
const getListeningPorts = async () => {
|
|
856
|
+
try {
|
|
857
|
+
const { stdout } = await execa("ss", ["-tlnp"]);
|
|
858
|
+
return stdout.split("\n").filter(Boolean);
|
|
859
|
+
} catch {
|
|
860
|
+
return [];
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
//#endregion
|
|
793
864
|
//#region src/routes/apps.ts
|
|
794
865
|
const appsRoute = new Hono();
|
|
795
866
|
appsRoute.get("/", async (c) => {
|
|
@@ -807,6 +878,52 @@ appsRoute.get("/", async (c) => {
|
|
|
807
878
|
return c.json([]);
|
|
808
879
|
}
|
|
809
880
|
});
|
|
881
|
+
appsRoute.get("/:name/status", async (c) => {
|
|
882
|
+
const name = c.req.param("name");
|
|
883
|
+
const lines = Math.min(parseInt(c.req.query("lines") || "20", 10), 100);
|
|
884
|
+
try {
|
|
885
|
+
validateAppName(name);
|
|
886
|
+
} catch (err) {
|
|
887
|
+
return c.json({ error: err instanceof Error ? err.message : "Invalid input" }, 400);
|
|
888
|
+
}
|
|
889
|
+
const currentRelease = await getCurrentRelease(name);
|
|
890
|
+
const ports = await listAllInstances(name);
|
|
891
|
+
const lastDeployStatus = currentRelease ? await fs.readFile(path.join(currentRelease.path, "status"), "utf-8").catch(() => null) : null;
|
|
892
|
+
const instances = await Promise.all(ports.map(async (port) => {
|
|
893
|
+
const state = await getServiceState(`${name}@${port}`);
|
|
894
|
+
const logs = await getServiceLogs(`${name}@${port}`, lines);
|
|
895
|
+
return {
|
|
896
|
+
port: Number(port),
|
|
897
|
+
state,
|
|
898
|
+
logs
|
|
899
|
+
};
|
|
900
|
+
}));
|
|
901
|
+
return c.json({
|
|
902
|
+
name,
|
|
903
|
+
currentRelease: currentRelease?.id ?? null,
|
|
904
|
+
lastDeploy: lastDeployStatus ? {
|
|
905
|
+
releaseId: currentRelease.id,
|
|
906
|
+
status: lastDeployStatus.trim()
|
|
907
|
+
} : null,
|
|
908
|
+
instances
|
|
909
|
+
});
|
|
910
|
+
});
|
|
911
|
+
appsRoute.post("/:name/restart", requireScope("rw"), async (c) => {
|
|
912
|
+
const name = c.req.param("name");
|
|
913
|
+
try {
|
|
914
|
+
validateAppName(name);
|
|
915
|
+
} catch (err) {
|
|
916
|
+
return c.json({ error: err instanceof Error ? err.message : "Invalid input" }, 400);
|
|
917
|
+
}
|
|
918
|
+
const ports = await listAllInstances(name);
|
|
919
|
+
if (ports.length === 0) return c.json({ error: "No instances found for this app" }, 404);
|
|
920
|
+
await Promise.all(ports.map((port) => stop(`${name}@${port}`)));
|
|
921
|
+
await Promise.all(ports.map((port) => start(`${name}@${port}`)));
|
|
922
|
+
return c.json({
|
|
923
|
+
success: true,
|
|
924
|
+
restarted: ports.map(Number)
|
|
925
|
+
});
|
|
926
|
+
});
|
|
810
927
|
//#endregion
|
|
811
928
|
//#region src/routes/logs.ts
|
|
812
929
|
const logsRoute = new Hono();
|
|
@@ -853,6 +970,48 @@ tokensRoute.delete("/:id", async (c) => {
|
|
|
853
970
|
return c.json({ success: true });
|
|
854
971
|
});
|
|
855
972
|
//#endregion
|
|
973
|
+
//#region src/routes/status.ts
|
|
974
|
+
const statusRoute = new Hono();
|
|
975
|
+
statusRoute.get("/", async (c) => {
|
|
976
|
+
const [rpxyState, rpxyLogs, ports] = await Promise.all([
|
|
977
|
+
getServiceState("rpxy"),
|
|
978
|
+
getServiceLogs("rpxy", 20),
|
|
979
|
+
getListeningPorts()
|
|
980
|
+
]);
|
|
981
|
+
return c.json({
|
|
982
|
+
daemon: {
|
|
983
|
+
version,
|
|
984
|
+
uptime: process.uptime(),
|
|
985
|
+
port: Number(process.env.PORT) || 4e3
|
|
986
|
+
},
|
|
987
|
+
rpxy: {
|
|
988
|
+
state: rpxyState,
|
|
989
|
+
logs: rpxyLogs
|
|
990
|
+
},
|
|
991
|
+
ports
|
|
992
|
+
});
|
|
993
|
+
});
|
|
994
|
+
//#endregion
|
|
995
|
+
//#region src/routes/system.ts
|
|
996
|
+
const systemRoute = new Hono();
|
|
997
|
+
systemRoute.post("/rpxy/restart", requireScope("rw"), async (c) => {
|
|
998
|
+
await restart("rpxy");
|
|
999
|
+
return c.json({
|
|
1000
|
+
success: true,
|
|
1001
|
+
message: "rpxy restarted"
|
|
1002
|
+
});
|
|
1003
|
+
});
|
|
1004
|
+
systemRoute.post("/daemon/restart", requireScope("rw"), (c) => {
|
|
1005
|
+
spawn("bash", ["-c", "sleep 1 && systemctl restart patiom-daemon"], {
|
|
1006
|
+
detached: true,
|
|
1007
|
+
stdio: "ignore"
|
|
1008
|
+
}).unref();
|
|
1009
|
+
return c.json({
|
|
1010
|
+
success: true,
|
|
1011
|
+
message: "Daemon restarting..."
|
|
1012
|
+
});
|
|
1013
|
+
});
|
|
1014
|
+
//#endregion
|
|
856
1015
|
//#region src/server.ts
|
|
857
1016
|
const app = new Hono();
|
|
858
1017
|
app.onError((err, c) => {
|
|
@@ -871,6 +1030,8 @@ app.route("/db", dbRoute);
|
|
|
871
1030
|
app.route("/apps", appsRoute);
|
|
872
1031
|
app.route("/logs", logsRoute);
|
|
873
1032
|
app.route("/tokens", tokensRoute);
|
|
1033
|
+
app.route("/status", statusRoute);
|
|
1034
|
+
app.route("/system", systemRoute);
|
|
874
1035
|
const startServer = () => {
|
|
875
1036
|
serve({
|
|
876
1037
|
fetch: app.fetch,
|