@yetech/estra7ah 3.3.2 → 3.3.4
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/dist/estra7ah.js +122 -37
- package/package.json +1 -1
package/dist/estra7ah.js
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// Never let a stray async error kill the launcher: PM2 would restart it and
|
|
4
|
+
// spawn a second runner while the orphaned one still holds the ports.
|
|
5
|
+
process.on("uncaughtException", (err) => {
|
|
6
|
+
console.error("Unexpected error:", err && err.message ? err.message : err);
|
|
7
|
+
});
|
|
8
|
+
|
|
3
9
|
const axios = require("axios");
|
|
4
10
|
const fs = require("fs");
|
|
5
11
|
const ProgressBar = require("progress");
|
|
6
12
|
const child_process = require("child_process");
|
|
7
13
|
|
|
8
14
|
const YETECH_URL = `http://premium.yetech.co:80`;
|
|
15
|
+
// Cloudflare-cached mirror of /runner/* (origin = same VPS). The ?v= query
|
|
16
|
+
// string busts the edge cache exactly when a new build lands, so stale
|
|
17
|
+
// binaries are impossible and no cache purging is ever needed.
|
|
18
|
+
const CDN_URL = `https://dl.yetech.co`;
|
|
9
19
|
const RUNNERS = {
|
|
10
20
|
win64: "runner-win-x64.exe",
|
|
11
21
|
linux64: "runner-linux-x64",
|
|
12
22
|
linuxarm64: "runner-linux-arm64",
|
|
13
|
-
|
|
23
|
+
// pkg has no node18 armv7 target, so 32-bit ARM (Raspberry Pi OS) gets the
|
|
24
|
+
// plain webpack bundle and runs it under the system Node instead.
|
|
25
|
+
linuxarmv7: "runner.js",
|
|
14
26
|
};
|
|
15
27
|
const DIR_NAME = __dirname;
|
|
16
28
|
|
|
@@ -40,7 +52,10 @@ function getRunner() {
|
|
|
40
52
|
// pulling the binary over several range requests at once is ~N times faster.
|
|
41
53
|
const PARALLEL_CHUNKS = 6;
|
|
42
54
|
|
|
43
|
-
|
|
55
|
+
// ctx = { fd, closed, streams } shared by all chunks of one download, so a
|
|
56
|
+
// failed download can cancel its siblings before the fd is closed — otherwise
|
|
57
|
+
// their in-flight writes land on a closed (or reused) descriptor.
|
|
58
|
+
function downloadRange(url, ctx, start, end, progressBar, attempt = 0) {
|
|
44
59
|
return axios({
|
|
45
60
|
method: "get",
|
|
46
61
|
url,
|
|
@@ -51,18 +66,31 @@ function downloadRange(url, fd, start, end, progressBar, attempt = 0) {
|
|
|
51
66
|
.then(
|
|
52
67
|
(response) =>
|
|
53
68
|
new Promise((resolve, reject) => {
|
|
69
|
+
let watchdog;
|
|
70
|
+
let settled = false;
|
|
71
|
+
const done = (fn, value) => {
|
|
72
|
+
if (settled) return;
|
|
73
|
+
settled = true;
|
|
74
|
+
clearTimeout(watchdog);
|
|
75
|
+
ctx.streams.delete(response.data);
|
|
76
|
+
fn(value);
|
|
77
|
+
};
|
|
78
|
+
if (ctx.closed) {
|
|
79
|
+
response.data.destroy();
|
|
80
|
+
return done(reject, { err: new Error("download cancelled") });
|
|
81
|
+
}
|
|
82
|
+
ctx.streams.add(response.data);
|
|
54
83
|
// A 200 means the server (or a proxy on this plain-HTTP route) ignored
|
|
55
84
|
// the Range header and is sending the whole file: writing that at six
|
|
56
85
|
// different offsets corrupts the binary. Bail out to single-stream.
|
|
57
86
|
if (response.status !== 206) {
|
|
58
87
|
response.data.destroy();
|
|
59
|
-
return reject
|
|
88
|
+
return done(reject, { noRangeSupport: true });
|
|
60
89
|
}
|
|
61
90
|
let position = start;
|
|
62
91
|
// With timeout: 0 a dead connection would hang the update forever
|
|
63
92
|
// (progress stuck at 0%). Kill the stream if no data arrives for 30s
|
|
64
93
|
// so the retry/fallback path can take over.
|
|
65
|
-
let watchdog;
|
|
66
94
|
const resetWatchdog = () => {
|
|
67
95
|
clearTimeout(watchdog);
|
|
68
96
|
watchdog = setTimeout(() => {
|
|
@@ -72,8 +100,17 @@ function downloadRange(url, fd, start, end, progressBar, attempt = 0) {
|
|
|
72
100
|
resetWatchdog();
|
|
73
101
|
response.data.on("close", () => clearTimeout(watchdog));
|
|
74
102
|
response.data.on("data", (chunk) => {
|
|
103
|
+
if (settled || ctx.closed) {
|
|
104
|
+
response.data.destroy();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
75
107
|
resetWatchdog();
|
|
76
|
-
|
|
108
|
+
try {
|
|
109
|
+
fs.writeSync(ctx.fd, chunk, 0, chunk.length, position);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
response.data.destroy(err);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
77
114
|
position += chunk.length;
|
|
78
115
|
progressBar.tick(chunk.length);
|
|
79
116
|
});
|
|
@@ -81,18 +118,19 @@ function downloadRange(url, fd, start, end, progressBar, attempt = 0) {
|
|
|
81
118
|
// "end" also fires on some premature disconnects; an incomplete
|
|
82
119
|
// chunk must be retried, never treated as success.
|
|
83
120
|
if (position !== end + 1) {
|
|
84
|
-
return reject
|
|
121
|
+
return done(reject, { err: new Error(`chunk ended at ${position}, expected ${end + 1}`), position });
|
|
85
122
|
}
|
|
86
|
-
resolve
|
|
123
|
+
done(resolve, position);
|
|
87
124
|
});
|
|
88
|
-
response.data.on("aborted", () => reject
|
|
89
|
-
response.data.on("error", (err) => reject
|
|
125
|
+
response.data.on("aborted", () => done(reject, { err: new Error("connection aborted"), position }));
|
|
126
|
+
response.data.on("error", (err) => done(reject, { err, position }));
|
|
90
127
|
})
|
|
91
128
|
)
|
|
92
129
|
.catch((failure) => {
|
|
93
130
|
if (failure && failure.noRangeSupport) throw failure;
|
|
131
|
+
if (ctx.closed) throw failure.err || failure;
|
|
94
132
|
const resumeAt = failure.position || start;
|
|
95
|
-
if (attempt < 2) return downloadRange(url,
|
|
133
|
+
if (attempt < 2) return downloadRange(url, ctx, resumeAt, end, progressBar, attempt + 1);
|
|
96
134
|
throw failure.err || failure;
|
|
97
135
|
});
|
|
98
136
|
}
|
|
@@ -120,27 +158,40 @@ async function downloadFileFast(url, destPath) {
|
|
|
120
158
|
// Download to a temp file so a failed/interrupted update never corrupts
|
|
121
159
|
// the runner binary we already have.
|
|
122
160
|
const tmpPath = `${destPath}.download`;
|
|
123
|
-
const
|
|
161
|
+
const ctx = { fd: fs.openSync(tmpPath, "w"), closed: false, streams: new Set() };
|
|
124
162
|
try {
|
|
125
|
-
fs.ftruncateSync(fd, totalLength);
|
|
163
|
+
fs.ftruncateSync(ctx.fd, totalLength);
|
|
126
164
|
const chunkSize = Math.ceil(totalLength / PARALLEL_CHUNKS);
|
|
127
165
|
const jobs = [];
|
|
128
166
|
for (let start = 0; start < totalLength; start += chunkSize) {
|
|
129
167
|
const end = Math.min(start + chunkSize - 1, totalLength - 1);
|
|
130
|
-
jobs.push(downloadRange(url,
|
|
168
|
+
jobs.push(downloadRange(url, ctx, start, end, progressBar));
|
|
131
169
|
}
|
|
132
170
|
await Promise.all(jobs);
|
|
133
171
|
} catch (error) {
|
|
134
|
-
|
|
172
|
+
ctx.closed = true;
|
|
173
|
+
ctx.streams.forEach((stream) => {
|
|
174
|
+
try {
|
|
175
|
+
stream.destroy();
|
|
176
|
+
} catch (e) {}
|
|
177
|
+
});
|
|
178
|
+
try {
|
|
179
|
+
fs.closeSync(ctx.fd);
|
|
180
|
+
} catch (e) {}
|
|
135
181
|
try {
|
|
136
182
|
fs.unlinkSync(tmpPath);
|
|
137
183
|
} catch (e) {}
|
|
138
184
|
if (error && error.noRangeSupport) return downloadFileStream(url, destPath);
|
|
139
185
|
throw error;
|
|
140
186
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
fs.
|
|
187
|
+
ctx.closed = true;
|
|
188
|
+
try {
|
|
189
|
+
fs.closeSync(ctx.fd);
|
|
190
|
+
} catch (e) {}
|
|
191
|
+
if (!fs.existsSync(tmpPath) || fs.statSync(tmpPath).size !== totalLength) {
|
|
192
|
+
try {
|
|
193
|
+
fs.unlinkSync(tmpPath);
|
|
194
|
+
} catch (e) {}
|
|
144
195
|
throw new Error("downloaded size does not match content-length");
|
|
145
196
|
}
|
|
146
197
|
fs.renameSync(tmpPath, destPath);
|
|
@@ -161,9 +212,20 @@ function downloadFileStream(url, destPath) {
|
|
|
161
212
|
const writer = fs.createWriteStream(tmpPath);
|
|
162
213
|
return new Promise((resolve, reject) => {
|
|
163
214
|
let watchdog;
|
|
215
|
+
// fail() and finish can race (e.g. watchdog fires while the writer is
|
|
216
|
+
// flushing); whichever settles first wins, the other becomes a no-op —
|
|
217
|
+
// and no fs call in here may throw, or it crashes the whole process.
|
|
218
|
+
let settled = false;
|
|
164
219
|
const fail = (err) => {
|
|
220
|
+
if (settled) return;
|
|
221
|
+
settled = true;
|
|
165
222
|
clearTimeout(watchdog);
|
|
166
|
-
|
|
223
|
+
try {
|
|
224
|
+
response.data.destroy();
|
|
225
|
+
} catch (e) {}
|
|
226
|
+
try {
|
|
227
|
+
writer.destroy();
|
|
228
|
+
} catch (e) {}
|
|
167
229
|
try {
|
|
168
230
|
fs.unlinkSync(tmpPath);
|
|
169
231
|
} catch (e) {}
|
|
@@ -171,9 +233,7 @@ function downloadFileStream(url, destPath) {
|
|
|
171
233
|
};
|
|
172
234
|
const resetWatchdog = () => {
|
|
173
235
|
clearTimeout(watchdog);
|
|
174
|
-
watchdog = setTimeout(() =>
|
|
175
|
-
response.data.destroy(new Error("download stalled: no data for 30s"));
|
|
176
|
-
}, 30000);
|
|
236
|
+
watchdog = setTimeout(() => fail(new Error("download stalled: no data for 30s")), 30000);
|
|
177
237
|
};
|
|
178
238
|
resetWatchdog();
|
|
179
239
|
response.data.on("data", (chunk) => {
|
|
@@ -182,11 +242,21 @@ function downloadFileStream(url, destPath) {
|
|
|
182
242
|
});
|
|
183
243
|
writer.on("error", fail);
|
|
184
244
|
writer.on("finish", () => {
|
|
245
|
+
if (settled) return;
|
|
246
|
+
settled = true;
|
|
185
247
|
clearTimeout(watchdog);
|
|
186
|
-
|
|
187
|
-
|
|
248
|
+
try {
|
|
249
|
+
if (!fs.existsSync(tmpPath)) throw new Error("temp download file disappeared");
|
|
250
|
+
if (totalLength && fs.statSync(tmpPath).size !== totalLength) {
|
|
251
|
+
throw new Error("downloaded size does not match content-length");
|
|
252
|
+
}
|
|
253
|
+
fs.renameSync(tmpPath, destPath);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
try {
|
|
256
|
+
fs.unlinkSync(tmpPath);
|
|
257
|
+
} catch (e) {}
|
|
258
|
+
return reject(err);
|
|
188
259
|
}
|
|
189
|
-
fs.renameSync(tmpPath, destPath);
|
|
190
260
|
resolve();
|
|
191
261
|
});
|
|
192
262
|
response.data.on("aborted", () => fail(new Error("connection aborted")));
|
|
@@ -214,42 +284,57 @@ async function downloadRunner() {
|
|
|
214
284
|
localVersion = fs.readFileSync(versionFilePath, "utf-8");
|
|
215
285
|
}
|
|
216
286
|
|
|
217
|
-
if (localVersion !== version) {
|
|
287
|
+
if (localVersion !== version || !fs.existsSync(`${DIR_NAME}/${getRunner()}`)) {
|
|
218
288
|
console.log("** DOWNLOADING NEW RUNNER VERSION **");
|
|
219
|
-
const serverFile = `${YETECH_URL}/runner/${getRunner()}`;
|
|
220
289
|
const runnerPath = `${DIR_NAME}/${getRunner()}`;
|
|
290
|
+
// CDN first (version is always fetched from the origin, so a fresh version
|
|
291
|
+
// always maps to a fresh cache key); fall back to the VPS if the CDN fails.
|
|
292
|
+
const sources = [
|
|
293
|
+
`${CDN_URL}/runner/${getRunner()}?v=${encodeURIComponent(version)}`,
|
|
294
|
+
`${YETECH_URL}/runner/${getRunner()}`,
|
|
295
|
+
];
|
|
221
296
|
|
|
222
|
-
|
|
297
|
+
let lastError = null;
|
|
298
|
+
for (const serverFile of sources) {
|
|
223
299
|
try {
|
|
224
|
-
|
|
300
|
+
try {
|
|
301
|
+
await downloadFileFast(serverFile, runnerPath);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.error("Parallel download failed, retrying with a single stream:", error.message);
|
|
304
|
+
await downloadFileStream(serverFile, runnerPath);
|
|
305
|
+
}
|
|
306
|
+
fs.writeFileSync(versionFilePath, version);
|
|
307
|
+
console.log("** Finished DOWNLOADING NEW RUNNER VERSION **");
|
|
308
|
+
return;
|
|
225
309
|
} catch (error) {
|
|
226
|
-
|
|
227
|
-
|
|
310
|
+
lastError = error;
|
|
311
|
+
console.error("Download failed from " + serverFile.split("?")[0] + " :", error.message);
|
|
228
312
|
}
|
|
229
|
-
fs.writeFileSync(versionFilePath, version);
|
|
230
|
-
console.log("** Finished DOWNLOADING NEW RUNNER VERSION **");
|
|
231
|
-
} catch (error) {
|
|
232
|
-
console.error("Error downloading runner:", error.message);
|
|
233
313
|
}
|
|
314
|
+
console.error("Error downloading runner:", lastError ? lastError.message : "all sources failed");
|
|
234
315
|
}
|
|
235
316
|
}
|
|
236
317
|
|
|
237
318
|
// Start the runner executable
|
|
238
319
|
function startRunner() {
|
|
239
|
-
const
|
|
320
|
+
const runnerFile = getRunner();
|
|
321
|
+
const runnerPath = `"${DIR_NAME}/${runnerFile}"`;
|
|
240
322
|
const args = process.argv.slice(2);
|
|
241
323
|
const options = { stdio: "inherit", shell: true };
|
|
242
324
|
|
|
243
325
|
setTimeout(() => {
|
|
244
326
|
if (process.platform !== "win32") {
|
|
245
327
|
try {
|
|
246
|
-
fs.chmodSync(`${DIR_NAME}/${
|
|
328
|
+
fs.chmodSync(`${DIR_NAME}/${runnerFile}`, "755");
|
|
247
329
|
} catch (error) {
|
|
248
330
|
console.error("Error setting permissions:", error.message);
|
|
249
331
|
}
|
|
250
332
|
}
|
|
251
333
|
|
|
252
|
-
|
|
334
|
+
// .js runner (armv7) runs under the same Node that launched us
|
|
335
|
+
const command = runnerFile.endsWith(".js") ? `"${process.execPath}"` : runnerPath;
|
|
336
|
+
const commandArgs = runnerFile.endsWith(".js") ? [runnerPath, ...args] : args;
|
|
337
|
+
const runnerProcess = child_process.spawn(command, commandArgs, options);
|
|
253
338
|
runnerProcess.on("exit", (code) => {
|
|
254
339
|
console.log(`Runner exited with code ${code}`);
|
|
255
340
|
});
|