fix(cli): warn when systemd user lingering is disabled (#1855)

This commit is contained in:
Ibrahim Khan
2026-09-05 18:12:43 +03:00
committed by GitHub
parent 391ea57731
commit 3a96c7ce64
3 changed files with 183 additions and 6 deletions
+50
View File
@@ -283,6 +283,53 @@ function runStartupCommand(command, args, options = {}) {
return result;
}
function parseLingerState(stdout) {
if (typeof stdout !== 'string') {
return null;
}
const match = stdout.match(/^Linger=([A-Za-z]+)\s*$/m);
if (!match) {
return null;
}
const value = match[1].toLowerCase();
if (value === 'yes') return true;
if (value === 'no') return false;
return null;
}
function getCurrentUsername() {
try {
const name = os.userInfo().username;
if (typeof name === 'string' && name.length > 0) {
return name;
}
} catch {
// userInfo() can throw when the uid has no passwd entry; fall back to env.
}
return process.env.USER || process.env.LOGNAME || '';
}
// A systemd --user service only keeps running without an active login session
// when the user has lingering enabled. Detect it so `startup enable` can warn
// that the service may otherwise stop on logout. Returns null when the state
// cannot be determined (no username, loginctl unavailable, or odd output).
function getUserLingerEnabled(username) {
const user = typeof username === 'string' && username.length > 0 ? username : getCurrentUsername();
if (!user) {
return null;
}
let result;
try {
result = runStartupCommand('loginctl', ['show-user', user, '-p', 'Linger'], { allowFailure: true });
} catch {
return null;
}
if (result.status !== 0) {
return null;
}
return parseLingerState(result.stdout);
}
function getStartupStatus() {
const paths = getStartupServicePaths();
if (!paths.servicePath) {
@@ -296,6 +343,7 @@ function getStartupStatus() {
const enabledResult = runStartupCommand('systemctl', ['--user', 'is-enabled', 'openchamber.service'], { allowFailure: true });
const activeResult = runStartupCommand('systemctl', ['--user', 'is-active', 'openchamber.service'], { allowFailure: true });
const activeState = (activeResult.stdout || '').trim() || 'inactive';
const lingerUser = getCurrentUsername();
return {
supported: true,
platform: paths.platform,
@@ -303,6 +351,8 @@ function getStartupStatus() {
active: activeState === 'active',
activeState,
servicePath: paths.servicePath,
lingerEnabled: getUserLingerEnabled(lingerUser),
lingerUser: lingerUser || null,
};
}
return {
+41 -6
View File
@@ -9,7 +9,10 @@ import {
logStatus,
} from '../cli-output.js';
async function startupCommand(options, action = 'status') {
async function startupCommand(options, action = 'status', dependencies = {}) {
const getStatus = dependencies.getStartupStatus || getStartupStatus;
const enableService = dependencies.enableStartupService || enableStartupService;
const disableService = dependencies.disableStartupService || disableStartupService;
const normalized = typeof action === 'string' ? action.trim().toLowerCase() : 'status';
if (!['status', 'enable', 'disable'].includes(normalized)) {
throw new TunnelCliError(
@@ -20,14 +23,14 @@ async function startupCommand(options, action = 'status') {
let status;
if (normalized === 'enable') {
status = enableStartupService(options);
status = enableService(options);
} else if (normalized === 'disable') {
status = disableStartupService();
status = disableService();
} else {
status = getStartupStatus();
status = getStatus();
}
const result = { action: normalized, ...status };
let result = { action: normalized, ...status };
if (!result.supported) {
throw new TunnelCliError(
`Startup integration is not supported on ${result.platform}.`,
@@ -40,13 +43,30 @@ async function startupCommand(options, action = 'status') {
EXIT_CODE.GENERAL_ERROR
);
}
if (result.platform === 'linux' && result.enabled && result.lingerEnabled !== true) {
const user = result.lingerUser || '"$USER"';
const disabled = result.lingerEnabled === false;
result = {
...result,
messages: [{
level: 'warning',
code: disabled ? 'LINGER_DISABLED' : 'LINGER_UNKNOWN',
message: disabled
? `User lingering is disabled; the startup service may stop after logout. Run \`sudo loginctl enable-linger ${user}\` to keep it running.`
: `Could not verify user lingering; the startup service may stop after logout. Check with \`loginctl show-user ${user} -p Linger\`.`,
}],
};
}
if (isJsonMode(options)) {
printJson(result);
return;
}
if (isQuietMode(options)) {
process.stdout.write(`startup ${result.enabled ? 'enabled' : 'disabled'} platform:${result.platform} supported:${result.supported ? 'yes' : 'no'}${result.servicePath ? ` path:${result.servicePath}` : ''}\n`);
const lingerToken = result.platform === 'linux'
? ` linger:${result.lingerEnabled === true ? 'yes' : result.lingerEnabled === false ? 'no' : 'unknown'}`
: '';
process.stdout.write(`startup ${result.enabled ? 'enabled' : 'disabled'} platform:${result.platform} supported:${result.supported ? 'yes' : 'no'}${result.servicePath ? ` path:${result.servicePath}` : ''}${lingerToken}\n`);
return;
}
@@ -58,6 +78,21 @@ async function startupCommand(options, action = 'status') {
if (normalized === 'enable') {
logStatus('info', 'service command', 'openchamber serve --foreground');
}
if (result.platform === 'linux') {
if (result.lingerEnabled === true) {
logStatus('success', 'user lingering enabled');
} else if (result.lingerEnabled === false) {
logStatus(result.enabled ? 'warning' : 'info', `${result.enabled ? '[LINGER_DISABLED] ' : ''}user lingering is disabled`, result.enabled ? 'startup service may stop after logout' : undefined);
if (result.enabled) {
logStatus('info', '[ENABLE_LINGER]', `sudo loginctl enable-linger ${result.lingerUser || '"$USER"'}`);
}
} else {
logStatus(result.enabled ? 'warning' : 'info', result.enabled ? '[LINGER_UNKNOWN] could not verify user lingering' : 'user lingering state is unknown', result.enabled ? 'startup service may stop after logout' : undefined);
if (result.enabled) {
logStatus('info', '[CHECK_LINGER]', `loginctl show-user ${result.lingerUser || '"$USER"'} -p Linger`);
}
}
}
clackOutro(normalized === 'status' ? 'status complete' : `${normalized} complete`);
}