@torrent-tv/proxy 2.79.0 → 2.80.0
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/CHANGELOG.md +4 -0
- package/bin/cli.js +20 -1
- package/package.json +1 -1
- package/services/usrsctp-state.js +35 -6
- package/test/usrsctp-state.test.js +18 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
## 2.80.0
|
|
2
|
+
|
|
3
|
+
- **Fix**: The reading of usrsctp's own state is OFF unless `--usrsctp-state` is given, and its deadline is counted by a separate process. On 2026-09-05 a delivery probe declared a wedge that lasted half a second and cleared itself; the reading it triggered attached gdb to this process, which stops every one of its eighty threads for as long as it is attached. The log ended mid-second, `/healthz` stopped answering, and the viewer waited a minute and was told the proxy had sent no video. The fifteen-second guard could not fire — it was a timer inside the process gdb had stopped — and killing gdb left the main thread deadlocked for good, so only restarting the addon recovered it. A means of diagnosis may not stop the product: this one attaches to a live process and is triggered by a verdict with a known history of false positives, so it is switched on deliberately while somebody is watching, and `timeout` counts its seconds from outside.
|
|
4
|
+
|
|
1
5
|
## 2.79.0
|
|
2
6
|
|
|
3
7
|
- **Fix**: Starting an encoder no longer stops another one. The line that did it looked for a live run whose own start was not below the new one's and killed it — a rule left from when a session held exactly one run and "the previous one" meant "the only one". Once a session could hold several, it began killing runs the plan had decided to keep: 294 stops for that reason in eight minutes of field 2026-09-05, against four starts a viewer had asked for. Who is stopped is decided in one place now, and starting is not it.
|
package/bin/cli.js
CHANGED
|
@@ -85,6 +85,10 @@ program
|
|
|
85
85
|
.option("--no-transcode-audio", "Disable optional HLS AAC audio transcoding")
|
|
86
86
|
.option("--no-port-mapping", "Disable automatic UPnP/NAT-PMP port mapping")
|
|
87
87
|
.option("--delivery-sink", "Serve /api/delivery-sink, a torrent-free byte stream for delivery testing")
|
|
88
|
+
.option(
|
|
89
|
+
"--usrsctp-state",
|
|
90
|
+
"Read usrsctp's association state with gdb when a wedge is declared. OFF by default: gdb attaches to THIS process and stops every thread of it while it works."
|
|
91
|
+
)
|
|
88
92
|
.option("--max-disk-bytes <bytes>", "Cap total downloaded torrent data (0 = disabled; default min(10GB, half free disk))")
|
|
89
93
|
.option("--memory-bytes <bytes>", "Per-torrent budget for pieces kept in memory before spilling to disk (default 512MB)")
|
|
90
94
|
.option("--ffmpeg-bin <path>", "Path to ffmpeg binary")
|
|
@@ -345,7 +349,22 @@ try {
|
|
|
345
349
|
// declared (roadmap item 11) — no source rebuild, the module ships
|
|
346
350
|
// unstripped. A host without gdb just never gets a reading, the same way a
|
|
347
351
|
// host without tcpdump never gets a packet capture.
|
|
348
|
-
|
|
352
|
+
// OFF UNLESS ASKED FOR, and the reason is a field incident rather than
|
|
353
|
+
// caution. On 2026-09-05 a delivery probe declared a wedge that lasted half a
|
|
354
|
+
// second and cleared itself; the reading it triggered attached gdb to this
|
|
355
|
+
// process, which stopped all eighty of its threads. The log ended mid-second,
|
|
356
|
+
// /healthz stopped answering, and the viewer waited a minute and was told the
|
|
357
|
+
// proxy had sent no video. The fifteen-second guard could not fire — it is a
|
|
358
|
+
// timer inside the process gdb had stopped — and killing gdb left the main
|
|
359
|
+
// thread deadlocked for good, so only restarting the addon recovered it.
|
|
360
|
+
//
|
|
361
|
+
// A means of diagnosis may not stop the product. This one attaches to a live
|
|
362
|
+
// process and is triggered by a verdict with a known history of false
|
|
363
|
+
// positives, so it is a thing to switch on deliberately while watching, not
|
|
364
|
+
// something that arms itself.
|
|
365
|
+
usrsctpStateReader = options.usrsctpState === true
|
|
366
|
+
? createUsrsctpStateReader({ log: (message) => logger.info(message) })
|
|
367
|
+
: null;
|
|
349
368
|
|
|
350
369
|
// What this process holds, once a minute. The kernel killed the proxy on
|
|
351
370
|
// 2026-08-28 at 2.4 GB resident and the log had never said a word about
|
package/package.json
CHANGED
|
@@ -17,9 +17,14 @@
|
|
|
17
17
|
* only as long as someone remembers to copy it back after a container is
|
|
18
18
|
* recreated.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* **THIS STOPS THE PRODUCT WHILE IT WORKS, so it is off unless asked for.**
|
|
21
|
+
* `gdb` attaches with ptrace and pauses every thread of this process for as
|
|
22
|
+
* long as it is attached. The manual procedure this automates took a fraction
|
|
23
|
+
* of a second; on 2026-09-05 the automatic one held the proxy for four minutes
|
|
24
|
+
* and the viewer was told the proxy had sent no video. Two things follow, and
|
|
25
|
+
* both are in the code below: the deadline is counted by a separate process,
|
|
26
|
+
* because a timer inside a stopped process never fires; and `--usrsctp-state`
|
|
27
|
+
* has to be given for any of this to happen at all.
|
|
23
28
|
*/
|
|
24
29
|
|
|
25
30
|
import { spawn } from "node:child_process";
|
|
@@ -91,9 +96,29 @@ export function createUsrsctpStateReader({
|
|
|
91
96
|
resolve();
|
|
92
97
|
};
|
|
93
98
|
try {
|
|
99
|
+
// THE DEADLINE IS SET OUTSIDE THE PROCESS THAT IS BEING STOPPED.
|
|
100
|
+
//
|
|
101
|
+
// gdb attaches with ptrace and stops every thread of this process
|
|
102
|
+
// while it works — including whichever one would have counted the
|
|
103
|
+
// seconds. The guard below used to be a `setTimeout` here, and on
|
|
104
|
+
// 2026-09-05 it did not fire: gdb held the proxy for four minutes,
|
|
105
|
+
// the log ended mid-second and the viewer was told the proxy had
|
|
106
|
+
// sent no video. `timeout` is a separate process and keeps counting
|
|
107
|
+
// whatever happens to this one.
|
|
94
108
|
child = spawnProcess(
|
|
95
|
-
"
|
|
96
|
-
[
|
|
109
|
+
"timeout",
|
|
110
|
+
[
|
|
111
|
+
"-s",
|
|
112
|
+
"KILL",
|
|
113
|
+
String(Math.ceil(GDB_TIMEOUT_MS / 1000)),
|
|
114
|
+
"gdb",
|
|
115
|
+
"-q",
|
|
116
|
+
"-batch",
|
|
117
|
+
"-p",
|
|
118
|
+
String(pid),
|
|
119
|
+
"-x",
|
|
120
|
+
scriptPath
|
|
121
|
+
],
|
|
97
122
|
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
98
123
|
);
|
|
99
124
|
} catch (error) {
|
|
@@ -112,13 +137,17 @@ export function createUsrsctpStateReader({
|
|
|
112
137
|
finish();
|
|
113
138
|
});
|
|
114
139
|
child.on("close", finish);
|
|
140
|
+
// A second guard, for the case where `timeout` itself is not on the
|
|
141
|
+
// host: it can only fire while this process is running, which is
|
|
142
|
+
// exactly what cannot be relied on here, so it is a backstop and not
|
|
143
|
+
// the deadline.
|
|
115
144
|
const killer = setTimeout(() => {
|
|
116
145
|
try {
|
|
117
146
|
child.kill("SIGKILL");
|
|
118
147
|
} catch {
|
|
119
148
|
// already gone
|
|
120
149
|
}
|
|
121
|
-
}, GDB_TIMEOUT_MS);
|
|
150
|
+
}, GDB_TIMEOUT_MS * 2);
|
|
122
151
|
if (typeof killer.unref === "function") {
|
|
123
152
|
killer.unref();
|
|
124
153
|
}
|
|
@@ -62,7 +62,7 @@ async function waitFor(check) {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
test("
|
|
65
|
+
test("the deadline is counted outside the process gdb stops", async () => {
|
|
66
66
|
const { spawnProcess, calls } = makeSpawn("state=8 rwnd=95890\n");
|
|
67
67
|
const lines = [];
|
|
68
68
|
const reader = createUsrsctpStateReader({
|
|
@@ -74,7 +74,23 @@ test("gdb is invoked attached to this process with the bundled script", async ()
|
|
|
74
74
|
assert.equal(started, true);
|
|
75
75
|
await waitFor(() => lines.length > 0);
|
|
76
76
|
assert.equal(calls.length, 1);
|
|
77
|
-
|
|
77
|
+
// `timeout` is a separate process, so it keeps counting while gdb holds every
|
|
78
|
+
// thread of this one. A `setTimeout` here cannot fire — measured in the field
|
|
79
|
+
// on 2026-09-05, where gdb held the proxy for four minutes and the guard set
|
|
80
|
+
// for fifteen seconds never ran.
|
|
81
|
+
assert.equal(calls[0].command, "timeout");
|
|
82
|
+
assert.deepEqual(calls[0].args, [
|
|
83
|
+
"-s",
|
|
84
|
+
"KILL",
|
|
85
|
+
"15",
|
|
86
|
+
"gdb",
|
|
87
|
+
"-q",
|
|
88
|
+
"-batch",
|
|
89
|
+
"-p",
|
|
90
|
+
"4242",
|
|
91
|
+
"-x",
|
|
92
|
+
SCTPSTATE_SCRIPT_PATH
|
|
93
|
+
]);
|
|
78
94
|
assert.match(lines[0], /state=8 rwnd=95890/);
|
|
79
95
|
assert.match(lines[0], /test wedge/);
|
|
80
96
|
});
|