@torrent-tv/proxy 2.89.18 → 2.89.19
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/package.json +1 -1
- package/utils/client-log-file.js +147 -42
package/package.json
CHANGED
package/utils/client-log-file.js
CHANGED
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
* the evidence for exactly this reason.
|
|
10
10
|
*
|
|
11
11
|
* Written here instead, next to the proxy's own log, on the same durable
|
|
12
|
-
* directory (`/data` on the addon host). One file per viewing session
|
|
13
|
-
* the two halves join without guessing: the session's start,
|
|
12
|
+
* directory (`/data` on the addon host). One file per viewing session and
|
|
13
|
+
* torrent, named so the two halves join without guessing: the session's start,
|
|
14
|
+
* then the torrent.
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
|
-
import { createWriteStream, mkdirSync,
|
|
17
|
+
import { createWriteStream, fstatSync, mkdirSync, openSync, renameSync } from "node:fs";
|
|
17
18
|
import { dirname, join } from "node:path";
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -82,45 +83,106 @@ function stamp(iso) {
|
|
|
82
83
|
*
|
|
83
84
|
* @param {string} proxyLogPath - The proxy's own log file; its directory is
|
|
84
85
|
* used, so the two halves are always side by side.
|
|
85
|
-
* @returns {{ write: (session: object, lines: string[]) => void, close: () => void }}
|
|
86
|
+
* @returns {{ write: (session: object, lines: string[]) => void, close: () => Promise<void> }}
|
|
86
87
|
*/
|
|
87
88
|
export function createClientLogFiles(proxyLogPath) {
|
|
88
89
|
const directory = typeof proxyLogPath === "string" && proxyLogPath.length > 0
|
|
89
90
|
? dirname(proxyLogPath)
|
|
90
91
|
: "";
|
|
91
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* @typedef {object} OpenFile
|
|
94
|
+
* @property {import("node:fs").WriteStream} stream
|
|
95
|
+
* @property {string} path
|
|
96
|
+
* @property {number} bytes
|
|
97
|
+
* @property {string} prefix - `client-<start>-<session>`, the part every file
|
|
98
|
+
* of one session shares, so they are found together.
|
|
99
|
+
* @property {string} film - Which torrent the name carries; empty while it
|
|
100
|
+
* still says `no-torrent-yet`.
|
|
101
|
+
*/
|
|
102
|
+
/** @type {Map<string, OpenFile>} */
|
|
92
103
|
const open = new Map();
|
|
104
|
+
/**
|
|
105
|
+
* Files taken out of `open` whose last lines are still being flushed: a
|
|
106
|
+
* rotated file, and the file of a film the session has moved away from.
|
|
107
|
+
*
|
|
108
|
+
* @type {Set<Promise<void>>}
|
|
109
|
+
*/
|
|
110
|
+
const ending = new Set();
|
|
93
111
|
|
|
94
112
|
/**
|
|
95
|
-
*
|
|
113
|
+
* Finish a file that is no longer written to, and remember the wait so that
|
|
114
|
+
* `close()` does not return before its last lines are on disk.
|
|
96
115
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
|
|
100
|
-
|
|
116
|
+
* @param {OpenFile} record
|
|
117
|
+
* @returns {void}
|
|
118
|
+
*/
|
|
119
|
+
function finish(record) {
|
|
120
|
+
const done = new Promise((resolve) => {
|
|
121
|
+
try {
|
|
122
|
+
record.stream.end(() => resolve(undefined));
|
|
123
|
+
} catch {
|
|
124
|
+
// A stream that cannot be ended has nothing left to flush.
|
|
125
|
+
resolve(undefined);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
ending.add(done);
|
|
129
|
+
void done.then(() => ending.delete(done));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Which torrent a batch names, or empty when none is chosen yet.
|
|
101
134
|
*
|
|
102
|
-
* @param {{
|
|
103
|
-
* @returns {
|
|
135
|
+
* @param {{ torrentName: string, infoHash: string }} session
|
|
136
|
+
* @returns {string}
|
|
104
137
|
*/
|
|
105
|
-
function
|
|
106
|
-
if (!
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
138
|
+
function filmOf(session) {
|
|
139
|
+
if (!session.torrentName) return "";
|
|
140
|
+
return session.infoHash || session.torrentName;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {string} prefix
|
|
145
|
+
* @param {{ torrentName: string, infoHash: string }} session
|
|
146
|
+
* @returns {string}
|
|
147
|
+
*/
|
|
148
|
+
function pathFor(prefix, session) {
|
|
110
149
|
const film = session.torrentName
|
|
111
150
|
? `${safeName(session.torrentName, 60)}-${safeName(session.infoHash, 8)}`
|
|
112
151
|
: "no-torrent-yet";
|
|
113
|
-
|
|
152
|
+
return join(directory, `${prefix}-${film}.log`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Open a file for appending.
|
|
157
|
+
*
|
|
158
|
+
* The descriptor is opened synchronously and handed to the stream, so the
|
|
159
|
+
* file exists under its path before this returns. A stream that opens its
|
|
160
|
+
* own file does so later, and a rename made in between would leave the
|
|
161
|
+
* stream creating a second file under the old name.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} key
|
|
164
|
+
* @param {string} prefix
|
|
165
|
+
* @param {{ torrentName: string, infoHash: string }} session
|
|
166
|
+
* @returns {OpenFile | null}
|
|
167
|
+
*/
|
|
168
|
+
function openFile(key, prefix, session) {
|
|
169
|
+
const path = pathFor(prefix, session);
|
|
114
170
|
try {
|
|
115
171
|
mkdirSync(directory, { recursive: true });
|
|
172
|
+
const fd = openSync(path, "a");
|
|
173
|
+
/** @type {OpenFile} */
|
|
116
174
|
const record = {
|
|
117
|
-
stream: createWriteStream(path, {
|
|
175
|
+
stream: createWriteStream(path, { fd }),
|
|
118
176
|
path,
|
|
119
|
-
bytes:
|
|
177
|
+
bytes: fstatSync(fd).size,
|
|
178
|
+
prefix,
|
|
179
|
+
film: filmOf(session)
|
|
120
180
|
};
|
|
121
181
|
// A file that stops being writable must not take the proxy down with it,
|
|
122
182
|
// and must not be retried on every line.
|
|
123
|
-
record.stream.on("error", () =>
|
|
183
|
+
record.stream.on("error", () => {
|
|
184
|
+
if (open.get(key) === record) open.delete(key);
|
|
185
|
+
});
|
|
124
186
|
open.set(key, record);
|
|
125
187
|
return record;
|
|
126
188
|
} catch {
|
|
@@ -128,6 +190,51 @@ export function createClientLogFiles(proxyLogPath) {
|
|
|
128
190
|
}
|
|
129
191
|
}
|
|
130
192
|
|
|
193
|
+
/**
|
|
194
|
+
* The file for this session, opened on first use.
|
|
195
|
+
*
|
|
196
|
+
* Keyed by the session's own id rather than by the name. The first batch
|
|
197
|
+
* always arrives before a torrent is chosen, and the lines it carries — the
|
|
198
|
+
* page opening, the proxy being chosen, a connection failing — belong to the
|
|
199
|
+
* film the session goes on to play. So the file is RENAMED when the first
|
|
200
|
+
* torrent arrives, and those lines stay in it.
|
|
201
|
+
*
|
|
202
|
+
* A different torrent chosen later in the same page starts a file of its
|
|
203
|
+
* own under the same prefix: renaming again would leave the first film's
|
|
204
|
+
* lines under the second film's name, where nobody looking for the first
|
|
205
|
+
* film finds them.
|
|
206
|
+
*
|
|
207
|
+
* @param {{ sessionId: string, startedAt: string, torrentName: string, infoHash: string }} session
|
|
208
|
+
* @returns {OpenFile | null}
|
|
209
|
+
*/
|
|
210
|
+
function fileFor(session) {
|
|
211
|
+
if (!directory) return null;
|
|
212
|
+
const key = session.sessionId;
|
|
213
|
+
const film = filmOf(session);
|
|
214
|
+
const existing = open.get(key);
|
|
215
|
+
if (!existing) {
|
|
216
|
+
const prefix = `client-${stamp(session.startedAt)}-${safeName(session.sessionId, 8)}`;
|
|
217
|
+
return openFile(key, prefix, session);
|
|
218
|
+
}
|
|
219
|
+
if (film === "" || film === existing.film) return existing;
|
|
220
|
+
if (existing.film === "") {
|
|
221
|
+
const path = pathFor(existing.prefix, session);
|
|
222
|
+
try {
|
|
223
|
+
renameSync(existing.path, path);
|
|
224
|
+
existing.path = path;
|
|
225
|
+
} catch {
|
|
226
|
+
// The lines keep going to the file under its old name; a name that
|
|
227
|
+
// cannot change is better than a session's log lost to an exception.
|
|
228
|
+
}
|
|
229
|
+
// Set even when the rename failed, so it is not retried on every batch.
|
|
230
|
+
existing.film = film;
|
|
231
|
+
return existing;
|
|
232
|
+
}
|
|
233
|
+
open.delete(key);
|
|
234
|
+
finish(existing);
|
|
235
|
+
return openFile(key, existing.prefix, session);
|
|
236
|
+
}
|
|
237
|
+
|
|
131
238
|
return {
|
|
132
239
|
write(session, lines) {
|
|
133
240
|
const record = fileFor(session);
|
|
@@ -138,40 +245,38 @@ export function createClientLogFiles(proxyLogPath) {
|
|
|
138
245
|
record.stream.write(text);
|
|
139
246
|
}
|
|
140
247
|
if (record.bytes >= MAX_BYTES) {
|
|
248
|
+
// Renamed before it is ended: the stream writes through its own
|
|
249
|
+
// descriptor, so the lines still in flight follow the file to its new
|
|
250
|
+
// name, and the next batch opens a fresh file under the old one
|
|
251
|
+
// instead of appending to the file that is about to be moved.
|
|
141
252
|
try {
|
|
142
|
-
record.
|
|
143
|
-
try {
|
|
144
|
-
renameSync(record.path, `${record.path}.1`);
|
|
145
|
-
} catch {
|
|
146
|
-
// Renaming a file that has gone leaves nothing to do.
|
|
147
|
-
}
|
|
148
|
-
});
|
|
253
|
+
renameSync(record.path, `${record.path}.1`);
|
|
149
254
|
} catch {
|
|
150
|
-
// A rotation that cannot happen leaves the file growing
|
|
151
|
-
// better than losing the session's log
|
|
255
|
+
// A rotation that cannot happen leaves the file growing under its
|
|
256
|
+
// name, which is better than losing the session's log. It is tried
|
|
257
|
+
// again after as much again, not on every batch.
|
|
258
|
+
record.bytes = 0;
|
|
259
|
+
return;
|
|
152
260
|
}
|
|
153
|
-
open.delete(session.sessionId);
|
|
261
|
+
if (open.get(session.sessionId) === record) open.delete(session.sessionId);
|
|
262
|
+
finish(record);
|
|
154
263
|
}
|
|
155
264
|
},
|
|
156
265
|
/**
|
|
157
266
|
* Finish every open file.
|
|
158
267
|
*
|
|
159
|
-
* Awaitable, and that is not a convenience: a write stream
|
|
160
|
-
*
|
|
161
|
-
*
|
|
268
|
+
* Awaitable, and that is not a convenience: a write stream flushes
|
|
269
|
+
* asynchronously, so a process that ends without waiting loses the last
|
|
270
|
+
* lines of every session — which are the ones a crash is explained by.
|
|
271
|
+
* That includes the files already set aside by a rotation or by a change
|
|
272
|
+
* of film.
|
|
162
273
|
*
|
|
163
274
|
* @returns {Promise<void>}
|
|
164
275
|
*/
|
|
165
276
|
async close() {
|
|
166
|
-
const
|
|
167
|
-
try {
|
|
168
|
-
record.stream.end(resolve);
|
|
169
|
-
} catch {
|
|
170
|
-
resolve(undefined);
|
|
171
|
-
}
|
|
172
|
-
}));
|
|
277
|
+
for (const record of open.values()) finish(record);
|
|
173
278
|
open.clear();
|
|
174
|
-
await Promise.all(ending);
|
|
279
|
+
await Promise.all([...ending]);
|
|
175
280
|
}
|
|
176
281
|
};
|
|
177
282
|
}
|