Initial Proper Commit

This commit is contained in:
Ducko 2021-12-09 16:25:14 +00:00
parent b9ccf7f08e
commit e3e0bb0b17
40 changed files with 4281 additions and 8 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
node_modules
*.asar

View file

@ -4,15 +4,17 @@
## Goals
- **Hotpluggable** - just swap the asar file, nothing else needed
- **Lightweight** - it should be at least as fast or lightweight, hopefully more
- **No Tracking** - no crash reporting, etc
- **No Tracking** - no crash reporting, error tracking, etc
- **Minimal** - generally only doing what is needed (see: implementation)
## Implementation
Below is a list in order of priority, marked as complete when finished:
- [ ] Stub everything
- [ ] Bootstrapping
- [ ] Splash screen
- [X] Bootstrapping
- [X] Splash screen
- [X] Error handling
- [ ] A bunch of specific minor fixes / features
- [ ] Handle hardware acceleration
- [ ] Auto start
- [ ] Updating
- [ ] Updater v1
- [ ] Updater v2
- [ ] First run
- [ ] Self-write updater code (currently mostly copied)
- [ ] Self-write some small parts of internals

34
src/Constants.js Normal file
View file

@ -0,0 +1,34 @@
// Bootstrap consts, heavily copied as don't want to mess with it
const { releaseChannel } = require('./utils/buildInfo');
const { getSettings } = require('./appSettings');
const settings = getSettings();
function capitalizeFirstLetter(s) {
return s.charAt(0).toUpperCase() + s.slice(1);
}
const appNameSuffix = releaseChannel === 'stable' ? '' : capitalizeFirstLetter(releaseChannel);
const APP_COMPANY = 'Discord Inc';
const APP_DESCRIPTION = 'Discord - https://discord.com';
const APP_NAME = 'Discord' + appNameSuffix;
const APP_NAME_FOR_HUMANS = 'Discord' + (appNameSuffix !== '' ? ' ' + appNameSuffix : '');
const APP_ID_BASE = 'com.squirrel';
const APP_ID = `${APP_ID_BASE}.${APP_NAME}.${APP_NAME}`;
const APP_PROTOCOL = 'Discord';
const API_ENDPOINT = settings.get('API_ENDPOINT') || 'https://discord.com/api';
const UPDATE_ENDPOINT = settings.get('UPDATE_ENDPOINT') || API_ENDPOINT;
const NEW_UPDATE_ENDPOINT = settings.get('NEW_UPDATE_ENDPOINT') || 'https://discord.com/api/updates/';
module.exports = {
APP_COMPANY,
APP_DESCRIPTION,
APP_NAME,
APP_NAME_FOR_HUMANS,
APP_ID,
APP_PROTOCOL,
API_ENDPOINT,
NEW_UPDATE_ENDPOINT,
UPDATE_ENDPOINT
};

6
src/GPUSettings.js Normal file
View file

@ -0,0 +1,6 @@
// Idk why Discord has to use this
exports.replace = (GPUSettings) => {
for (const name of Object.keys(GPUSettings)) {
exports[name] = GPUSettings[name];
}
};

7
src/appSettings.js Normal file
View file

@ -0,0 +1,7 @@
const Settings = require('./utils/Settings');
const paths = require('./paths');
const settings = new Settings(paths.getUserData());
exports.getSettings = () => settings;
exports.init = () => {}; // Stub as we setup on require

6
src/autoStart/index.js Normal file
View file

@ -0,0 +1,6 @@
// Stub for now at least
exports.install = (callback) => { callback(); };
exports.update = (callback) => { callback(); };
exports.uninstall = (callback) => { callback(); };
exports.isInstalled = (callback) => { callback(true); }; // Stub to true or false?

63
src/bootstrap.js vendored Normal file
View file

@ -0,0 +1,63 @@
const { join } = require('path');
const NodeModule = require('module');
const { app } = require('electron');
const log = require('./utils/log');
const requireNative = require('./utils/requireNative');
const paths = require('./paths');
const buildInfo = require('./utils/buildInfo');
// Just required for startup
const appSettings = require('./appSettings');
const GPUSettings = require('./GPUSettings');
const crashReporterSetup = require('./crashReporterSetup');
const splashScreen = require('./splash/splashScreen');
const Constants = require('./Constants');
const autoStart = require('./autoStart');
const updater = require('./updater/updater');
const moduleUpdater = require('./updater/moduleUpdater');
const appUpdater = require('./updater/appUpdater');
let desktopCore;
const startCore = () => {
desktopCore = requireNative('discord_desktop_core');
log('Bootstrap', 'Required desktop_core:', desktopCore);
desktopCore.startup({
paths,
splashScreen,
moduleUpdater,
autoStart,
buildInfo,
appSettings,
Constants,
GPUSettings,
updater,
crashReporterSetup
});
};
const startUpdate = () => {
appUpdater.update(false, () => {
startCore();
}, () => {
desktopCore.setMainWindowVisible(true);
});
};
module.exports = () => {
// Paths logging
log('Paths', `Init! Returns:
getUserData: ${paths.getUserData()}
getUserDataVersioned: ${paths.getUserDataVersioned()}
getResources: ${paths.getResources()}
getModuleDataPath: ${paths.getModuleDataPath()}
getInstallPath: ${paths.getInstallPath()}`);
if (app.isReady()) {
startUpdate();
} else {
app.once('ready', startUpdate);
}
};

View file

@ -0,0 +1,5 @@
// Much crash reporting, such wow
exports.init = () => {};
exports.isInitialized = () => true;
exports.metadata = {};

40
src/errorHandler.js Normal file
View file

@ -0,0 +1,40 @@
const { app } = require("electron");
const log = require('./utils/log');
exports.init = () => {
/* process.on('uncaughtException', error => {
const stack = error.stack ? error.stack : String(error);
const message = `Uncaught exception:\n ${stack}`;
console.warn(message);
if (!isErrorSafeToSuppress(error)) {
_electron.dialog.showErrorBox('A JavaScript error occurred in the main process', message);
}
}); */
};
exports.fatal = (err) => {
const options = {
type: 'error',
message: 'A fatal Javascript error occured',
detail: err && err.stack ? err.stack : String(err)
};
const callback = _ => app.quit();
const electronMajor = parseInt(process.versions.electron.split('.')[0]);
if (electronMajor >= 6) {
_electron.dialog.showMessageBox(null, options).then(callback);
} else {
_electron.dialog.showMessageBox(options, callback);
}
log('ErrorHandler', 'Fatal:', err);
};
exports.handled = (err) => {
log('ErrorHandler', 'Handled:', err);
};

3
src/firstRun/index.js Normal file
View file

@ -0,0 +1,3 @@
// Stub for now at least
exports.update = (callback) => { callback(); };

7
src/index.js Normal file
View file

@ -0,0 +1,7 @@
const log = require('./utils/log');
log('', 'Initing...');
const bootstrap = require('./bootstrap');
bootstrap(); // Start bootstrap

5
src/ipcMain.js Normal file
View file

@ -0,0 +1,5 @@
// Discord's wrapper around ipcMain
const { ipcMain } = require('electron');
exports.on = (event, callback) => ipcMain.on('DISCORD_' + event, callback);
exports.removeListener = (event, callback) => ipcMain.removeListener('DISCORD_' + event, callback);

873
src/package-lock.json generated Normal file
View file

@ -0,0 +1,873 @@
{
"name": "openasar",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "openasar",
"dependencies": {
"mkdirp": "^0.5.1",
"request": "2.88.0",
"yauzl": "^2.10.0"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"dependencies": {
"safer-buffer": "~2.1.0"
}
},
"node_modules/assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
"engines": {
"node": ">=0.8"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"node_modules/aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
"engines": {
"node": "*"
}
},
"node_modules/aws4": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA=="
},
"node_modules/bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
"dependencies": {
"tweetnacl": "^0.14.3"
}
},
"node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=",
"engines": {
"node": "*"
}
},
"node_modules/caseless": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"node_modules/dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
"dependencies": {
"assert-plus": "^1.0.0"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
"integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
"dependencies": {
"jsbn": "~0.1.0",
"safer-buffer": "^2.1.0"
}
},
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"node_modules/extsprintf": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
"engines": [
"node >=0.6.0"
]
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
},
"node_modules/fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=",
"dependencies": {
"pend": "~1.2.0"
}
},
"node_modules/forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
"engines": {
"node": "*"
}
},
"node_modules/form-data": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 0.12"
}
},
"node_modules/getpass": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
"integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
"dependencies": {
"assert-plus": "^1.0.0"
}
},
"node_modules/har-schema": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
"integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
"engines": {
"node": ">=4"
}
},
"node_modules/har-validator": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz",
"integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
"deprecated": "this library is no longer supported",
"dependencies": {
"ajv": "^6.12.3",
"har-schema": "^2.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
"integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
"dependencies": {
"assert-plus": "^1.0.0",
"jsprim": "^1.2.2",
"sshpk": "^1.7.0"
},
"engines": {
"node": ">=0.8",
"npm": ">=1.3.7"
}
},
"node_modules/is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
},
"node_modules/isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
},
"node_modules/jsbn": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
},
"node_modules/json-schema": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"node_modules/json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
},
"node_modules/jsprim": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
"integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==",
"dependencies": {
"assert-plus": "1.0.0",
"extsprintf": "1.3.0",
"json-schema": "0.4.0",
"verror": "1.10.0"
},
"engines": {
"node": ">=0.6.0"
}
},
"node_modules/mime-db": {
"version": "1.51.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
"integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.34",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz",
"integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==",
"dependencies": {
"mime-db": "1.51.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
},
"node_modules/mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dependencies": {
"minimist": "^1.2.5"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
"engines": {
"node": "*"
}
},
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA="
},
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
},
"node_modules/psl": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
"integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ=="
},
"node_modules/punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
},
"node_modules/qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/request": {
"version": "2.88.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
"integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
"deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142",
"dependencies": {
"aws-sign2": "~0.7.0",
"aws4": "^1.8.0",
"caseless": "~0.12.0",
"combined-stream": "~1.0.6",
"extend": "~3.0.2",
"forever-agent": "~0.6.1",
"form-data": "~2.3.2",
"har-validator": "~5.1.0",
"http-signature": "~1.2.0",
"is-typedarray": "~1.0.0",
"isstream": "~0.1.2",
"json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19",
"oauth-sign": "~0.9.0",
"performance-now": "^2.1.0",
"qs": "~6.5.2",
"safe-buffer": "^5.1.2",
"tough-cookie": "~2.4.3",
"tunnel-agent": "^0.6.0",
"uuid": "^3.3.2"
},
"engines": {
"node": ">= 4"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/sshpk": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
"integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==",
"dependencies": {
"asn1": "~0.2.3",
"assert-plus": "^1.0.0",
"bcrypt-pbkdf": "^1.0.0",
"dashdash": "^1.12.0",
"ecc-jsbn": "~0.1.1",
"getpass": "^0.1.1",
"jsbn": "~0.1.0",
"safer-buffer": "^2.0.2",
"tweetnacl": "~0.14.0"
},
"bin": {
"sshpk-conv": "bin/sshpk-conv",
"sshpk-sign": "bin/sshpk-sign",
"sshpk-verify": "bin/sshpk-verify"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tough-cookie": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
"integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
"dependencies": {
"psl": "^1.1.24",
"punycode": "^1.4.1"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"node_modules/tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
},
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dependencies": {
"punycode": "^2.1.0"
}
},
"node_modules/uri-js/node_modules/punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
"engines": {
"node": ">=6"
}
},
"node_modules/uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"bin": {
"uuid": "bin/uuid"
}
},
"node_modules/verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
"engines": [
"node >=0.6.0"
],
"dependencies": {
"assert-plus": "^1.0.0",
"core-util-is": "1.0.2",
"extsprintf": "^1.2.0"
}
},
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=",
"dependencies": {
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
}
},
"dependencies": {
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
},
"asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"requires": {
"safer-buffer": "~2.1.0"
}
},
"assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
},
"aws4": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA=="
},
"bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
"requires": {
"tweetnacl": "^0.14.3"
}
},
"buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI="
},
"caseless": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"requires": {
"delayed-stream": "~1.0.0"
}
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
"requires": {
"assert-plus": "^1.0.0"
}
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
},
"ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
"integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
"requires": {
"jsbn": "~0.1.0",
"safer-buffer": "^2.1.0"
}
},
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"extsprintf": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
},
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
},
"fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=",
"requires": {
"pend": "~1.2.0"
}
},
"forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
},
"form-data": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
}
},
"getpass": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
"integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
"requires": {
"assert-plus": "^1.0.0"
}
},
"har-schema": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
"integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
},
"har-validator": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz",
"integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
"requires": {
"ajv": "^6.12.3",
"har-schema": "^2.0.0"
}
},
"http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
"integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
"requires": {
"assert-plus": "^1.0.0",
"jsprim": "^1.2.2",
"sshpk": "^1.7.0"
}
},
"is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
},
"isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
},
"jsbn": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
},
"json-schema": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="
},
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
},
"jsprim": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
"integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==",
"requires": {
"assert-plus": "1.0.0",
"extsprintf": "1.3.0",
"json-schema": "0.4.0",
"verror": "1.10.0"
}
},
"mime-db": {
"version": "1.51.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
"integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g=="
},
"mime-types": {
"version": "2.1.34",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz",
"integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==",
"requires": {
"mime-db": "1.51.0"
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "^1.2.5"
}
},
"oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
},
"pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA="
},
"performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
},
"psl": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
"integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ=="
},
"punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
},
"qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
},
"request": {
"version": "2.88.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
"integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
"requires": {
"aws-sign2": "~0.7.0",
"aws4": "^1.8.0",
"caseless": "~0.12.0",
"combined-stream": "~1.0.6",
"extend": "~3.0.2",
"forever-agent": "~0.6.1",
"form-data": "~2.3.2",
"har-validator": "~5.1.0",
"http-signature": "~1.2.0",
"is-typedarray": "~1.0.0",
"isstream": "~0.1.2",
"json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19",
"oauth-sign": "~0.9.0",
"performance-now": "^2.1.0",
"qs": "~6.5.2",
"safe-buffer": "^5.1.2",
"tough-cookie": "~2.4.3",
"tunnel-agent": "^0.6.0",
"uuid": "^3.3.2"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sshpk": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
"integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==",
"requires": {
"asn1": "~0.2.3",
"assert-plus": "^1.0.0",
"bcrypt-pbkdf": "^1.0.0",
"dashdash": "^1.12.0",
"ecc-jsbn": "~0.1.1",
"getpass": "^0.1.1",
"jsbn": "~0.1.0",
"safer-buffer": "^2.0.2",
"tweetnacl": "~0.14.0"
}
},
"tough-cookie": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
"integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
"requires": {
"psl": "^1.1.24",
"punycode": "^1.4.1"
}
},
"tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
"requires": {
"safe-buffer": "^5.0.1"
}
},
"tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
},
"uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"requires": {
"punycode": "^2.1.0"
},
"dependencies": {
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
}
}
},
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
},
"verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
"requires": {
"assert-plus": "^1.0.0",
"core-util-is": "1.0.2",
"extsprintf": "^1.2.0"
}
},
"yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=",
"requires": {
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
}
}
}

View file

@ -1,9 +1,12 @@
{
"name": "openasar",
"description": "Discord Client for Desktop - Bootstrapper",
"description": "Open-source alternative of Discord desktop's app.asar",
"main": "index.js",
"dependencies": {
"mkdirp": "^0.5.1",
"request": "2.88.0",
"yauzl": "^2.10.0"
}
}

25
src/paths.js Normal file
View file

@ -0,0 +1,25 @@
const { join, dirname, basename } = require('path');
const { app } = require('electron');
const log = require('./utils/log');
const buildInfo = require('./utils/buildInfo');
const appDir = 'discord' + (buildInfo.releaseChannel === 'stable' ? '' : buildInfo.releaseChannel); // Clean channel naming up later to util?
const userData = join(app.getPath('appData'), appDir);
const userDataVersioned = join(userData, buildInfo.version);
const exeDir = dirname(app.getPath('exe'));
const installPath = /^app-[0-9]+\.[0-9]+\.[0-9]+/.test(basename(exeDir)) ? join(exeDir, '..') : null;
const moduleData = buildInfo.newUpdater ? join(userData, 'module_data') : join(userDataVersioned, 'modules');
exports.getUserData = () => userData;
exports.getUserDataVersioned = () => userDataVersioned;
exports.getResources = () => process.resourcesPath; // Discord uses path and require.main.filename here because ??
exports.getModuleDataPath = () => moduleData;
exports.getInstallPath = () => installPath;
exports.init = () => {}; // Stub as we setup on require

89
src/splash/index.html Normal file
View file

@ -0,0 +1,89 @@
<div id="text">openasar go brr</div>
<div id="bar-container"></div>
<div id="bar-fill"></div>
<style>
html, body {
margin: 0;
padding: 0;
background: #101418;
}
* {
color: rgba(255, 255, 255, 0.97);
font-family: sans-serif;
}
#text {
position: absolute;
top: 45%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 7vw;
text-align: center;
text-transform: capitalize;
width: 100%;
}
#bar-container, #bar-fill {
position: absolute;
top: 58%;
left: 50%;
transform: translate(-50%, -50%);
width: 80%;
height: 5%;
border-radius: 4px;
display: none;
}
#bar-container {
background: #202428;
}
#bar-fill {
background: rgb(88, 101, 242);
width: 0;
transform: translate(0%, -50%);
left: 10%;
}
</style>
<script>
const text = document.querySelector('#text');
const barContainer = document.querySelector('#bar-container');
const barFill = document.querySelector('#bar-fill');
DiscordSplash.signalReady();
DiscordSplash.onStateUpdate(({ status, current, total, progress }) => {
console.log('onStateUpdate', progress);
text.textContent = status.replaceAll('-', ' ');
if (progress) {
barContainer.style.display = 'block';
barFill.style.display = 'block';
barFill.style.width = 80 * (progress / 100) + '%';
} else {
barContainer.style.display = '';
barFill.style.display = '';
}
});
DiscordSplash.onQuoteUpdate((e) => {
console.log('onQuoteUpdate', e);
})
</script>

36
src/splash/preload.js Normal file
View file

@ -0,0 +1,36 @@
"use strict";
const {
app,
contextBridge,
ipcRenderer
} = require('electron');
const {
saferShellOpenExternal
} = require('../utils/securityUtils');
contextBridge.exposeInMainWorld('DiscordSplash', {
getReleaseChannel: () => {
const buildInfo = require('../utils/buildInfo');
return buildInfo.releaseChannel;
},
signalReady: () => {
ipcRenderer.send('DISCORD_SPLASH_SCREEN_READY');
},
onStateUpdate: callback => {
ipcRenderer.on('DISCORD_SPLASH_UPDATE_STATE', (_, state) => {
callback(state);
});
},
onQuoteUpdate: callback => {
ipcRenderer.on('DISCORD_SPLASH_SCREEN_QUOTE', (_, quote) => {
callback(quote);
});
},
openUrl: saferShellOpenExternal,
quitDiscord: () => {
ipcRenderer.send('DISCORD_SPLASH_SCREEN_QUIT');
}
});

486
src/splash/splashScreen.js Normal file
View file

@ -0,0 +1,486 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.initSplash = initSplash;
exports.focusWindow = focusWindow;
exports.pageReady = pageReady;
exports.events = exports.APP_SHOULD_SHOW = exports.APP_SHOULD_LAUNCH = void 0;
var _electron = require("electron");
var _events = require("events");
var _fs = _interopRequireDefault(require("fs"));
var _path = _interopRequireDefault(require("path"));
var _url = _interopRequireDefault(require("url"));
var _Backoff = _interopRequireDefault(require("../utils/Backoff"));
var moduleUpdater = _interopRequireWildcard(require("../updater/moduleUpdater"));
var paths = _interopRequireWildcard(require("../paths"));
var _securityUtils = require("../utils/securityUtils");
var _updater = require("../updater/updater");
var _ipcMain = _interopRequireDefault(require("../ipcMain"));
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const UPDATE_TIMEOUT_WAIT = 10000;
const RETRY_CAP_SECONDS = 60; // citron note: atom seems to add about 50px height to the frame on mac but not windows
// TODO: see if we can eliminate fudge by using useContentSize BrowserWindow option
const LOADING_WINDOW_WIDTH = 300;
const LOADING_WINDOW_HEIGHT = process.platform === 'darwin' ? 300 : 350; // TODO: addModulesListener events should use Module's constants
const CHECKING_FOR_UPDATES = 'checking-for-updates';
const UPDATE_CHECK_FINISHED = 'update-check-finished';
const UPDATE_FAILURE = 'update-failure';
const LAUNCHING = 'launching';
const DOWNLOADING_MODULE = 'downloading-module';
const DOWNLOADING_UPDATES = 'downloading-updates';
const DOWNLOADING_MODULES_FINISHED = 'downloading-modules-finished';
const DOWNLOADING_MODULE_PROGRESS = 'downloading-module-progress';
const DOWNLOADED_MODULE = 'downloaded-module';
const NO_PENDING_UPDATES = 'no-pending-updates';
const INSTALLING_MODULE = 'installing-module';
const INSTALLING_UPDATES = 'installing-updates';
const INSTALLED_MODULE = 'installed-module';
const INSTALLING_MODULE_PROGRESS = 'installing-module-progress';
const INSTALLING_MODULES_FINISHED = 'installing-modules-finished';
const UPDATE_MANUALLY = 'update-manually';
const APP_SHOULD_LAUNCH = 'APP_SHOULD_LAUNCH';
exports.APP_SHOULD_LAUNCH = APP_SHOULD_LAUNCH;
const APP_SHOULD_SHOW = 'APP_SHOULD_SHOW';
exports.APP_SHOULD_SHOW = APP_SHOULD_SHOW;
const events = new _events.EventEmitter();
exports.events = events;
function webContentsSend(win, event, ...args) {
if (win != null && win.webContents != null) {
win.webContents.send(`DISCORD_${event}`, ...args);
}
}
let splashWindow;
let modulesListeners;
let updateTimeout;
let updateAttempt;
let splashState;
let launchedMainWindow;
let quoteCachePath;
let restartRequired = false;
let newUpdater;
const updateBackoff = new _Backoff.default(1000, 30000); // TODO(eiz): some of this logic should probably not live in the splash.
//
// Disabled because Rust interop stuff is going on in here.
/* eslint-disable camelcase */
class TaskProgress {
constructor() {
this.inProgress = new Map();
this.finished = new Set();
this.allTasks = new Set();
}
recordProgress(progress, task) {
this.allTasks.add(task.package_sha256);
if (progress.state !== _updater.TASK_STATE_WAITING) {
this.inProgress.set(task.package_sha256, progress.percent);
if (progress.state === _updater.TASK_STATE_COMPLETE) {
this.finished.add(task.package_sha256);
}
}
}
updateSplashState(newState) {
if (this.inProgress.size > 0 && this.inProgress.size > this.finished.size) {
let totalPercent = 0;
for (const item of this.inProgress.values()) {
totalPercent += item;
}
totalPercent /= this.allTasks.size;
splashState = {
current: this.finished.size + 1,
total: this.allTasks.size,
progress: totalPercent
};
updateSplashState(newState);
return true;
}
return false;
}
}
async function updateUntilCurrent() {
const retryOptions = {
skip_host_delta: false,
skip_module_delta: {}
};
while (true) {
updateSplashState(CHECKING_FOR_UPDATES);
try {
let installedAnything = false;
const downloads = new TaskProgress();
const installs = new TaskProgress();
await newUpdater.updateToLatestWithOptions(retryOptions, progress => {
const task = progress.task;
const downloadTask = task.HostDownload || task.ModuleDownload;
const installTask = task.HostInstall || task.ModuleInstall;
installedAnything = true;
if (downloadTask != null) {
downloads.recordProgress(progress, downloadTask);
}
if (installTask != null) {
installs.recordProgress(progress, installTask);
if (progress.state.Failed != null) {
if (task.HostInstall != null) {
retryOptions.skip_host_delta = true;
} else if (task.ModuleInstall != null) {
retryOptions.skip_module_delta[installTask.version.module.name] = true;
}
}
}
if (!downloads.updateSplashState(DOWNLOADING_UPDATES)) {
installs.updateSplashState(INSTALLING_UPDATES);
}
});
if (!installedAnything) {
await newUpdater.startCurrentVersion();
newUpdater.setRunningInBackground();
newUpdater.collectGarbage();
launchMainWindow();
updateBackoff.succeed();
updateSplashState(LAUNCHING);
return;
}
} catch (e) {
console.error('Update failed', e);
await new Promise(resolve => {
const delayMs = updateBackoff.fail(resolve);
splashState.seconds = Math.round(delayMs / 1000);
updateSplashState(UPDATE_FAILURE);
});
}
}
}
/* eslint-enable camelcase */
function initOldUpdater() {
modulesListeners = {};
addModulesListener(CHECKING_FOR_UPDATES, () => {
startUpdateTimeout();
updateSplashState(CHECKING_FOR_UPDATES);
});
addModulesListener(UPDATE_CHECK_FINISHED, ({
succeeded,
updateCount,
manualRequired
}) => {
stopUpdateTimeout();
if (!succeeded) {
scheduleUpdateCheck();
updateSplashState(UPDATE_FAILURE);
} else if (updateCount === 0) {
moduleUpdater.setInBackground();
launchMainWindow();
updateSplashState(LAUNCHING);
}
});
addModulesListener(DOWNLOADING_MODULE, ({
name,
current,
total
}) => {
stopUpdateTimeout();
splashState = {
current,
total
};
updateSplashState(DOWNLOADING_UPDATES);
});
addModulesListener(DOWNLOADING_MODULE_PROGRESS, ({
name,
progress
}) => {
splashState.progress = progress;
updateSplashState(DOWNLOADING_UPDATES);
});
addModulesListener(DOWNLOADED_MODULE, ({
name,
current,
total,
succeeded
}) => {
delete splashState.progress;
if (name === 'host') {
restartRequired = true;
}
});
addModulesListener(DOWNLOADING_MODULES_FINISHED, ({
succeeded,
failed
}) => {
if (failed > 0) {
scheduleUpdateCheck();
updateSplashState(UPDATE_FAILURE);
} else {
process.nextTick(() => {
if (restartRequired) {
moduleUpdater.quitAndInstallUpdates();
} else {
moduleUpdater.installPendingUpdates();
}
});
}
});
addModulesListener(NO_PENDING_UPDATES, () => moduleUpdater.checkForUpdates());
addModulesListener(INSTALLING_MODULE, ({
name,
current,
total
}) => {
splashState = {
current,
total
};
updateSplashState(INSTALLING_UPDATES);
});
addModulesListener(INSTALLED_MODULE, ({
name,
current,
total,
succeeded
}) => delete splashState.progress);
addModulesListener(INSTALLING_MODULE_PROGRESS, ({
name,
progress
}) => {
splashState.progress = progress;
updateSplashState(INSTALLING_UPDATES);
});
addModulesListener(INSTALLING_MODULES_FINISHED, ({
succeeded,
failed
}) => moduleUpdater.checkForUpdates());
addModulesListener(UPDATE_MANUALLY, ({
newVersion
}) => {
splashState.newVersion = newVersion;
updateSplashState(UPDATE_MANUALLY);
});
}
function initSplash(startMinimized = false) {
splashState = {};
launchedMainWindow = false;
updateAttempt = 0;
newUpdater = (0, _updater.getUpdater)();
if (newUpdater == null) {
initOldUpdater();
}
launchSplashWindow(startMinimized);
quoteCachePath = _path.default.join(paths.getUserData(), 'quotes.json');
_ipcMain.default.on('UPDATED_QUOTES', (_event, quotes) => cacheLatestQuotes(quotes));
}
function destroySplash() {
stopUpdateTimeout();
if (splashWindow) {
splashWindow.setSkipTaskbar(true); // defer the window hiding for a short moment so it gets covered by the main window
const _nukeWindow = () => {
if (splashWindow != null) {
splashWindow.hide();
splashWindow.close();
splashWindow = null;
}
};
setTimeout(_nukeWindow, 100);
}
}
function addModulesListener(event, listener) {
if (newUpdater != null) return;
modulesListeners[event] = listener;
moduleUpdater.events.addListener(event, listener);
}
function removeModulesListeners() {
if (newUpdater != null) return;
for (const event of Object.keys(modulesListeners)) {
moduleUpdater.events.removeListener(event, modulesListeners[event]);
}
}
function startUpdateTimeout() {
if (!updateTimeout) {
updateTimeout = setTimeout(() => scheduleUpdateCheck(), UPDATE_TIMEOUT_WAIT);
}
}
function stopUpdateTimeout() {
if (updateTimeout) {
clearTimeout(updateTimeout);
updateTimeout = null;
}
}
function updateSplashState(event) {
if (splashWindow != null && !splashWindow.isDestroyed() && !splashWindow.webContents.isDestroyed()) {
webContentsSend(splashWindow, 'SPLASH_UPDATE_STATE', {
status: event,
...splashState
});
}
}
function launchSplashWindow(startMinimized) {
const windowConfig = {
width: LOADING_WINDOW_WIDTH,
height: LOADING_WINDOW_HEIGHT,
transparent: false,
frame: false,
resizable: false,
center: true,
show: false,
webPreferences: {
nodeIntegration: false,
enableRemoteModule: false,
contextIsolation: true,
preload: _path.default.join(__dirname, 'preload.js')
}
};
splashWindow = new _electron.BrowserWindow(windowConfig); // prevent users from dropping links to navigate in splash window
splashWindow.webContents.on('will-navigate', e => e.preventDefault());
splashWindow.webContents.on('new-window', (e, windowURL) => {
e.preventDefault();
(0, _securityUtils.saferShellOpenExternal)(windowURL); // exit, but delay half a second because openExternal is about to fire
// some events to things that are freed by app.quit.
setTimeout(_electron.app.quit, 500);
});
if (process.platform !== 'darwin') {
// citron note: this causes a crash on quit while the window is open on osx
splashWindow.on('closed', () => {
splashWindow = null;
if (!launchedMainWindow) {
// user has closed this window before we launched the app, so let's quit
_electron.app.quit();
}
});
}
_ipcMain.default.on('SPLASH_SCREEN_READY', () => {
const cachedQuote = chooseCachedQuote();
if (cachedQuote) {
webContentsSend(splashWindow, 'SPLASH_SCREEN_QUOTE', cachedQuote);
}
if (splashWindow && !startMinimized) {
splashWindow.show();
}
if (newUpdater != null) {
updateUntilCurrent();
} else {
moduleUpdater.installPendingUpdates();
}
});
_ipcMain.default.on('SPLASH_SCREEN_QUIT', () => {
_electron.app.quit();
});
const splashUrl = _url.default.format({
protocol: 'file',
slashes: true,
pathname: _path.default.join(__dirname, 'index.html')
});
splashWindow.loadURL(splashUrl);
}
function launchMainWindow() {
removeModulesListeners();
if (!launchedMainWindow && splashWindow != null) {
launchedMainWindow = true;
events.emit(APP_SHOULD_LAUNCH);
}
}
function scheduleUpdateCheck() {
// TODO: can we use backoff here?
updateAttempt += 1;
const retryInSeconds = Math.min(updateAttempt * 10, RETRY_CAP_SECONDS);
splashState.seconds = retryInSeconds;
setTimeout(() => moduleUpdater.checkForUpdates(), retryInSeconds * 1000);
}
function focusWindow() {
if (splashWindow != null) {
splashWindow.focus();
}
}
function pageReady() {
destroySplash();
process.nextTick(() => events.emit(APP_SHOULD_SHOW));
}
function cacheLatestQuotes(quotes) {
_fs.default.writeFile(quoteCachePath, JSON.stringify(quotes), e => {
if (e) {
console.warn('Failed updating quote cache with error: ', e);
}
});
}
function chooseCachedQuote() {
let cachedQuote = null;
try {
const cachedQuotes = JSON.parse(_fs.default.readFileSync(quoteCachePath));
cachedQuote = cachedQuotes[Math.floor(Math.random() * cachedQuotes.length)];
} catch (_err) {}
return cachedQuote;
}

View file

@ -0,0 +1,95 @@
const { BrowserWindow, app } = require('electron');
const { format } = require('url');
const { join } = require('path');
const ipcMain = require('../ipcMain');
const LOADING_WINDOW_WIDTH = 300;
const LOADING_WINDOW_HEIGHT = process.platform === 'darwin' ? 300 : 350; // TODO: addModulesListener events should use Module's constants
let window;
const APP_SHOULD_LAUNCH = 'APP_SHOULD_LAUNCH';
exports.APP_SHOULD_LAUNCH = APP_SHOULD_LAUNCH;
const APP_SHOULD_SHOW = 'APP_SHOULD_SHOW';
exports.APP_SHOULD_SHOW = APP_SHOULD_SHOW;
const events = new (require('events')).EventEmitter();
exports.events = events;
exports.initSplash = (startMinimized = false) => { // Make splash window
const windowConfig = {
width: LOADING_WINDOW_WIDTH,
height: LOADING_WINDOW_HEIGHT,
transparent: false,
frame: false,
resizable: false,
center: true,
show: false,
webPreferences: {
nodeIntegration: false,
enableRemoteModule: false,
contextIsolation: true,
preload: join(__dirname, 'preload.js')
}
};
window = new BrowserWindow(windowConfig);
window.on('closed', () => { // Quit app on splash screen close
app.quit();
window = null;
});
// IPC "handlers"
ipcMain.on('SPLASH_SCREEN_READY', () => {
if (!startMinimized && window) window.show();
// Update and stuff
events.emit(APP_SHOULD_LAUNCH);
});
if (!startMinimized && window) window.show();
// Update and stuff
events.emit(APP_SHOULD_LAUNCH);
ipcMain.on('SPLASH_SCREEN_QUIT', () => {
app.quit();
});
const splashUrl = format({
protocol: 'file',
slashes: true,
pathname: join(__dirname, 'index.html')
});
window.loadURL(splashUrl);
};
exports.focusWindow = () => { // Focus splash window
if (window) window.focus();
};
const killWindow = () => {
if (!window) return;
window.setSkipTaskbar(true);
setTimeout(() => {
window.hide();
window.close();
window = null;
}, 100);
};
exports.pageReady = () => { // Kill splash window, emit
killWindow();
process.nextTick(() => events.emit(APP_SHOULD_SHOW));
};

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

11
src/splash/web/index.html Normal file
View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Discord Updater</title>
</head>
<body>
<div id="splash-mount"></div>
<script src="index.js"></script>
</body>
</html>

41
src/splash/web/index.js Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,6 @@
{
"width": "300px",
"height": "300px",
"inDuration": 700,
"outDuration": 333
}

73
src/updater/appUpdater.js Normal file
View file

@ -0,0 +1,73 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.update = update;
exports.focusSplash = focusSplash;
var _fs = _interopRequireDefault(require("fs"));
var _path = _interopRequireDefault(require("path"));
var moduleUpdater = _interopRequireWildcard(require("./moduleUpdater"));
var paths = _interopRequireWildcard(require("../paths"));
var _updater = require("./updater");
var _appSettings = require("../appSettings");
var autoStart = _interopRequireWildcard(require("../autoStart"));
var _buildInfo = _interopRequireDefault(require("../utils/buildInfo"));
var _errorHandler = require("../errorHandler");
var firstRun = _interopRequireWildcard(require("../firstRun"));
var splashScreen = _interopRequireWildcard(require("../splash/splashScreen"));
var _Constants = require("../Constants");
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
// settings
const USE_PINNED_UPDATE_MANIFEST = 'USE_PINNED_UPDATE_MANIFEST';
function update(startMinimized, doneCallback, showCallback) {
const settings = (0, _appSettings.getSettings)();
if ((0, _updater.tryInitUpdater)(_buildInfo.default, _Constants.NEW_UPDATE_ENDPOINT)) {
const updater = (0, _updater.getUpdater)();
const usePinnedUpdateManifest = settings.get(USE_PINNED_UPDATE_MANIFEST);
updater.on('host-updated', () => {
autoStart.update(() => {});
});
updater.on('unhandled-exception', _errorHandler.fatal);
updater.on(_updater.INCONSISTENT_INSTALLER_STATE_ERROR, _errorHandler.fatal);
updater.on('update-error', _errorHandler.handled);
if (usePinnedUpdateManifest) {
const manifestPath = _path.default.join(paths.getUserData(), 'pinned_update.json');
updater.setPinnedManifestSync(JSON.parse(_fs.default.readFileSync(manifestPath)));
}
firstRun.performFirstRunTasks(updater);
} else {
moduleUpdater.init(_Constants.UPDATE_ENDPOINT, settings, _buildInfo.default);
}
splashScreen.initSplash(startMinimized);
splashScreen.events.once(splashScreen.APP_SHOULD_LAUNCH, doneCallback);
splashScreen.events.once(splashScreen.APP_SHOULD_SHOW, showCallback);
}
function focusSplash() {
splashScreen.focusWindow();
}

199
src/updater/hostUpdater.js Normal file
View file

@ -0,0 +1,199 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _electron = require("electron");
var _events = require("events");
var _request = _interopRequireDefault(require("./request"));
var squirrelUpdate = _interopRequireWildcard(require("./squirrelUpdate"));
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function versionParse(verString) {
return verString.split('.').map(i => parseInt(i));
}
function versionNewer(verA, verB) {
let i = 0;
while (true) {
const a = verA[i];
const b = verB[i];
i++;
if (a === undefined) {
return false;
} else {
if (b === undefined || a > b) {
return true;
}
if (a < b) {
return false;
}
}
}
}
class AutoUpdaterWin32 extends _events.EventEmitter {
constructor() {
super();
this.updateUrl = null;
this.updateVersion = null;
}
setFeedURL(updateUrl) {
this.updateUrl = updateUrl;
}
quitAndInstall() {
if (squirrelUpdate.updateExistsSync()) {
squirrelUpdate.restart(_electron.app, this.updateVersion || _electron.app.getVersion());
} else {
require('auto-updater').quitAndInstall();
}
}
downloadAndInstallUpdate(callback) {
squirrelUpdate.spawnUpdateInstall(this.updateUrl, progress => {
this.emit('update-progress', progress);
}).catch(err => callback(err)).then(() => callback());
}
checkForUpdates() {
if (this.updateUrl == null) {
throw new Error('Update URL is not set');
}
this.emit('checking-for-update');
if (!squirrelUpdate.updateExistsSync()) {
this.emit('update-not-available');
return;
}
squirrelUpdate.spawnUpdate(['--check', this.updateUrl], (error, stdout) => {
if (error != null) {
this.emit('error', error);
return;
}
try {
// Last line of the output is JSON details about the releases
const json = stdout.trim().split('\n').pop();
const releasesFound = JSON.parse(json).releasesToApply;
if (releasesFound == null || releasesFound.length == 0) {
this.emit('update-not-available');
return;
}
const update = releasesFound.pop();
this.emit('update-available');
this.downloadAndInstallUpdate(error => {
if (error != null) {
this.emit('error', error);
return;
}
this.updateVersion = update.version;
this.emit('update-downloaded', {}, update.release, update.version, new Date(), this.updateUrl, this.quitAndInstall.bind(this));
});
} catch (error) {
error.stdout = stdout;
this.emit('error', error);
}
});
}
} // todo
class AutoUpdaterLinux extends _events.EventEmitter {
constructor() {
super();
this.updateUrl = null;
}
setFeedURL(url) {
this.updateUrl = url;
}
quitAndInstall() {
// Just restart. The splash screen will hit the update manually state and
// prompt the user to download the new package.
_electron.app.relaunch();
_electron.app.quit();
}
async checkForUpdates() {
const currVersion = versionParse(_electron.app.getVersion());
this.emit('checking-for-update');
try {
const response = await _request.default.get(this.updateUrl);
if (response.statusCode === 204) {
// you are up to date
this.emit('update-not-available');
return;
}
let latestVerStr = '';
let latestVersion = [];
try {
const latestMetadata = JSON.parse(response.body);
latestVerStr = latestMetadata.name;
latestVersion = versionParse(latestVerStr);
} catch (_) {}
if (versionNewer(latestVersion, currVersion)) {
console.log('[Updates] You are out of date!'); // you need to update
this.emit('update-manually', latestVerStr);
} else {
console.log('[Updates] You are living in the future!');
this.emit('update-not-available');
}
} catch (err) {
console.error('[Updates] Error fetching ' + this.updateUrl + ': ' + err.message);
this.emit('error', err);
}
}
}
let autoUpdater; // TODO
// events: checking-for-update, update-available, update-not-available, update-manually, update-downloaded, error
// also, checkForUpdates, setFeedURL, quitAndInstall
// also, see electron.autoUpdater, and its API
switch (process.platform) {
case 'darwin':
autoUpdater = require('electron').autoUpdater;
break;
case 'win32':
autoUpdater = new AutoUpdaterWin32();
break;
case 'linux':
autoUpdater = new AutoUpdaterLinux();
break;
}
var _default = autoUpdater;
exports.default = _default;
module.exports = exports.default;

View file

@ -0,0 +1,967 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.initPathsOnly = initPathsOnly;
exports.init = init;
exports.checkForUpdates = checkForUpdates;
exports.setInBackground = setInBackground;
exports.quitAndInstallUpdates = quitAndInstallUpdates;
exports.isInstalled = isInstalled;
exports.getInstalled = getInstalled;
exports.install = install;
exports.installPendingUpdates = installPendingUpdates;
exports.supportsEventObjects = exports.events = exports.NO_PENDING_UPDATES = exports.INSTALLING_MODULE_PROGRESS = exports.INSTALLING_MODULE = exports.INSTALLING_MODULES_FINISHED = exports.DOWNLOADED_MODULE = exports.UPDATE_MANUALLY = exports.DOWNLOADING_MODULES_FINISHED = exports.DOWNLOADING_MODULE_PROGRESS = exports.DOWNLOADING_MODULE = exports.UPDATE_CHECK_FINISHED = exports.INSTALLED_MODULE = exports.CHECKING_FOR_UPDATES = void 0;
var _fs = _interopRequireDefault(require("fs"));
var _path = _interopRequireDefault(require("path"));
var _module = _interopRequireDefault(require("module"));
var _events = require("events");
var _mkdirp = _interopRequireDefault(require("mkdirp"));
var _process = require("process");
var _yauzl = _interopRequireDefault(require("yauzl"));
var _Backoff = _interopRequireDefault(require("../utils/Backoff"));
var paths = _interopRequireWildcard(require("../paths"));
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
// Manages additional module installation and management.
// We add the module folder path to require() lookup paths here.
// undocumented node API
const originalFs = require('original-fs'); // events
const CHECKING_FOR_UPDATES = 'checking-for-updates';
exports.CHECKING_FOR_UPDATES = CHECKING_FOR_UPDATES;
const INSTALLED_MODULE = 'installed-module';
exports.INSTALLED_MODULE = INSTALLED_MODULE;
const UPDATE_CHECK_FINISHED = 'update-check-finished';
exports.UPDATE_CHECK_FINISHED = UPDATE_CHECK_FINISHED;
const DOWNLOADING_MODULE = 'downloading-module';
exports.DOWNLOADING_MODULE = DOWNLOADING_MODULE;
const DOWNLOADING_MODULE_PROGRESS = 'downloading-module-progress';
exports.DOWNLOADING_MODULE_PROGRESS = DOWNLOADING_MODULE_PROGRESS;
const DOWNLOADING_MODULES_FINISHED = 'downloading-modules-finished';
exports.DOWNLOADING_MODULES_FINISHED = DOWNLOADING_MODULES_FINISHED;
const UPDATE_MANUALLY = 'update-manually';
exports.UPDATE_MANUALLY = UPDATE_MANUALLY;
const DOWNLOADED_MODULE = 'downloaded-module';
exports.DOWNLOADED_MODULE = DOWNLOADED_MODULE;
const INSTALLING_MODULES_FINISHED = 'installing-modules-finished';
exports.INSTALLING_MODULES_FINISHED = INSTALLING_MODULES_FINISHED;
const INSTALLING_MODULE = 'installing-module';
exports.INSTALLING_MODULE = INSTALLING_MODULE;
const INSTALLING_MODULE_PROGRESS = 'installing-module-progress';
exports.INSTALLING_MODULE_PROGRESS = INSTALLING_MODULE_PROGRESS;
const NO_PENDING_UPDATES = 'no-pending-updates'; // settings
exports.NO_PENDING_UPDATES = NO_PENDING_UPDATES;
const ALWAYS_ALLOW_UPDATES = 'ALWAYS_ALLOW_UPDATES';
const SKIP_HOST_UPDATE = 'SKIP_HOST_UPDATE';
const SKIP_MODULE_UPDATE = 'SKIP_MODULE_UPDATE';
const ALWAYS_BOOTSTRAP_MODULES = 'ALWAYS_BOOTSTRAP_MODULES';
const USE_LOCAL_MODULE_VERSIONS = 'USE_LOCAL_MODULE_VERSIONS';
class Events extends _events.EventEmitter {
constructor() {
super();
this.history = [];
}
append(evt) {
evt.now = String(_process.hrtime.bigint());
if (this._eventIsInteresting(evt)) {
this.history.push(evt);
}
process.nextTick(() => this.emit(evt.type, evt));
}
_eventIsInteresting(evt) {
return evt.type !== DOWNLOADING_MODULE_PROGRESS && evt.type !== INSTALLING_MODULE_PROGRESS;
}
}
class LogStream {
constructor(logPath) {
try {
this.logStream = _fs.default.createWriteStream(logPath, {
flags: 'a'
});
} catch (e) {
console.error(`Failed to create ${logPath}: ${String(e)}`);
}
}
log(message) {
message = `[Modules] ${message}`;
console.log(message);
if (this.logStream) {
this.logStream.write(message);
this.logStream.write('\r\n');
}
}
end() {
if (this.logStream) {
this.logStream.end();
this.logStream = null;
}
}
}
const request = require('./request');
const REQUEST_TIMEOUT = 15000;
const backoff = new _Backoff.default(1000, 20000);
const events = new Events();
exports.events = events;
const supportsEventObjects = true;
exports.supportsEventObjects = supportsEventObjects;
let logger;
let locallyInstalledModules;
let moduleInstallPath;
let installedModulesFilePath;
let moduleDownloadPath;
let bootstrapping;
let hostUpdater;
let hostUpdateAvailable;
let skipHostUpdate;
let skipModuleUpdate;
let checkingForUpdates;
let remoteBaseURL;
let remoteQuery;
let settings;
let remoteModuleVersions;
let installedModules;
let download;
let unzip;
let newInstallInProgress;
let localModuleVersionsFilePath;
let updatable;
let bootstrapManifestFilePath;
let runningInBackground = false;
function initPathsOnly(_buildInfo) {
if (locallyInstalledModules || moduleInstallPath) {
return;
} // If we have `localModulesRoot` in our buildInfo file, we do not fetch modules
// from remote, and rely on our locally bundled ones.
// Typically used for development mode, or private builds.
locallyInstalledModules = _buildInfo.localModulesRoot != null;
if (locallyInstalledModules) {
if (_module.default.globalPaths.indexOf(_buildInfo.localModulesRoot) === -1) {
_module.default.globalPaths.push(_buildInfo.localModulesRoot);
}
} else {
moduleInstallPath = _path.default.join(paths.getUserDataVersioned(), 'modules');
if (_module.default.globalPaths.indexOf(moduleInstallPath) === -1) {
_module.default.globalPaths.push(moduleInstallPath);
}
}
}
function init(_endpoint, _settings, _buildInfo) {
const endpoint = _endpoint;
settings = _settings;
const buildInfo = _buildInfo;
updatable = buildInfo.version != '0.0.0' && !buildInfo.debug || settings.get(ALWAYS_ALLOW_UPDATES);
initPathsOnly(buildInfo);
logger = new LogStream(_path.default.join(paths.getUserData(), 'modules.log'));
bootstrapping = false;
hostUpdateAvailable = false;
checkingForUpdates = false;
skipHostUpdate = settings.get(SKIP_HOST_UPDATE) || !updatable;
skipModuleUpdate = settings.get(SKIP_MODULE_UPDATE) || locallyInstalledModules || !updatable;
localModuleVersionsFilePath = _path.default.join(paths.getUserData(), 'local_module_versions.json');
bootstrapManifestFilePath = _path.default.join(paths.getResources(), 'bootstrap', 'manifest.json');
installedModules = {};
remoteModuleVersions = {};
newInstallInProgress = {};
download = {
// currently downloading
active: false,
// {name, version}
queue: [],
// current queue index being downloaded
next: 0,
// download failure count
failures: 0
};
unzip = {
// currently unzipping
active: false,
// {name, version, zipfile}
queue: [],
// current queue index being unzipped
next: 0,
// unzip failure count
failures: 0
};
logger.log(`Modules initializing`);
logger.log(`Distribution: ${locallyInstalledModules ? 'local' : 'remote'}`);
logger.log(`Host updates: ${skipHostUpdate ? 'disabled' : 'enabled'}`);
logger.log(`Module updates: ${skipModuleUpdate ? 'disabled' : 'enabled'}`);
if (!locallyInstalledModules) {
installedModulesFilePath = _path.default.join(moduleInstallPath, 'installed.json');
moduleDownloadPath = _path.default.join(moduleInstallPath, 'pending');
_mkdirp.default.sync(moduleDownloadPath);
logger.log(`Module install path: ${moduleInstallPath}`);
logger.log(`Module installed file path: ${installedModulesFilePath}`);
logger.log(`Module download path: ${moduleDownloadPath}`);
let failedLoadingInstalledModules = false;
try {
installedModules = JSON.parse(_fs.default.readFileSync(installedModulesFilePath));
} catch (err) {
failedLoadingInstalledModules = true;
}
cleanDownloadedModules(installedModules);
bootstrapping = failedLoadingInstalledModules || settings.get(ALWAYS_BOOTSTRAP_MODULES);
}
hostUpdater = require('./hostUpdater'); // TODO: hostUpdater constants
hostUpdater.on('checking-for-update', () => events.append({
type: CHECKING_FOR_UPDATES
}));
hostUpdater.on('update-available', () => hostOnUpdateAvailable());
hostUpdater.on('update-progress', progress => hostOnUpdateProgress(progress));
hostUpdater.on('update-not-available', () => hostOnUpdateNotAvailable());
hostUpdater.on('update-manually', newVersion => hostOnUpdateManually(newVersion));
hostUpdater.on('update-downloaded', () => hostOnUpdateDownloaded());
hostUpdater.on('error', err => hostOnError(err));
const setFeedURL = hostUpdater.setFeedURL.bind(hostUpdater);
remoteBaseURL = `${endpoint}/modules/${buildInfo.releaseChannel}`; // eslint-disable-next-line camelcase
remoteQuery = {
host_version: buildInfo.version
};
switch (process.platform) {
case 'darwin':
setFeedURL(`${endpoint}/updates/${buildInfo.releaseChannel}?platform=osx&version=${buildInfo.version}`);
remoteQuery.platform = 'osx';
break;
case 'win32':
// Squirrel for Windows can't handle query params
// https://github.com/Squirrel/Squirrel.Windows/issues/132
setFeedURL(`${endpoint}/updates/${buildInfo.releaseChannel}`);
remoteQuery.platform = 'win';
break;
case 'linux':
setFeedURL(`${endpoint}/updates/${buildInfo.releaseChannel}?platform=linux&version=${buildInfo.version}`);
remoteQuery.platform = 'linux';
break;
}
}
function cleanDownloadedModules(installedModules) {
try {
const entries = _fs.default.readdirSync(moduleDownloadPath) || [];
entries.forEach(entry => {
const entryPath = _path.default.join(moduleDownloadPath, entry);
let isStale = true;
for (const moduleName of Object.keys(installedModules)) {
if (entryPath === installedModules[moduleName].updateZipfile) {
isStale = false;
break;
}
}
if (isStale) {
_fs.default.unlinkSync(_path.default.join(moduleDownloadPath, entry));
}
});
} catch (err) {
logger.log('Could not clean downloaded modules');
logger.log(err.stack);
}
}
function hostOnUpdateAvailable() {
logger.log(`Host update is available.`);
hostUpdateAvailable = true;
events.append({
type: UPDATE_CHECK_FINISHED,
succeeded: true,
updateCount: 1,
manualRequired: false
});
events.append({
type: DOWNLOADING_MODULE,
name: 'host',
current: 1,
total: 1,
foreground: !runningInBackground
});
}
function hostOnUpdateProgress(progress) {
logger.log(`Host update progress: ${progress}%`);
events.append({
type: DOWNLOADING_MODULE_PROGRESS,
name: 'host',
progress: progress
});
}
function hostOnUpdateNotAvailable() {
logger.log(`Host is up to date.`);
if (!skipModuleUpdate) {
checkForModuleUpdates();
} else {
events.append({
type: UPDATE_CHECK_FINISHED,
succeeded: true,
updateCount: 0,
manualRequired: false
});
}
}
function hostOnUpdateManually(newVersion) {
logger.log(`Host update is available. Manual update required!`);
hostUpdateAvailable = true;
checkingForUpdates = false;
events.append({
type: UPDATE_MANUALLY,
newVersion: newVersion
});
events.append({
type: UPDATE_CHECK_FINISHED,
succeeded: true,
updateCount: 1,
manualRequired: true
});
}
function hostOnUpdateDownloaded() {
logger.log(`Host update downloaded.`);
checkingForUpdates = false;
events.append({
type: DOWNLOADED_MODULE,
name: 'host',
current: 1,
total: 1,
succeeded: true
});
events.append({
type: DOWNLOADING_MODULES_FINISHED,
succeeded: 1,
failed: 0
});
}
function hostOnError(err) {
logger.log(`Host update failed: ${err}`); // [adill] osx unsigned builds will fire this code signing error inside setFeedURL and
// if we don't do anything about it hostUpdater.checkForUpdates() will never respond.
if (err && String(err).indexOf('Could not get code signature for running application') !== -1) {
console.warn('Skipping host updates due to code signing failure.');
skipHostUpdate = true;
}
checkingForUpdates = false;
if (!hostUpdateAvailable) {
events.append({
type: UPDATE_CHECK_FINISHED,
succeeded: false,
updateCount: 0,
manualRequired: false
});
} else {
events.append({
type: DOWNLOADED_MODULE,
name: 'host',
current: 1,
total: 1,
succeeded: false
});
events.append({
type: DOWNLOADING_MODULES_FINISHED,
succeeded: 0,
failed: 1
});
}
}
function checkForUpdates() {
if (checkingForUpdates) return;
checkingForUpdates = true;
hostUpdateAvailable = false;
if (skipHostUpdate) {
events.append({
type: CHECKING_FOR_UPDATES
});
hostOnUpdateNotAvailable();
} else {
logger.log('Checking for host updates.');
hostUpdater.checkForUpdates();
}
} // Indicates that the initial update process is complete and that future updates
// are background updates. This merely affects the content of the events sent to
// the app so that analytics can correctly attribute module download/installs
// depending on whether they were ui-blocking or not.
function setInBackground() {
runningInBackground = true;
}
function getRemoteModuleName(name) {
if (process.platform === 'win32' && process.arch === 'x64') {
return `${name}.x64`;
}
return name;
}
async function checkForModuleUpdates() {
const query = { ...remoteQuery,
_: Math.floor(Date.now() / 1000 / 60 / 5)
};
const url = `${remoteBaseURL}/versions.json`;
logger.log(`Checking for module updates at ${url}`);
let response;
try {
response = await request.get({
url,
qs: query,
timeout: REQUEST_TIMEOUT
});
checkingForUpdates = false;
} catch (err) {
checkingForUpdates = false;
logger.log(`Failed fetching module versions: ${String(err)}`);
events.append({
type: UPDATE_CHECK_FINISHED,
succeeded: false,
updateCount: 0,
manualRequired: false
});
return;
}
remoteModuleVersions = JSON.parse(response.body);
if (settings.get(USE_LOCAL_MODULE_VERSIONS)) {
try {
remoteModuleVersions = JSON.parse(_fs.default.readFileSync(localModuleVersionsFilePath));
console.log('Using local module versions: ', remoteModuleVersions);
} catch (err) {
console.warn('Failed to parse local module versions: ', err);
}
}
const updatesToDownload = [];
for (const moduleName of Object.keys(installedModules)) {
const installedModule = installedModules[moduleName];
const installed = installedModule.installedVersion;
if (installed === null) {
continue;
}
const update = installedModule.updateVersion || 0;
const remote = remoteModuleVersions[getRemoteModuleName(moduleName)] || 0;
if (installed !== remote && update !== remote) {
logger.log(`Module update available: ${moduleName}@${remote} [installed: ${installed}]`);
updatesToDownload.push({
name: moduleName,
version: remote
});
}
}
events.append({
type: UPDATE_CHECK_FINISHED,
succeeded: true,
updateCount: updatesToDownload.length,
manualRequired: false
});
if (updatesToDownload.length === 0) {
logger.log(`No module updates available.`);
} else {
updatesToDownload.forEach(e => addModuleToDownloadQueue(e.name, e.version));
}
}
function addModuleToDownloadQueue(name, version, authToken) {
download.queue.push({
name,
version,
authToken
});
process.nextTick(() => processDownloadQueue());
}
async function processDownloadQueue() {
if (download.active) return;
if (download.queue.length === 0) return;
download.active = true;
const queuedModule = download.queue[download.next];
download.next += 1;
events.append({
type: DOWNLOADING_MODULE,
name: queuedModule.name,
current: download.next,
total: download.queue.length,
foreground: !runningInBackground
});
let progress = 0;
let receivedBytes = 0;
const url = `${remoteBaseURL}/${encodeURIComponent(getRemoteModuleName(queuedModule.name))}/${encodeURIComponent(queuedModule.version)}`;
logger.log(`Fetching ${queuedModule.name}@${queuedModule.version} from ${url}`);
const headers = {};
if (queuedModule.authToken) {
headers['Authorization'] = queuedModule.authToken;
}
const moduleZipPath = _path.default.join(moduleDownloadPath, `${queuedModule.name}-${queuedModule.version}.zip`);
const stream = _fs.default.createWriteStream(moduleZipPath);
stream.on('progress', ({
receivedBytes: newReceivedBytes,
totalBytes
}) => {
receivedBytes = newReceivedBytes;
const newProgress = Math.min(Math.floor(100 * (receivedBytes / totalBytes)), 100);
if (progress !== newProgress) {
progress = newProgress;
logger.log(`Streaming ${queuedModule.name}@${queuedModule.version} to ${moduleZipPath}: ${progress}%`);
events.append({
type: DOWNLOADING_MODULE_PROGRESS,
name: queuedModule.name,
progress: progress
});
}
});
logger.log(`Streaming ${queuedModule.name}@${queuedModule.version} to ${moduleZipPath}`);
try {
const response = await request.get({
url,
qs: remoteQuery,
headers,
timeout: REQUEST_TIMEOUT,
stream
});
finishModuleDownload(queuedModule.name, queuedModule.version, moduleZipPath, receivedBytes, response.statusCode === 200);
} catch (err) {
logger.log(`Failed fetching module ${queuedModule.name}@${queuedModule.version}: ${String(err)}`);
finishModuleDownload(queuedModule.name, queuedModule.version, null, receivedBytes, false);
}
}
function commitInstalledModules() {
const data = JSON.stringify(installedModules, null, 2);
_fs.default.writeFileSync(installedModulesFilePath, data);
}
function finishModuleDownload(name, version, zipfile, receivedBytes, succeeded) {
if (!installedModules[name]) {
installedModules[name] = {};
}
if (succeeded) {
installedModules[name].updateVersion = version;
installedModules[name].updateZipfile = zipfile;
commitInstalledModules();
} else {
download.failures += 1;
}
events.append({
type: DOWNLOADED_MODULE,
name: name,
current: download.next,
total: download.queue.length,
succeeded: succeeded,
receivedBytes: receivedBytes
});
if (download.next >= download.queue.length) {
const successes = download.queue.length - download.failures;
logger.log(`Finished module downloads. [success: ${successes}] [failure: ${download.failures}]`);
events.append({
type: DOWNLOADING_MODULES_FINISHED,
succeeded: successes,
failed: download.failures
});
download.queue = [];
download.next = 0;
download.failures = 0;
download.active = false;
} else {
const continueDownloads = () => {
download.active = false;
processDownloadQueue();
};
if (succeeded) {
backoff.succeed();
process.nextTick(continueDownloads);
} else {
logger.log(`Waiting ${Math.floor(backoff.current)}ms before next download.`);
backoff.fail(continueDownloads);
}
}
if (newInstallInProgress[name]) {
addModuleToUnzipQueue(name, version, zipfile);
}
}
function addModuleToUnzipQueue(name, version, zipfile) {
unzip.queue.push({
name,
version,
zipfile
});
process.nextTick(() => processUnzipQueue());
}
function processUnzipQueue() {
if (unzip.active) return;
if (unzip.queue.length === 0) return;
unzip.active = true;
const queuedModule = unzip.queue[unzip.next];
const installedModule = installedModules[queuedModule.name];
const installedVersion = installedModule != null ? installedModule.installedVersion : null;
unzip.next += 1;
events.append({
type: INSTALLING_MODULE,
name: queuedModule.name,
current: unzip.next,
total: unzip.queue.length,
foreground: !runningInBackground,
oldVersion: installedVersion,
newVersion: queuedModule.version
});
let hasErrored = false;
const onError = (error, zipfile) => {
if (hasErrored) return;
hasErrored = true;
logger.log(`Failed installing ${queuedModule.name}@${queuedModule.version}: ${String(error)}`);
succeeded = false;
if (zipfile) {
zipfile.close();
}
finishModuleUnzip(queuedModule, succeeded);
};
let succeeded = true;
const extractRoot = _path.default.join(moduleInstallPath, queuedModule.name);
logger.log(`Installing ${queuedModule.name}@${queuedModule.version} from ${queuedModule.zipfile}`);
const processZipfile = (err, zipfile) => {
if (err) {
onError(err, null);
return;
}
const totalEntries = zipfile.entryCount;
let processedEntries = 0;
zipfile.on('entry', entry => {
processedEntries += 1;
const percent = Math.min(Math.floor(processedEntries / totalEntries * 100), 100);
events.append({
type: INSTALLING_MODULE_PROGRESS,
name: queuedModule.name,
progress: percent
}); // skip directories
if (/\/$/.test(entry.fileName)) {
zipfile.readEntry();
return;
}
zipfile.openReadStream(entry, (err, stream) => {
if (err) {
onError(err, zipfile);
return;
}
stream.on('error', e => onError(e, zipfile));
(0, _mkdirp.default)(_path.default.join(extractRoot, _path.default.dirname(entry.fileName)), err => {
if (err) {
onError(err, zipfile);
return;
} // [adill] createWriteStream via original-fs is broken in Electron 4.0.0-beta.6 with .asar files
// so we unzip to a temporary filename and rename it afterwards
const tempFileName = _path.default.join(extractRoot, entry.fileName + '.tmp');
const finalFileName = _path.default.join(extractRoot, entry.fileName);
const writeStream = originalFs.createWriteStream(tempFileName);
writeStream.on('error', e => {
stream.destroy();
try {
originalFs.unlinkSync(tempFileName);
} catch (err) {}
onError(e, zipfile);
});
writeStream.on('finish', () => {
try {
originalFs.unlinkSync(finalFileName);
} catch (err) {}
try {
originalFs.renameSync(tempFileName, finalFileName);
} catch (err) {
onError(err, zipfile);
return;
}
zipfile.readEntry();
});
stream.pipe(writeStream);
});
});
});
zipfile.on('error', err => {
onError(err, zipfile);
});
zipfile.on('end', () => {
if (!succeeded) return;
installedModules[queuedModule.name].installedVersion = queuedModule.version;
finishModuleUnzip(queuedModule, succeeded);
});
zipfile.readEntry();
};
try {
_yauzl.default.open(queuedModule.zipfile, {
lazyEntries: true,
autoClose: true
}, processZipfile);
} catch (err) {
onError(err, null);
}
}
function finishModuleUnzip(unzippedModule, succeeded) {
delete newInstallInProgress[unzippedModule.name];
delete installedModules[unzippedModule.name].updateZipfile;
delete installedModules[unzippedModule.name].updateVersion;
commitInstalledModules();
if (!succeeded) {
unzip.failures += 1;
}
events.append({
type: INSTALLED_MODULE,
name: unzippedModule.name,
current: unzip.next,
total: unzip.queue.length,
succeeded: succeeded
});
if (unzip.next >= unzip.queue.length) {
const successes = unzip.queue.length - unzip.failures;
bootstrapping = false;
logger.log(`Finished module installations. [success: ${successes}] [failure: ${unzip.failures}]`);
unzip.queue = [];
unzip.next = 0;
unzip.failures = 0;
unzip.active = false;
events.append({
type: INSTALLING_MODULES_FINISHED,
succeeded: successes,
failed: unzip.failures
});
return;
}
process.nextTick(() => {
unzip.active = false;
processUnzipQueue();
});
}
function quitAndInstallUpdates() {
logger.log(`Relaunching to install ${hostUpdateAvailable ? 'host' : 'module'} updates...`);
if (hostUpdateAvailable) {
hostUpdater.quitAndInstall();
} else {
relaunch();
}
}
function relaunch() {
logger.end();
const {
app
} = require('electron');
app.relaunch();
app.quit();
}
function isInstalled(name, version) {
const metadata = installedModules[name];
if (locallyInstalledModules) return true;
if (metadata && metadata.installedVersion > 0) {
if (!version) return true;
if (metadata.installedVersion === version) return true;
}
return false;
}
function getInstalled() {
return { ...installedModules
};
}
function install(name, defer, options) {
let {
version,
authToken
} = options || {};
if (isInstalled(name, version)) {
if (!defer) {
events.append({
type: INSTALLED_MODULE,
name: name,
current: 1,
total: 1,
succeeded: true
});
}
return;
}
if (newInstallInProgress[name]) return;
if (!updatable) {
logger.log(`Not updatable; ignoring request to install ${name}...`);
return;
}
if (defer) {
if (version) {
throw new Error(`Cannot defer install for a specific version module (${name}, ${version})`);
}
logger.log(`Deferred install for ${name}...`);
installedModules[name] = {
installedVersion: 0
};
commitInstalledModules();
} else {
logger.log(`Starting to install ${name}...`);
if (!version) {
version = remoteModuleVersions[name] || 0;
}
newInstallInProgress[name] = version;
addModuleToDownloadQueue(name, version, authToken);
}
}
function installPendingUpdates() {
const updatesToInstall = [];
if (bootstrapping) {
let modules = {};
try {
modules = JSON.parse(_fs.default.readFileSync(bootstrapManifestFilePath));
} catch (err) {}
for (const moduleName of Object.keys(modules)) {
installedModules[moduleName] = {
installedVersion: 0
};
const zipfile = _path.default.join(paths.getResources(), 'bootstrap', `${moduleName}.zip`);
updatesToInstall.push({
moduleName,
update: modules[moduleName],
zipfile
});
}
}
for (const moduleName of Object.keys(installedModules)) {
const update = installedModules[moduleName].updateVersion || 0;
const zipfile = installedModules[moduleName].updateZipfile;
if (update > 0 && zipfile != null) {
updatesToInstall.push({
moduleName,
update,
zipfile
});
}
}
if (updatesToInstall.length > 0) {
logger.log(`${bootstrapping ? 'Bootstrapping' : 'Installing updates'}...`);
updatesToInstall.forEach(e => addModuleToUnzipQueue(e.moduleName, e.update, e.zipfile));
} else {
logger.log('No updates to install');
events.append({
type: NO_PENDING_UPDATES
});
}
}

186
src/updater/request.js Normal file
View file

@ -0,0 +1,186 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _electron = require("electron");
var _querystring = _interopRequireDefault(require("querystring"));
var _request = _interopRequireDefault(require("request"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const DEFAULT_REQUEST_TIMEOUT = 30000;
function makeHTTPResponse({
method,
url,
headers,
statusCode,
statusMessage
}, body) {
return {
method,
url,
headers,
statusCode,
statusMessage,
body
};
}
function makeHTTPStatusError(response) {
const err = new Error(`HTTP Error: Status Code ${response.statusCode}`);
err.response = response;
return err;
}
function handleHTTPResponse(resolve, reject, response, stream) {
const totalBytes = parseInt(response.headers['content-length'] || 1, 10);
let receivedBytes = 0;
const chunks = []; // don't stream response if it's a failure
if (response.statusCode >= 300) {
stream = null;
}
response.on('data', chunk => {
if (stream != null) {
receivedBytes += chunk.length;
stream.write(chunk);
stream.emit('progress', {
totalBytes,
receivedBytes
});
return;
}
chunks.push(chunk);
});
response.on('end', () => {
if (stream != null) {
stream.on('finish', () => resolve(makeHTTPResponse(response, null)));
stream.end();
return;
}
const res = makeHTTPResponse(response, Buffer.concat(chunks));
if (res.statusCode >= 300) {
reject(makeHTTPStatusError(res));
return;
}
resolve(res);
});
}
function nodeRequest({
method,
url,
headers,
qs,
timeout,
body,
stream
}) {
return new Promise((resolve, reject) => {
const req = (0, _request.default)({
method,
url,
qs,
headers,
followAllRedirects: true,
encoding: null,
timeout: timeout != null ? timeout : DEFAULT_REQUEST_TIMEOUT,
body
});
req.on('response', response => handleHTTPResponse(resolve, reject, response, stream));
req.on('error', err => reject(err));
});
}
async function electronRequest({
method,
url,
headers,
qs,
timeout,
body,
stream
}) {
await _electron.app.whenReady();
const {
net,
session
} = require('electron');
const req = net.request({
method,
url: `${url}${qs != null ? `?${_querystring.default.stringify(qs)}` : ''}`,
redirect: 'follow',
session: session.defaultSession
});
if (headers != null) {
for (const headerKey of Object.keys(headers)) {
req.setHeader(headerKey, headers[headerKey]);
}
}
if (body != null) {
req.write(body, 'utf-8');
}
return new Promise((resolve, reject) => {
const reqTimeout = setTimeout(() => {
req.abort();
reject(new Error(`network timeout: ${url}`));
}, timeout != null ? timeout : DEFAULT_REQUEST_TIMEOUT);
req.on('login', (authInfo, callback) => callback());
req.on('response', response => {
clearTimeout(reqTimeout);
handleHTTPResponse(resolve, reject, response, stream);
});
req.on('error', err => {
clearTimeout(reqTimeout);
reject(err);
});
req.end();
});
}
async function requestWithMethod(method, options) {
if (typeof options === 'string') {
options = {
url: options
};
}
options = { ...options,
method
};
try {
return await electronRequest(options);
} catch (err) {
console.log(`Error downloading with electron net: ${err.message}`);
console.log('Falling back to node net library..');
}
return nodeRequest(options);
} // only supports get for now, since retrying is non-idempotent and
// we'd want to grovel the errors to make sure it's safe to retry
for (const method of ['get']) {
requestWithMethod[method] = requestWithMethod.bind(null, method.toUpperCase());
}
var _default = requestWithMethod;
exports.default = _default;
module.exports = exports.default;

View file

@ -0,0 +1,218 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.spawnUpdateInstall = spawnUpdateInstall;
exports.spawnUpdate = spawnUpdate;
exports.installProtocol = installProtocol;
exports.handleStartupEvent = handleStartupEvent;
exports.updateExistsSync = updateExistsSync;
exports.restart = restart;
var _child_process = _interopRequireDefault(require("child_process"));
var _fs = _interopRequireDefault(require("fs"));
var _path = _interopRequireDefault(require("path"));
var autoStart = _interopRequireWildcard(require("../autoStart"));
var windowsUtils = _interopRequireWildcard(require("./windowsUtils"));
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
// citron note: this assumes the execPath is in the format Discord/someVersion/Discord.exe
const appFolder = _path.default.resolve(process.execPath, '..');
const rootFolder = _path.default.resolve(appFolder, '..');
const exeName = _path.default.basename(process.execPath);
const updateExe = _path.default.join(rootFolder, 'Update.exe'); // Specialized spawn function specifically used for spawning the updater in
// update mode. Calls back with progress percentages.
// Returns Promise.
function spawnUpdateInstall(updateUrl, progressCallback) {
return new Promise((resolve, reject) => {
const proc = _child_process.default.spawn(updateExe, ['--update', updateUrl]);
proc.on('error', reject);
proc.on('exit', code => {
if (code !== 0) {
return reject(new Error(`Update failed with exit code ${code}`));
}
return resolve();
});
let lastProgress = -1;
function parseProgress() {
const lines = stdout.split(/\r?\n/);
if (lines.length === 1) return; // return the last (possibly incomplete) line to stdout for parsing again
stdout = lines.pop();
let currentProgress;
for (const line of lines) {
if (!/^\d\d?$/.test(line)) continue;
const progress = Number(line); // make sure that this number is steadily increasing
if (lastProgress > progress) continue;
currentProgress = progress;
}
if (currentProgress == null) return;
lastProgress = currentProgress;
progressCallback(Math.min(currentProgress, 100));
}
let stdout = '';
proc.stdout.on('data', chunk => {
stdout += String(chunk);
parseProgress();
});
});
} // Spawn the Update.exe with the given arguments and invoke the callback when
// the command completes.
function spawnUpdate(args, callback) {
windowsUtils.spawn(updateExe, args, callback);
} // Create a desktop and start menu shortcut by using the command line API
// provided by Squirrel's Update.exe
function createShortcuts(callback, updateOnly) {
// move icon out to a more stable location, to keep shortcuts from breaking as much
const icoSrc = _path.default.join(appFolder, 'app.ico');
const icoDest = _path.default.join(rootFolder, 'app.ico');
let icoForTarget = icoDest;
try {
const ico = _fs.default.readFileSync(icoSrc);
_fs.default.writeFileSync(icoDest, ico);
} catch (e) {
// if we can't write there for some reason, just use the source.
icoForTarget = icoSrc;
}
const createShortcutArgs = ['--createShortcut', exeName, '--setupIcon', icoForTarget];
if (updateOnly) {
createShortcutArgs.push('--updateOnly');
}
spawnUpdate(createShortcutArgs, callback);
} // Add a protocol registration for this application.
function installProtocol(protocol, callback) {
const queue = [['HKCU\\Software\\Classes\\' + protocol, '/ve', '/d', `URL:${protocol} Protocol`], ['HKCU\\Software\\Classes\\' + protocol, '/v', 'URL Protocol'], ['HKCU\\Software\\Classes\\' + protocol + '\\DefaultIcon', '/ve', '/d', '"' + process.execPath + '",-1'], ['HKCU\\Software\\Classes\\' + protocol + '\\shell\\open\\command', '/ve', '/d', `"${process.execPath}" --url -- "%1"`]];
windowsUtils.addToRegistry(queue, callback);
}
function terminate(app) {
app.quit();
process.exit(0);
} // Remove the desktop and start menu shortcuts by using the command line API
// provided by Squirrel's Update.exe
function removeShortcuts(callback) {
spawnUpdate(['--removeShortcut', exeName], callback);
} // Update the desktop and start menu shortcuts by using the command line API
// provided by Squirrel's Update.exe
function updateShortcuts(callback) {
createShortcuts(callback, true);
} // Purge the protocol for this applicationstart.
function uninstallProtocol(protocol, callback) {
windowsUtils.spawnReg(['delete', 'HKCU\\Software\\Classes\\' + protocol, '/f'], callback);
}
function maybeInstallNewUpdaterSeedDb() {
const installerDbSrc = _path.default.join(appFolder, 'installer.db');
const installerDbDest = _path.default.join(rootFolder, 'installer.db');
if (_fs.default.existsSync(installerDbSrc)) {
_fs.default.renameSync(installerDbSrc, installerDbDest);
}
} // Handle squirrel events denoted by --squirrel-* command line arguments.
// returns `true` if regular startup should be prevented
function handleStartupEvent(protocol, app, squirrelCommand) {
switch (squirrelCommand) {
case '--squirrel-install':
createShortcuts(() => {
autoStart.install(() => {
installProtocol(protocol, () => {
// Squirrel doesn't have a way to include app-level files.
// We get around this for new updater hosts, which rely on
// a seeded manifest, by bubbling the db up from the versioned-app
// directory if it exists.
maybeInstallNewUpdaterSeedDb();
terminate(app);
});
});
}, false);
return true;
case '--squirrel-updated':
updateShortcuts(() => {
autoStart.update(() => {
installProtocol(protocol, () => {
terminate(app);
});
});
});
return true;
case '--squirrel-uninstall':
removeShortcuts(() => {
autoStart.uninstall(() => {
uninstallProtocol(protocol, () => {
terminate(app);
});
});
});
return true;
case '--squirrel-obsolete':
terminate(app);
return true;
default:
return false;
}
} // Are we using Squirrel for updates?
function updateExistsSync() {
return _fs.default.existsSync(updateExe);
} // Restart app as the new version
function restart(app, newVersion) {
app.once('will-quit', () => {
const execPath = _path.default.resolve(rootFolder, `app-${newVersion}/${exeName}`);
_child_process.default.spawn(execPath, [], {
detached: true
});
});
app.quit();
}

460
src/updater/updater.js Normal file
View file

@ -0,0 +1,460 @@
"use strict";
// Too much Rust integration stuff in here.
/* eslint camelcase: 0 */
const childProcess = require('child_process');
const {
app
} = require('electron');
const {
EventEmitter
} = require('events');
const NodeModule = require('module');
const path = require('path');
const {
hrtime
} = require('process');
let instance;
const TASK_STATE_COMPLETE = 'Complete';
const TASK_STATE_FAILED = 'Failed';
const TASK_STATE_WAITING = 'Waiting';
const TASK_STATE_WORKING = 'Working';
const INCONSISTENT_INSTALLER_STATE_ERROR = 'InconsistentInstallerState'; // The dumb linters are mad at each other.
// eslint-disable-next-line quotes
const INVALID_UPDATER_ERROR = "Can't send request to updater because the native updater isn't loaded.";
class Updater extends EventEmitter {
constructor(options) {
super();
let nativeUpdaterModule = options.nativeUpdaterModule;
if (nativeUpdaterModule == null) {
try {
// eslint-disable-next-line import/no-unresolved
nativeUpdaterModule = require('../../../updater');
} catch (e) {
if (e.code === 'MODULE_NOT_FOUND') {
return;
}
throw e;
}
}
this.committedHostVersion = null;
this.committedModules = new Set();
this.rootPath = options.root_path;
this.nextRequestId = 0;
this.requests = new Map();
this.updateEventHistory = [];
this.isRunningInBackground = false;
this.currentlyDownloading = {};
this.currentlyInstalling = {};
this.hasEmittedUnhandledException = false;
this.nativeUpdater = new nativeUpdaterModule.Updater({
response_handler: this._handleResponse.bind(this),
...options
});
}
get valid() {
return this.nativeUpdater != null;
}
_sendRequest(detail, progressCallback = null) {
if (!this.valid) {
throw new Error(INVALID_UPDATER_ERROR);
}
const requestId = this.nextRequestId++;
return new Promise((resolve, reject) => {
this.requests.set(requestId, {
resolve,
reject,
progressCallback
});
this.nativeUpdater.command(JSON.stringify([requestId, detail]));
});
}
_sendRequestSync(detail) {
if (!this.valid) {
throw new Error(INVALID_UPDATER_ERROR);
}
const requestId = this.nextRequestId++;
return this.nativeUpdater.command_blocking(JSON.stringify([requestId, detail]));
}
_handleResponse(response) {
try {
const [id, detail] = JSON.parse(response);
const request = this.requests.get(id);
if (request == null) {
console.error('Received response ', detail, ' for a request (', id, ') not in the updater request map.');
return;
}
if (detail['Error'] != null) {
const {
kind,
details,
severity
} = detail['Error'];
const e = new Error(`(${kind}) ${details}`);
if (severity === 'Fatal') {
const handled = this.emit(kind, e);
if (!handled) {
throw e;
}
} else {
this.emit('update-error', e);
request.reject(e);
this.requests.delete(id);
}
} else if (detail === 'Ok') {
request.resolve();
this.requests.delete(id);
} else if (detail['VersionInfo'] != null) {
request.resolve(detail['VersionInfo']);
this.requests.delete(id);
} else if (detail['ManifestInfo'] != null) {
request.resolve(detail['ManifestInfo']);
this.requests.delete(id);
} else if (detail['TaskProgress'] != null) {
const msg = detail['TaskProgress'];
const progress = {
task: msg[0],
state: msg[1],
percent: msg[2],
bytesProcessed: msg[3]
};
this._recordTaskProgress(progress);
if (request.progressCallback != null) {
request.progressCallback(progress);
}
if (progress.task['HostInstall'] != null && progress.state === TASK_STATE_COMPLETE) {
this.emit('host-updated');
}
} else {
console.warn('Unknown updater response', detail);
}
} catch (e) {
console.error('Unhandled exception in updater response handler:', e); // Report the first time this happens, but don't spam.
if (!this.hasEmittedUnhandledException) {
this.hasEmittedUnhandledException = true;
this.emit('unhandled-exception', e);
}
}
}
_handleSyncResponse(response) {
const detail = JSON.parse(response);
if (detail['Error'] != null) {
throw new Error(detail['Error']);
} else if (detail === 'Ok') {
return;
} else if (detail['VersionInfo'] != null) {
return detail['VersionInfo'];
}
console.warn('Unknown updater response', detail);
}
_getHostPath() {
const [major, minor, revision] = this.committedHostVersion;
const hostVersionStr = `${major}.${minor}.${revision}`;
return path.join(this.rootPath, `app-${hostVersionStr}`);
}
_startCurrentVersionInner(options, versions) {
if (this.committedHostVersion == null) {
this.committedHostVersion = versions.current_host;
}
const hostPath = this._getHostPath();
const hostExePath = path.join(hostPath, path.basename(process.execPath));
if (path.resolve(hostExePath) != path.resolve(process.execPath) && !(options === null || options === void 0 ? void 0 : options.allowObsoleteHost)) {
app.once('will-quit', () => {
// TODO(eiz): the actual, correct way to do this (win32) is to inherit a
// handle to the current process into a new child process which then
// waits for that process handle to exit, then runs the new electron.
// This requires either implementing a separate updater exe process (big
// todo item atm) or likely modifying Electron?
//
// I intend to do it properly once the new production updater .exe is a
// thing.
childProcess.spawn(hostExePath, [], {
detached: true,
stdio: 'inherit'
});
});
console.log(`Restarting from ${path.resolve(process.execPath)} to ${path.resolve(hostExePath)}`);
app.quit();
return;
}
this._commitModulesInner(versions);
}
_commitModulesInner(versions) {
const hostPath = this._getHostPath();
const modulesPath = path.join(hostPath, 'modules');
for (const module in versions.current_modules) {
const moduleVersion = versions.current_modules[module];
const moduleSearchPath = path.join(modulesPath, `${module}-${moduleVersion}`);
if (!this.committedModules.has(module) && NodeModule.globalPaths.indexOf(moduleSearchPath) === -1) {
this.committedModules.add(module);
NodeModule.globalPaths.push(moduleSearchPath);
}
}
}
_recordDownloadProgress(name, progress) {
const now = String(hrtime.bigint());
if (progress.state === TASK_STATE_WORKING && !this.currentlyDownloading[name]) {
this.currentlyDownloading[name] = true;
this.updateEventHistory.push({
type: 'downloading-module',
name: name,
now: now
});
} else if (progress.state === TASK_STATE_COMPLETE || progress.state === TASK_STATE_FAILED) {
this.currentlyDownloading[name] = false;
this.updateEventHistory.push({
type: 'downloaded-module',
name: name,
now: now,
succeeded: progress.state === TASK_STATE_COMPLETE,
receivedBytes: progress.bytesProcessed
});
}
}
_recordInstallProgress(name, progress, newVersion, isDelta) {
const now = String(hrtime.bigint());
if (progress.state === TASK_STATE_WORKING && !this.currentlyInstalling[name]) {
this.currentlyInstalling[name] = true;
this.updateEventHistory.push({
type: 'installing-module',
name,
now,
newVersion,
foreground: !this.isRunningInBackground
});
} else if (progress.state === TASK_STATE_COMPLETE || progress.state === TASK_STATE_FAILED) {
this.currentlyInstalling[name] = false;
this.updateEventHistory.push({
type: 'installed-module',
name,
now,
newVersion,
succeeded: progress.state === TASK_STATE_COMPLETE,
delta: isDelta,
foreground: !this.isRunningInBackground
});
}
}
_recordTaskProgress(progress) {
if (progress.task.HostDownload != null) {
this._recordDownloadProgress('host', progress);
} else if (progress.task.HostInstall != null) {
this._recordInstallProgress('host', progress, null, progress.task.HostInstall.from_version != null);
} else if (progress.task.ModuleDownload != null) {
this._recordDownloadProgress(progress.task.ModuleDownload.version.module.name, progress);
} else if (progress.task.ModuleInstall != null) {
this._recordInstallProgress(progress.task.ModuleInstall.version.module.name, progress, progress.task.ModuleInstall.version.version, progress.task.ModuleInstall.from_version != null);
}
}
queryCurrentVersions() {
return this._sendRequest('QueryCurrentVersions');
}
queryCurrentVersionsSync() {
return this._handleSyncResponse(this._sendRequestSync('QueryCurrentVersions'));
}
repair(progressCallback) {
return this.repairWithOptions(null, progressCallback);
}
repairWithOptions(options, progressCallback) {
return this._sendRequest({
Repair: {
options
}
}, progressCallback);
}
collectGarbage() {
return this._sendRequest('CollectGarbage');
}
setRunningManifest(manifest) {
return this._sendRequest({
SetManifests: ['Running', manifest]
});
}
setPinnedManifestSync(manifest) {
return this._handleSyncResponse(this._sendRequestSync({
SetManifests: ['Pinned', manifest]
}));
}
installModule(name, progressCallback) {
return this.installModuleWithOptions(name, null, progressCallback);
}
installModuleWithOptions(name, options, progressCallback) {
return this._sendRequest({
InstallModule: {
name,
options
}
}, progressCallback);
}
updateToLatest(progressCallback) {
return this.updateToLatestWithOptions(null, progressCallback);
}
updateToLatestWithOptions(options, progressCallback) {
return this._sendRequest({
UpdateToLatest: {
options
}
}, progressCallback);
} // If the running host is current, adopt the current installed modules and
// set up the module search path accordingly. If the running host is not
// current, start the new current host and exit this process.
async startCurrentVersion(options) {
const versions = await this.queryCurrentVersions();
await this.setRunningManifest(versions.last_successful_update);
this._startCurrentVersionInner(options, versions);
}
startCurrentVersionSync(options) {
const versions = this.queryCurrentVersionsSync();
this._startCurrentVersionInner(options, versions);
}
async commitModules(versions) {
if (this.committedHostVersion == null) {
throw new Error('Cannot commit modules before host version.');
}
if (versions == null) {
versions = await this.queryCurrentVersions();
}
this._commitModulesInner(versions);
}
setRunningInBackground() {
this.isRunningInBackground = true;
}
queryAndTruncateHistory() {
const history = this.updateEventHistory;
this.updateEventHistory = [];
return history;
}
getKnownFolder(name) {
if (!this.valid) {
throw new Error(INVALID_UPDATER_ERROR);
}
return this.nativeUpdater.known_folder(name);
}
createShortcut(options) {
if (!this.valid) {
throw new Error(INVALID_UPDATER_ERROR);
}
return this.nativeUpdater.create_shortcut(options);
}
}
function getUpdaterPlatformName(platform) {
switch (platform) {
case 'darwin':
return 'osx';
case 'win32':
return 'win';
default:
return platform;
}
}
function tryInitUpdater(buildInfo, repositoryUrl) {
// We can't require this in module scope because it's not part of the
// bootstrapper, which carries a copy of the Updater class.
const paths = require('../paths');
const rootPath = paths.getInstallPath(); // If we're not running from an actual install directory, don't bother trying
// to initialize the updater.
if (rootPath == null) {
return false;
}
instance = new Updater({
release_channel: buildInfo.releaseChannel,
platform: getUpdaterPlatformName(process.platform),
repository_url: repositoryUrl,
root_path: rootPath
});
return instance.valid;
}
function getUpdater() {
if (instance != null && instance.valid) {
return instance;
}
}
module.exports = {
Updater,
tryInitUpdater,
getUpdater,
TASK_STATE_COMPLETE,
TASK_STATE_FAILED,
TASK_STATE_WAITING,
TASK_STATE_WORKING,
INCONSISTENT_INSTALLER_STATE_ERROR
};

View file

@ -0,0 +1,82 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.spawn = spawn;
exports.spawnReg = spawnReg;
exports.addToRegistry = addToRegistry;
var _child_process = _interopRequireDefault(require("child_process"));
var _path = _interopRequireDefault(require("path"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const regExe = process.env.SystemRoot ? _path.default.join(process.env.SystemRoot, 'System32', 'reg.exe') : 'reg.exe'; // Spawn a command and invoke the callback when it completes with an error
// and the output from standard out.
function spawn(command, args, callback) {
let stdout = '';
let spawnedProcess;
try {
// TODO: contrary to below, it should not throw any error
spawnedProcess = _child_process.default.spawn(command, args);
} catch (err) {
// Spawn can throw an error
process.nextTick(() => {
if (callback != null) {
callback(err, stdout);
}
});
return;
} // TODO: we need to specify the encoding for the data if we're going to concat it as a string
spawnedProcess.stdout.on('data', data => {
stdout += data;
});
let err = null; // TODO: close event might not get called, we should
// callback on error https://nodejs.org/api/child_process.html#child_process_event_error
spawnedProcess.on('error', err => {
// TODO: there should always be an error
if (err != null) {
err = err;
}
}); // TODO: don't listen to close, but listen to exit instead
spawnedProcess.on('close', (code, signal) => {
if (err === null && code !== 0) {
err = new Error('Command failed: ' + (signal || code));
}
if (err != null) {
err.code = err.code || code;
err.stdout = err.stdout || stdout;
}
if (callback != null) {
callback(err, stdout);
}
});
} // Spawn reg.exe and callback when it completes
function spawnReg(args, callback) {
return spawn(regExe, args, callback);
} // TODO: since we're doing this one by one, we could have a more graceful way of processing the queue
// rather than mutating the array
function addToRegistry(queue, callback) {
if (queue.length === 0) {
return callback && callback();
}
const args = queue.shift();
args.unshift('add');
args.push('/f');
return spawnReg(args, () => addToRegistry(queue, callback));
}

94
src/utils/Backoff.js Normal file
View file

@ -0,0 +1,94 @@
class Backoff { // Heavily based on original for compat
/**
* Create a backoff instance can automatically backoff retries.
*/
constructor(min = 500, max = null, jitter = true) {
this.min = min;
this.max = max != null ? max : min * 10;
this.jitter = jitter;
this._current = min;
this._timeoutId = null;
this._fails = 0;
}
/**
* Return the number of failures.
*/
get fails() {
return this._fails;
}
/**
* Current backoff value in milliseconds.
*/
get current() {
return this._current;
}
/**
* A callback is going to fire.
*/
get pending() {
return this._timeoutId != null;
}
/**
* Clear any pending callbacks and reset the backoff.
*/
succeed() {
this.cancel();
this._fails = 0;
this._current = this.min;
}
/**
* Increment the backoff and schedule a callback if provided.
*/
fail(callback) {
this._fails += 1;
let delay = this._current * 2;
if (this.jitter) {
delay *= Math.random();
}
this._current = Math.min(this._current + delay, this.max);
if (callback != null) {
if (this._timeoutId != null) {
throw new Error('callback already pending');
}
this._timeoutId = setTimeout(() => {
try {
if (callback != null) {
callback();
}
} finally {
this._timeoutId = null;
}
}, this._current);
}
return this._current;
}
/**
* Clear any pending callbacks.
*/
cancel() {
if (this._timeoutId != null) {
clearTimeout(this._timeoutId);
this._timeoutId = null;
}
}
}
module.exports = Backoff;

64
src/utils/Settings.js Normal file
View file

@ -0,0 +1,64 @@
const { readFileSync, statSync, writeFileSync } = require('fs');
const { join } = require('path');
const log = require('./log');
class Settings { // Heavily based on original for compat, but simplified and tweaked
constructor(root) {
this.path = join(root, 'settings.json');
try {
this.lastSaved = readFileSync(this.path);
this.settings = JSON.parse(this.lastSaved);
} catch (e) {
this.lastSaved = '';
this.settings = {};
}
this.lastModified = this.getLastModified();
log('AppSettings', 'Loaded settings.json with path', this.path, 'with settings', this.settings, 'and last modified', this.lastModified);
}
getLastModified() {
try {
return statSync(this.path).mtime.getTime();
} catch (e) {
return 0;
}
}
get(key, defaultValue = false) {
return this.settings[key] || defaultValue;
}
set(key, value) {
this.settings[key] = value;
}
save() {
if (this.lastModified && this.lastModified !== this.getLastModified()) {
log('AppSettings', 'Refusing to save settings.json due to last modified date mismatch');
return;
}
try {
const toSave = JSON.stringify(this.settings, null, 2);
if (this.lastSaved != toSave) {
this.lastSaved = toSave;
writeFileSync(this.path, toSave);
this.lastModified = this.getLastModified();
}
log('AppSettings', 'Saved settings.json');
} catch (err) {
log('AppSettings', 'Failed to save settings.json', err);
}
}
}
module.exports = Settings;

8
src/utils/buildInfo.js Normal file
View file

@ -0,0 +1,8 @@
// Discord uses require but we'll use JSON parse for vaguely more security-ish
const { readFileSync } = require('fs');
const { join } = require('path');
const buildInfoPath = join(process.resourcesPath, 'build_info.json');
const buildInfo = JSON.parse(readFileSync(buildInfoPath, 'utf8'));
module.exports = buildInfo;

3
src/utils/log.js Normal file
View file

@ -0,0 +1,3 @@
const rgb = (r, g, b, text) => `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
module.exports = (area, ...args) => console.log(`[${rgb(88, 101, 242, 'OpenAsar')}${area ? ` > ${area}` : ''}]`, ...args);

View file

@ -0,0 +1,3 @@
// From Discord to only require native modules like discord_desktop_core
module.paths = [];
module.exports = require;

View file

@ -0,0 +1,45 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.saferShellOpenExternal = saferShellOpenExternal;
exports.checkUrlOriginMatches = checkUrlOriginMatches;
var _electron = require("electron");
var _url = _interopRequireDefault(require("url"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const BLOCKED_URL_PROTOCOLS = ['file:', 'javascript:', 'vbscript:', 'data:', 'about:', 'chrome:', 'ms-cxh:', 'ms-cxh-full:', 'ms-word:'];
function saferShellOpenExternal(externalUrl) {
let parsedUrl;
try {
parsedUrl = _url.default.parse(externalUrl);
} catch (_) {
return Promise.reject();
}
if (parsedUrl.protocol == null || BLOCKED_URL_PROTOCOLS.includes(parsedUrl.protocol.toLowerCase())) {
return Promise.reject();
}
return _electron.shell.openExternal(externalUrl);
}
function checkUrlOriginMatches(urlA, urlB) {
let parsedUrlA;
let parsedUrlB;
try {
parsedUrlA = _url.default.parse(urlA);
parsedUrlB = _url.default.parse(urlB);
} catch (_) {
return false;
}
return parsedUrlA.protocol === parsedUrlB.protocol && parsedUrlA.slashes === parsedUrlB.slashes && parsedUrlA.host === parsedUrlB.host;
}

13
src/utils/stub.js Normal file
View file

@ -0,0 +1,13 @@
const log = require('./log');
module.exports = (debugName) => {
return new Proxy({}, {
get(target, prop, receiver) {
log('Stub', `${debugName}: Tried getting ${prop}`);
},
set(target, prop, value, receiver) {
log('Stub', `${debugName}: Tried setting ${prop}, ${value}`);
}
});
};

13
test.sh Executable file
View file

@ -0,0 +1,13 @@
#!/bin/sh
echo "Packing asar..."
asar pack src app.asar # Package asar
# asar list app.asar # List asar for debugging / testing
echo "Copying asar..."
cp app.asar /opt/discord-canary/resources/app.asar # Overwrite app.asar for Linux Canary
echo "Running discord..."
echo ""
discord-canary # Run it