From 17d3adde079746804e52d2a69a3daeef8f19558c Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Tue, 9 Jul 2024 22:15:42 +0800 Subject: [PATCH 1/4] Fix #184: Use `electron/net` instead of `https` to download updates. At least I can confirm that `versions.json` retrieval uses proxy now (so that I can boot discord). But I do not know how to force a self-update or modules update, so rest of the update functionalities are untested, for now. --- src/asarUpdate.js | 24 +++------------ src/updater/moduleUpdater.js | 58 ++++++++++++++---------------------- src/utils/get.js | 17 +++++++++++ 3 files changed, 43 insertions(+), 56 deletions(-) create mode 100644 src/utils/get.js diff --git a/src/asarUpdate.js b/src/asarUpdate.js index 5bffc4420..620e846b9 100644 --- a/src/asarUpdate.js +++ b/src/asarUpdate.js @@ -1,4 +1,4 @@ -const { get } = require('https'); +const { get } = require('./utils/get'); const fs = require('original-fs'); // Use original-fs, not Electron's modified fs const { join } = require('path'); @@ -6,29 +6,13 @@ const asarPath = join(__filename, '..'); const asarUrl = `https://github.com/GooseMod/OpenAsar/releases/download/${oaVersion.split('-')[0]}/app.asar`; -// todo: have these https utils centralised? -const redirs = url => new Promise(res => get(url, r => { // Minimal wrapper around https.get to follow redirects - const loc = r.headers.location; - if (loc) return redirs(loc).then(res); - - res(r); -})); - module.exports = async () => { // (Try) update asar if (!oaVersion.includes('-')) return; log('AsarUpdate', 'Updating...'); - const res = (await redirs(asarUrl)); - - let data = []; - res.on('data', d => { - data.push(d); - }); + const buf = (await get(asarUrl))[1]; - res.on('end', () => { - const buf = Buffer.concat(data); - if (!buf.toString('hex').startsWith('04000000')) return log('AsarUpdate', 'Download error'); // Not like ASAR header + if (!buf || !buf.toString('hex').startsWith('04000000')) return log('AsarUpdate', 'Download error'); // Request failed or ASAR header not present - fs.writeFile(asarPath, buf, e => log('AsarUpdate', 'Downloaded', e ?? '')); - }); + fs.writeFile(asarPath, buf, e => log('AsarUpdate', 'Downloaded', e ?? '')); }; \ No newline at end of file diff --git a/src/updater/moduleUpdater.js b/src/updater/moduleUpdater.js index 0545d2756..56f7bae33 100644 --- a/src/updater/moduleUpdater.js +++ b/src/updater/moduleUpdater.js @@ -3,7 +3,7 @@ const fs = require('fs'); const Module = require('module'); const { execFile } = require('child_process'); const { app, autoUpdater } = require('electron'); -const { get } = require('https'); +const { get } = require('../utils/get'); const paths = require('../paths'); @@ -32,20 +32,6 @@ const resetTracking = () => { installing = Object.assign({}, base); }; -const req = url => new Promise(res => get(url, r => { // Minimal wrapper around https.get to include body - let dat = ''; - r.on('data', b => dat += b.toString()); - - r.on('end', () => res([ r, dat ])); -})); - -const redirs = url => new Promise(res => get(url, r => { // Minimal wrapper around https.get to follow redirects - const loc = r.headers.location; - if (loc) return redirs(loc).then(res); - - res(r); -})); - exports.init = (endpoint, { releaseChannel, version }) => { skipHost = settings.get('SKIP_HOST_UPDATE'); skipModule = settings.get('SKIP_MODULE_UPDATE'); @@ -77,10 +63,10 @@ exports.init = (endpoint, { releaseChannel, version }) => { } checkForUpdates() { - req(this.url).then(([ r, b ]) => { - if (r.statusCode === 204) return this.emit('update-not-available'); + get(this.url).then(([r, b]) => { + if (!b || r === 204) return this.emit('update-not-available'); - this.emit('update-manually', b); + this.emit('update-manually', b.toString()); }); } @@ -111,7 +97,12 @@ exports.init = (endpoint, { releaseChannel, version }) => { }; const checkModules = async () => { - remote = JSON.parse((await req(baseUrl + '/versions.json' + qs))[1]); + const buf = (await get(baseUrl + '/versions.json' + qs))[1]; + if (!buf) { + log('Modules', 'versions.json retrieval failure.'); + return; + } + remote = JSON.parse(buf.toString()); for (const name in installed) { const inst = installed[name].installedVersion; @@ -131,27 +122,22 @@ const downloadModule = async (name, ver) => { downloading.total++; const path = join(downloadPath, name + '-' + ver + '.zip'); - const file = fs.createWriteStream(path); // log('Modules', 'Downloading', `${name}@${ver}`); - let success, total, cur = 0; - const res = await redirs(baseUrl + '/' + name + '/' + ver + qs); - success = res.statusCode === 200; - total = parseInt(res.headers['content-length'] ?? 1, 10); - - res.pipe(file); + let success, total, cur = 0; + const res = await get(baseUrl + '/' + name + '/' + ver + qs); + success = res[0] === 200; - res.on('data', c => { - cur += c.length; - - events.emit('downloading-module', { name, cur, total }); - }); - - await new Promise((res) => file.on('close', res)); - - if (success) commitManifest(); - else downloading.fail++; + // todo: if a progress-bar-like interface and stream-like file writing are still wanted, implement + if (success) { + total = parseInt(res[2].get('content-length') ?? 1, 10); + events.emit('downloading-module', { name, total, total }); + fs.writeFile(path, res[1], e => log('ModuleUpdate', 'Writing to file failed:', e)); + commitManifest(); + } else { + downloading.fail++; + } events.emit('downloaded-module', { name diff --git a/src/utils/get.js b/src/utils/get.js new file mode 100644 index 000000000..9571ece08 --- /dev/null +++ b/src/utils/get.js @@ -0,0 +1,17 @@ +const { net } = require('electron'); + +// returns a promise that resolves to [statusCode, arrayBuffer, headers] +// [code, null, null] if request failed +module.exports = async (url) => { + const request = new Request(url, { + method: 'GET', + redirect: 'follow' + }); + const response = await net.fetch(request); + + if (response.ok) { + return [response.status, await response.arrayBuffer(), response.headers()]; + } else { + return [response.status, null, null]; + } +}; \ No newline at end of file From 7e5008d9c25fcc9fefee9a174fae2635a400c689 Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Tue, 9 Jul 2024 23:07:21 +0800 Subject: [PATCH 2/4] Is strictly part of last commit. I staged wrongly. --- src/asarUpdate.js | 2 +- src/updater/moduleUpdater.js | 4 ++-- src/utils/get.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/asarUpdate.js b/src/asarUpdate.js index 620e846b9..8c294615a 100644 --- a/src/asarUpdate.js +++ b/src/asarUpdate.js @@ -1,4 +1,4 @@ -const { get } = require('./utils/get'); +const get = require('./utils/get'); const fs = require('original-fs'); // Use original-fs, not Electron's modified fs const { join } = require('path'); diff --git a/src/updater/moduleUpdater.js b/src/updater/moduleUpdater.js index 56f7bae33..6af646a0b 100644 --- a/src/updater/moduleUpdater.js +++ b/src/updater/moduleUpdater.js @@ -3,7 +3,7 @@ const fs = require('fs'); const Module = require('module'); const { execFile } = require('child_process'); const { app, autoUpdater } = require('electron'); -const { get } = require('../utils/get'); +const get = require('../utils/get'); const paths = require('../paths'); @@ -63,7 +63,7 @@ exports.init = (endpoint, { releaseChannel, version }) => { } checkForUpdates() { - get(this.url).then(([r, b]) => { + get(this.url).then(([r, b, _headers]) => { if (!b || r === 204) return this.emit('update-not-available'); this.emit('update-manually', b.toString()); diff --git a/src/utils/get.js b/src/utils/get.js index 9571ece08..9ef94e4d2 100644 --- a/src/utils/get.js +++ b/src/utils/get.js @@ -1,6 +1,6 @@ const { net } = require('electron'); -// returns a promise that resolves to [statusCode, arrayBuffer, headers] +// returns a promise that resolves to [statusCode, Buffer, headers] // [code, null, null] if request failed module.exports = async (url) => { const request = new Request(url, { @@ -10,7 +10,7 @@ module.exports = async (url) => { const response = await net.fetch(request); if (response.ok) { - return [response.status, await response.arrayBuffer(), response.headers()]; + return [response.status, Buffer.from(await response.arrayBuffer()), response.headers]; } else { return [response.status, null, null]; } From 2b4290fd9f5adb7e0ae7f3396c021319bd95d3b0 Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Wed, 7 Aug 2024 21:46:17 +0800 Subject: [PATCH 3/4] Bring update download progress bar back. With a new networking utility `request`, trying to simplify the confusing `net.request()` interface. --- src/asarUpdate.js | 2 +- src/updater/moduleUpdater.js | 39 ++++++++++++++++++++++++------------ src/utils/get.js | 26 ++++++++++++++++++++---- 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/asarUpdate.js b/src/asarUpdate.js index 8c294615a..620e846b9 100644 --- a/src/asarUpdate.js +++ b/src/asarUpdate.js @@ -1,4 +1,4 @@ -const get = require('./utils/get'); +const { get } = require('./utils/get'); const fs = require('original-fs'); // Use original-fs, not Electron's modified fs const { join } = require('path'); diff --git a/src/updater/moduleUpdater.js b/src/updater/moduleUpdater.js index 6af646a0b..30a592c07 100644 --- a/src/updater/moduleUpdater.js +++ b/src/updater/moduleUpdater.js @@ -3,7 +3,7 @@ const fs = require('fs'); const Module = require('module'); const { execFile } = require('child_process'); const { app, autoUpdater } = require('electron'); -const get = require('../utils/get'); +const { get, request } = require('../utils/get'); const paths = require('../paths'); @@ -122,22 +122,35 @@ const downloadModule = async (name, ver) => { downloading.total++; const path = join(downloadPath, name + '-' + ver + '.zip'); + const file = fs.createWriteStream(path); // log('Modules', 'Downloading', `${name}@${ver}`); let success, total, cur = 0; - const res = await get(baseUrl + '/' + name + '/' + ver + qs); - success = res[0] === 200; - - // todo: if a progress-bar-like interface and stream-like file writing are still wanted, implement - if (success) { - total = parseInt(res[2].get('content-length') ?? 1, 10); - events.emit('downloading-module', { name, total, total }); - fs.writeFile(path, res[1], e => log('ModuleUpdate', 'Writing to file failed:', e)); - commitManifest(); - } else { - downloading.fail++; - } + request( + baseUrl + '/' + name + '/' + ver + qs, + res => { + success = (res.statusCode === 200); + // res.headers is a + // https://www.electronjs.org/docs/latest/api/incoming-message#responseheaders + total = parseInt(res.headers['content-length'][0] ?? 1, 10); + }, + chunk => { + cur += chunk.length; + events.emit('downloading-module', { name, cur, total }); + + file.write(chunk); + }, + () => { + file.close(); + } + ); + + // block till file.close() + await new Promise(res => file.on('close', res)); + + if (success) commitManifest(); + else downloading.fail++; events.emit('downloaded-module', { name diff --git a/src/utils/get.js b/src/utils/get.js index 9ef94e4d2..2c8665b71 100644 --- a/src/utils/get.js +++ b/src/utils/get.js @@ -2,16 +2,34 @@ const { net } = require('electron'); // returns a promise that resolves to [statusCode, Buffer, headers] // [code, null, null] if request failed -module.exports = async (url) => { - const request = new Request(url, { +module.exports.get = async (url) => { + const response = await net.fetch(new Request(url, { method: 'GET', redirect: 'follow' - }); - const response = await net.fetch(request); + })); if (response.ok) { return [response.status, Buffer.from(await response.arrayBuffer()), response.headers]; } else { return [response.status, null, null]; } +}; + +// issues a GET request following redirects, calling provided callbacks to return data and statues: +// - `response_cb` is called with a `IncomingMessage` (https://www.electronjs.org/docs/latest/api/incoming-message) on receiving an HTTP response +// - `data_cb` is called with a chunk for every data chunk arrived +// - `end_cb` is called when there are no more data +module.exports.request = (url, response_cb, data_cb, end_cb) => { + const request = net.request({ + url: url, + redirect: 'follow', + }); + + request.on('response', response => { + response_cb(response); + response.on('data', data_cb); + response.on('end', end_cb) + }); + + request.end(); }; \ No newline at end of file From 1f514f0adcf39273025905e4449ba0f6f893a6a6 Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Wed, 7 Aug 2024 21:46:41 +0800 Subject: [PATCH 4/4] Apply nits suggested. --- src/updater/moduleUpdater.js | 2 +- src/utils/get.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/updater/moduleUpdater.js b/src/updater/moduleUpdater.js index 30a592c07..9702ae33e 100644 --- a/src/updater/moduleUpdater.js +++ b/src/updater/moduleUpdater.js @@ -63,7 +63,7 @@ exports.init = (endpoint, { releaseChannel, version }) => { } checkForUpdates() { - get(this.url).then(([r, b, _headers]) => { + get(this.url).then(([r, b]) => { if (!b || r === 204) return this.emit('update-not-available'); this.emit('update-manually', b.toString()); diff --git a/src/utils/get.js b/src/utils/get.js index 2c8665b71..126f4e8fe 100644 --- a/src/utils/get.js +++ b/src/utils/get.js @@ -2,7 +2,7 @@ const { net } = require('electron'); // returns a promise that resolves to [statusCode, Buffer, headers] // [code, null, null] if request failed -module.exports.get = async (url) => { +module.exports.get = async url => { const response = await net.fetch(new Request(url, { method: 'GET', redirect: 'follow'