From 5d7d72a3e210976dcc88962742e8c59f3ee73548 Mon Sep 17 00:00:00 2001 From: Martin Fournier Date: Tue, 28 Dec 2021 15:21:11 -0500 Subject: [PATCH 1/2] Add authorization token to file system api --- .gitignore | 1 + electron/api-server.js | 124 ++++++++++++++ electron/main.js | 196 ++++++++++++---------- electron/package-lock.json | 325 +++++++++++++++++++++++++++++++++++++ electron/package.json | 10 +- package.json | 1 - package.sh | 3 - 7 files changed, 564 insertions(+), 96 deletions(-) create mode 100644 electron/api-server.js create mode 100644 electron/package-lock.json diff --git a/.gitignore b/.gitignore index 123dcb1ab..1ca4801dc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ Changelog.txt Netburner.txt /doc/build /node_modules +/electron/node_modules /dist/*.map /test/*.map /test/*.bundle.* diff --git a/electron/api-server.js b/electron/api-server.js new file mode 100644 index 000000000..8ac7531eb --- /dev/null +++ b/electron/api-server.js @@ -0,0 +1,124 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const http = require("http"); +const crypto = require('crypto'); +const log = require('electron-log'); +const Config = require('electron-config'); +const config = new Config(); + +let server; +let window; + +function initialize(win, callback) { + window = win; + server = http.createServer(async function (req, res) { + let body = ""; + + req.on("data", (chunk) => { + body += chunk.toString(); // convert Buffer to string + }); + req.on("end", () => { + const providedToken = req.headers?.authorization?.replace('Bearer ', '') ?? ''; + const isValid = providedToken === getAuthenticationToken(); + if (isValid) { + log.log('Valid authentication token'); + } else { + log.log('Invalid authentication token'); + res.writeHead(401); + res.write('Invalid authentication token'); + res.end(); + return; + } + + let data; + try { + data = JSON.parse(body); + } catch (error) { + log.warn(`Invalid body data`); + res.writeHead(400); + res.write('Invalid body data'); + res.end(); + return; + } + + if (data) { + window.webContents.executeJavaScript(`document.saveFile("${data.filename}", "${data.code}")`).then((result) => { + res.write(result); + res.end(); + }); + } + }); + }); + + const autostart = config.get('autostart', false); + if (autostart) { + return enable(callback); + } + + if (callback) return callback(); + return Promise.resolve(); +} + + +function enable(callback) { + if (isListening()) { + log.warn('API server already listening'); + return; + } + + const port = config.get('port', 9990); + log.log(`Starting http server on port ${port}`); + return server.listen(port, "127.0.0.1", callback); +} + +function disable() { + if (!isListening()) { + log.warn('API server not listening'); + return; + } + + log.log('Stopping http server'); + return server.close(); +} + +function toggleServer() { + if (isListening()) { + return disable(); + } else { + return enable(); + } +} + +function isListening() { + return server?.listening ?? false; +} + +function toggleAutostart() { + const newValue = !isAutostart(); + config.set('autostart', newValue); + log.log(`New autostart value is '${newValue}'`); +} + +function isAutostart() { + return config.get('autostart'); +} + +function getAuthenticationToken() { + const token = config.get('token'); + if (token) return token; + + const newToken = generateToken(); + config.set('token', newToken); + return newToken; +} + +function generateToken() { + const buffer = crypto.randomBytes(48); + return buffer.toString('base64') +} + +module.exports = { + initialize, + enable, disable, toggleServer, + toggleAutostart, isAutostart, + getAuthenticationToken, isListening, +} diff --git a/electron/main.js b/electron/main.js index c63406261..885ee2464 100644 --- a/electron/main.js +++ b/electron/main.js @@ -1,8 +1,9 @@ /* eslint-disable no-process-exit */ /* eslint-disable @typescript-eslint/no-var-requires */ -const { app, BrowserWindow, Menu, shell, dialog } = require("electron"); +const { app, BrowserWindow, Menu, shell, dialog, clipboard } = require("electron"); const log = require('electron-log'); const greenworks = require("./greenworks"); +const api = require("./api-server"); log.catchErrors(); log.info(`Started app: ${JSON.stringify(process.argv)}`); @@ -19,24 +20,106 @@ if (greenworks.init()) { } const debug = false; - let win = null; -require("http") - .createServer(async function (req, res) { - let body = ""; - req.on("data", (chunk) => { - body += chunk.toString(); // convert Buffer to string - }); - req.on("end", () => { - const data = JSON.parse(body); - win.webContents.executeJavaScript(`document.saveFile("${data.filename}", "${data.code}")`).then((result) => { - res.write(result); - res.end(); - }); - }); +const getMenu = (win) => Menu.buildFromTemplate([ + { + label: "Edit", + submenu: [ + { label: "Undo", accelerator: "CmdOrCtrl+Z", selector: "undo:" }, + { label: "Redo", accelerator: "Shift+CmdOrCtrl+Z", selector: "redo:" }, + { type: "separator" }, + { label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" }, + { label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" }, + { label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" }, + { label: "Select All", accelerator: "CmdOrCtrl+A", selector: "selectAll:" }, + ], + }, + { + label: "Reloads", + submenu: [ + { + label: "Reload", + accelerator: "f5", + click: () => { + win.loadFile("index.html"); + }, + }, + { + label: "Reload & Kill All Scripts", + click: () => reloadAndKill(win) + }, + ], + }, + { + label: "Fullscreen", + submenu: [ + { + label: "Toggle", + accelerator: "f9", + click: (() => { + let full = false; + return () => { + full = !full; + win.setFullScreen(full); + }; + })(), + }, + ], + }, + { + label: "API Server", + submenu: [ + { + label: api.isListening() ? 'Disable Server' : 'Enable Server', + click: (async () => { + await api.toggleServer(); + Menu.setApplicationMenu(getMenu()); + }) + }, + { + label: api.isAutostart() ? 'Disable Autostart' : 'Enable Autostart', + click: (async () => { + api.toggleAutostart(); + Menu.setApplicationMenu(getMenu()); + }) + }, + { + label: 'Copy Auth Token', + click: (async () => { + const token = api.getAuthenticationToken(); + log.log('Wrote authentication token to clipboard'); + clipboard.writeText(token); + }) + }, + ] + }, + { + label: "Debug", + submenu: [ + { + label: "Activate", + click: () => win.webContents.openDevTools(), + }, + ], + }, +]); + +const reloadAndKill = (win, killScripts = true) => { + log.info('Reloading & Killing all scripts...'); + setStopProcessHandler(app, win, false); + if (win.achievementsIntervalID) clearInterval(win.achievementsIntervalID); + win.webContents.forcefullyCrashRenderer(); + win.on('closed', () => { + // Wait for window to be closed before opening the new one to prevent race conditions + log.debug('Opening new window'); + const newWindow = createWindow(killScripts); + api.initialize(newWindow, () => Menu.setApplicationMenu(getMenu(win))); + setStopProcessHandler(app, newWindow, true); }) - .listen(9990, "127.0.0.1"); + win.close(); +}; + function createWindow(killall) { win = new BrowserWindow({ @@ -83,19 +166,7 @@ function createWindow(killall) { }, 1000); win.achievementsIntervalID = intervalID; - const reloadAndKill = (killScripts = true) => { - log.info('Reloading & Killing all scripts...'); - setStopProcessHandler(app, win, false); - if (intervalID) clearInterval(intervalID); - win.webContents.forcefullyCrashRenderer(); - win.on('closed', () => { - // Wait for window to be closed before opening the new one to prevent race conditions - log.debug('Opening new window'); - const newWindow = createWindow(killScripts); - setStopProcessHandler(app, newWindow, true); - }) - win.close(); - }; + const promptForReload = () => { win.off('unresponsive', promptForReload); dialog.showMessageBox({ @@ -111,7 +182,7 @@ function createWindow(killall) { noLink: true, }).then(({response, checkboxChecked}) => { if (response === 0) { - reloadAndKill(checkboxChecked); + reloadAndKill(win, checkboxChecked); } else { win.on('unresponsive', promptForReload) } @@ -119,64 +190,8 @@ function createWindow(killall) { } win.on('unresponsive', promptForReload); - // Create the Application's main menu - Menu.setApplicationMenu( - Menu.buildFromTemplate([ - { - label: "Edit", - submenu: [ - { label: "Undo", accelerator: "CmdOrCtrl+Z", selector: "undo:" }, - { label: "Redo", accelerator: "Shift+CmdOrCtrl+Z", selector: "redo:" }, - { type: "separator" }, - { label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" }, - { label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" }, - { label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" }, - { label: "Select All", accelerator: "CmdOrCtrl+A", selector: "selectAll:" }, - ], - }, - { - label: "reloads", - submenu: [ - { - label: "reload", - accelerator: "f5", - click: () => { - win.loadFile("index.html"); - }, - }, - { - label: "reload & kill all scripts", - click: reloadAndKill - }, - ], - }, - { - label: "fullscreen", - submenu: [ - { - label: "toggle", - accelerator: "f9", - click: (() => { - let full = false; - return () => { - full = !full; - win.setFullScreen(full); - }; - })(), - }, - ], - }, - { - label: "debug", - submenu: [ - { - label: "activate", - click: () => win.webContents.openDevTools(), - }, - ], - }, - ]), - ); + // // Create the Application's main menu + // Menu.setApplicationMenu(getMenu()); return win; } @@ -191,6 +206,8 @@ function setStopProcessHandler(app, window, enabled) { clearInterval(window.achievementsIntervalID); } + api.disable(); + // We'll try to execute javascript on the page to see if we're stuck let canRunJS = false; win.webContents.executeJavaScript('window.stop(); document.close()', true) @@ -238,8 +255,9 @@ function setStopProcessHandler(app, window, enabled) { } } -app.whenReady().then(() => { +app.whenReady().then(async () => { log.info('Application is ready!'); const win = createWindow(process.argv.includes("--no-scripts")); + await api.initialize(win, () => Menu.setApplicationMenu(getMenu(win))); setStopProcessHandler(app, win, true); }); diff --git a/electron/package-lock.json b/electron/package-lock.json new file mode 100644 index 000000000..00b2d1d7e --- /dev/null +++ b/electron/package-lock.json @@ -0,0 +1,325 @@ +{ + "name": "bitburner", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "bitburner", + "version": "1.0.0", + "dependencies": { + "electron-config": "^2.0.0", + "electron-log": "^4.4.4" + } + }, + "node_modules/conf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/conf/-/conf-1.4.0.tgz", + "integrity": "sha512-bzlVWS2THbMetHqXKB8ypsXN4DQ/1qopGwNJi1eYbpwesJcd86FBjFciCQX/YwAhp9bM7NVnPFqZ5LpV7gP0Dg==", + "dependencies": { + "dot-prop": "^4.1.0", + "env-paths": "^1.0.0", + "make-dir": "^1.0.0", + "pkg-up": "^2.0.0", + "write-file-atomic": "^2.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/dot-prop": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz", + "integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==", + "dependencies": { + "is-obj": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/electron-config": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/electron-config/-/electron-config-2.0.0.tgz", + "integrity": "sha512-5mGwRK4lsAo6tiy4KNF/zUInYpUGr7JJzLA8FHOoqBWV3kkKJWSrDXo4Uk2Ffm5aeQ1o73XuorfkYhaWFV2O4g==", + "deprecated": "Renamed to `electron-store`.", + "dependencies": { + "conf": "^1.0.0" + } + }, + "node_modules/electron-log": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.4.4.tgz", + "integrity": "sha512-jcNtrVmKXG+CHchLo/jnjjQ9K4/ORguWD23H2nqApTwisQ4Qo3IRQtLiorubajX0Uxg76Xm/Yt+eNfQMoHVr5w==" + }, + "node_modules/env-paths": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-1.0.0.tgz", + "integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA=", + "engines": { + "node": ">=4" + } + }, + "node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "engines": { + "node": ">=4" + } + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "engines": { + "node": ">=4" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz", + "integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=", + "dependencies": { + "find-up": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/signal-exit": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", + "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==" + }, + "node_modules/write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "dependencies": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + } + }, + "dependencies": { + "conf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/conf/-/conf-1.4.0.tgz", + "integrity": "sha512-bzlVWS2THbMetHqXKB8ypsXN4DQ/1qopGwNJi1eYbpwesJcd86FBjFciCQX/YwAhp9bM7NVnPFqZ5LpV7gP0Dg==", + "requires": { + "dot-prop": "^4.1.0", + "env-paths": "^1.0.0", + "make-dir": "^1.0.0", + "pkg-up": "^2.0.0", + "write-file-atomic": "^2.3.0" + } + }, + "dot-prop": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz", + "integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==", + "requires": { + "is-obj": "^1.0.0" + } + }, + "electron-config": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/electron-config/-/electron-config-2.0.0.tgz", + "integrity": "sha512-5mGwRK4lsAo6tiy4KNF/zUInYpUGr7JJzLA8FHOoqBWV3kkKJWSrDXo4Uk2Ffm5aeQ1o73XuorfkYhaWFV2O4g==", + "requires": { + "conf": "^1.0.0" + } + }, + "electron-log": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.4.4.tgz", + "integrity": "sha512-jcNtrVmKXG+CHchLo/jnjjQ9K4/ORguWD23H2nqApTwisQ4Qo3IRQtLiorubajX0Uxg76Xm/Yt+eNfQMoHVr5w==" + }, + "env-paths": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-1.0.0.tgz", + "integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA=" + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "^2.0.0" + } + }, + "graceful-fs": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "requires": { + "pify": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + }, + "pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz", + "integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=", + "requires": { + "find-up": "^2.1.0" + } + }, + "signal-exit": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", + "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==" + }, + "write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + } + } +} diff --git a/electron/package.json b/electron/package.json index 1a5da80c5..8612cb794 100755 --- a/electron/package.json +++ b/electron/package.json @@ -5,20 +5,24 @@ "main": "main.js", "author": "Daniel Xie & Olivier Gagnon", "mac": { - "icon": "./public/icons/mac/icon.icns", + "icon": "./public/icons/mac/icon.icns", "category": "public.app-category.games" }, "win": { - "icon": "./public/icons/png/256x256.png" + "icon": "./public/icons/png/256x256.png" }, "files": [ "./build/**/*", "./dist/**/*", "./node_modules/**/*", - "./public/**/*", + "./public/**/*", "*.js" ], "directories": { "buildResources": "public" + }, + "dependencies": { + "electron-config": "^2.0.0", + "electron-log": "^4.4.4" } } diff --git a/package.json b/package.json index 747b5d943..1d2964298 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,6 @@ "babel-loader": "^8.0.5", "cypress": "^8.3.1", "electron": "^14.0.2", - "electron-log": "^4.4.3", "electron-packager": "^15.4.0", "eslint": "^7.24.0", "fork-ts-checker-webpack-plugin": "^6.3.3", diff --git a/package.sh b/package.sh index 840aa6134..1783f3475 100755 --- a/package.sh +++ b/package.sh @@ -16,7 +16,4 @@ cp main.css .package/main.css cp dist/vendor.bundle.js .package/dist/vendor.bundle.js cp main.bundle.js .package/main.bundle.js -# Adding electron-log dependency -cp -r node_modules/electron-log .package/node_modules/electron-log - npm run electron:packager From a098289856f173ca350034f9334bd2978ad738a9 Mon Sep 17 00:00:00 2001 From: Martin Fournier Date: Wed, 29 Dec 2021 08:46:56 -0500 Subject: [PATCH 2/2] Refactor electron app into multiple files Gracefully handle http-server start error & cleanup logs --- electron/achievements.js | 35 +++++++ electron/api-server.js | 45 +++++++-- electron/gameWindow.js | 55 ++++++++++ electron/main.js | 210 ++++----------------------------------- electron/menu.js | 101 +++++++++++++++++++ electron/utils.js | 78 +++++++++++++++ 6 files changed, 323 insertions(+), 201 deletions(-) create mode 100644 electron/achievements.js create mode 100644 electron/gameWindow.js create mode 100644 electron/menu.js create mode 100644 electron/utils.js diff --git a/electron/achievements.js b/electron/achievements.js new file mode 100644 index 000000000..b7283f28e --- /dev/null +++ b/electron/achievements.js @@ -0,0 +1,35 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const greenworks = require("./greenworks"); + +function enableAchievementsInterval(window) { + // This is backward but the game fills in an array called `document.achievements` and we retrieve it from + // here. Hey if it works it works. + const steamAchievements = greenworks.getAchievementNames(); + const intervalID = setInterval(async () => { + try { + const playerAchievements = await window.webContents.executeJavaScript("document.achievements"); + for (const ach of playerAchievements) { + if (!steamAchievements.includes(ach)) continue; + greenworks.activateAchievement(ach, () => undefined); + } + } catch (error) { + log.error(error); + + // The interval probably did not get cleared after a window kill + log.warn('Clearing achievements timer'); + clearInterval(intervalID); + return; + } + }, 1000); + window.achievementsIntervalID = intervalID; +} + +function disableAchievementsInterval(window) { + if (window.achievementsIntervalID) { + clearInterval(window.achievementsIntervalID); + } +} + +module.exports = { + enableAchievementsInterval, disableAchievementsInterval +} diff --git a/electron/api-server.js b/electron/api-server.js index 8ac7531eb..5de6f2bcb 100644 --- a/electron/api-server.js +++ b/electron/api-server.js @@ -1,14 +1,14 @@ /* eslint-disable @typescript-eslint/no-var-requires */ const http = require("http"); -const crypto = require('crypto'); -const log = require('electron-log'); -const Config = require('electron-config'); +const crypto = require("crypto"); +const log = require("electron-log"); +const Config = require("electron-config"); const config = new Config(); let server; let window; -function initialize(win, callback) { +async function initialize(win) { window = win; server = http.createServer(async function (req, res) { let body = ""; @@ -20,7 +20,7 @@ function initialize(win, callback) { const providedToken = req.headers?.authorization?.replace('Bearer ', '') ?? ''; const isValid = providedToken === getAuthenticationToken(); if (isValid) { - log.log('Valid authentication token'); + log.debug('Valid authentication token'); } else { log.log('Invalid authentication token'); res.writeHead(401); @@ -51,29 +51,52 @@ function initialize(win, callback) { const autostart = config.get('autostart', false); if (autostart) { - return enable(callback); + try { + await enable() + } catch (error) { + return Promise.reject(error); + } } - if (callback) return callback(); return Promise.resolve(); } -function enable(callback) { +function enable() { if (isListening()) { log.warn('API server already listening'); - return; + return Promise.resolve(); } const port = config.get('port', 9990); log.log(`Starting http server on port ${port}`); - return server.listen(port, "127.0.0.1", callback); + + // https://stackoverflow.com/a/62289870 + let startFinished = false; + return new Promise((resolve, reject) => { + server.listen(port, "127.0.0.1", () => { + if (!startFinished) { + startFinished = true; + resolve(); + } + }); + server.once('error', (err) => { + if (!startFinished) { + startFinished = true; + console.log( + 'There was an error starting the server in the error listener:', + err + ); + reject(err); + } + }); + }); } function disable() { if (!isListening()) { log.warn('API server not listening'); - return; + return Promise.resolve(); } log.log('Stopping http server'); diff --git a/electron/gameWindow.js b/electron/gameWindow.js new file mode 100644 index 000000000..c16891558 --- /dev/null +++ b/electron/gameWindow.js @@ -0,0 +1,55 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { app, BrowserWindow, shell } = require("electron"); +const log = require("electron-log"); +const utils = require("./utils"); +const achievements = require("./achievements"); +const menu = require("./menu"); +const api = require("./api-server"); + +const debug = process.argv.includes("--debug"); + +async function createWindow(killall) { + const window = new BrowserWindow({ + show: false, + backgroundThrottling: false, + backgroundColor: "#000000", + }); + + window.removeMenu(); + window.maximize(); + noScripts = killall ? { query: { noScripts: killall } } : {}; + window.loadFile("index.html", noScripts); + window.show(); + if (debug) window.webContents.openDevTools(); + + window.webContents.on("new-window", function (e, url) { + // make sure local urls stay in electron perimeter + if (url.substr(0, "file://".length) === "file://") { + return; + } + + // and open every other protocols on the browser + e.preventDefault(); + shell.openExternal(url); + }); + window.webContents.backgroundThrottling = false; + + achievements.enableAchievementsInterval(window); + utils.attachUnresponsiveAppHandler(window); + + try { + await api.initialize(window); + } catch (error) { + log.error(error); + utils.showErrorBox('Error starting http server', error); + } + + menu.refreshMenu(window); + utils.setStopProcessHandler(app, window, true); + + return window; +} + +module.exports = { + createWindow, +} diff --git a/electron/main.js b/electron/main.js index 885ee2464..6a7d814d2 100644 --- a/electron/main.js +++ b/electron/main.js @@ -1,9 +1,12 @@ /* eslint-disable no-process-exit */ /* eslint-disable @typescript-eslint/no-var-requires */ -const { app, BrowserWindow, Menu, shell, dialog, clipboard } = require("electron"); -const log = require('electron-log'); +const { app } = require("electron"); +const log = require("electron-log"); const greenworks = require("./greenworks"); const api = require("./api-server"); +const gameWindow = require("./gameWindow"); +const achievements = require("./achievements"); +const utils = require("./utils"); log.catchErrors(); log.info(`Started app: ${JSON.stringify(process.argv)}`); @@ -19,203 +22,25 @@ if (greenworks.init()) { log.warn("Steam API has failed to initialize."); } -const debug = false; -let win = null; - -const getMenu = (win) => Menu.buildFromTemplate([ - { - label: "Edit", - submenu: [ - { label: "Undo", accelerator: "CmdOrCtrl+Z", selector: "undo:" }, - { label: "Redo", accelerator: "Shift+CmdOrCtrl+Z", selector: "redo:" }, - { type: "separator" }, - { label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" }, - { label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" }, - { label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" }, - { label: "Select All", accelerator: "CmdOrCtrl+A", selector: "selectAll:" }, - ], - }, - { - label: "Reloads", - submenu: [ - { - label: "Reload", - accelerator: "f5", - click: () => { - win.loadFile("index.html"); - }, - }, - { - label: "Reload & Kill All Scripts", - click: () => reloadAndKill(win) - }, - ], - }, - { - label: "Fullscreen", - submenu: [ - { - label: "Toggle", - accelerator: "f9", - click: (() => { - let full = false; - return () => { - full = !full; - win.setFullScreen(full); - }; - })(), - }, - ], - }, - { - label: "API Server", - submenu: [ - { - label: api.isListening() ? 'Disable Server' : 'Enable Server', - click: (async () => { - await api.toggleServer(); - Menu.setApplicationMenu(getMenu()); - }) - }, - { - label: api.isAutostart() ? 'Disable Autostart' : 'Enable Autostart', - click: (async () => { - api.toggleAutostart(); - Menu.setApplicationMenu(getMenu()); - }) - }, - { - label: 'Copy Auth Token', - click: (async () => { - const token = api.getAuthenticationToken(); - log.log('Wrote authentication token to clipboard'); - clipboard.writeText(token); - }) - }, - ] - }, - { - label: "Debug", - submenu: [ - { - label: "Activate", - click: () => win.webContents.openDevTools(), - }, - ], - }, -]); - -const reloadAndKill = (win, killScripts = true) => { - log.info('Reloading & Killing all scripts...'); - setStopProcessHandler(app, win, false); - if (win.achievementsIntervalID) clearInterval(win.achievementsIntervalID); - win.webContents.forcefullyCrashRenderer(); - win.on('closed', () => { - // Wait for window to be closed before opening the new one to prevent race conditions - log.debug('Opening new window'); - const newWindow = createWindow(killScripts); - api.initialize(newWindow, () => Menu.setApplicationMenu(getMenu(win))); - setStopProcessHandler(app, newWindow, true); - }) - win.close(); -}; - - -function createWindow(killall) { - win = new BrowserWindow({ - show: false, - backgroundThrottling: false, - backgroundColor: "#000000", - }); - - win.removeMenu(); - win.maximize(); - noScripts = killall ? { query: { noScripts: killall } } : {}; - win.loadFile("index.html", noScripts); - win.show(); - if (debug) win.webContents.openDevTools(); - - win.webContents.on("new-window", function (e, url) { - // make sure local urls stay in electron perimeter - if (url.substr(0, "file://".length) === "file://") { - return; - } - - // and open every other protocols on the browser - e.preventDefault(); - shell.openExternal(url); - }); - win.webContents.backgroundThrottling = false; - - // This is backward but the game fills in an array called `document.achievements` and we retrieve it from - // here. Hey if it works it works. - const achievements = greenworks.getAchievementNames(); - const intervalID = setInterval(async () => { - try { - const achs = await win.webContents.executeJavaScript("document.achievements"); - for (const ach of achs) { - if (!achievements.includes(ach)) continue; - greenworks.activateAchievement(ach, () => undefined); - } - } catch (error) { - // The interval properly did not properly get cleared after a window kill - log.warn('Clearing achievements timer'); - clearInterval(intervalID); - return; - } - }, 1000); - win.achievementsIntervalID = intervalID; - - - const promptForReload = () => { - win.off('unresponsive', promptForReload); - dialog.showMessageBox({ - type: 'error', - title: 'Bitburner > Application Unresponsive', - message: 'The application is unresponsive, possibly due to an infinite loop in your scripts.', - detail:' Did you forget a ns.sleep(x)?\n\n' + - 'The application will be restarted for you, do you want to kill all running scripts?', - buttons: ['Restart', 'Cancel'], - defaultId: 0, - checkboxLabel: 'Kill all running scripts', - checkboxChecked: true, - noLink: true, - }).then(({response, checkboxChecked}) => { - if (response === 0) { - reloadAndKill(win, checkboxChecked); - } else { - win.on('unresponsive', promptForReload) - } - }); - } - win.on('unresponsive', promptForReload); - - // // Create the Application's main menu - // Menu.setApplicationMenu(getMenu()); - - return win; -} - function setStopProcessHandler(app, window, enabled) { const closingWindowHandler = async (e) => { // We need to prevent the default closing event to add custom logic e.preventDefault(); // First we clear the achievement timer - if (window.achievementsIntervalID) { - clearInterval(window.achievementsIntervalID); - } + achievements.disableAchievementsInterval(window); + // Shutdown the http server api.disable(); // We'll try to execute javascript on the page to see if we're stuck let canRunJS = false; - win.webContents.executeJavaScript('window.stop(); document.close()', true) + window.webContents.executeJavaScript('window.stop(); document.close()', true) .then(() => canRunJS = true); setTimeout(() => { // Wait a few milliseconds to prevent a race condition before loading the exit screen - win.webContents.stop(); - win.loadFile("exit.html") + window.webContents.stop(); + window.loadFile("exit.html") }, 20); // Wait 200ms, if the promise has not yet resolved, let's crash the process since we're possibly in a stuck scenario @@ -223,11 +48,11 @@ function setStopProcessHandler(app, window, enabled) { if (!canRunJS) { // We're stuck, let's crash the process log.warn('Forcefully crashing the renderer process'); - win.webContents.forcefullyCrashRenderer(); + gameWindow.webContents.forcefullyCrashRenderer(); } log.debug('Destroying the window'); - win.destroy(); + window.destroy(); }, 200); } @@ -255,9 +80,14 @@ function setStopProcessHandler(app, window, enabled) { } } +function startWindow(noScript) { + gameWindow.createWindow(noScript); +} + +utils.initialize(setStopProcessHandler, startWindow); + app.whenReady().then(async () => { log.info('Application is ready!'); - const win = createWindow(process.argv.includes("--no-scripts")); - await api.initialize(win, () => Menu.setApplicationMenu(getMenu(win))); - setStopProcessHandler(app, win, true); + utils.initialize(setStopProcessHandler, startWindow); + startWindow(process.argv.includes("--no-scripts")) }); diff --git a/electron/menu.js b/electron/menu.js new file mode 100644 index 000000000..9b73b984c --- /dev/null +++ b/electron/menu.js @@ -0,0 +1,101 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { Menu, clipboard } = require("electron"); +const log = require("electron-log"); +const api = require("./api-server"); +const utils = require("./utils"); + +function getMenu(window) { + return Menu.buildFromTemplate([ + { + label: "Edit", + submenu: [ + { label: "Undo", accelerator: "CmdOrCtrl+Z", selector: "undo:" }, + { label: "Redo", accelerator: "Shift+CmdOrCtrl+Z", selector: "redo:" }, + { type: "separator" }, + { label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" }, + { label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" }, + { label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" }, + { label: "Select All", accelerator: "CmdOrCtrl+A", selector: "selectAll:" }, + ], + }, + { + label: "Reloads", + submenu: [ + { + label: "Reload", + accelerator: "f5", + click: () => window.loadFile("index.html"), + }, + { + label: "Reload & Kill All Scripts", + click: () => utils.reloadAndKill(window, true), + }, + ], + }, + { + label: "Fullscreen", + submenu: [ + { + label: "Toggle", + accelerator: "f9", + click: (() => { + let full = false; + return () => { + full = !full; + window.setFullScreen(full); + }; + })(), + }, + ], + }, + { + label: "API Server", + submenu: [ + { + label: api.isListening() ? 'Disable Server' : 'Enable Server', + click: (async () => { + try { + await api.toggleServer(); + } catch (error) { + log.error(error); + utils.showErrorBox('Error Toggling Server', error); + } + refreshMenu(window); + }) + }, + { + label: api.isAutostart() ? 'Disable Autostart' : 'Enable Autostart', + click: (async () => { + api.toggleAutostart(); + refreshMenu(window); + }) + }, + { + label: 'Copy Auth Token', + click: (async () => { + const token = api.getAuthenticationToken(); + log.log('Wrote authentication token to clipboard'); + clipboard.writeText(token); + }) + }, + ] + }, + { + label: "Debug", + submenu: [ + { + label: "Activate", + click: () => window.webContents.openDevTools(), + }, + ], + }, + ]); +} + +function refreshMenu(window) { + Menu.setApplicationMenu(getMenu(window)); +} + +module.exports = { + getMenu, refreshMenu, +} diff --git a/electron/utils.js b/electron/utils.js new file mode 100644 index 000000000..4821360f2 --- /dev/null +++ b/electron/utils.js @@ -0,0 +1,78 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { app, dialog } = require("electron"); +const log = require("electron-log"); + +const achievements = require("./achievements"); +const api = require("./api-server"); + +let setStopProcessHandler = () => { + // Will be overwritten by the initialize function called in main +} +let createWindowHandler = () => { + // Will be overwritten by the initialize function called in main +} + +function initialize(stopHandler, createHandler) { + setStopProcessHandler = stopHandler; + createWindowHandler = createHandler +} + +function reloadAndKill(window, killScripts) { + log.info('Reloading & Killing all scripts...'); + setStopProcessHandler(app, window, false); + + achievements.disableAchievementsInterval(window); + api.disable(); + + window.webContents.forcefullyCrashRenderer(); + window.on('closed', () => { + // Wait for window to be closed before opening the new one to prevent race conditions + log.debug('Opening new window'); + createWindowHandler(killScripts); + }) + + window.close(); +} + +function promptForReload(window) { + detachUnresponsiveAppHandler(window); + dialog.showMessageBox({ + type: 'error', + title: 'Bitburner > Application Unresponsive', + message: 'The application is unresponsive, possibly due to an infinite loop in your scripts.', + detail:' Did you forget a ns.sleep(x)?\n\n' + + 'The application will be restarted for you, do you want to kill all running scripts?', + buttons: ['Restart', 'Cancel'], + defaultId: 0, + checkboxLabel: 'Kill all running scripts', + checkboxChecked: true, + noLink: true, + }).then(({response, checkboxChecked}) => { + if (response === 0) { + reloadAndKill(window, checkboxChecked); + } else { + attachUnresponsiveAppHandler(window); + } + }); +} + +function attachUnresponsiveAppHandler(window) { + window.on('unresponsive', () => promptForReload(window)); +} + +function detachUnresponsiveAppHandler(window) { + window.off('unresponsive', () => promptForReload(window)); +} + +function showErrorBox(title, error) { + dialog.showErrorBox( + title, + `${error.name}\n\n${error.message}` + ); +} + +module.exports = { + initialize, setStopProcessHandler, reloadAndKill, showErrorBox, + attachUnresponsiveAppHandler, detachUnresponsiveAppHandler, +} +