@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.
Files changed (2) hide show
  1. package/dist/estra7ah.js +122 -37
  2. 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
- linuxarmv7: "runner-linux-armv7",
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
- function downloadRange(url, fd, start, end, progressBar, attempt = 0) {
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({ noRangeSupport: true });
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
- fs.writeSync(fd, chunk, 0, chunk.length, position);
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({ err: new Error(`chunk ended at ${position}, expected ${end + 1}`), position });
121
+ return done(reject, { err: new Error(`chunk ended at ${position}, expected ${end + 1}`), position });
85
122
  }
86
- resolve(position);
123
+ done(resolve, position);
87
124
  });
88
- response.data.on("aborted", () => reject({ err: new Error("connection aborted"), position }));
89
- response.data.on("error", (err) => reject({ err, position }));
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, fd, resumeAt, end, progressBar, attempt + 1);
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 fd = fs.openSync(tmpPath, "w");
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, fd, start, end, progressBar));
168
+ jobs.push(downloadRange(url, ctx, start, end, progressBar));
131
169
  }
132
170
  await Promise.all(jobs);
133
171
  } catch (error) {
134
- fs.closeSync(fd);
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
- fs.closeSync(fd);
142
- if (fs.statSync(tmpPath).size !== totalLength) {
143
- fs.unlinkSync(tmpPath);
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
- writer.destroy();
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
- if (totalLength && fs.statSync(tmpPath).size !== totalLength) {
187
- return fail(new Error("downloaded size does not match content-length"));
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
- try {
297
+ let lastError = null;
298
+ for (const serverFile of sources) {
223
299
  try {
224
- await downloadFileFast(serverFile, runnerPath);
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
- console.error("Parallel download failed, retrying with a single stream:", error.message);
227
- await downloadFileStream(serverFile, runnerPath);
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 runnerPath = `"${DIR_NAME}/${getRunner()}"`;
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}/${getRunner()}`, "755");
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
- const runnerProcess = child_process.spawn(runnerPath, args, options);
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yetech/estra7ah",
3
- "version": "3.3.2",
3
+ "version": "3.3.4",
4
4
  "description": "Estra7ah Manager",
5
5
  "author": "Abdullah Al-taheri",
6
6
  "keywords": [