fix(web): update foreground systemd services safely (#2542)

* fix(web): update foreground systemd services safely

* clarify desktop and remote updates

* Revert "clarify desktop and remote updates"

This reverts commit aaf4516277de003e132d549d5281811fae96cfd5.

* test-web-systemd-updates
This commit is contained in:
Syu
2026-08-07 00:10:29 +03:00
committed by GitHub
parent e8caed45c9
commit 668a6f54fe
3 changed files with 212 additions and 1 deletions
@@ -1,3 +1,21 @@
const SYSTEMD_SERVICE_UNIT_PATTERN = /^[A-Za-z0-9:_.@-]+\.service$/;
function resolveSystemdServiceUnit(environment) {
if (!environment.INVOCATION_ID) {
return null;
}
const configuredUnit = typeof environment.OPENCHAMBER_SYSTEMD_UNIT === 'string'
? environment.OPENCHAMBER_SYSTEMD_UNIT.trim()
: '';
const unit = configuredUnit || 'openchamber.service';
return SYSTEMD_SERVICE_UNIT_PATTERN.test(unit) ? unit : null;
}
function quotePosixShell(value) {
return `'${String(value).replace(/'/g, "'\\''")}'`;
}
export const registerOpenChamberRoutes = (app, dependencies) => {
const {
fs,
@@ -54,7 +72,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
app.post('/api/openchamber/update-install', async (_req, res) => {
try {
const { spawn: spawnChild } = await import('child_process');
const { spawn: spawnChild, spawnSync } = await import('child_process');
const {
checkForUpdates,
getUpdateCommand,
@@ -110,6 +128,55 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
}
const launchMode = storedOptions.launchMode === 'foreground' ? 'foreground' : 'daemon';
const isForegroundService = launchMode === 'foreground';
const systemdServiceUnit = isForegroundService ? resolveSystemdServiceUnit(process.env) : null;
if (isForegroundService) {
if (!systemdServiceUnit) {
return res.status(409).json({
error: 'Foreground servers must be updated by their service manager. Set OPENCHAMBER_SYSTEMD_UNIT when running under systemd, or run openchamber update and restart the service.',
});
}
const updateJobName = `openchamber-update-${Date.now()}`;
const updateLogPath = `journalctl --user-unit ${updateJobName}.service`;
const updateScript = [
'set -eu',
updateCmd,
`systemctl --user restart ${quotePosixShell(systemdServiceUnit)}`,
].join('\n');
const systemdRun = spawnSync('systemd-run', [
'--user',
`--unit=${updateJobName}`,
'--collect',
'--service-type=exec',
`--setenv=PATH=${process.env.PATH || ''}`,
'/bin/sh',
'-c',
updateScript,
], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 5000,
});
if (systemdRun.status !== 0) {
const detail = (systemdRun.stderr || systemdRun.stdout || '').trim();
return res.status(409).json({
error: detail || `Could not queue update job for ${systemdServiceUnit}`,
});
}
return res.json({
success: true,
message: 'Update queued; OpenChamber will restart after installation completes',
version: updateInfo.version,
packageManager: pm,
autoRestart: true,
restartManager: 'systemd',
jobId: updateJobName,
logPath: updateLogPath,
});
}
const isWindows = process.platform === 'win32';
const quotePosix = (value) => `'${String(value).replace(/'/g, "'\\''")}'`;