feat(console-update): systemd restart launcher for Linux supervision
Linux deployments under a systemd unit can now use the online update panel: MULTI_SIMADMIN_SYSTEMD_UNIT (+ optional _SCOPE=user|system) hands the restart to systemctl; createPlatformRestartLauncher picks launchd on macOS and systemd on Linux, degrading to 'unsupported' otherwise.
This commit is contained in:
@@ -6,6 +6,8 @@ import {
|
||||
ConsoleUpdateService,
|
||||
UpdateError,
|
||||
createLaunchdRestartLauncher,
|
||||
createPlatformRestartLauncher,
|
||||
createSystemdRestartLauncher,
|
||||
parseManifestVersion,
|
||||
parseShortStat,
|
||||
type UpdateCommandResult,
|
||||
@@ -466,3 +468,73 @@ describe('createLaunchdRestartLauncher', () => {
|
||||
await expect(launcher.restart()).rejects.toMatchObject({ code: 'COMMAND', statusCode: 500 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('createSystemdRestartLauncher', () => {
|
||||
it('is unsupported without a unit or with a malformed unit name', () => {
|
||||
for (const unit of [undefined, '', 'evil; rm -rf /', 'multi-simadmin']) {
|
||||
const launcher = createSystemdRestartLauncher({
|
||||
env: unit === undefined ? {} : { MULTI_SIMADMIN_SYSTEMD_UNIT: unit },
|
||||
runner: { run: async () => ({ code: 0, stdout: '', stderr: '' }) },
|
||||
});
|
||||
expect(launcher.supported).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('restarts through the configured scope and unit', async () => {
|
||||
const calls: string[][] = [];
|
||||
const launcher = createSystemdRestartLauncher({
|
||||
env: { MULTI_SIMADMIN_SYSTEMD_UNIT: 'multi-simadmin-api.service' },
|
||||
runner: {
|
||||
run: async (command, args) => {
|
||||
calls.push([command, ...args]);
|
||||
return { code: 0, stdout: '', stderr: '' };
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(launcher.supported).toBe(true);
|
||||
await launcher.restart();
|
||||
expect(calls[0]).toEqual(['systemctl', 'user', 'restart', 'multi-simadmin-api.service']);
|
||||
});
|
||||
|
||||
it('supports the system scope and surfaces systemctl failures', async () => {
|
||||
const calls: string[][] = [];
|
||||
const launcher = createSystemdRestartLauncher({
|
||||
env: { MULTI_SIMADMIN_SYSTEMD_UNIT: 'multi-simadmin-api.service', MULTI_SIMADMIN_SYSTEMD_SCOPE: 'system' },
|
||||
runner: {
|
||||
run: async (command, args) => {
|
||||
calls.push([command, ...args]);
|
||||
return { code: 1, stdout: '', stderr: 'not logged in' };
|
||||
},
|
||||
},
|
||||
});
|
||||
await launcher.restart().catch(() => {});
|
||||
expect(calls[0]).toEqual(['systemctl', 'system', 'restart', 'multi-simadmin-api.service']);
|
||||
await expect(launcher.restart()).rejects.toMatchObject({ code: 'COMMAND', statusCode: 500 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPlatformRestartLauncher', () => {
|
||||
const runner = { run: async () => ({ code: 0, stdout: '', stderr: '' }) };
|
||||
|
||||
it('prefers systemd on Linux when a unit is configured', () => {
|
||||
const launcher = createPlatformRestartLauncher({
|
||||
env: { MULTI_SIMADMIN_HOST_PLATFORM: 'linux', MULTI_SIMADMIN_SYSTEMD_UNIT: 'api.service' },
|
||||
runner,
|
||||
});
|
||||
expect(launcher.supported).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps the launchd launcher everywhere else', () => {
|
||||
const linux = createPlatformRestartLauncher({
|
||||
env: { MULTI_SIMADMIN_HOST_PLATFORM: 'linux' },
|
||||
runner,
|
||||
});
|
||||
const darwin = createPlatformRestartLauncher({
|
||||
env: { MULTI_SIMADMIN_HOST_PLATFORM: 'darwin', XPC_SERVICE_NAME: 'x' },
|
||||
runner,
|
||||
uid: () => 501,
|
||||
});
|
||||
expect(linux.supported).toBe(false);
|
||||
expect(darwin.supported).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -241,6 +241,47 @@ export function createLaunchdRestartLauncher(input: {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Linux systemd counterpart to the launchd launcher. The unit and scope are
|
||||
* supplied explicitly through the environment so a supervised deployment can
|
||||
* hand restarts to its service manager; unset variables keep it unsupported.
|
||||
*/
|
||||
export function createSystemdRestartLauncher(input: {
|
||||
readonly env: Readonly<Record<string, string | undefined>>;
|
||||
readonly runner: UpdateCommandRunner;
|
||||
}): RestartLauncher {
|
||||
const unit = input.env.MULTI_SIMADMIN_SYSTEMD_UNIT ?? '';
|
||||
const scope = input.env.MULTI_SIMADMIN_SYSTEMD_SCOPE === 'system' ? 'system' : 'user';
|
||||
const validUnit = /^[A-Za-z0-9@_.\-]+\.service$/.test(unit);
|
||||
const supported = validUnit;
|
||||
return {
|
||||
supported,
|
||||
async restart() {
|
||||
if (!supported)
|
||||
throw new UpdateError('NOT_SUPPORTED', '未配置 systemd 服务单元(MULTI_SIMADMIN_SYSTEMD_UNIT)。', 501);
|
||||
const result = await input.runner.run('systemctl', [scope, 'restart', unit]);
|
||||
if (result.code !== 0)
|
||||
throw new UpdateError(
|
||||
'COMMAND',
|
||||
text(result.stderr, 200) || 'systemctl restart 失败。',
|
||||
500,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Picks the restart launcher matching the host service manager. */
|
||||
export function createPlatformRestartLauncher(input: {
|
||||
readonly env: Readonly<Record<string, string | undefined>>;
|
||||
readonly runner: UpdateCommandRunner;
|
||||
readonly uid?: () => number | undefined;
|
||||
}): RestartLauncher {
|
||||
const platform = input.env.MULTI_SIMADMIN_HOST_PLATFORM ?? process.platform;
|
||||
return platform === 'linux' && input.env.MULTI_SIMADMIN_SYSTEMD_UNIT !== undefined
|
||||
? createSystemdRestartLauncher(input)
|
||||
: createLaunchdRestartLauncher(input);
|
||||
}
|
||||
|
||||
export interface ConsoleUpdateServiceOptions {
|
||||
readonly db: Database.Database;
|
||||
readonly version: string;
|
||||
|
||||
Reference in New Issue
Block a user