mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-06-04 10:14:47 +08:00
1512e3f19e
`setup/service.ts` decides between a real systemd user unit and the nohup fallback by running `systemctl --user daemon-reload` and watching for the call to succeed. In contexts that bypass pam_systemd — `su -`, `pct enter`, headless containers — `XDG_RUNTIME_DIR` and `DBUS_SESSION_BUS_ADDRESS` are not exported, the probe fails with `Failed to connect to bus: No medium found`, and we install nohup despite the user manager being healthy on disk. Before the probe, check whether linger is enabled for the current user and `/run/user/<uid>` exists; if so, re-derive the env vars from disk and let them propagate to subsequent `systemctl --user` calls via `process.env` (execSync inherits by default). If the probe still fails after that, the existing nohup fallback runs unchanged — and the warning log now records *which* precondition failed so the cause is visible without grepping setup.log. The pure decision function lives in `setup/systemd-user-env.ts` so it can be tested without execSync. New regression test in `setup/systemd-user-env.test.ts` covers the #2482 repro plus the already_set / no_linger / no_runtime_dir / no_user / no_uid branches. Closes #1981 Closes #2482
91 lines
2.6 KiB
TypeScript
91 lines
2.6 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
|
|
import { computeUserSystemdEnv } from './systemd-user-env.js';
|
|
|
|
function existsFrom(paths: Set<string>) {
|
|
return (p: string) => paths.has(p);
|
|
}
|
|
|
|
describe('computeUserSystemdEnv', () => {
|
|
it('populates env when linger is on and the runtime dir exists (#2482 repro)', () => {
|
|
const result = computeUserSystemdEnv({
|
|
uid: 1000,
|
|
user: 'nanoclaw',
|
|
env: {},
|
|
exists: existsFrom(
|
|
new Set(['/var/lib/systemd/linger/nanoclaw', '/run/user/1000']),
|
|
),
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
reason: 'populated',
|
|
XDG_RUNTIME_DIR: '/run/user/1000',
|
|
DBUS_SESSION_BUS_ADDRESS: 'unix:path=/run/user/1000/bus',
|
|
});
|
|
});
|
|
|
|
it('no-ops when XDG_RUNTIME_DIR is already set (SSH login path)', () => {
|
|
const result = computeUserSystemdEnv({
|
|
uid: 1000,
|
|
user: 'nanoclaw',
|
|
env: { XDG_RUNTIME_DIR: '/run/user/1000' },
|
|
exists: existsFrom(
|
|
new Set(['/var/lib/systemd/linger/nanoclaw', '/run/user/1000']),
|
|
),
|
|
});
|
|
|
|
expect(result.reason).toBe('already_set');
|
|
expect(result.XDG_RUNTIME_DIR).toBeUndefined();
|
|
expect(result.DBUS_SESSION_BUS_ADDRESS).toBeUndefined();
|
|
});
|
|
|
|
it('returns no_linger when the linger marker is absent', () => {
|
|
const result = computeUserSystemdEnv({
|
|
uid: 1000,
|
|
user: 'nanoclaw',
|
|
env: {},
|
|
exists: existsFrom(new Set(['/run/user/1000'])),
|
|
});
|
|
|
|
expect(result.reason).toBe('no_linger');
|
|
expect(result.XDG_RUNTIME_DIR).toBeUndefined();
|
|
});
|
|
|
|
it('returns no_runtime_dir when linger is on but /run/user/<uid> is missing', () => {
|
|
// The defensive guard from #2482: without this we would point env vars
|
|
// at a non-existent socket and the daemon-reload probe would fail with a
|
|
// less recoverable error than the bare "No medium found".
|
|
const result = computeUserSystemdEnv({
|
|
uid: 1000,
|
|
user: 'nanoclaw',
|
|
env: {},
|
|
exists: existsFrom(new Set(['/var/lib/systemd/linger/nanoclaw'])),
|
|
});
|
|
|
|
expect(result.reason).toBe('no_runtime_dir');
|
|
expect(result.XDG_RUNTIME_DIR).toBeUndefined();
|
|
});
|
|
|
|
it('returns no_user when USER and LOGNAME are both missing', () => {
|
|
const result = computeUserSystemdEnv({
|
|
uid: 1000,
|
|
user: undefined,
|
|
env: {},
|
|
exists: existsFrom(new Set()),
|
|
});
|
|
|
|
expect(result.reason).toBe('no_user');
|
|
});
|
|
|
|
it('returns no_uid when process.getuid is unavailable', () => {
|
|
const result = computeUserSystemdEnv({
|
|
uid: undefined,
|
|
user: 'nanoclaw',
|
|
env: {},
|
|
exists: existsFrom(new Set()),
|
|
});
|
|
|
|
expect(result.reason).toBe('no_uid');
|
|
});
|
|
});
|