@yetech/estra7ah 3.3.3 → 3.3.5
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 +114 -32
- package/package.json +1 -1
package/dist/estra7ah.js
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
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). Plain HTTP on
|
|
16
|
+
// purpose: the origin is HTTP-only, and download integrity is enforced by the
|
|
17
|
+
// content-length size check, so TLS to the edge buys nothing here while
|
|
18
|
+
// avoiding the origin-cert / SSL-mode requirement. The ?v= query string busts
|
|
19
|
+
// the edge cache exactly when a new build lands, so stale binaries are
|
|
20
|
+
// impossible and no cache purging is ever needed.
|
|
21
|
+
const CDN_URL = `http://dl.yetech.co`;
|
|
9
22
|
const RUNNERS = {
|
|
10
23
|
win64: "runner-win-x64.exe",
|
|
11
24
|
linux64: "runner-linux-x64",
|
|
@@ -42,7 +55,10 @@ function getRunner() {
|
|
|
42
55
|
// pulling the binary over several range requests at once is ~N times faster.
|
|
43
56
|
const PARALLEL_CHUNKS = 6;
|
|
44
57
|
|
|
45
|
-
|
|
58
|
+
// ctx = { fd, closed, streams } shared by all chunks of one download, so a
|
|
59
|
+
// failed download can cancel its siblings before the fd is closed — otherwise
|
|
60
|
+
// their in-flight writes land on a closed (or reused) descriptor.
|
|
61
|
+
function downloadRange(url, ctx, start, end, progressBar, attempt = 0) {
|
|
46
62
|
return axios({
|
|
47
63
|
method: "get",
|
|
48
64
|
url,
|
|
@@ -53,18 +69,31 @@ function downloadRange(url, fd, start, end, progressBar, attempt = 0) {
|
|
|
53
69
|
.then(
|
|
54
70
|
(response) =>
|
|
55
71
|
new Promise((resolve, reject) => {
|
|
72
|
+
let watchdog;
|
|
73
|
+
let settled = false;
|
|
74
|
+
const done = (fn, value) => {
|
|
75
|
+
if (settled) return;
|
|
76
|
+
settled = true;
|
|
77
|
+
clearTimeout(watchdog);
|
|
78
|
+
ctx.streams.delete(response.data);
|
|
79
|
+
fn(value);
|
|
80
|
+
};
|
|
81
|
+
if (ctx.closed) {
|
|
82
|
+
response.data.destroy();
|
|
83
|
+
return done(reject, { err: new Error("download cancelled") });
|
|
84
|
+
}
|
|
85
|
+
ctx.streams.add(response.data);
|
|
56
86
|
// A 200 means the server (or a proxy on this plain-HTTP route) ignored
|
|
57
87
|
// the Range header and is sending the whole file: writing that at six
|
|
58
88
|
// different offsets corrupts the binary. Bail out to single-stream.
|
|
59
89
|
if (response.status !== 206) {
|
|
60
90
|
response.data.destroy();
|
|
61
|
-
return reject
|
|
91
|
+
return done(reject, { noRangeSupport: true });
|
|
62
92
|
}
|
|
63
93
|
let position = start;
|
|
64
94
|
// With timeout: 0 a dead connection would hang the update forever
|
|
65
95
|
// (progress stuck at 0%). Kill the stream if no data arrives for 30s
|
|
66
96
|
// so the retry/fallback path can take over.
|
|
67
|
-
let watchdog;
|
|
68
97
|
const resetWatchdog = () => {
|
|
69
98
|
clearTimeout(watchdog);
|
|
70
99
|
watchdog = setTimeout(() => {
|
|
@@ -74,8 +103,17 @@ function downloadRange(url, fd, start, end, progressBar, attempt = 0) {
|
|
|
74
103
|
resetWatchdog();
|
|
75
104
|
response.data.on("close", () => clearTimeout(watchdog));
|
|
76
105
|
response.data.on("data", (chunk) => {
|
|
106
|
+
if (settled || ctx.closed) {
|
|
107
|
+
response.data.destroy();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
77
110
|
resetWatchdog();
|
|
78
|
-
|
|
111
|
+
try {
|
|
112
|
+
fs.writeSync(ctx.fd, chunk, 0, chunk.length, position);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
response.data.destroy(err);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
79
117
|
position += chunk.length;
|
|
80
118
|
progressBar.tick(chunk.length);
|
|
81
119
|
});
|
|
@@ -83,18 +121,19 @@ function downloadRange(url, fd, start, end, progressBar, attempt = 0) {
|
|
|
83
121
|
// "end" also fires on some premature disconnects; an incomplete
|
|
84
122
|
// chunk must be retried, never treated as success.
|
|
85
123
|
if (position !== end + 1) {
|
|
86
|
-
return reject
|
|
124
|
+
return done(reject, { err: new Error(`chunk ended at ${position}, expected ${end + 1}`), position });
|
|
87
125
|
}
|
|
88
|
-
resolve
|
|
126
|
+
done(resolve, position);
|
|
89
127
|
});
|
|
90
|
-
response.data.on("aborted", () => reject
|
|
91
|
-
response.data.on("error", (err) => reject
|
|
128
|
+
response.data.on("aborted", () => done(reject, { err: new Error("connection aborted"), position }));
|
|
129
|
+
response.data.on("error", (err) => done(reject, { err, position }));
|
|
92
130
|
})
|
|
93
131
|
)
|
|
94
132
|
.catch((failure) => {
|
|
95
133
|
if (failure && failure.noRangeSupport) throw failure;
|
|
134
|
+
if (ctx.closed) throw failure.err || failure;
|
|
96
135
|
const resumeAt = failure.position || start;
|
|
97
|
-
if (attempt < 2) return downloadRange(url,
|
|
136
|
+
if (attempt < 2) return downloadRange(url, ctx, resumeAt, end, progressBar, attempt + 1);
|
|
98
137
|
throw failure.err || failure;
|
|
99
138
|
});
|
|
100
139
|
}
|
|
@@ -122,27 +161,40 @@ async function downloadFileFast(url, destPath) {
|
|
|
122
161
|
// Download to a temp file so a failed/interrupted update never corrupts
|
|
123
162
|
// the runner binary we already have.
|
|
124
163
|
const tmpPath = `${destPath}.download`;
|
|
125
|
-
const
|
|
164
|
+
const ctx = { fd: fs.openSync(tmpPath, "w"), closed: false, streams: new Set() };
|
|
126
165
|
try {
|
|
127
|
-
fs.ftruncateSync(fd, totalLength);
|
|
166
|
+
fs.ftruncateSync(ctx.fd, totalLength);
|
|
128
167
|
const chunkSize = Math.ceil(totalLength / PARALLEL_CHUNKS);
|
|
129
168
|
const jobs = [];
|
|
130
169
|
for (let start = 0; start < totalLength; start += chunkSize) {
|
|
131
170
|
const end = Math.min(start + chunkSize - 1, totalLength - 1);
|
|
132
|
-
jobs.push(downloadRange(url,
|
|
171
|
+
jobs.push(downloadRange(url, ctx, start, end, progressBar));
|
|
133
172
|
}
|
|
134
173
|
await Promise.all(jobs);
|
|
135
174
|
} catch (error) {
|
|
136
|
-
|
|
175
|
+
ctx.closed = true;
|
|
176
|
+
ctx.streams.forEach((stream) => {
|
|
177
|
+
try {
|
|
178
|
+
stream.destroy();
|
|
179
|
+
} catch (e) {}
|
|
180
|
+
});
|
|
181
|
+
try {
|
|
182
|
+
fs.closeSync(ctx.fd);
|
|
183
|
+
} catch (e) {}
|
|
137
184
|
try {
|
|
138
185
|
fs.unlinkSync(tmpPath);
|
|
139
186
|
} catch (e) {}
|
|
140
187
|
if (error && error.noRangeSupport) return downloadFileStream(url, destPath);
|
|
141
188
|
throw error;
|
|
142
189
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
fs.
|
|
190
|
+
ctx.closed = true;
|
|
191
|
+
try {
|
|
192
|
+
fs.closeSync(ctx.fd);
|
|
193
|
+
} catch (e) {}
|
|
194
|
+
if (!fs.existsSync(tmpPath) || fs.statSync(tmpPath).size !== totalLength) {
|
|
195
|
+
try {
|
|
196
|
+
fs.unlinkSync(tmpPath);
|
|
197
|
+
} catch (e) {}
|
|
146
198
|
throw new Error("downloaded size does not match content-length");
|
|
147
199
|
}
|
|
148
200
|
fs.renameSync(tmpPath, destPath);
|
|
@@ -163,9 +215,20 @@ function downloadFileStream(url, destPath) {
|
|
|
163
215
|
const writer = fs.createWriteStream(tmpPath);
|
|
164
216
|
return new Promise((resolve, reject) => {
|
|
165
217
|
let watchdog;
|
|
218
|
+
// fail() and finish can race (e.g. watchdog fires while the writer is
|
|
219
|
+
// flushing); whichever settles first wins, the other becomes a no-op —
|
|
220
|
+
// and no fs call in here may throw, or it crashes the whole process.
|
|
221
|
+
let settled = false;
|
|
166
222
|
const fail = (err) => {
|
|
223
|
+
if (settled) return;
|
|
224
|
+
settled = true;
|
|
167
225
|
clearTimeout(watchdog);
|
|
168
|
-
|
|
226
|
+
try {
|
|
227
|
+
response.data.destroy();
|
|
228
|
+
} catch (e) {}
|
|
229
|
+
try {
|
|
230
|
+
writer.destroy();
|
|
231
|
+
} catch (e) {}
|
|
169
232
|
try {
|
|
170
233
|
fs.unlinkSync(tmpPath);
|
|
171
234
|
} catch (e) {}
|
|
@@ -173,9 +236,7 @@ function downloadFileStream(url, destPath) {
|
|
|
173
236
|
};
|
|
174
237
|
const resetWatchdog = () => {
|
|
175
238
|
clearTimeout(watchdog);
|
|
176
|
-
watchdog = setTimeout(() =>
|
|
177
|
-
response.data.destroy(new Error("download stalled: no data for 30s"));
|
|
178
|
-
}, 30000);
|
|
239
|
+
watchdog = setTimeout(() => fail(new Error("download stalled: no data for 30s")), 30000);
|
|
179
240
|
};
|
|
180
241
|
resetWatchdog();
|
|
181
242
|
response.data.on("data", (chunk) => {
|
|
@@ -184,11 +245,21 @@ function downloadFileStream(url, destPath) {
|
|
|
184
245
|
});
|
|
185
246
|
writer.on("error", fail);
|
|
186
247
|
writer.on("finish", () => {
|
|
248
|
+
if (settled) return;
|
|
249
|
+
settled = true;
|
|
187
250
|
clearTimeout(watchdog);
|
|
188
|
-
|
|
189
|
-
|
|
251
|
+
try {
|
|
252
|
+
if (!fs.existsSync(tmpPath)) throw new Error("temp download file disappeared");
|
|
253
|
+
if (totalLength && fs.statSync(tmpPath).size !== totalLength) {
|
|
254
|
+
throw new Error("downloaded size does not match content-length");
|
|
255
|
+
}
|
|
256
|
+
fs.renameSync(tmpPath, destPath);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
try {
|
|
259
|
+
fs.unlinkSync(tmpPath);
|
|
260
|
+
} catch (e) {}
|
|
261
|
+
return reject(err);
|
|
190
262
|
}
|
|
191
|
-
fs.renameSync(tmpPath, destPath);
|
|
192
263
|
resolve();
|
|
193
264
|
});
|
|
194
265
|
response.data.on("aborted", () => fail(new Error("connection aborted")));
|
|
@@ -218,21 +289,32 @@ async function downloadRunner() {
|
|
|
218
289
|
|
|
219
290
|
if (localVersion !== version || !fs.existsSync(`${DIR_NAME}/${getRunner()}`)) {
|
|
220
291
|
console.log("** DOWNLOADING NEW RUNNER VERSION **");
|
|
221
|
-
const serverFile = `${YETECH_URL}/runner/${getRunner()}`;
|
|
222
292
|
const runnerPath = `${DIR_NAME}/${getRunner()}`;
|
|
293
|
+
// CDN first (version is always fetched from the origin, so a fresh version
|
|
294
|
+
// always maps to a fresh cache key); fall back to the VPS if the CDN fails.
|
|
295
|
+
const sources = [
|
|
296
|
+
`${CDN_URL}/runner/${getRunner()}?v=${encodeURIComponent(version)}`,
|
|
297
|
+
`${YETECH_URL}/runner/${getRunner()}`,
|
|
298
|
+
];
|
|
223
299
|
|
|
224
|
-
|
|
300
|
+
let lastError = null;
|
|
301
|
+
for (const serverFile of sources) {
|
|
225
302
|
try {
|
|
226
|
-
|
|
303
|
+
try {
|
|
304
|
+
await downloadFileFast(serverFile, runnerPath);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.error("Parallel download failed, retrying with a single stream:", error.message);
|
|
307
|
+
await downloadFileStream(serverFile, runnerPath);
|
|
308
|
+
}
|
|
309
|
+
fs.writeFileSync(versionFilePath, version);
|
|
310
|
+
console.log("** Finished DOWNLOADING NEW RUNNER VERSION **");
|
|
311
|
+
return;
|
|
227
312
|
} catch (error) {
|
|
228
|
-
|
|
229
|
-
|
|
313
|
+
lastError = error;
|
|
314
|
+
console.error("Download failed from " + serverFile.split("?")[0] + " :", error.message);
|
|
230
315
|
}
|
|
231
|
-
fs.writeFileSync(versionFilePath, version);
|
|
232
|
-
console.log("** Finished DOWNLOADING NEW RUNNER VERSION **");
|
|
233
|
-
} catch (error) {
|
|
234
|
-
console.error("Error downloading runner:", error.message);
|
|
235
316
|
}
|
|
317
|
+
console.error("Error downloading runner:", lastError ? lastError.message : "all sources failed");
|
|
236
318
|
}
|
|
237
319
|
}
|
|
238
320
|
|