fix: harden stack restart migration

This commit is contained in:
Eyejoker
2026-03-29 07:33:13 +09:00
parent ca578d1627
commit 3dd41f749e
6 changed files with 259 additions and 6 deletions

View File

@@ -5,14 +5,21 @@ import { getServiceManager, isRoot } from './platform.js';
import { getConfiguredServiceNames } from './service-defs.js';
type ServiceManager = ReturnType<typeof getServiceManager>;
export const STACK_RESTART_UNIT_NAME = 'ejclaw-stack-restart.service';
const UNIT_NOT_FOUND_PATTERN =
/(Unit .* not found|Could not find the requested service|not-found)/i;
const MANAGED_SERVICE_CALLER_FALLBACK_MESSAGE =
'Stack restart unit is not installed yet. Run `npm run setup -- --step service` from an external shell before retrying from a managed EJClaw service.';
interface RestartStackDeps {
execFileSyncImpl?: typeof execFileSync;
runningAsRoot?: boolean;
serviceManager?: ServiceManager;
direct?: boolean;
serviceId?: string | null;
}
export function restartStackServices(
function restartStackServicesDirect(
projectRoot: string,
deps: RestartStackDeps = {},
): string[] {
@@ -44,9 +51,73 @@ export function restartStackServices(
return services;
}
export async function run(_args: string[]): Promise<void> {
const services = restartStackServices(process.cwd());
console.log(`Restarted and verified: ${services.join(', ')}`);
export function restartStackServices(
projectRoot: string,
deps: RestartStackDeps = {},
): string[] {
const serviceManager = deps.serviceManager ?? getServiceManager();
if (serviceManager !== 'systemd') {
throw new Error(
'restart:stack only supports Linux systemd services in this repo',
);
}
const services = getConfiguredServiceNames(projectRoot);
const execImpl = deps.execFileSyncImpl ?? execFileSync;
const systemctlArgs = deps.runningAsRoot ?? isRoot() ? [] : ['--user'];
if (deps.direct) {
return restartStackServicesDirect(projectRoot, deps);
}
try {
execImpl(
'systemctl',
[...systemctlArgs, 'start', '--wait', STACK_RESTART_UNIT_NAME],
{
stdio: 'ignore',
},
);
} catch (error) {
const stderr =
error && typeof error === 'object' && 'stderr' in error
? String((error as { stderr?: unknown }).stderr || '')
: '';
const stdout =
error && typeof error === 'object' && 'stdout' in error
? String((error as { stdout?: unknown }).stdout || '')
: '';
const message =
error instanceof Error ? error.message : `${stdout}\n${stderr}`.trim();
const combined = `${message}\n${stdout}\n${stderr}`;
const unitMissing = UNIT_NOT_FOUND_PATTERN.test(combined);
const callerServiceId =
deps.serviceId === undefined ? process.env.SERVICE_ID : deps.serviceId;
const managedServiceCaller = Boolean(callerServiceId);
if (!unitMissing) {
throw error;
}
if (managedServiceCaller) {
throw new Error(MANAGED_SERVICE_CALLER_FALLBACK_MESSAGE);
}
return restartStackServicesDirect(projectRoot, deps);
}
return services;
}
export async function run(args: string[]): Promise<void> {
const direct = args.includes('--direct');
const services = restartStackServices(process.cwd(), { direct });
if (direct) {
console.log(`Restarted and verified: ${services.join(', ')}`);
return;
}
console.log(
`Restarted and verified via ${STACK_RESTART_UNIT_NAME}: ${services.join(', ')}`,
);
}
if (process.argv[1]) {