agentworkshop 0.5.3 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,86 @@
1
+ // ============================================================
2
+ // 单实例互斥(hardening ST-1)—— 零依赖,CLI / 启动脚本通用。
3
+ // 锁文件 <configRoot>/.runtime/aw.lock 原子创建(wx):
4
+ // { pid, startedAt, mode, port }
5
+ // 已存在且 PID 存活 → 视为已有实例(返回冲突信息,调用方以退出码 2 退出);
6
+ // PID 已死(崩溃/强杀残留) → 自动接管重写。进程退出时 releaseLock 兜底清理。
7
+ // ============================================================
8
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
9
+ import { join } from 'node:path'
10
+
11
+ function pidAlive(pid) {
12
+ if (!Number.isInteger(pid) || pid <= 0) return false
13
+ try {
14
+ // signal 0 探活:进程存在但不发信号;ESRCH=不存在,EPERM=存在(他人进程)
15
+ process.kill(pid, 0)
16
+ return true
17
+ }
18
+ catch (err) {
19
+ return err?.code === 'EPERM'
20
+ }
21
+ }
22
+
23
+ /**
24
+ * 尝试获取单实例锁。
25
+ * @returns {{ ok: true, lockPath: string, release: () => void }
26
+ * | { ok: false, lockPath: string, holder: { pid: number, startedAt: string, mode: string, port: number | null } }}
27
+ */
28
+ export function acquireLock(configRoot, { mode = 'prod', port = null } = {}) {
29
+ const runtimeDir = join(configRoot, '.runtime')
30
+ const lockPath = join(runtimeDir, 'aw.lock')
31
+ mkdirSync(runtimeDir, { recursive: true })
32
+ if (existsSync(lockPath)) {
33
+ let holder = null
34
+ try {
35
+ holder = JSON.parse(readFileSync(lockPath, 'utf8'))
36
+ }
37
+ catch { /* 损坏锁文件按残留处理 */ }
38
+ if (holder && pidAlive(holder.pid)) {
39
+ return { ok: false, lockPath, holder: { ...holder, port: holder.port ?? null } }
40
+ }
41
+ try { rmSync(lockPath) } catch { /* 抢不掉则下方 wx 再失败 */ }
42
+ }
43
+ const me = { pid: process.pid, startedAt: new Date().toISOString(), mode, port }
44
+ try {
45
+ writeFileSync(lockPath, JSON.stringify(me, null, 2), { flag: 'wx' })
46
+ }
47
+ catch (err) {
48
+ // 与并发启动者竞态:按冲突处理
49
+ let holder = null
50
+ try { holder = JSON.parse(readFileSync(lockPath, 'utf8')) } catch { /* ignore */ }
51
+ if (holder && pidAlive(holder.pid)) {
52
+ return { ok: false, lockPath, holder: { ...holder, port: holder.port ?? null } }
53
+ }
54
+ throw err
55
+ }
56
+ let released = false
57
+ const release = () => {
58
+ if (released) return
59
+ released = true
60
+ try {
61
+ const cur = JSON.parse(readFileSync(lockPath, 'utf8'))
62
+ if (cur?.pid !== process.pid) return // 锁已被他人接管,不误删
63
+ rmSync(lockPath)
64
+ }
65
+ catch { /* 已不存在/损坏,忽略 */ }
66
+ }
67
+ process.once('exit', release)
68
+ process.once('SIGINT', () => { release(); process.exit(130) })
69
+ process.once('SIGTERM', () => { release(); process.exit(143) })
70
+ return { ok: true, lockPath, release }
71
+ }
72
+
73
+ /** 端口占用探测:返回占用进程 PID(未占用返回 null)。TCP try-listen 后立刻释放。 */
74
+ export async function checkPort(host, port) {
75
+ const net = await import('node:net')
76
+ return new Promise((resolveProbe) => {
77
+ const srv = net.createServer()
78
+ srv.once('error', (err) => {
79
+ resolveProbe(err?.code === 'EADDRINUSE' ? port : null)
80
+ srv.close()
81
+ })
82
+ srv.listen(port, host, () => {
83
+ srv.close(() => resolveProbe(null))
84
+ })
85
+ })
86
+ }
@@ -1 +0,0 @@
1
- {"id":"fb769eda-b1db-437b-adc7-ecf71eb3b30b","timestamp":1788339632657,"prerendered":[]}