✨ feat(vscode): webhook 改认 GitHub 风格头并支持 GitHub,事件按仓库分给对应窗口
- 事件头只认 X-GitHub-Event、签名只认 X-Hub-Signature-256: sha256=<hex>(Gitea v28 也发这两个头);ping 正常应答,form 编码返回 415 并提示改成 JSON - 只有 pull_request 事件才当 PR 事件,PR 合并状态直接取 payload;评论人只认 login - 每个事件带上 payload 里的仓库,不是本窗口 origin 的事件整条忽略,provider 按事件里的仓库打开 - 本机多个窗口时由占到端口的窗口把验过签的请求转给其余窗口,它关掉后其余窗口自动接手端口 - 删掉不再使用的 pendingHooks 遗留代码
This commit is contained in:
1 parent
5a1c0e20a2
commit
ac3c882672
12 files changed
+1057
-534
No files matched your search
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 本机开着好几个 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<void> {
|
||||
await fsp.mkdir(this.dir, { recursive: true })
|
||||
await fsp.writeFile(path.join(this.dir, String(port)), String(process.pid))
|
||||
}
|
||||
|
||||
async remove(port: number): Promise<void> {
|
||||
await fsp.rm(path.join(this.dir, String(port)), { force: true })
|
||||
}
|
||||
|
||||
/** 还活着的窗口的本地端口;进程已经退出的登记顺手删掉,窗口崩溃时没来得及注销的就是这么清掉的。 */
|
||||
async ports(): Promise<number[]> {
|
||||
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<string, string>
|
||||
body: Buffer
|
||||
}
|
||||
|
||||
/** 把请求转给每个登记过的窗口;连不上的登记删掉,其余失败只记日志,一个窗口出错不影响别的窗口。 */
|
||||
export async function forwardToPeers(registry: PeerRegistry, request: RelayedRequest): Promise<void> {
|
||||
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),
|
||||
})
|
||||
}
|
||||
}))
|
||||
}
|
||||
Reference in new issue
Block a user