/** * 本机开着好几个 VS Code 窗口时只有一个窗口占得到 webhook 端口,它把验过签的请求原样转给其余窗口,每个窗口再按事件里的仓库决定管不管。 * * 没占到端口的窗口在 `127.0.0.1` 上另开一个随机端口,登记到 [PeerRegistry] 的目录里:一个端口一个文件,文件内容是该窗口扩展进程的 pid。 */ import type { Buffer } from 'node:buffer' import { promises as fsp } from 'node:fs' import { tmpdir } from 'node:os' import path from 'node:path' import process from 'node:process' import { logger } from '../logging/logger' /** 转发时带上的请求头:解析事件和校验签名只看这几个。 */ export const RELAYED_HEADERS = ['content-type', 'x-github-event', 'x-hub-signature-256'] as const /** 各公网端口登记目录的上级目录,放在系统临时目录里,换了 VS Code profile 的窗口也找得到彼此。 */ export function defaultPeerRoot(): string { return path.join(tmpdir(), 'superpowers-webhook') } /** 进程还在就算活着;没权限探测(`EPERM`)说明进程存在,只是属于别的用户。 */ function processAlive(pid: number): boolean { if (!Number.isInteger(pid) || pid <= 0) return false try { process.kill(pid, 0) return true } catch (err) { return (err as NodeJS.ErrnoException).code === 'EPERM' } } /** 没占到公网端口的窗口在这里登记自己的本地端口,占到端口的窗口从这里找转发对象。 */ export class PeerRegistry { constructor(private readonly dir: string) {} async add(port: number): Promise { await fsp.mkdir(this.dir, { recursive: true }) await fsp.writeFile(path.join(this.dir, String(port)), String(process.pid)) } async remove(port: number): Promise { await fsp.rm(path.join(this.dir, String(port)), { force: true }) } /** 还活着的窗口的本地端口;进程已经退出的登记顺手删掉,窗口崩溃时没来得及注销的就是这么清掉的。 */ async ports(): Promise { let names: string[] try { names = await fsp.readdir(this.dir) } catch { return [] } const out: number[] = [] for (const name of names) { const port = Number(name) if (!Number.isInteger(port) || port <= 0) continue const pid = Number(await fsp.readFile(path.join(this.dir, name), 'utf8').catch(() => '')) if (processAlive(pid)) out.push(port) else await this.remove(port) } return out } } export interface RelayedRequest { /** 原请求的路径,如 `/webhook`。 */ url: string headers: Record body: Buffer } /** 把请求转给每个登记过的窗口;连不上的登记删掉,其余失败只记日志,一个窗口出错不影响别的窗口。 */ export async function forwardToPeers(registry: PeerRegistry, request: RelayedRequest): Promise { const ports = await registry.ports() await Promise.all(ports.map(async (port) => { try { await fetch(`http://127.0.0.1:${port}${request.url}`, { method: 'POST', headers: request.headers, body: request.body, signal: AbortSignal.timeout(5000), }) } catch (err) { const cause = (err as { cause?: { code?: unknown } }).cause if (cause?.code === 'ECONNREFUSED') await registry.remove(port) logger.add({ level: 'warn', source: 'webhook', message: `转发给本机其它窗口失败 127.0.0.1:${port}`, details: err instanceof Error ? err.message : String(err), }) } })) }