Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/bind/bind-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,27 @@ WantedBy=default.target

execSync('systemctl --user daemon-reload', { stdio: 'inherit' });
execSync('systemctl --user enable --now imcodes', { stdio: 'inherit' });

// Enable lingering so the service keeps running when the user logs out
// / SSH disconnects. Without this, systemd-logind tears down the
// per-user `systemd --user` instance after the last session ends, and
// imcodes goes down with it. Symptom in the wild: daemon "mysteriously
// disappears" overnight on every server bound via `imcodes bind` —
// exactly the 212/213/215 family of incidents on 2026-05-09.
//
// Best-effort: lingering requires polkit auth on some distros and may
// legitimately fail in rootless containers. Don't gate the rest of the
// bind flow on it — log a hint so the operator can run it themselves.
// `setup-flow.ts.installSystemdService` does the equivalent (line 415).
try {
execSync('loginctl enable-linger', { stdio: 'ignore' });
console.log('Systemd user-linger enabled (daemon survives logout).');
} catch {
console.log(
'Note: could not enable systemd user-linger automatically. The daemon '
+ 'will stop when you log out unless you run: `loginctl enable-linger`',
);
}
console.log(`Systemd user service installed: ${servicePath}`);
}

Expand Down
62 changes: 62 additions & 0 deletions test/util/systemd-linger-contract.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Contract test: every code path that installs the imcodes systemd
* `--user` service MUST also call `loginctl enable-linger`.
*
* Why: without lingering, systemd-logind tears down the per-user
* `systemd --user` instance after the last session ends, and any
* `--user` services (including imcodes) go down with it. Symptom in
* the wild: daemon "mysteriously disappears" overnight on every
* server set up by `imcodes bind` — exactly the 212/213/215 family of
* incidents on 2026-05-09 ("怎么又挂了").
*
* `setup-flow.ts.installSystemdService` had this since 2026-04 (line
* 415); `bind-flow.ts.installSystemdService` was missing it,
* fingerprint-mapping every server installed via `imcodes bind` to
* the same recurring outage. Adding the line is one trivial edit, but
* the FAILURE MODE is "silent until the user is offline for a few
* hours" — exactly the kind of thing that regresses unnoticed if a
* future refactor removes the call.
*
* jsdom-style mock-the-world tests would need a heavy execSync mock
* harness for a single line. A source-content scan catches the
* regression with one regex per file — cheap, deterministic, no
* runtime dependencies on systemctl/loginctl actually existing.
*/
import { describe, expect, it } from 'vitest';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';

const REPO_ROOT = resolve(__dirname, '..', '..');

describe('systemd user-service install paths must enable lingering', () => {
// Both files install the imcodes.service unit at
// ~/.config/systemd/user/imcodes.service. After the install, both
// MUST call `loginctl enable-linger` so the daemon survives logout.
const targets = [
'src/setup/setup-flow.ts',
'src/bind/bind-flow.ts',
];

for (const rel of targets) {
it(`${rel} calls loginctl enable-linger`, () => {
const src = readFileSync(resolve(REPO_ROOT, rel), 'utf8');
// Must reference loginctl enable-linger somewhere in the file.
// Allow both with and without an explicit user argument
// (`loginctl enable-linger` defaults to the calling user, and
// both call sites today rely on that default).
expect(src).toMatch(/loginctl\s+enable-linger\b/);
// And it must be passed to execSync (so it actually runs at
// install time — not just in a comment).
expect(src).toMatch(/execSync\([^)]*loginctl\s+enable-linger/);
});
}

it('the contract test itself names the failure mode (so future readers know why)', () => {
// Self-pin: if someone deletes the rationale comment, the test
// file no longer documents the failure mode and a future reader
// might "simplify" the install flow by removing the linger call.
const self = readFileSync(__filename, 'utf8');
expect(self).toMatch(/systemd-logind/);
expect(self).toMatch(/lingering/i);
});
});
Loading