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,
|
ConsoleUpdateService,
|
||||||
UpdateError,
|
UpdateError,
|
||||||
createLaunchdRestartLauncher,
|
createLaunchdRestartLauncher,
|
||||||
|
createPlatformRestartLauncher,
|
||||||
|
createSystemdRestartLauncher,
|
||||||
parseManifestVersion,
|
parseManifestVersion,
|
||||||
parseShortStat,
|
parseShortStat,
|
||||||
type UpdateCommandResult,
|
type UpdateCommandResult,
|
||||||
@@ -466,3 +468,73 @@ describe('createLaunchdRestartLauncher', () => {
|
|||||||
await expect(launcher.restart()).rejects.toMatchObject({ code: 'COMMAND', statusCode: 500 });
|
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 {
|
export interface ConsoleUpdateServiceOptions {
|
||||||
readonly db: Database.Database;
|
readonly db: Database.Database;
|
||||||
readonly version: string;
|
readonly version: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user