mirror of
https://github.com/YuzuZensai/Termix.git
synced 2026-09-14 02:59:06 +00:00
136 lines
3.8 KiB
TypeScript
136 lines
3.8 KiB
TypeScript
import type { Express } from "express";
|
|||
|
|
import { execCommand } from "../widgets/common-utils.js";
|
||
|
|
import { execElevated } from "./exec-elevated.js";
|
||
|
|
import { managerHandler, ManagerInputError } from "./route-helpers.js";
|
||
|
|
import {
|
||
|
|
isValidSystemdUnit,
|
||
|
|
isValidServiceAction,
|
||
|
|
type ServiceAction,
|
||
|
|
} from "./validation.js";
|
||
|
|
import type { ManagerRoutesDeps } from "./types.js";
|
||
|
|
|
||
|
|
export interface SystemdService {
|
||
|
|
unit: string;
|
||
|
|
load: string;
|
||
|
|
active: string;
|
||
|
|
sub: string;
|
||
|
|
description: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
const LIST_SERVICES_CMD =
|
||
|
|
"systemctl list-units --type=service --all --no-legend --no-pager --plain 2>/dev/null";
|
||
|
|
|
||
|
|
/** Parse `systemctl list-units --plain` output into structured rows. */
|
||
|
|
export function parseServiceList(output: string): SystemdService[] {
|
||
|
|
const services: SystemdService[] = [];
|
||
|
|
for (const raw of output.split("\n")) {
|
||
|
|
const line = raw.trim();
|
||
|
|
if (!line) continue;
|
||
|
|
// Columns: UNIT LOAD ACTIVE SUB DESCRIPTION (description may contain spaces)
|
||
|
|
const parts = line.split(/\s+/);
|
||
|
|
if (parts.length < 4) continue;
|
||
|
|
const [unit, load, active, sub, ...rest] = parts;
|
||
|
|
if (!unit.endsWith(".service")) continue;
|
||
|
|
services.push({
|
||
|
|
unit,
|
||
|
|
load,
|
||
|
|
active,
|
||
|
|
sub,
|
||
|
|
description: rest.join(" "),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return services;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildServiceActionCommand(
|
||
|
|
unit: string,
|
||
|
|
action: ServiceAction,
|
||
|
|
): string {
|
||
|
|
return `systemctl ${action} ${unit}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function registerServiceRoutes(
|
||
|
|
app: Express,
|
||
|
|
{ validateHostId, runOnHost }: ManagerRoutesDeps,
|
||
|
|
): void {
|
||
|
|
/**
|
||
|
|
* @openapi
|
||
|
|
* /host-metrics/managers/services/{id}:
|
||
|
|
* get:
|
||
|
|
* summary: List systemd services
|
||
|
|
* tags: [Host Metrics]
|
||
|
|
* parameters:
|
||
|
|
* - in: path
|
||
|
|
* name: id
|
||
|
|
* required: true
|
||
|
|
* schema: { type: integer }
|
||
|
|
* responses:
|
||
|
|
* 200: { description: List of services. }
|
||
|
|
*/
|
||
|
|
app.get(
|
||
|
|
"/host-metrics/managers/services/:id",
|
||
|
|
validateHostId,
|
||
|
|
managerHandler(runOnHost, "read", "services_list", async (client) => {
|
||
|
|
const { stdout } = await execCommand(client, LIST_SERVICES_CMD, 20000);
|
||
|
|
return { services: parseServiceList(stdout) };
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @openapi
|
||
|
|
* /host-metrics/managers/services/{id}/action:
|
||
|
|
* post:
|
||
|
|
* summary: Start/stop/restart/enable/disable a systemd service
|
||
|
|
* tags: [Host Metrics]
|
||
|
|
* parameters:
|
||
|
|
* - in: path
|
||
|
|
* name: id
|
||
|
|
* required: true
|
||
|
|
* schema: { type: integer }
|
||
|
|
* requestBody:
|
||
|
|
* required: true
|
||
|
|
* content:
|
||
|
|
* application/json:
|
||
|
|
* schema:
|
||
|
|
* type: object
|
||
|
|
* properties:
|
||
|
|
* unit: { type: string }
|
||
|
|
* action: { type: string }
|
||
|
|
* responses:
|
||
|
|
* 200: { description: Action result. }
|
||
|
|
* 400: { description: Invalid unit or action. }
|
||
|
|
* 403: { description: Elevation required or denied. }
|
||
|
|
*/
|
||
|
|
app.post(
|
||
|
|
"/host-metrics/managers/services/:id/action",
|
||
|
|
validateHostId,
|
||
|
|
managerHandler(
|
||
|
|
runOnHost,
|
||
|
|
"execute",
|
||
|
|
"services_action",
|
||
|
|
async (client, host, req) => {
|
||
|
|
const { unit, action } = req.body as {
|
||
|
|
unit?: string;
|
||
|
|
action?: string;
|
||
|
|
};
|
||
|
|
if (!isValidSystemdUnit(unit)) {
|
||
|
|
throw new ManagerInputError("Invalid unit name");
|
||
|
|
}
|
||
|
|
if (!isValidServiceAction(action)) {
|
||
|
|
throw new ManagerInputError("Invalid action");
|
||
|
|
}
|
||
|
|
const result = await execElevated(
|
||
|
|
client,
|
||
|
|
buildServiceActionCommand(unit, action),
|
||
|
|
host.sudoPassword,
|
||
|
|
{ forceSudo: true, timeoutMs: 30000 },
|
||
|
|
);
|
||
|
|
return {
|
||
|
|
success: result.code === 0,
|
||
|
|
output: result.stdout || result.stderr,
|
||
|
|
};
|
||
|
|
},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|