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);
|
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 });
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user