You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

203 lines
5.3 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* Flock RPA 本机互斥锁 — 防止 embed Worker 与 batch 探针同时开浏览器打同一出口 IP
*/
import fs from "node:fs";
import path from "node:path";
const LOCK_DIR = path.join(process.cwd(), ".rpa");
const LOCK_PATH = path.join(LOCK_DIR, "flock-rpa.lock");
export type FlockLockInfo = {
owner: string;
pid: number;
at: string;
};
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isPidAlive(pid: number): boolean {
if (!Number.isFinite(pid) || pid <= 0) {
return false;
}
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
export function readFlockExclusiveLock(): FlockLockInfo | null {
try {
if (!fs.existsSync(LOCK_PATH)) {
return null;
}
const raw = JSON.parse(fs.readFileSync(LOCK_PATH, "utf8")) as FlockLockInfo;
if (!raw?.owner || !raw?.pid) {
return null;
}
return raw;
} catch {
return null;
}
}
/** 锁是否被其他存活进程占用 */
export function isFlockExclusiveBusy(ignoreOwner?: string): boolean {
const info = readFlockExclusiveLock();
if (!info) {
return false;
}
if (!isPidAlive(info.pid)) {
try {
fs.unlinkSync(LOCK_PATH);
} catch {
/* ignore */
}
return false;
}
if (ignoreOwner && info.owner === ignoreOwner && info.pid === process.pid) {
return false;
}
return info.pid !== process.pid;
}
export function getFlockExclusiveBusyMessage(): string | null {
if (!isFlockExclusiveBusy()) {
return null;
}
const info = readFlockExclusiveLock();
const who = info?.owner === "batch" ? "本地批量探针" : "其他 Flock 查价任务";
return `Flock 官网访问正被${who}占用(串行防互踩),请待其结束后再试`;
}
/** 仅当本地 batch 探针占锁时用于网站入队拒绝Worker 占锁时仍可入队排队) */
export function getFlockBatchExclusiveBusyMessage(): string | null {
const info = readFlockExclusiveLock();
if (!info || info.owner !== "batch") {
return null;
}
if (!isPidAlive(info.pid)) {
try {
fs.unlinkSync(LOCK_PATH);
} catch {
/* ignore */
}
return null;
}
return "Flock 官网访问正被本地批量探针占用(串行防互踩),请待探针结束后再在网站查询";
}
function tryAcquireOnce(owner: string): boolean {
fs.mkdirSync(LOCK_DIR, { recursive: true });
const stale = readFlockExclusiveLock();
if (stale && !isPidAlive(stale.pid)) {
try {
fs.unlinkSync(LOCK_PATH);
} catch {
/* ignore */
}
}
try {
const fd = fs.openSync(LOCK_PATH, "wx");
const payload: FlockLockInfo = {
owner,
pid: process.pid,
at: new Date().toISOString(),
};
fs.writeFileSync(fd, JSON.stringify(payload), "utf8");
fs.closeSync(fd);
return true;
} catch (error) {
const code =
error && typeof error === "object" && "code" in error
? String((error as { code: unknown }).code)
: "";
if (code === "EEXIST") {
const again = readFlockExclusiveLock();
if (again && again.pid === process.pid) {
return true;
}
if (again && !isPidAlive(again.pid)) {
try {
fs.unlinkSync(LOCK_PATH);
} catch {
/* ignore */
}
return tryAcquireOnce(owner);
}
return false;
}
throw error;
}
}
export function releaseFlockExclusiveLock(owner: string): void {
const info = readFlockExclusiveLock();
if (!info) {
return;
}
if (info.pid !== process.pid && info.owner !== owner) {
return;
}
try {
fs.unlinkSync(LOCK_PATH);
} catch {
/* ignore */
}
}
/** 等待获取本机唯一 Flock 浏览器锁 */
export async function withFlockExclusiveLock<T>(
owner: string,
fn: () => Promise<T>,
options?: { waitMs?: number; pollMs?: number },
): Promise<T> {
const waitMs = options?.waitMs ?? 180_000;
const pollMs = options?.pollMs ?? 1_000;
const deadline = Date.now() + waitMs;
while (!tryAcquireOnce(owner)) {
if (Date.now() >= deadline) {
throw new Error(
getFlockExclusiveBusyMessage() ??
"Flock 官网访问繁忙,请稍后重试",
);
}
const info = readFlockExclusiveLock();
console.warn(
`[flock-lock] 等待互斥锁 owner=${info?.owner ?? "?"} pid=${info?.pid ?? "?"}`,
);
await sleep(pollMs);
}
console.log(`[flock-lock] 已获取互斥锁 owner=${owner} pid=${process.pid}`);
try {
return await fn();
} finally {
releaseFlockExclusiveLock(owner);
console.log(`[flock-lock] 已释放互斥锁 owner=${owner}`);
}
}
/** 失败是否不应 BullMQ 重试(上限/拒号重试只会加重 IP 风控) */
export function isFlockNonRetryableFailure(message?: string): boolean {
if (!message) {
return false;
}
return (
/flock_quote_limit|thank you for your interest|报价.*上限|quote\s*limit/i.test(
message,
) ||
/flock_account_rejected|issue creating your account/i.test(message) ||
/flock_account_exists|already (have|registered)/i.test(message) ||
/flock_login_failed|session_expired|未配置 FLOCK_LOGIN_/i.test(message) ||
/互斥锁|正被.*占用|防互踩/i.test(message) ||
/FLOCK_RATES_PENDING|still looking for the best rates|约需\s*15\s*分钟/i.test(
message,
)
);
}