@torrent-tv/proxy 2.64.1 → 2.64.3
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/CLAUDE.md +3 -0
- package/docs/logs.md +43 -0
- package/package.json +1 -1
- package/routes/api/sources/stats/get.js +5 -0
- package/services/memory-report.js +23 -4
- package/services/torrent-pool.js +19 -6
package/CLAUDE.md
CHANGED
|
@@ -42,6 +42,9 @@ Linux-only host (e.g. POSIX-only signals must degrade elsewhere).
|
|
|
42
42
|
`routes/*` are now thin HTTP translators.
|
|
43
43
|
- `container-index/` — legacy readers (ebml-reader, matroska/mp4/avi keyframe and
|
|
44
44
|
subtitle tables) — used internally by `container/*`, deprecated as direct import.
|
|
45
|
+
- `docs/logs.md` — where to find logs (HA `docker logs` + `/data/proxy.log`, DO
|
|
46
|
+
`infra-server-1` with forwarded frontend `POST /api/client-logs`). Browser
|
|
47
|
+
console not needed.
|
|
45
48
|
- `playback-planner.js` — single ffmpeg probe returns audioCodec, videoCodec,
|
|
46
49
|
container, durationSeconds. `mode` is advisory; the browser decides.
|
|
47
50
|
- `hls-session-manager.js` — one ffmpeg per (source, file, settings). Serves a
|
package/docs/logs.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Logs — where to look
|
|
2
|
+
|
|
3
|
+
All logs are visible via container `docker logs`, no browser console copy-paste needed.
|
|
4
|
+
|
|
5
|
+
## HA proxy (aarch64, addon `b34a1737_torrent_tv_proxy`)
|
|
6
|
+
|
|
7
|
+
Image `b34a1737/aarch64-addon-torrent_tv_proxy:<version>` = `@torrent-tv/proxy` `<version>` (see `ha-addon/torrent_tv_proxy/config.yaml`).
|
|
8
|
+
|
|
9
|
+
- Console + file are the same: the proxy logs to both `stdout` and `/data/proxy.log` on start (`logging to /data/proxy.log as well as the console`).
|
|
10
|
+
- Via container:
|
|
11
|
+
```bash
|
|
12
|
+
ssh ha "sudo docker logs app_b34a1737_torrent_tv_proxy --tail 200"
|
|
13
|
+
ssh ha "sudo docker exec app_b34a1737_torrent_tv_proxy cat /data/proxy.log | tail -n 300"
|
|
14
|
+
```
|
|
15
|
+
- Filter by session: every transcode session logs its id, e.g. `003ed2fd-7c9b-4cd5-9d05-ff875ff2be23`, `hold segment-00068.mp4 failed`, `encode-run failed`, `EXITED_*`.
|
|
16
|
+
- Host file `/data/proxy.log` survives `docker logs` rotation; `core dumps: 1 present` line shows kept dump under `/data`.
|
|
17
|
+
|
|
18
|
+
SSH to HA requires `MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com` — server `OpenSSH_10.3` on `homeassistant.local` offers only `*-etm` (`Unable to negotiate` / `Corrupted MAC` otherwise). Already in `~/.ssh/config` `Host ha`.
|
|
19
|
+
|
|
20
|
+
## DO server (webauth.courses, `infra-server-1`)
|
|
21
|
+
|
|
22
|
+
- Server is `infra-server-1` (`ghcr.io/torrent-tv/server:latest`) on `do` (`206.189.97.152`).
|
|
23
|
+
- Frontend logs are forwarded: browser batches `{sessionId, tag, signalSessionId, lines: [{level,ts,msg}]}` and `POST`s to `https://webauth.courses/api/client-logs` (`server/routes/api/client-logs/post.js:56`), server does `console.log` with prefix `[client <tag> <id> sig=<webrtcSessionId>]`. They appear together with backend logs in the same container.
|
|
24
|
+
- Via container:
|
|
25
|
+
```bash
|
|
26
|
+
ssh do "docker logs infra-server-1 --tail 200 | cat"
|
|
27
|
+
# filter a single viewing:
|
|
28
|
+
ssh do "docker logs infra-server-1 --tail 500 | grep 003ed2fd"
|
|
29
|
+
ssh do "docker logs infra-server-1 --tail 500 | grep '\[client'"
|
|
30
|
+
```
|
|
31
|
+
- No need to open eruda or copy browser console on phone — it is already in `infra-server-1` logs.
|
|
32
|
+
|
|
33
|
+
## Quick triage
|
|
34
|
+
|
|
35
|
+
- Rewind/seek bug (hold 0ms → 500): HA `hold ... failed after 0ms → 500` + `encode-run` lines for the same `<sessionId>`, and DO `[client ...] fragLoadError / levelLoadError 500` for same `sn`.
|
|
36
|
+
- Cushion / link budget: HA `memory: rss=... anon=...` and `cushion` lines; DO `[eta]` / `[cushion]` from client.
|
|
37
|
+
- Update check: `ssh ha "sudo docker exec hassio_cli ha apps info b34a1737_torrent_tv_proxy | grep version"` and `ssh ha "sudo docker ps --filter name=app_b34a1737_torrent_tv_proxy"`.
|
|
38
|
+
|
|
39
|
+
## Related
|
|
40
|
+
|
|
41
|
+
- `ha-addon/CLAUDE.md` — addon build/update detour via `hassio_cli`, cache-bust via `config.yaml` version.
|
|
42
|
+
- `docs/container-architecture.md` — what container/track classes log and where.
|
|
43
|
+
- `server/routes/api/client-logs/post.js:1` — sanitization (control chars → space, `MAX_LINES 50`, `MAX_MSG_LEN 2000`).
|
package/package.json
CHANGED
|
@@ -34,8 +34,13 @@ export async function handleApiSourceStatsGet(req, reply, { sourceRegistry, torr
|
|
|
34
34
|
torrent = await torrentPool.getTorrent(sourceRecord.sourceType, sourceRecord.source);
|
|
35
35
|
} catch (error) {
|
|
36
36
|
const message = error instanceof Error ? error.message : String(error);
|
|
37
|
+
logger.warn(`stats: getTorrent failed for ${sourceKey.slice(0, 8)}: ${message} stack=${error instanceof Error ? error.stack?.split("\n")[1]?.trim() ?? "" : ""}`);
|
|
37
38
|
return reply.code(500).send({ error: `Failed to load torrent: ${message}` });
|
|
38
39
|
}
|
|
40
|
+
if (!torrent) {
|
|
41
|
+
logger.warn(`stats: torrent missing for ${sourceKey.slice(0, 8)} (getTorrent returned null/undefined)`);
|
|
42
|
+
return reply.code(500).send({ error: "Torrent instance not found" });
|
|
43
|
+
}
|
|
39
44
|
|
|
40
45
|
const fileIndexRaw = typeof req.query.fileIndex === "string" ? req.query.fileIndex : "";
|
|
41
46
|
const fileIndex = fileIndexRaw !== "" && /^\d+$/.test(fileIndexRaw) ? Number(fileIndexRaw) : null;
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
|
|
22
22
|
import { readFile, statfs } from "node:fs/promises";
|
|
23
23
|
import os from "node:os";
|
|
24
|
+
import v8 from "node:v8";
|
|
25
|
+
import path from "node:path";
|
|
24
26
|
|
|
25
27
|
/** How often the reading is taken and written. */
|
|
26
28
|
export const MEMORY_REPORT_INTERVAL_MS = 60_000;
|
|
@@ -234,6 +236,7 @@ export function startMemoryReport({
|
|
|
234
236
|
diskPath = "",
|
|
235
237
|
intervalMs = MEMORY_REPORT_INTERVAL_MS
|
|
236
238
|
}) {
|
|
239
|
+
let highWaterRss = 0;
|
|
237
240
|
const tick = async () => {
|
|
238
241
|
try {
|
|
239
242
|
let stores = [];
|
|
@@ -243,21 +246,37 @@ export function startMemoryReport({
|
|
|
243
246
|
// silent-ok: a store list that cannot be read must not stop the reading
|
|
244
247
|
// that matters, which is the process's own.
|
|
245
248
|
}
|
|
249
|
+
const processMemory = readProcessMemory();
|
|
246
250
|
if (scope === "thread") {
|
|
247
|
-
log(describeMemory({ scope, label, process:
|
|
251
|
+
log(describeMemory({ scope, label, process: processMemory, stores }));
|
|
248
252
|
return;
|
|
249
253
|
}
|
|
250
254
|
const { bytes, measured } = await availableMemory();
|
|
255
|
+
const anonymousBytes = await readAnonymousMemory();
|
|
256
|
+
const diskFreeBytes = diskPath ? await readDiskFree(diskPath) : null;
|
|
251
257
|
log(describeMemory({
|
|
252
258
|
scope,
|
|
253
259
|
label,
|
|
254
|
-
process:
|
|
260
|
+
process: processMemory,
|
|
255
261
|
availableBytes: bytes,
|
|
256
262
|
availableMeasured: measured,
|
|
257
|
-
anonymousBytes
|
|
258
|
-
diskFreeBytes
|
|
263
|
+
anonymousBytes,
|
|
264
|
+
diskFreeBytes,
|
|
259
265
|
stores
|
|
260
266
|
}));
|
|
267
|
+
// High-water and heap snapshot on growth — gives a file to open in Chrome DevTools
|
|
268
|
+
// when the kernel is about to kill the process. One snapshot per high-water.
|
|
269
|
+
if (processMemory.rss > highWaterRss + 100 * 1024 * 1024 && processMemory.rss > 500 * 1024 * 1024) {
|
|
270
|
+
highWaterRss = processMemory.rss;
|
|
271
|
+
try {
|
|
272
|
+
const snapPath = path.join(os.tmpdir(), `heap-${Date.now()}-${processMemory.rss}.heapsnapshot`);
|
|
273
|
+
v8.writeHeapSnapshot(snapPath);
|
|
274
|
+
log(`memory: wrote heap snapshot to ${snapPath} rss=${Math.round(processMemory.rss / (1024 * 1024))}MB anon=${anonymousBytes ? Math.round(anonymousBytes / (1024 * 1024)) : "?"}MB`);
|
|
275
|
+
} catch {}
|
|
276
|
+
}
|
|
277
|
+
if (anonymousBytes !== null && processMemory.rss > 800 * 1024 * 1024) {
|
|
278
|
+
log(`memory: high rss=${Math.round(processMemory.rss / (1024 * 1024))}MB anon=${Math.round(anonymousBytes / (1024 * 1024))}MB heap=${Math.round(processMemory.heapUsed / (1024 * 1024))}MB — watch for OOM`);
|
|
279
|
+
}
|
|
261
280
|
} catch {
|
|
262
281
|
// silent-ok: a reading that fails is not worth ending the series over.
|
|
263
282
|
}
|
package/services/torrent-pool.js
CHANGED
|
@@ -1191,7 +1191,7 @@ export class TorrentPool {
|
|
|
1191
1191
|
`(~${(freed / (1024 * 1024)).toFixed(0)} MB)`
|
|
1192
1192
|
);
|
|
1193
1193
|
this.#cancelIdleRemoval(torrent);
|
|
1194
|
-
this.#removeTorrent(torrent);
|
|
1194
|
+
this.#removeTorrent(torrent, "disk-cap");
|
|
1195
1195
|
used -= freed;
|
|
1196
1196
|
}
|
|
1197
1197
|
}
|
|
@@ -1582,15 +1582,20 @@ export class TorrentPool {
|
|
|
1582
1582
|
return;
|
|
1583
1583
|
}
|
|
1584
1584
|
this.#cancelIdleRemoval(torrent);
|
|
1585
|
+
const name = typeof torrent.name === "string" ? torrent.name : "(unknown)";
|
|
1586
|
+
const ih = String(torrent.infoHash ?? "?").slice(0, 8);
|
|
1587
|
+
logger.info(`torrent-pool: scheduling idle removal for "${name}" [${ih}] in ${TORRENT_IDLE_TTL_MS / 1000}s`);
|
|
1585
1588
|
const timer = setTimeout(() => {
|
|
1586
1589
|
this.#idleTimers.delete(torrent);
|
|
1587
1590
|
// Re-check: a new acquire since scheduling would have cancelled this
|
|
1588
1591
|
// timer, but guard anyway against a race.
|
|
1589
1592
|
const usage = this.fileUsageByTorrent.get(torrent);
|
|
1590
1593
|
if (usage && usage.size > 0) {
|
|
1594
|
+
logger.info(`torrent-pool: idle timer fired for "${name}" [${ih}] but refcount ${usage.size} >0 — keep`);
|
|
1591
1595
|
return;
|
|
1592
1596
|
}
|
|
1593
|
-
|
|
1597
|
+
logger.warn(`torrent-pool: idle TTL fired for "${name}" [${ih}] — removing torrent (reason=idle-ttl caller=scheduleIdleRemoval)`);
|
|
1598
|
+
this.#removeTorrent(torrent, "idle-ttl");
|
|
1594
1599
|
}, TORRENT_IDLE_TTL_MS);
|
|
1595
1600
|
timer.unref?.();
|
|
1596
1601
|
this.#idleTimers.set(torrent, timer);
|
|
@@ -1615,12 +1620,20 @@ export class TorrentPool {
|
|
|
1615
1620
|
* disk while the proxy keeps running. Best-effort.
|
|
1616
1621
|
*
|
|
1617
1622
|
* @param {import("webtorrent").Torrent} torrent
|
|
1623
|
+
* @param {string} [reason="unknown"] - Why removal was requested (idle-ttl, disk-cap, evict, api).
|
|
1618
1624
|
* @returns {void}
|
|
1619
1625
|
*/
|
|
1620
|
-
#removeTorrent(torrent) {
|
|
1626
|
+
#removeTorrent(torrent, reason = "unknown") {
|
|
1621
1627
|
if (!torrent) {
|
|
1622
1628
|
return;
|
|
1623
1629
|
}
|
|
1630
|
+
const infoHash = String(torrent.infoHash ?? "?").slice(0, 8);
|
|
1631
|
+
const name = typeof torrent.name === "string" ? torrent.name : "(unknown)";
|
|
1632
|
+
const usage = this.fileUsageByTorrent.get(torrent);
|
|
1633
|
+
const refcount = usage ? usage.size : 0;
|
|
1634
|
+
const hasData = (() => { try { return torrentDownloadedBytes(torrent); } catch { return -1; } })();
|
|
1635
|
+
// Capture caller for diagnostics — not for control flow.
|
|
1636
|
+
const caller = new Error().stack?.split("\n")[2]?.trim() ?? "";
|
|
1624
1637
|
// Drop it from the source→torrent map so a later request re-adds it.
|
|
1625
1638
|
for (const [key, value] of this.torrents) {
|
|
1626
1639
|
if (value === torrent) {
|
|
@@ -1631,14 +1644,14 @@ export class TorrentPool {
|
|
|
1631
1644
|
this.fileUsageByTorrent.delete(torrent);
|
|
1632
1645
|
this.#lastAccess.delete(torrent);
|
|
1633
1646
|
this.#readPositionByTorrent.delete(torrent);
|
|
1634
|
-
|
|
1647
|
+
logger.warn(`torrent-pool: removing torrent "${name}" [${infoHash}] reason=${reason} refcount=${refcount} downloaded=${hasData}B caller=${caller}`);
|
|
1635
1648
|
try {
|
|
1636
1649
|
torrent.destroy({ destroyStore: true }, () => {
|
|
1637
|
-
logger.info(`torrent-pool: removed
|
|
1650
|
+
logger.info(`torrent-pool: removed torrent "${name}" [${infoHash}] reason=${reason} and its store`);
|
|
1638
1651
|
});
|
|
1639
1652
|
} catch (error) {
|
|
1640
1653
|
const message = error instanceof Error ? error.message : String(error);
|
|
1641
|
-
logger.warn(`torrent-pool: failed to remove
|
|
1654
|
+
logger.warn(`torrent-pool: failed to remove torrent "${name}" [${infoHash}] reason=${reason}: ${message}`);
|
|
1642
1655
|
}
|
|
1643
1656
|
}
|
|
1644
1657
|
|