fix: PID file permission handling + root check
- Require root when dnsmasq is needed (clear error message) - Handle stale PID files owned by different user (remove + recreate) - Create bastion dir with 755 permissions - 3 new PID file tests (30 total) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -92,24 +92,38 @@ export async function startBastion(overrides: Partial<BastionConfig> = {}): Prom
|
||||
config = populateNetworkConfig(config);
|
||||
|
||||
// PID file management: kill old instance if running
|
||||
const pidFile = `${config.bastionDir}/bastion.pid`;
|
||||
mkdirSync(config.bastionDir, { recursive: true });
|
||||
// Bastion needs root for dnsmasq (DHCP port 67)
|
||||
if (!config.skipDnsmasq && process.getuid?.() !== 0) {
|
||||
logger.error("Must run as root (dnsmasq needs DHCP/TFTP ports). Use: sudo lab init bastion standalone start");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (existsSync(pidFile)) {
|
||||
const oldPid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
|
||||
if (!isNaN(oldPid)) {
|
||||
try {
|
||||
process.kill(oldPid, "SIGTERM");
|
||||
logger.info(`Killed old bastion process (PID ${oldPid})`);
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
} catch {
|
||||
// Process already dead, continue
|
||||
mkdirSync(config.bastionDir, { recursive: true, mode: 0o755 });
|
||||
const pidFile = `${config.bastionDir}/bastion.pid`;
|
||||
|
||||
// Kill old instance if running
|
||||
try {
|
||||
if (existsSync(pidFile)) {
|
||||
const oldPid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
|
||||
if (!isNaN(oldPid)) {
|
||||
try {
|
||||
process.kill(oldPid, "SIGTERM");
|
||||
logger.info(`Killed old bastion process (PID ${oldPid})`);
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
} catch {
|
||||
// Process already dead
|
||||
}
|
||||
}
|
||||
// Remove stale PID file (may be owned by different user)
|
||||
try { unlinkSync(pidFile); } catch { /* ignore */ }
|
||||
}
|
||||
} catch {
|
||||
// Can't read PID file — try to remove it
|
||||
try { unlinkSync(pidFile); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// Write current PID
|
||||
writeFileSync(pidFile, String(process.pid));
|
||||
writeFileSync(pidFile, String(process.pid), { mode: 0o644 });
|
||||
|
||||
// Prepare directories
|
||||
mkdirSync(config.tftpDir, { recursive: true });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { mkdirSync, rmSync, existsSync, readFileSync } from "node:fs";
|
||||
import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { StateManager } from "../src/services/state.js";
|
||||
@@ -103,3 +103,38 @@ describe("StateManager", () => {
|
||||
expect(parsed.installed["aa:bb:cc:dd:ee:ff"].hostname).toBe("node1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("PID file handling", () => {
|
||||
let testDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
testDir = join(tmpdir(), `bastion-pid-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||
mkdirSync(testDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("handles stale PID file from previous run", () => {
|
||||
const pidFile = join(testDir, "bastion.pid");
|
||||
// Simulate a stale PID file with a dead process
|
||||
writeFileSync(pidFile, "999999999");
|
||||
// Should be readable
|
||||
const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
|
||||
expect(pid).toBe(999999999);
|
||||
});
|
||||
|
||||
it("handles corrupted PID file gracefully", () => {
|
||||
const pidFile = join(testDir, "bastion.pid");
|
||||
writeFileSync(pidFile, "not-a-number\n");
|
||||
const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
|
||||
expect(isNaN(pid)).toBe(true);
|
||||
});
|
||||
|
||||
it("handles missing bastion directory", () => {
|
||||
const missingDir = join(testDir, "nonexistent", "deep");
|
||||
mkdirSync(missingDir, { recursive: true });
|
||||
expect(existsSync(missingDir)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user