@patiom/daemon 0.0.4 → 0.0.6
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 +168 -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.6";
|
|
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,10 +404,11 @@ 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
|
|
411
|
+
Environment=npm_config_verifyDepsBeforeRun=false
|
|
381
412
|
Environment=PATH=${nodeBinPath}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
382
413
|
|
|
383
414
|
DynamicUser=yes
|
|
@@ -461,9 +492,18 @@ const requireScope = (scope) => {
|
|
|
461
492
|
//#region src/core/validation.ts
|
|
462
493
|
const SAFE_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/u;
|
|
463
494
|
const ULID_PATTERN = /^[0-9A-HJKMNP-TV-Z]{26}$/u;
|
|
495
|
+
const RESERVED_NAMES = new Set([
|
|
496
|
+
"rpxy",
|
|
497
|
+
"daemon",
|
|
498
|
+
"patiom",
|
|
499
|
+
"patiom-server",
|
|
500
|
+
"system",
|
|
501
|
+
"status"
|
|
502
|
+
]);
|
|
464
503
|
const validateAppName = (name) => {
|
|
465
504
|
if (!name || !SAFE_NAME_PATTERN.test(name)) throw new Error("Invalid app name. Use only letters, numbers, hyphens, underscores, and dots.");
|
|
466
505
|
if (name.includes("..") || name.includes("/") || name.includes("\\")) throw new Error("Invalid app name. Path traversal characters are not allowed.");
|
|
506
|
+
if (RESERVED_NAMES.has(name.toLowerCase())) throw new Error(`'${name}' is a reserved name and cannot be used as an app name.`);
|
|
467
507
|
};
|
|
468
508
|
const validateReleaseId = (releaseId) => {
|
|
469
509
|
if (!releaseId || !ULID_PATTERN.test(releaseId)) throw new Error("Invalid release ID. Must be a 26-character ULID.");
|
|
@@ -790,6 +830,38 @@ dbRoute.delete("/:name", requireScope("rw"), async (c) => {
|
|
|
790
830
|
});
|
|
791
831
|
});
|
|
792
832
|
//#endregion
|
|
833
|
+
//#region src/core/diagnostics.ts
|
|
834
|
+
const getServiceState = async (name) => {
|
|
835
|
+
try {
|
|
836
|
+
const { stdout } = await execa("systemctl", ["is-active", name]);
|
|
837
|
+
return stdout.trim();
|
|
838
|
+
} catch {
|
|
839
|
+
return "unknown";
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
const getServiceLogs = async (name, lines = 20) => {
|
|
843
|
+
try {
|
|
844
|
+
const { stdout } = await execa("journalctl", [
|
|
845
|
+
"-u",
|
|
846
|
+
name,
|
|
847
|
+
"--no-pager",
|
|
848
|
+
"-n",
|
|
849
|
+
String(lines)
|
|
850
|
+
]);
|
|
851
|
+
return stdout.split("\n").filter(Boolean);
|
|
852
|
+
} catch {
|
|
853
|
+
return [];
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
const getListeningPorts = async () => {
|
|
857
|
+
try {
|
|
858
|
+
const { stdout } = await execa("ss", ["-tlnp"]);
|
|
859
|
+
return stdout.split("\n").filter(Boolean);
|
|
860
|
+
} catch {
|
|
861
|
+
return [];
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
//#endregion
|
|
793
865
|
//#region src/routes/apps.ts
|
|
794
866
|
const appsRoute = new Hono();
|
|
795
867
|
appsRoute.get("/", async (c) => {
|
|
@@ -807,6 +879,52 @@ appsRoute.get("/", async (c) => {
|
|
|
807
879
|
return c.json([]);
|
|
808
880
|
}
|
|
809
881
|
});
|
|
882
|
+
appsRoute.get("/:name/status", async (c) => {
|
|
883
|
+
const name = c.req.param("name");
|
|
884
|
+
const lines = Math.min(parseInt(c.req.query("lines") || "20", 10), 100);
|
|
885
|
+
try {
|
|
886
|
+
validateAppName(name);
|
|
887
|
+
} catch (err) {
|
|
888
|
+
return c.json({ error: err instanceof Error ? err.message : "Invalid input" }, 400);
|
|
889
|
+
}
|
|
890
|
+
const currentRelease = await getCurrentRelease(name);
|
|
891
|
+
const ports = await listAllInstances(name);
|
|
892
|
+
const lastDeployStatus = currentRelease ? await fs.readFile(path.join(currentRelease.path, "status"), "utf-8").catch(() => null) : null;
|
|
893
|
+
const instances = await Promise.all(ports.map(async (port) => {
|
|
894
|
+
const state = await getServiceState(`${name}@${port}`);
|
|
895
|
+
const logs = await getServiceLogs(`${name}@${port}`, lines);
|
|
896
|
+
return {
|
|
897
|
+
port: Number(port),
|
|
898
|
+
state,
|
|
899
|
+
logs
|
|
900
|
+
};
|
|
901
|
+
}));
|
|
902
|
+
return c.json({
|
|
903
|
+
name,
|
|
904
|
+
currentRelease: currentRelease?.id ?? null,
|
|
905
|
+
lastDeploy: lastDeployStatus ? {
|
|
906
|
+
releaseId: currentRelease.id,
|
|
907
|
+
status: lastDeployStatus.trim()
|
|
908
|
+
} : null,
|
|
909
|
+
instances
|
|
910
|
+
});
|
|
911
|
+
});
|
|
912
|
+
appsRoute.post("/:name/restart", requireScope("rw"), async (c) => {
|
|
913
|
+
const name = c.req.param("name");
|
|
914
|
+
try {
|
|
915
|
+
validateAppName(name);
|
|
916
|
+
} catch (err) {
|
|
917
|
+
return c.json({ error: err instanceof Error ? err.message : "Invalid input" }, 400);
|
|
918
|
+
}
|
|
919
|
+
const ports = await listAllInstances(name);
|
|
920
|
+
if (ports.length === 0) return c.json({ error: "No instances found for this app" }, 404);
|
|
921
|
+
await Promise.all(ports.map((port) => stop(`${name}@${port}`)));
|
|
922
|
+
await Promise.all(ports.map((port) => start(`${name}@${port}`)));
|
|
923
|
+
return c.json({
|
|
924
|
+
success: true,
|
|
925
|
+
restarted: ports.map(Number)
|
|
926
|
+
});
|
|
927
|
+
});
|
|
810
928
|
//#endregion
|
|
811
929
|
//#region src/routes/logs.ts
|
|
812
930
|
const logsRoute = new Hono();
|
|
@@ -853,6 +971,48 @@ tokensRoute.delete("/:id", async (c) => {
|
|
|
853
971
|
return c.json({ success: true });
|
|
854
972
|
});
|
|
855
973
|
//#endregion
|
|
974
|
+
//#region src/routes/status.ts
|
|
975
|
+
const statusRoute = new Hono();
|
|
976
|
+
statusRoute.get("/", async (c) => {
|
|
977
|
+
const [rpxyState, rpxyLogs, ports] = await Promise.all([
|
|
978
|
+
getServiceState("rpxy"),
|
|
979
|
+
getServiceLogs("rpxy", 20),
|
|
980
|
+
getListeningPorts()
|
|
981
|
+
]);
|
|
982
|
+
return c.json({
|
|
983
|
+
daemon: {
|
|
984
|
+
version,
|
|
985
|
+
uptime: process.uptime(),
|
|
986
|
+
port: Number(process.env.PORT) || 4e3
|
|
987
|
+
},
|
|
988
|
+
rpxy: {
|
|
989
|
+
state: rpxyState,
|
|
990
|
+
logs: rpxyLogs
|
|
991
|
+
},
|
|
992
|
+
ports
|
|
993
|
+
});
|
|
994
|
+
});
|
|
995
|
+
//#endregion
|
|
996
|
+
//#region src/routes/system.ts
|
|
997
|
+
const systemRoute = new Hono();
|
|
998
|
+
systemRoute.post("/rpxy/restart", requireScope("rw"), async (c) => {
|
|
999
|
+
await restart("rpxy");
|
|
1000
|
+
return c.json({
|
|
1001
|
+
success: true,
|
|
1002
|
+
message: "rpxy restarted"
|
|
1003
|
+
});
|
|
1004
|
+
});
|
|
1005
|
+
systemRoute.post("/daemon/restart", requireScope("rw"), (c) => {
|
|
1006
|
+
spawn("bash", ["-c", "sleep 1 && systemctl restart patiom-daemon"], {
|
|
1007
|
+
detached: true,
|
|
1008
|
+
stdio: "ignore"
|
|
1009
|
+
}).unref();
|
|
1010
|
+
return c.json({
|
|
1011
|
+
success: true,
|
|
1012
|
+
message: "Daemon restarting..."
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
1015
|
+
//#endregion
|
|
856
1016
|
//#region src/server.ts
|
|
857
1017
|
const app = new Hono();
|
|
858
1018
|
app.onError((err, c) => {
|
|
@@ -871,6 +1031,8 @@ app.route("/db", dbRoute);
|
|
|
871
1031
|
app.route("/apps", appsRoute);
|
|
872
1032
|
app.route("/logs", logsRoute);
|
|
873
1033
|
app.route("/tokens", tokensRoute);
|
|
1034
|
+
app.route("/status", statusRoute);
|
|
1035
|
+
app.route("/system", systemRoute);
|
|
874
1036
|
const startServer = () => {
|
|
875
1037
|
serve({
|
|
876
1038
|
fetch: app.fetch,
|