Skip to content

Commit 1bad698

Browse files
authored
Merge pull request #2 from htlin222/claude/add-pid-lock-mechanism-FREK9
2 parents d1c4164 + d39969d commit 1bad698

3 files changed

Lines changed: 96 additions & 0 deletions

File tree

src/bot.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import { run, sequentialize } from "@grammyjs/runner";
1010
import { Bot } from "grammy";
1111
import {
1212
ALLOWED_USERS,
13+
PID_LOCK_FILE,
1314
RESTART_FILE,
1415
TELEGRAM_TOKEN,
1516
WORKING_DIR,
1617
} from "./config";
18+
import { acquirePidLock, releasePidLock } from "./pid-lock";
1719
import {
1820
handleBookmarks,
1921
handleBranch,
@@ -139,6 +141,16 @@ bot.catch((err) => {
139141
console.error("Bot error:", err);
140142
});
141143

144+
// ============== PID Lock ==============
145+
146+
const lockResult = acquirePidLock(PID_LOCK_FILE);
147+
if (!lockResult.acquired) {
148+
console.error(
149+
`Another instance is already running (PID ${lockResult.existingPid}). Exiting.`,
150+
);
151+
process.exit(1);
152+
}
153+
142154
// ============== Startup ==============
143155

144156
console.log("=".repeat(50));
@@ -221,12 +233,14 @@ const stopRunner = () => {
221233

222234
process.on("SIGINT", () => {
223235
console.log("Received SIGINT");
236+
releasePidLock(PID_LOCK_FILE);
224237
stopRunner();
225238
process.exit(0);
226239
});
227240

228241
process.on("SIGTERM", () => {
229242
console.log("Received SIGTERM");
243+
releasePidLock(PID_LOCK_FILE);
230244
stopRunner();
231245
process.exit(0);
232246
});

src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ const INSTANCE_TEMP_DIR = `/tmp/ctb-${INSTANCE_HASH}`;
342342

343343
export const SESSION_FILE = `${INSTANCE_TEMP_DIR}/session.json`;
344344
export const RESTART_FILE = `${INSTANCE_TEMP_DIR}/restart.json`;
345+
export const PID_LOCK_FILE = `${INSTANCE_TEMP_DIR}/pid.lock`;
345346
export const TEMP_DIR = `${INSTANCE_TEMP_DIR}/downloads`;
346347

347348
// Temp paths that are always allowed for bot operations

src/pid-lock.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* PID lock mechanism to prevent multiple bot instances from running simultaneously.
3+
*
4+
* When two instances poll the same Telegram bot token, both receive the same updates.
5+
* In-process deduplication can't help across processes, so we use a PID lock file
6+
* to ensure only one instance runs at a time.
7+
*/
8+
9+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
10+
11+
/**
12+
* Check if a process with the given PID is currently running.
13+
*/
14+
function isProcessAlive(pid: number): boolean {
15+
try {
16+
// signal 0 doesn't kill the process, just checks if it exists
17+
process.kill(pid, 0);
18+
return true;
19+
} catch {
20+
return false;
21+
}
22+
}
23+
24+
export interface PidLockResult {
25+
acquired: boolean;
26+
/** The PID of the existing process if lock was not acquired */
27+
existingPid?: number;
28+
}
29+
30+
/**
31+
* Try to acquire a PID lock file.
32+
*
33+
* - If no lock file exists, writes the current PID and returns success.
34+
* - If a lock file exists with a dead process (stale lock), overwrites it.
35+
* - If a lock file exists with a live process, returns failure with the existing PID.
36+
*/
37+
export function acquirePidLock(lockPath: string): PidLockResult {
38+
const currentPid = process.pid;
39+
40+
if (existsSync(lockPath)) {
41+
try {
42+
const content = readFileSync(lockPath, "utf-8").trim();
43+
const existingPid = Number.parseInt(content, 10);
44+
45+
if (!Number.isNaN(existingPid) && existingPid > 0) {
46+
if (isProcessAlive(existingPid)) {
47+
return { acquired: false, existingPid };
48+
}
49+
// Stale lock from a dead process - take over
50+
console.log(
51+
`Removing stale PID lock (PID ${existingPid} is no longer running)`,
52+
);
53+
}
54+
} catch {
55+
// Corrupted lock file - overwrite it
56+
console.log("Removing corrupted PID lock file");
57+
}
58+
}
59+
60+
writeFileSync(lockPath, String(currentPid), "utf-8");
61+
return { acquired: true };
62+
}
63+
64+
/**
65+
* Release the PID lock file. Only removes it if it contains our own PID
66+
* (prevents accidentally removing a lock held by another process).
67+
*/
68+
export function releasePidLock(lockPath: string): void {
69+
try {
70+
if (!existsSync(lockPath)) return;
71+
72+
const content = readFileSync(lockPath, "utf-8").trim();
73+
const lockedPid = Number.parseInt(content, 10);
74+
75+
if (lockedPid === process.pid) {
76+
unlinkSync(lockPath);
77+
}
78+
} catch {
79+
// Best-effort cleanup
80+
}
81+
}

0 commit comments

Comments
 (0)