@cc-remote/iroh 1.0.0-rc.5 → 1.0.0-rc.6
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.
- package/EXIT-HANG-FINDINGS.md +142 -0
- package/index.js +52 -52
- package/package.json +8 -8
- package/src/endpoint.rs +11 -2
- package/src/watch.rs +16 -6
- package/test/exit-hang-repro.mjs +141 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# iroh-js: 下游 "exit 144 / SIGTERM at teardown" 排查结论
|
|
2
|
+
|
|
3
|
+
## 修正(2026-06-28,下游 macOS 复验后)
|
|
4
|
+
|
|
5
|
+
下游用 `async_hooks` 在全量/子集 vitest 上复验,结论需要分两层,**本文档下半部分
|
|
6
|
+
原先把二者合并、过度归因了**,在此更正:
|
|
7
|
+
|
|
8
|
+
- **绑定层确有的问题(本文档主体,结论仍成立)**:fire-and-forget / 永不 resolve 的
|
|
9
|
+
napi `async` 调用(已确认的实例:`endpoint.online()`)会留下一个 pending 的
|
|
10
|
+
`napi_resolve_deferred`,ref 住 libuv loop;它对 `_getActiveHandles()` 不可见、
|
|
11
|
+
且 **`close()` 取消不掉**。这是值得在 iroh-ffi 侧改进的真实缺陷。
|
|
12
|
+
- **但它不必然是某个下游测试套件 exit-144 的主因**:下游的 `noiroh` 子集(完全不加载
|
|
13
|
+
iroh、transport 被 mock、不调用任何 `online()`)**同样 144**,且该子集 worker 里
|
|
14
|
+
`napi_resolve_deferred = 0`。顶住 loop 的是**测试侧普通 Node 句柄泄漏**:大量未清
|
|
15
|
+
`Timeout`、某 worker 高达 **954 个 `FILEHANDLE`**、`DNSCHANNEL`、子进程的
|
|
16
|
+
`PROCESSWRAP/PIPEWRAP`、未关的 `TCPWRAP`。
|
|
17
|
+
- **正确结论**:该套件的 144 是"**多源句柄泄漏 → worker 退不掉 → tinypool 收尾
|
|
18
|
+
SIGTERM**"的聚合问题。`online()` 至多是 iroh/relay 那几个 worker 的额外一份泄漏。
|
|
19
|
+
彻底修需要**测试侧清理**(afterEach 关 server/子进程/fd、unref 或清定时器)**+**
|
|
20
|
+
iroh-ffi 侧让 `online()` 不悬挂,两边都做;只改 `online()` 不解决。
|
|
21
|
+
|
|
22
|
+
下面原始分析对"napi 悬挂 async 如何隐形顶住 loop"的机制描述与复现仍然有效,只是它的
|
|
23
|
+
**适用范围**应理解为"绑定层的一类隐患",而非"任意下游 144 的唯一根因"。
|
|
24
|
+
|
|
25
|
+
### 已在本仓库修复的两个绑定层缺陷
|
|
26
|
+
|
|
27
|
+
1. **`watch_*` abort(SIGABRT/134)**:同步 `#[napi]` 方法里用 `tokio::spawn`
|
|
28
|
+
导致 "no reactor running" → abort。改用 `napi::bindgen_prelude::spawn`(直接
|
|
29
|
+
`Runtime::spawn` 到 napi 全局 runtime)。见 `src/watch.rs`。
|
|
30
|
+
2. **`online()` 悬挂不受 `close()` 取消**:`endpoint.online()` 现在 race
|
|
31
|
+
`endpoint.closed()`(`EndpointClosed::run_until`),所以 `close()` 能让一个
|
|
32
|
+
pending 的 `online()` 立即返回、释放 loop。见 `src/endpoint.rs`。
|
|
33
|
+
复现矩阵里 `ok-online-close` 现已 exit 0(改前为 HANG)。
|
|
34
|
+
|
|
35
|
+
仍需注意:**fire-and-forget 且既不 await 又不 close 的 `online()`**(矩阵里
|
|
36
|
+
`hang-online`)依然会顶住进程 —— 没有任何信号可供取消,这本质上是调用方的
|
|
37
|
+
责任;但现在只要在 teardown `close()` 即可干净释放。`acceptNext()` 同理,
|
|
38
|
+
close() 一直就能取消它。
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## TL;DR(绑定层隐患的机制)
|
|
43
|
+
|
|
44
|
+
下游进程在「测试跑完、准备退出」阶段退不掉,被外层(tinypool / vitest / CI 的
|
|
45
|
+
超时)`SIGTERM` 强杀 → `128 + 15 = 144`。
|
|
46
|
+
|
|
47
|
+
根因**不是**原生线程或 libuv 句柄顶住 event loop,而是:
|
|
48
|
+
|
|
49
|
+
> **某个 napi `async` 方法的 Promise 被「发起但从不 await / 永不 resolve」,
|
|
50
|
+
> 留下一个 pending 的 `napi_resolve_deferred` 异步资源,它 ref 住了 libuv
|
|
51
|
+
> event loop,使 Node 永远无法自行 drain 退出。**
|
|
52
|
+
|
|
53
|
+
这条 ref 有两个特性,正好解释了之前排查的所有困惑:
|
|
54
|
+
|
|
55
|
+
1. **对 `process._getActiveHandles()` / `_getActiveRequests()` 完全不可见**
|
|
56
|
+
(二者都返回 `[]`)——所以之前 dump 句柄查不到任何东西。
|
|
57
|
+
2. 只有**真正加载了原生模块、且留了一个这样的悬挂调用的 worker** 才会触发。
|
|
58
|
+
纯 JS 的未 resolve Promise **不会**顶住 libuv;必须是 napi async 调用。
|
|
59
|
+
这解释了「必须有 iroh worker」以及「哪些测试文件落到同一个 worker」带来的
|
|
60
|
+
规模/组合敏感性(不是真的和『32 个』这个数字有关,而是和『某个会留悬挂
|
|
61
|
+
async 调用的测试是否被调度进一个 iroh worker』有关)。
|
|
62
|
+
|
|
63
|
+
它**可以**通过 `async_hooks` 看到 —— 表现为一个一直存活的
|
|
64
|
+
`napi_resolve_deferred` 资源。这就是在真实测试套件里定位元凶的方法。
|
|
65
|
+
|
|
66
|
+
## 复现(在 Linux 上即可复现,与平台无关)
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
node test/exit-hang-repro.mjs # 跑完整 matrix
|
|
70
|
+
node test/exit-hang-repro.mjs diagnose # 用 async_hooks 显示那条隐形 ref
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
matrix 实测结果:
|
|
74
|
+
|
|
75
|
+
| 场景 | 操作 | 结果 |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `clean-bind-close` | bind + close | ✅ exit 0 |
|
|
78
|
+
| `clean-bind-noclose` | bind,不 close | ✅ exit 0 |
|
|
79
|
+
| `hang-online` | `ep.online()` 不 await | ❌ HANG → SIGTERM |
|
|
80
|
+
| `hang-online-close` | `ep.online()` 后 `close()` | ❌ **仍 HANG**(close 取消不了 online) |
|
|
81
|
+
| `hang-accept` | `ep.acceptNext()` 不 await | ❌ HANG → SIGTERM |
|
|
82
|
+
| `ok-accept-close` | `ep.acceptNext()` 后 close | ✅ exit 0(close 让 acceptNext resolve) |
|
|
83
|
+
|
|
84
|
+
`diagnose` 输出:
|
|
85
|
+
```
|
|
86
|
+
still-alive async resources pinning the loop: {"PROMISE":2,"napi_resolve_deferred":1,"Timeout":1}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## 关键细节
|
|
90
|
+
|
|
91
|
+
- **`endpoint.online()` 最危险**:它等待 home relay,且**不被 `close()` 取消**。
|
|
92
|
+
在 relay-only / 离线 / 拿不到 relay 时它**永不 resolve**,fire-and-forget
|
|
93
|
+
调用会永久顶住进程,即便事后 `close()` 也没用。
|
|
94
|
+
- **`endpoint.acceptNext()`** 也会顶住,但 `close()` 能让它 resolve(返回
|
|
95
|
+
`None`)从而释放 loop —— 所以只要每个 endpoint 都被 close,悬挂的 accept
|
|
96
|
+
循环会自然收尾。
|
|
97
|
+
- 任何「发起但不结束」的 async 方法(`connection.closed()`、各种 `read*`/
|
|
98
|
+
`accept*`/`stopped()` 等长等待)同理。
|
|
99
|
+
|
|
100
|
+
## 为什么单进程 / Linux 规模测试不崩
|
|
101
|
+
|
|
102
|
+
排查中在 Linux 上跑了:单进程各种 bind/close 组合、32 进程并发、
|
|
103
|
+
fork+IPC+tinypool 式 SIGTERM terminate —— **全部干净退出**。因为这些路径里
|
|
104
|
+
没有留下「永不 resolve 的悬挂 async 调用」。下游的 relay 相关测试里有(最可能
|
|
105
|
+
是 `online()` 或一个没被 close 收尾的后台 `acceptNext()`/watch 循环),macOS
|
|
106
|
+
上 home relay 行为又和沙箱里(无网络、relay 立即失败)不同,于是只在那边显形。
|
|
107
|
+
|
|
108
|
+
## 建议的修法(按优先级)
|
|
109
|
+
|
|
110
|
+
### A. 下游(能立即自验,优先)
|
|
111
|
+
1. 用 `node test/exit-hang-repro.mjs diagnose` 同款 `async_hooks` 钩子(或
|
|
112
|
+
`why-is-node-running`)跑一遍全量测试,定位那个一直存活的
|
|
113
|
+
`napi_resolve_deferred`,找到对应的 iroh 调用。
|
|
114
|
+
2. 永远不要 fire-and-forget iroh 的 async 方法。对 `online()` 这类:
|
|
115
|
+
`await Promise.race([ep.online(), timeout(ms)])`,或干脆不调用。
|
|
116
|
+
3. 每个 `Endpoint` 在测试 teardown 里 `await ep.close()`;后台 `acceptNext()`
|
|
117
|
+
循环要能在 close 后退出。
|
|
118
|
+
|
|
119
|
+
### B. 绑定层(本仓,需在 macOS 下游复验)
|
|
120
|
+
- 给所有「可能无限等待」的 async 方法补文档,明确:必须 await/race,且
|
|
121
|
+
**`online()` 不受 `close()` 取消**。
|
|
122
|
+
- 可考虑让 `online()` 等方法响应 `close()`(随 endpoint 关闭而 resolve/reject),
|
|
123
|
+
使悬挂调用在 shutdown 时能释放 loop。需评估 iroh-core 语义。
|
|
124
|
+
- 可考虑提供 `Endpoint.close()` 后把内部 endpoint 真正 drop,确保后台任务停。
|
|
125
|
+
|
|
126
|
+
## 顺带发现的另一个独立 bug(不是 144 的元凶,是 SIGABRT)
|
|
127
|
+
|
|
128
|
+
所有 `watch_*` 同步方法(`watchAddr` / `watchHomeRelay` /
|
|
129
|
+
`watchNetworkChange` / `Connection.watchPaths` / `watchPathEvents`)在函数体里
|
|
130
|
+
调用 `n0_future::task::spawn`(= `tokio::spawn`),但同步 `#[napi]` 函数运行在
|
|
131
|
+
JS 主线程、**不在 tokio runtime 上下文**,于是 panic:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
thread '<unnamed>' panicked at iroh-js/src/watch.rs:50:16:
|
|
135
|
+
there is no reactor running, must be called from the context of a Tokio 1.x runtime
|
|
136
|
+
fatal runtime error: failed to initiate panic, error 5, aborting
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
→ 进程 **abort(SIGABRT / exit 134)**。任何调用这些 `watch*` 的代码都会直接崩。
|
|
140
|
+
修法:在这些方法里用 napi 的 runtime handle 来 spawn(`napi::tokio::spawn` /
|
|
141
|
+
`tokio_runtime::spawn`),或把它们改成 `async fn` 以进入 runtime 上下文。
|
|
142
|
+
(下游目前没报 134,说明暂未用到 watch,但这是真实缺陷。)
|
package/index.js
CHANGED
|
@@ -77,8 +77,8 @@ function requireNative() {
|
|
|
77
77
|
try {
|
|
78
78
|
const binding = require('@cc-remote/iroh-android-arm64')
|
|
79
79
|
const bindingPackageVersion = require('@cc-remote/iroh-android-arm64/package.json').version
|
|
80
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
81
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
80
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
81
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
82
82
|
}
|
|
83
83
|
return binding
|
|
84
84
|
} catch (e) {
|
|
@@ -93,8 +93,8 @@ function requireNative() {
|
|
|
93
93
|
try {
|
|
94
94
|
const binding = require('@cc-remote/iroh-android-arm-eabi')
|
|
95
95
|
const bindingPackageVersion = require('@cc-remote/iroh-android-arm-eabi/package.json').version
|
|
96
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
97
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
96
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
97
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
98
98
|
}
|
|
99
99
|
return binding
|
|
100
100
|
} catch (e) {
|
|
@@ -114,8 +114,8 @@ function requireNative() {
|
|
|
114
114
|
try {
|
|
115
115
|
const binding = require('@cc-remote/iroh-win32-x64-gnu')
|
|
116
116
|
const bindingPackageVersion = require('@cc-remote/iroh-win32-x64-gnu/package.json').version
|
|
117
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
118
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
117
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
118
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
119
119
|
}
|
|
120
120
|
return binding
|
|
121
121
|
} catch (e) {
|
|
@@ -130,8 +130,8 @@ function requireNative() {
|
|
|
130
130
|
try {
|
|
131
131
|
const binding = require('@cc-remote/iroh-win32-x64-msvc')
|
|
132
132
|
const bindingPackageVersion = require('@cc-remote/iroh-win32-x64-msvc/package.json').version
|
|
133
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
134
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
133
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
134
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
135
135
|
}
|
|
136
136
|
return binding
|
|
137
137
|
} catch (e) {
|
|
@@ -147,8 +147,8 @@ function requireNative() {
|
|
|
147
147
|
try {
|
|
148
148
|
const binding = require('@cc-remote/iroh-win32-ia32-msvc')
|
|
149
149
|
const bindingPackageVersion = require('@cc-remote/iroh-win32-ia32-msvc/package.json').version
|
|
150
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
151
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
150
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
151
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
152
152
|
}
|
|
153
153
|
return binding
|
|
154
154
|
} catch (e) {
|
|
@@ -163,8 +163,8 @@ function requireNative() {
|
|
|
163
163
|
try {
|
|
164
164
|
const binding = require('@cc-remote/iroh-win32-arm64-msvc')
|
|
165
165
|
const bindingPackageVersion = require('@cc-remote/iroh-win32-arm64-msvc/package.json').version
|
|
166
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
167
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
166
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
167
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
168
168
|
}
|
|
169
169
|
return binding
|
|
170
170
|
} catch (e) {
|
|
@@ -182,8 +182,8 @@ function requireNative() {
|
|
|
182
182
|
try {
|
|
183
183
|
const binding = require('@cc-remote/iroh-darwin-universal')
|
|
184
184
|
const bindingPackageVersion = require('@cc-remote/iroh-darwin-universal/package.json').version
|
|
185
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
186
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
185
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
186
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
187
187
|
}
|
|
188
188
|
return binding
|
|
189
189
|
} catch (e) {
|
|
@@ -198,8 +198,8 @@ function requireNative() {
|
|
|
198
198
|
try {
|
|
199
199
|
const binding = require('@cc-remote/iroh-darwin-x64')
|
|
200
200
|
const bindingPackageVersion = require('@cc-remote/iroh-darwin-x64/package.json').version
|
|
201
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
202
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
201
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
202
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
203
203
|
}
|
|
204
204
|
return binding
|
|
205
205
|
} catch (e) {
|
|
@@ -214,8 +214,8 @@ function requireNative() {
|
|
|
214
214
|
try {
|
|
215
215
|
const binding = require('@cc-remote/iroh-darwin-arm64')
|
|
216
216
|
const bindingPackageVersion = require('@cc-remote/iroh-darwin-arm64/package.json').version
|
|
217
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
218
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
217
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
218
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
219
219
|
}
|
|
220
220
|
return binding
|
|
221
221
|
} catch (e) {
|
|
@@ -234,8 +234,8 @@ function requireNative() {
|
|
|
234
234
|
try {
|
|
235
235
|
const binding = require('@cc-remote/iroh-freebsd-x64')
|
|
236
236
|
const bindingPackageVersion = require('@cc-remote/iroh-freebsd-x64/package.json').version
|
|
237
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
238
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
237
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
238
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
239
239
|
}
|
|
240
240
|
return binding
|
|
241
241
|
} catch (e) {
|
|
@@ -250,8 +250,8 @@ function requireNative() {
|
|
|
250
250
|
try {
|
|
251
251
|
const binding = require('@cc-remote/iroh-freebsd-arm64')
|
|
252
252
|
const bindingPackageVersion = require('@cc-remote/iroh-freebsd-arm64/package.json').version
|
|
253
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
254
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
253
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
254
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
255
255
|
}
|
|
256
256
|
return binding
|
|
257
257
|
} catch (e) {
|
|
@@ -271,8 +271,8 @@ function requireNative() {
|
|
|
271
271
|
try {
|
|
272
272
|
const binding = require('@cc-remote/iroh-linux-x64-musl')
|
|
273
273
|
const bindingPackageVersion = require('@cc-remote/iroh-linux-x64-musl/package.json').version
|
|
274
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
275
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
274
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
275
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
276
276
|
}
|
|
277
277
|
return binding
|
|
278
278
|
} catch (e) {
|
|
@@ -287,8 +287,8 @@ function requireNative() {
|
|
|
287
287
|
try {
|
|
288
288
|
const binding = require('@cc-remote/iroh-linux-x64-gnu')
|
|
289
289
|
const bindingPackageVersion = require('@cc-remote/iroh-linux-x64-gnu/package.json').version
|
|
290
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
291
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
290
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
291
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
292
292
|
}
|
|
293
293
|
return binding
|
|
294
294
|
} catch (e) {
|
|
@@ -305,8 +305,8 @@ function requireNative() {
|
|
|
305
305
|
try {
|
|
306
306
|
const binding = require('@cc-remote/iroh-linux-arm64-musl')
|
|
307
307
|
const bindingPackageVersion = require('@cc-remote/iroh-linux-arm64-musl/package.json').version
|
|
308
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
309
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
308
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
309
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
310
310
|
}
|
|
311
311
|
return binding
|
|
312
312
|
} catch (e) {
|
|
@@ -321,8 +321,8 @@ function requireNative() {
|
|
|
321
321
|
try {
|
|
322
322
|
const binding = require('@cc-remote/iroh-linux-arm64-gnu')
|
|
323
323
|
const bindingPackageVersion = require('@cc-remote/iroh-linux-arm64-gnu/package.json').version
|
|
324
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
325
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
324
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
325
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
326
326
|
}
|
|
327
327
|
return binding
|
|
328
328
|
} catch (e) {
|
|
@@ -339,8 +339,8 @@ function requireNative() {
|
|
|
339
339
|
try {
|
|
340
340
|
const binding = require('@cc-remote/iroh-linux-arm-musleabihf')
|
|
341
341
|
const bindingPackageVersion = require('@cc-remote/iroh-linux-arm-musleabihf/package.json').version
|
|
342
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
343
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
342
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
343
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
344
344
|
}
|
|
345
345
|
return binding
|
|
346
346
|
} catch (e) {
|
|
@@ -355,8 +355,8 @@ function requireNative() {
|
|
|
355
355
|
try {
|
|
356
356
|
const binding = require('@cc-remote/iroh-linux-arm-gnueabihf')
|
|
357
357
|
const bindingPackageVersion = require('@cc-remote/iroh-linux-arm-gnueabihf/package.json').version
|
|
358
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
359
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
358
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
359
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
360
360
|
}
|
|
361
361
|
return binding
|
|
362
362
|
} catch (e) {
|
|
@@ -373,8 +373,8 @@ function requireNative() {
|
|
|
373
373
|
try {
|
|
374
374
|
const binding = require('@cc-remote/iroh-linux-loong64-musl')
|
|
375
375
|
const bindingPackageVersion = require('@cc-remote/iroh-linux-loong64-musl/package.json').version
|
|
376
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
377
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
376
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
377
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
378
378
|
}
|
|
379
379
|
return binding
|
|
380
380
|
} catch (e) {
|
|
@@ -389,8 +389,8 @@ function requireNative() {
|
|
|
389
389
|
try {
|
|
390
390
|
const binding = require('@cc-remote/iroh-linux-loong64-gnu')
|
|
391
391
|
const bindingPackageVersion = require('@cc-remote/iroh-linux-loong64-gnu/package.json').version
|
|
392
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
393
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
392
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
393
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
394
394
|
}
|
|
395
395
|
return binding
|
|
396
396
|
} catch (e) {
|
|
@@ -407,8 +407,8 @@ function requireNative() {
|
|
|
407
407
|
try {
|
|
408
408
|
const binding = require('@cc-remote/iroh-linux-riscv64-musl')
|
|
409
409
|
const bindingPackageVersion = require('@cc-remote/iroh-linux-riscv64-musl/package.json').version
|
|
410
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
411
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
410
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
411
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
412
412
|
}
|
|
413
413
|
return binding
|
|
414
414
|
} catch (e) {
|
|
@@ -423,8 +423,8 @@ function requireNative() {
|
|
|
423
423
|
try {
|
|
424
424
|
const binding = require('@cc-remote/iroh-linux-riscv64-gnu')
|
|
425
425
|
const bindingPackageVersion = require('@cc-remote/iroh-linux-riscv64-gnu/package.json').version
|
|
426
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
427
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
426
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
427
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
428
428
|
}
|
|
429
429
|
return binding
|
|
430
430
|
} catch (e) {
|
|
@@ -440,8 +440,8 @@ function requireNative() {
|
|
|
440
440
|
try {
|
|
441
441
|
const binding = require('@cc-remote/iroh-linux-ppc64-gnu')
|
|
442
442
|
const bindingPackageVersion = require('@cc-remote/iroh-linux-ppc64-gnu/package.json').version
|
|
443
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
444
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
443
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
444
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
445
445
|
}
|
|
446
446
|
return binding
|
|
447
447
|
} catch (e) {
|
|
@@ -456,8 +456,8 @@ function requireNative() {
|
|
|
456
456
|
try {
|
|
457
457
|
const binding = require('@cc-remote/iroh-linux-s390x-gnu')
|
|
458
458
|
const bindingPackageVersion = require('@cc-remote/iroh-linux-s390x-gnu/package.json').version
|
|
459
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
460
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
459
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
460
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
461
461
|
}
|
|
462
462
|
return binding
|
|
463
463
|
} catch (e) {
|
|
@@ -476,8 +476,8 @@ function requireNative() {
|
|
|
476
476
|
try {
|
|
477
477
|
const binding = require('@cc-remote/iroh-openharmony-arm64')
|
|
478
478
|
const bindingPackageVersion = require('@cc-remote/iroh-openharmony-arm64/package.json').version
|
|
479
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
480
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
479
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
480
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
481
481
|
}
|
|
482
482
|
return binding
|
|
483
483
|
} catch (e) {
|
|
@@ -492,8 +492,8 @@ function requireNative() {
|
|
|
492
492
|
try {
|
|
493
493
|
const binding = require('@cc-remote/iroh-openharmony-x64')
|
|
494
494
|
const bindingPackageVersion = require('@cc-remote/iroh-openharmony-x64/package.json').version
|
|
495
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
496
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
495
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
496
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
497
497
|
}
|
|
498
498
|
return binding
|
|
499
499
|
} catch (e) {
|
|
@@ -508,8 +508,8 @@ function requireNative() {
|
|
|
508
508
|
try {
|
|
509
509
|
const binding = require('@cc-remote/iroh-openharmony-arm')
|
|
510
510
|
const bindingPackageVersion = require('@cc-remote/iroh-openharmony-arm/package.json').version
|
|
511
|
-
if (bindingPackageVersion !== '1.0.0-rc.
|
|
512
|
-
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.
|
|
511
|
+
if (bindingPackageVersion !== '1.0.0-rc.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
512
|
+
throw new Error(`Native binding package version mismatch, expected 1.0.0-rc.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
513
513
|
}
|
|
514
514
|
return binding
|
|
515
515
|
} catch (e) {
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"network",
|
|
13
13
|
"protocol"
|
|
14
14
|
],
|
|
15
|
-
"version": "1.0.0-rc.
|
|
15
|
+
"version": "1.0.0-rc.6",
|
|
16
16
|
"type": "commonjs",
|
|
17
17
|
"main": "index.js",
|
|
18
18
|
"types": "index.d.ts",
|
|
@@ -55,12 +55,12 @@
|
|
|
55
55
|
"access": "public"
|
|
56
56
|
},
|
|
57
57
|
"optionalDependencies": {
|
|
58
|
-
"@cc-remote/iroh-darwin-arm64": "1.0.0-rc.
|
|
59
|
-
"@cc-remote/iroh-linux-arm64-gnu": "1.0.0-rc.
|
|
60
|
-
"@cc-remote/iroh-linux-arm64-musl": "1.0.0-rc.
|
|
61
|
-
"@cc-remote/iroh-linux-x64-gnu": "1.0.0-rc.
|
|
62
|
-
"@cc-remote/iroh-linux-x64-musl": "1.0.0-rc.
|
|
63
|
-
"@cc-remote/iroh-linux-arm-gnueabihf": "1.0.0-rc.
|
|
64
|
-
"@cc-remote/iroh-linux-arm-musleabihf": "1.0.0-rc.
|
|
58
|
+
"@cc-remote/iroh-darwin-arm64": "1.0.0-rc.6",
|
|
59
|
+
"@cc-remote/iroh-linux-arm64-gnu": "1.0.0-rc.6",
|
|
60
|
+
"@cc-remote/iroh-linux-arm64-musl": "1.0.0-rc.6",
|
|
61
|
+
"@cc-remote/iroh-linux-x64-gnu": "1.0.0-rc.6",
|
|
62
|
+
"@cc-remote/iroh-linux-x64-musl": "1.0.0-rc.6",
|
|
63
|
+
"@cc-remote/iroh-linux-arm-gnueabihf": "1.0.0-rc.6",
|
|
64
|
+
"@cc-remote/iroh-linux-arm-musleabihf": "1.0.0-rc.6"
|
|
65
65
|
}
|
|
66
66
|
}
|
package/src/endpoint.rs
CHANGED
|
@@ -422,10 +422,19 @@ impl Endpoint {
|
|
|
422
422
|
.collect()
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
-
/// Resolves once the endpoint has a usable home relay
|
|
425
|
+
/// Resolves once the endpoint has a usable home relay, or earlier if the
|
|
426
|
+
/// endpoint is closed.
|
|
427
|
+
///
|
|
428
|
+
/// iroh's `online()` never returns until a home relay connects, and it does
|
|
429
|
+
/// **not** resolve when the endpoint is closed (it parks forever). A
|
|
430
|
+
/// fire-and-forget `online()` whose relay never connects would therefore
|
|
431
|
+
/// leave a napi async task pending indefinitely, which refs the Node event
|
|
432
|
+
/// loop (invisibly to `process._getActiveHandles()`) and stops the host
|
|
433
|
+
/// process from exiting. Racing it against endpoint closure means `close()`
|
|
434
|
+
/// releases a pending `online()` so the loop can drain.
|
|
426
435
|
#[napi]
|
|
427
436
|
pub async fn online(&self) {
|
|
428
|
-
self.inner.online().await;
|
|
437
|
+
let _ = self.inner.closed().run_until(self.inner.online()).await;
|
|
429
438
|
}
|
|
430
439
|
|
|
431
440
|
/// Insert (or replace) a relay configuration at runtime.
|
package/src/watch.rs
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
use n0_future::{StreamExt, task::AbortOnDropHandle};
|
|
2
|
-
use napi::
|
|
2
|
+
use napi::{
|
|
3
|
+
bindgen_prelude::spawn,
|
|
4
|
+
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
|
|
5
|
+
};
|
|
3
6
|
use napi_derive::napi;
|
|
4
7
|
use tokio::sync::Mutex;
|
|
5
8
|
|
|
9
|
+
// NOTE: these watchers are spawned from *synchronous* `#[napi]` methods, which
|
|
10
|
+
// run on the JS main thread — outside any tokio runtime context. Using
|
|
11
|
+
// `tokio::spawn` / `n0_future::task::spawn` there panics with "there is no
|
|
12
|
+
// reactor running" and aborts the process. `napi::bindgen_prelude::spawn`
|
|
13
|
+
// schedules onto napi's global runtime via `Runtime::spawn` directly, so it
|
|
14
|
+
// works without an entered runtime context.
|
|
15
|
+
|
|
6
16
|
use crate::{EndpointAddr, PathEvent, PathSnapshot};
|
|
7
17
|
|
|
8
18
|
/// Handle to a running watcher task. Call `stop()` (or drop) to unregister.
|
|
@@ -32,7 +42,7 @@ pub(crate) fn spawn_watch_addr(
|
|
|
32
42
|
endpoint: iroh::Endpoint,
|
|
33
43
|
cb: ThreadsafeFunction<EndpointAddr>,
|
|
34
44
|
) -> WatchHandle {
|
|
35
|
-
let task =
|
|
45
|
+
let task = spawn(async move {
|
|
36
46
|
use iroh::Watcher;
|
|
37
47
|
let mut stream = endpoint.watch_addr().stream();
|
|
38
48
|
while let Some(addr) = stream.next().await {
|
|
@@ -47,7 +57,7 @@ pub(crate) fn spawn_home_relay_watch(
|
|
|
47
57
|
endpoint: iroh::Endpoint,
|
|
48
58
|
cb: ThreadsafeFunction<Vec<String>>,
|
|
49
59
|
) -> WatchHandle {
|
|
50
|
-
let task =
|
|
60
|
+
let task = spawn(async move {
|
|
51
61
|
use iroh::Watcher;
|
|
52
62
|
let mut stream = endpoint.home_relay_status().stream();
|
|
53
63
|
while let Some(statuses) = stream.next().await {
|
|
@@ -62,7 +72,7 @@ pub(crate) fn spawn_network_change_watch(
|
|
|
62
72
|
endpoint: iroh::Endpoint,
|
|
63
73
|
cb: ThreadsafeFunction<()>,
|
|
64
74
|
) -> WatchHandle {
|
|
65
|
-
let task =
|
|
75
|
+
let task = spawn(async move {
|
|
66
76
|
loop {
|
|
67
77
|
endpoint.network_change().await;
|
|
68
78
|
cb.call(Ok(()), ThreadsafeFunctionCallMode::NonBlocking);
|
|
@@ -75,7 +85,7 @@ pub(crate) fn spawn_paths_watch(
|
|
|
75
85
|
conn: iroh::endpoint::Connection,
|
|
76
86
|
cb: ThreadsafeFunction<Vec<PathSnapshot>>,
|
|
77
87
|
) -> WatchHandle {
|
|
78
|
-
let task =
|
|
88
|
+
let task = spawn(async move {
|
|
79
89
|
let mut stream = conn.paths_stream();
|
|
80
90
|
while let Some(snapshot) = stream.next().await {
|
|
81
91
|
let mapped: Vec<PathSnapshot> = snapshot
|
|
@@ -100,7 +110,7 @@ pub(crate) fn spawn_path_events_watch(
|
|
|
100
110
|
conn: iroh::endpoint::Connection,
|
|
101
111
|
cb: ThreadsafeFunction<PathEvent>,
|
|
102
112
|
) -> WatchHandle {
|
|
103
|
-
let task =
|
|
113
|
+
let task = spawn(async move {
|
|
104
114
|
let mut stream = conn.path_events();
|
|
105
115
|
while let Some(event) = stream.next().await {
|
|
106
116
|
let mapped: PathEvent = event.into();
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Reproduction for the downstream "exit 144 / SIGTERM at process teardown" bug.
|
|
2
|
+
//
|
|
3
|
+
// ROOT CAUSE (reproduced on Linux): a napi *async* method whose returned
|
|
4
|
+
// promise is never awaited / never resolves leaves a pending
|
|
5
|
+
// `napi_resolve_deferred` async resource that REFS the libuv event loop, so
|
|
6
|
+
// Node can never drain its loop and exit on its own. The host process then
|
|
7
|
+
// hangs at teardown and is killed by an external timeout (tinypool / vitest /
|
|
8
|
+
// CI) -> 128 + SIGTERM(15) = 144.
|
|
9
|
+
//
|
|
10
|
+
// Two things make this hard to find downstream, both confirmed here:
|
|
11
|
+
// 1. The ref is INVISIBLE to process._getActiveHandles()/_getActiveRequests()
|
|
12
|
+
// (they report []), which is why the usual handle dump found nothing.
|
|
13
|
+
// 2. It only needs a worker that actually loaded the native module AND left
|
|
14
|
+
// one such call dangling — pure-JS unresolved promises do NOT pin libuv.
|
|
15
|
+
//
|
|
16
|
+
// It IS visible via async_hooks as a live `napi_resolve_deferred` (see the
|
|
17
|
+
// `diagnose` scenario) — that is how to locate the exact offending call in a
|
|
18
|
+
// real test suite (or use the `why-is-node-running` package).
|
|
19
|
+
//
|
|
20
|
+
// Usage:
|
|
21
|
+
// node test/exit-hang-repro.mjs <scenario>
|
|
22
|
+
// Scenarios (parent measures whether the child exits on its own in N seconds):
|
|
23
|
+
// clean-bind-close bind + close -> exits 0 (control)
|
|
24
|
+
// clean-bind-noclose bind, no close -> exits 0 (control)
|
|
25
|
+
// hang-online ep.online() not awaited -> HANG (no close: nothing cancels it)
|
|
26
|
+
// ok-online-close ep.online() then ep.close() -> exits 0 (close() now cancels online())
|
|
27
|
+
// hang-accept ep.acceptNext() not awaited -> HANG
|
|
28
|
+
// ok-accept-close ep.acceptNext() then close() -> exits 0 (close cancels acceptNext())
|
|
29
|
+
// diagnose show the async_hooks resource that pins the loop
|
|
30
|
+
//
|
|
31
|
+
// With no scenario, runs the whole matrix as a parent harness.
|
|
32
|
+
import { spawn } from 'node:child_process'
|
|
33
|
+
import { fileURLToPath } from 'node:url'
|
|
34
|
+
import { dirname } from 'node:path'
|
|
35
|
+
import { createRequire } from 'node:module'
|
|
36
|
+
|
|
37
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
38
|
+
const __dirname = dirname(__filename)
|
|
39
|
+
const require = createRequire(import.meta.url)
|
|
40
|
+
|
|
41
|
+
const scenario = process.argv[2]
|
|
42
|
+
|
|
43
|
+
async function buildMinimal(iroh) {
|
|
44
|
+
const b = iroh.Endpoint.builder()
|
|
45
|
+
iroh.presetMinimal(b)
|
|
46
|
+
b.alpns([Array.from(Buffer.from('exit-hang-repro/0'))])
|
|
47
|
+
return await b.bind()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function child(scenario) {
|
|
51
|
+
const iroh = require('../index.js')
|
|
52
|
+
const ep = await buildMinimal(iroh)
|
|
53
|
+
switch (scenario) {
|
|
54
|
+
case 'clean-bind-close':
|
|
55
|
+
await ep.close()
|
|
56
|
+
break
|
|
57
|
+
case 'clean-bind-noclose':
|
|
58
|
+
break
|
|
59
|
+
case 'hang-online':
|
|
60
|
+
ep.online().then(() => {}, () => {}) // fire-and-forget; never resolves offline
|
|
61
|
+
break
|
|
62
|
+
case 'ok-online-close':
|
|
63
|
+
ep.online().then(() => {}, () => {})
|
|
64
|
+
await ep.close() // close() now cancels the pending online() (binding races closed())
|
|
65
|
+
break
|
|
66
|
+
case 'hang-accept':
|
|
67
|
+
ep.acceptNext().then(() => {}, () => {})
|
|
68
|
+
break
|
|
69
|
+
case 'ok-accept-close':
|
|
70
|
+
ep.acceptNext().then(() => {}, () => {})
|
|
71
|
+
await ep.close() // close() makes acceptNext() resolve(None) -> loop released
|
|
72
|
+
break
|
|
73
|
+
default:
|
|
74
|
+
throw new Error(`unknown scenario ${scenario}`)
|
|
75
|
+
}
|
|
76
|
+
// No process.exit(): the whole point is to observe natural event-loop drain.
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function diagnose() {
|
|
80
|
+
const async_hooks = await import('node:async_hooks')
|
|
81
|
+
const iroh = require('../index.js')
|
|
82
|
+
const alive = new Map()
|
|
83
|
+
const hook = async_hooks.createHook({
|
|
84
|
+
init: (id, type) => alive.set(id, type),
|
|
85
|
+
destroy: (id) => alive.delete(id),
|
|
86
|
+
promiseResolve: (id) => alive.delete(id),
|
|
87
|
+
})
|
|
88
|
+
hook.enable()
|
|
89
|
+
const ep = await buildMinimal(iroh)
|
|
90
|
+
ep.online().then(() => {}, () => {}) // the offender
|
|
91
|
+
setTimeout(() => {
|
|
92
|
+
hook.disable()
|
|
93
|
+
const counts = {}
|
|
94
|
+
for (const t of alive.values()) counts[t] = (counts[t] || 0) + 1
|
|
95
|
+
console.log('still-alive async resources pinning the loop:', JSON.stringify(counts))
|
|
96
|
+
console.log('--> `napi_resolve_deferred` is the invisible ref from the dangling async call.')
|
|
97
|
+
process.exit(0)
|
|
98
|
+
}, 300)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const ALL = [
|
|
102
|
+
'clean-bind-close',
|
|
103
|
+
'clean-bind-noclose',
|
|
104
|
+
'hang-online',
|
|
105
|
+
'ok-online-close',
|
|
106
|
+
'hang-accept',
|
|
107
|
+
'ok-accept-close',
|
|
108
|
+
]
|
|
109
|
+
const DEADLINE_MS = 4000
|
|
110
|
+
|
|
111
|
+
function runChild(spec) {
|
|
112
|
+
return new Promise((resolve) => {
|
|
113
|
+
const start = Date.now()
|
|
114
|
+
const c = spawn(process.execPath, [__filename, spec], { stdio: ['ignore', 'ignore', 'ignore'] })
|
|
115
|
+
let timedOut = false
|
|
116
|
+
const t = setTimeout(() => {
|
|
117
|
+
timedOut = true
|
|
118
|
+
c.kill('SIGTERM')
|
|
119
|
+
}, DEADLINE_MS)
|
|
120
|
+
c.on('exit', (code, signal) => {
|
|
121
|
+
clearTimeout(t)
|
|
122
|
+
resolve({ spec, ms: Date.now() - start, code, signal, timedOut })
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (scenario === 'diagnose') {
|
|
128
|
+
await diagnose()
|
|
129
|
+
} else if (scenario) {
|
|
130
|
+
await child(scenario)
|
|
131
|
+
} else {
|
|
132
|
+
const results = []
|
|
133
|
+
for (const s of ALL) results.push(await runChild(s))
|
|
134
|
+
console.log('\n==================== exit-hang matrix ====================')
|
|
135
|
+
for (const r of results) {
|
|
136
|
+
const v = r.timedOut ? `HANG -> SIGTERM after ${r.ms}ms` : `clean exit code=${r.code} in ${r.ms}ms`
|
|
137
|
+
console.log(`${r.spec.padEnd(20)} ${v}`)
|
|
138
|
+
}
|
|
139
|
+
console.log('\nRun `node test/exit-hang-repro.mjs diagnose` to see the invisible pin.')
|
|
140
|
+
process.exitCode = results.some((r) => r.timedOut) ? 1 : 0
|
|
141
|
+
}
|