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:
Michal
2026-03-17 22:31:17 +00:00
parent 52e1932bde
commit 4d2e8677d4
2 changed files with 62 additions and 13 deletions

View File

@@ -92,24 +92,38 @@ export async function startBastion(overrides: Partial<BastionConfig> = {}): Prom
config = populateNetworkConfig(config); config = populateNetworkConfig(config);
// PID file management: kill old instance if running // PID file management: kill old instance if running
const pidFile = `${config.bastionDir}/bastion.pid`; // Bastion needs root for dnsmasq (DHCP port 67)
mkdirSync(config.bastionDir, { recursive: true }); 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)) { mkdirSync(config.bastionDir, { recursive: true, mode: 0o755 });
const oldPid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10); const pidFile = `${config.bastionDir}/bastion.pid`;
if (!isNaN(oldPid)) {
try { // Kill old instance if running
process.kill(oldPid, "SIGTERM"); try {
logger.info(`Killed old bastion process (PID ${oldPid})`); if (existsSync(pidFile)) {
await new Promise((r) => setTimeout(r, 1000)); const oldPid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
} catch { if (!isNaN(oldPid)) {
// Process already dead, continue 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 // Write current PID
writeFileSync(pidFile, String(process.pid)); writeFileSync(pidFile, String(process.pid), { mode: 0o644 });
// Prepare directories // Prepare directories
mkdirSync(config.tftpDir, { recursive: true }); mkdirSync(config.tftpDir, { recursive: true });

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest"; 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 { join } from "node:path";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { StateManager } from "../src/services/state.js"; 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"); 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);
});
});