Compare commits
No commits in common. "f0d6c62347026251a9f1abdba637cd161a3f793c" and "0c512a0cbc171c51bfb0fc368b8a060495d54d87" have entirely different histories.
f0d6c62347
...
0c512a0cbc
3 changed files with 2 additions and 154 deletions
11
src/index.ts
11
src/index.ts
|
|
@ -2,7 +2,6 @@ import * as path from "path";
|
||||||
import { loadEnv, loadDisclawConfig } from "./config/loader";
|
import { loadEnv, loadDisclawConfig } from "./config/loader";
|
||||||
import { DisclawDatabase } from "./db/database";
|
import { DisclawDatabase } from "./db/database";
|
||||||
import { startBot } from "./bot";
|
import { startBot } from "./bot";
|
||||||
import { acquireLock } from "./runtime/lockfile";
|
|
||||||
|
|
||||||
const ROOT_DIR = path.resolve(__dirname, "..");
|
const ROOT_DIR = path.resolve(__dirname, "..");
|
||||||
|
|
||||||
|
|
@ -16,24 +15,18 @@ async function main(): Promise<void> {
|
||||||
`Config loaded. Workspaces root: ${disclawConfig.workspaces_root}`
|
`Config loaded. Workspaces root: ${disclawConfig.workspaces_root}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2. Acquire startup lock (before DB init)
|
// 2. Initialize database
|
||||||
const lockRelease = await acquireLock(
|
|
||||||
path.join(disclawConfig.workspaces_root, ".disclaw.lock")
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3. Initialize database
|
|
||||||
const dbPath = path.join(ROOT_DIR, "data", "disclaw.db");
|
const dbPath = path.join(ROOT_DIR, "data", "disclaw.db");
|
||||||
const db = new DisclawDatabase(dbPath);
|
const db = new DisclawDatabase(dbPath);
|
||||||
console.log(`Database initialized at ${dbPath}`);
|
console.log(`Database initialized at ${dbPath}`);
|
||||||
|
|
||||||
// 4. Start Discord bot
|
// 3. Start Discord bot
|
||||||
await startBot(envConfig, disclawConfig, db);
|
await startBot(envConfig, disclawConfig, db);
|
||||||
|
|
||||||
// Graceful shutdown
|
// Graceful shutdown
|
||||||
const shutdown = (): void => {
|
const shutdown = (): void => {
|
||||||
console.log("\nShutting down...");
|
console.log("\nShutting down...");
|
||||||
db.close();
|
db.close();
|
||||||
lockRelease();
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
import * as fs from "fs";
|
|
||||||
import * as path from "path";
|
|
||||||
|
|
||||||
interface LockData {
|
|
||||||
pid: number;
|
|
||||||
started: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isProcessRunning(pid: number): boolean {
|
|
||||||
try {
|
|
||||||
process.kill(pid, 0);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function acquireLock(lockPath: string): Promise<() => void> {
|
|
||||||
if (fs.existsSync(lockPath)) {
|
|
||||||
const raw = fs.readFileSync(lockPath, "utf-8");
|
|
||||||
let data: LockData;
|
|
||||||
try {
|
|
||||||
data = JSON.parse(raw) as LockData;
|
|
||||||
} catch {
|
|
||||||
// Corrupted lock file — treat as stale, overwrite
|
|
||||||
data = { pid: 0, started: "" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.pid && isProcessRunning(data.pid)) {
|
|
||||||
throw new Error(
|
|
||||||
`DisClaw bereits aktiv (PID ${data.pid}). Lockfile: ${lockPath}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Stale lock — fall through and overwrite
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure parent directory exists
|
|
||||||
const dir = path.dirname(lockPath);
|
|
||||||
if (!fs.existsSync(dir)) {
|
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const lockData: LockData = {
|
|
||||||
pid: process.pid,
|
|
||||||
started: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
fs.writeFileSync(lockPath, JSON.stringify(lockData), "utf-8");
|
|
||||||
|
|
||||||
const release = (): void => {
|
|
||||||
if (fs.existsSync(lockPath)) {
|
|
||||||
fs.unlinkSync(lockPath);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return release;
|
|
||||||
}
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
import * as fs from "fs";
|
|
||||||
import * as os from "os";
|
|
||||||
import * as path from "path";
|
|
||||||
import { describe, it, expect, afterEach } from "vitest";
|
|
||||||
import { acquireLock } from "../../src/runtime/lockfile";
|
|
||||||
|
|
||||||
function makeTmpDir(): string {
|
|
||||||
const suffix = Math.random().toString(36).slice(2);
|
|
||||||
const dir = path.join(os.tmpdir(), `disclaw-test-${suffix}`);
|
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("acquireLock", () => {
|
|
||||||
const dirs: string[] = [];
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
for (const dir of dirs) {
|
|
||||||
fs.rmSync(dir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
dirs.length = 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("(a) first acquire writes lock file with correct PID", async () => {
|
|
||||||
const dir = makeTmpDir();
|
|
||||||
dirs.push(dir);
|
|
||||||
const lockPath = path.join(dir, ".disclaw.lock");
|
|
||||||
|
|
||||||
const release = await acquireLock(lockPath);
|
|
||||||
|
|
||||||
expect(fs.existsSync(lockPath)).toBe(true);
|
|
||||||
const data = JSON.parse(fs.readFileSync(lockPath, "utf-8")) as {
|
|
||||||
pid: number;
|
|
||||||
started: string;
|
|
||||||
};
|
|
||||||
expect(data.pid).toBe(process.pid);
|
|
||||||
expect(typeof data.started).toBe("string");
|
|
||||||
expect(data.started).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
||||||
|
|
||||||
release();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("(b) second acquire on same live lock throws", async () => {
|
|
||||||
const dir = makeTmpDir();
|
|
||||||
dirs.push(dir);
|
|
||||||
const lockPath = path.join(dir, ".disclaw.lock");
|
|
||||||
|
|
||||||
const release = await acquireLock(lockPath);
|
|
||||||
|
|
||||||
await expect(acquireLock(lockPath)).rejects.toThrow(
|
|
||||||
`DisClaw bereits aktiv (PID ${process.pid})`
|
|
||||||
);
|
|
||||||
|
|
||||||
release();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("(c) stale lock (dead PID) is overwritten", async () => {
|
|
||||||
const dir = makeTmpDir();
|
|
||||||
dirs.push(dir);
|
|
||||||
const lockPath = path.join(dir, ".disclaw.lock");
|
|
||||||
|
|
||||||
// Write a stale lock with a PID that almost certainly doesn't exist
|
|
||||||
const staleData = { pid: 99999999, started: new Date().toISOString() };
|
|
||||||
fs.writeFileSync(lockPath, JSON.stringify(staleData), "utf-8");
|
|
||||||
|
|
||||||
const release = await acquireLock(lockPath);
|
|
||||||
|
|
||||||
const data = JSON.parse(fs.readFileSync(lockPath, "utf-8")) as {
|
|
||||||
pid: number;
|
|
||||||
started: string;
|
|
||||||
};
|
|
||||||
expect(data.pid).toBe(process.pid);
|
|
||||||
|
|
||||||
release();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("(d) release() deletes the lock file", async () => {
|
|
||||||
const dir = makeTmpDir();
|
|
||||||
dirs.push(dir);
|
|
||||||
const lockPath = path.join(dir, ".disclaw.lock");
|
|
||||||
|
|
||||||
const release = await acquireLock(lockPath);
|
|
||||||
expect(fs.existsSync(lockPath)).toBe(true);
|
|
||||||
|
|
||||||
release();
|
|
||||||
expect(fs.existsSync(lockPath)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
Reference in a new issue