diff --git a/manager/ejs/connections.ejs b/manager/ejs/connections.ejs index 4c1dc3a1..80fe1869 100644 --- a/manager/ejs/connections.ejs +++ b/manager/ejs/connections.ejs @@ -86,7 +86,7 @@ <% if (!simplified) { %>
<% } %> diff --git a/manager/ejs/passwords.ejs b/manager/ejs/passwords.ejs index 073bf0f8..06a3ba97 100644 --- a/manager/ejs/passwords.ejs +++ b/manager/ejs/passwords.ejs @@ -43,7 +43,7 @@ <% if (!simplified) { %> <% } %> diff --git a/manager/javascripts/connections.js b/manager/javascripts/connections.js index 296d7b2b..1e36bfaa 100644 --- a/manager/javascripts/connections.js +++ b/manager/javascripts/connections.js @@ -1,6 +1,9 @@ const ManagerPage = require("./managerpage"); const ejs = require('ejs') +/** Connections page, allows the user to set the ports and address for each Olympus instance + * + */ class ConnectionsPage extends ManagerPage { onBackClicked; onNextClicked; @@ -35,6 +38,8 @@ class ConnectionsPage extends ManagerPage { ejs.renderFile("./ejs/connections.ejs", this.options, {}, (err, str) => { if (!err) { this.render(str); + + /* Call the port setters to check if the ports are free */ this.setClientPort(this.instance.clientPort); this.setBackendPort(this.instance.backendPort); } else { @@ -45,6 +50,9 @@ class ConnectionsPage extends ManagerPage { super.show(); } + /** Asynchronously check if the client port is free and if it is, set the new value + * + */ async setClientPort(newPort) { const success = await this.instance.setClientPort(newPort); var successEls = this.element.querySelector(".client-port").querySelectorAll(".success"); @@ -57,6 +65,9 @@ class ConnectionsPage extends ManagerPage { } } + /** Asynchronously check if the backend port is free and if it is, set the new value + * + */ async setBackendPort(newPort) { const success = await this.instance.setBackendPort(newPort); var successEls = this.element.querySelector(".backend-port").querySelectorAll(".success"); diff --git a/manager/javascripts/dcsinstance.js b/manager/javascripts/dcsinstance.js index 2d23b207..b65d5405 100644 --- a/manager/javascripts/dcsinstance.js +++ b/manager/javascripts/dcsinstance.js @@ -8,12 +8,15 @@ const { checkPort, fetchWithTimeout } = require('./net') const dircompare = require('dir-compare'); const { spawn } = require('child_process'); const find = require('find-process'); -const { deleteMod, uninstallInstance } = require('./filesystem') +const { uninstallInstance } = require('./filesystem') const { showErrorPopup, showConfirmPopup } = require('./popup') class DCSInstance { static instances = null; + /** Static asynchronous method to retrieve all DCS instances. Only runs at startup + * + */ static async getInstances() { if (this.instances === null) { this.instances = await this.findInstances(); @@ -21,18 +24,25 @@ class DCSInstance { return this.instances; } + /** Static asynchronous method to find all existing DCS instances + * + */ static async findInstances() { let promise = new Promise((res, rej) => { + /* Get the Saved Games folder from the registry */ regedit.list(shellFoldersKey, function (err, result) { if (err) { rej(err); } else { + /* Check that the registry read was successfull */ if (result[shellFoldersKey] !== undefined && result[shellFoldersKey]["exists"] && result[shellFoldersKey]['values'][saveGamesKey] !== undefined && result[shellFoldersKey]['values'][saveGamesKey]['value'] !== undefined) { + /* Read all the folders in Saved Games */ const searchpath = result[shellFoldersKey]['values'][saveGamesKey]['value']; const folders = fs.readdirSync(searchpath); var instances = []; + /* A DCS Instance is created if either the appsettings.lua or serversettings.lua file is detected */ folders.forEach((folder) => { if (fs.existsSync(path.join(searchpath, folder, "Config", "appsettings.lua")) || fs.existsSync(path.join(searchpath, folder, "Config", "serversettings.lua"))) { @@ -73,8 +83,10 @@ class DCSInstance { this.folder = folder; this.name = path.basename(folder); + /* Check if the olympus.json file is detected. If true, Olympus is considered to be installed */ if (fs.existsSync(path.join(folder, "Config", "olympus.json"))) { try { + /* Read the olympus.json */ var config = JSON.parse(fs.readFileSync(path.join(folder, "Config", "olympus.json"))); this.clientPort = config["client"]["port"]; this.backendPort = config["server"]["port"]; @@ -84,6 +96,8 @@ class DCSInstance { console.error(err) } + /* Compare the contents of the installed Olympus instance and the one in the root folder. Exclude the databases folder, which users can edit. + If there is any difference, the instance is flagged as either corrupted or outdated */ this.installed = true; const options = { compareContent: true, @@ -107,6 +121,7 @@ class DCSInstance { } } + /* Periodically "ping" Olympus to check if either the client or the backend are active */ window.setInterval(async () => { await this.getData(); @@ -136,6 +151,9 @@ class DCSInstance { }, 1000); } + /** Asynchronously check if the client port is free and if it is, set the new value + * + */ async setClientPort(newPort) { if (await this.checkClientPort(newPort)) { console.log(`Instance ${this.folder} client port set to ${newPort}`) @@ -145,6 +163,9 @@ class DCSInstance { return false; } + /** Asynchronously check if the client port is free and if it is, set the new value + * + */ async setBackendPort(newPort) { if (await this.checkBackendPort(newPort)) { console.log(`Instance ${this.folder} client port set to ${newPort}`) @@ -154,22 +175,37 @@ class DCSInstance { return false; } + /** Set backend address + * + */ setBackendAddress(newAddress) { this.backendAddress = newAddress; } + /** Set Game Master password + * + */ setGameMasterPassword(newPassword) { this.gameMasterPassword = newPassword; } + /** Set Blue Commander password + * + */ setBlueCommanderPassword(newPassword) { this.blueCommanderPassword = newPassword; } + /** Set Red Commander password + * + */ setRedCommanderPassword(newPassword) { this.redCommanderPassword = newPassword; } + /** Check if the client port is free + * + */ async checkClientPort(port) { var promise = new Promise((res, rej) => { checkPort(port, async (portFree) => { @@ -198,6 +234,9 @@ class DCSInstance { return promise; } + /** Check if the backend port is free + * + */ async checkBackendPort(port) { var promise = new Promise((res, rej) => { checkPort(port, async (portFree) => { @@ -225,6 +264,9 @@ class DCSInstance { return promise; } + /** Asynchronously interrogate the webserver and the backend to check if they are active and to retrieve data. + * + */ async getData() { if (this.installed && !this.error) { fetchWithTimeout(`http://localhost:${this.clientPort}`, { timeout: 250 }) @@ -261,6 +303,9 @@ class DCSInstance { } } + /** Start the Olympus server associated with this instance + * + */ startServer() { console.log(`Starting server for instance at ${this.folder}`) const out = fs.openSync(`./${this.name}.log`, 'a'); @@ -274,6 +319,9 @@ class DCSInstance { sub.unref(); } + /** Start the Olympus client associated with this instance + * + */ startClient() { console.log(`Starting client for instance at ${this.folder}`) const out = fs.openSync(`./${this.name}.log`, 'a'); @@ -287,6 +335,7 @@ class DCSInstance { sub.unref(); } + /* Stop any node process running on the server port. This will stop either the server or the client depending on what is running */ stop() { find('port', this.clientPort) .then((list) => { @@ -312,6 +361,7 @@ class DCSInstance { }) } + /* Uninstall this instance */ uninstall() { showConfirmPopup("Are you sure you want to completely remove this Olympus installation?", () => uninstallInstance(this.folder, this.name).then( diff --git a/manager/javascripts/filesystem.js b/manager/javascripts/filesystem.js index 45555d9c..ae108e44 100644 --- a/manager/javascripts/filesystem.js +++ b/manager/javascripts/filesystem.js @@ -5,6 +5,9 @@ const path = require('path'); const { showWaitPopup } = require('./popup'); const homeDir = require('os').homedir(); +/** Conveniency function to asynchronously delete a single file, with error catching + * + */ async function deleteFile(filePath) { console.log(`Deleting ${filePath}`); var promise = new Promise((res, rej) => { @@ -27,6 +30,9 @@ async function deleteFile(filePath) { return promise; } +/** Given a list of Olympus instances, it fixes/updates them by deleting the existing installation and the copying over the clean files + * + */ async function fixInstances(instances) { var promise = new Promise((res, rej) => { var instancePromises = instances.map((instance) => { @@ -46,6 +52,9 @@ async function fixInstances(instances) { return promise; } +/** Uninstalls a specific instance given its folder + * + */ async function uninstallInstance(folder, name) { console.log(`Uninstalling Olympus from ${folder}`) showWaitPopup("Please wait while the Olympus installation is being uninstalled.") @@ -59,6 +68,9 @@ async function uninstallInstance(folder, name) { return promise; } +/** Installs the Hooks script + * + */ async function installHooks(folder) { console.log(`Installing hooks in ${folder}`) var promise = new Promise((res, rej) => { @@ -76,6 +88,10 @@ async function installHooks(folder) { return promise; } +/** Installs the Mod folder + * + */ +// TODO: add option to not copy over databases async function installMod(folder) { console.log(`Installing mod in ${folder}`) var promise = new Promise((res, rej) => { @@ -93,6 +109,9 @@ async function installMod(folder) { return promise; } +/** Installs the olympus.json file + * + */ async function installJSON(folder) { console.log(`Installing config in ${folder}`) var promise = new Promise((res, rej) => { @@ -110,38 +129,9 @@ async function installJSON(folder) { return promise; } -async function applyConfiguration(folder, instance) { - console.log(`Applying configuration to Olympus in ${folder}`); - var promise = new Promise((res, rej) => { - if (fs.existsSync(path.join(folder, "Config", "olympus.json"))) { - var config = JSON.parse(fs.readFileSync(path.join(folder, "Config", "olympus.json"))); - - config["client"]["port"] = instance.clientPort; - config["server"]["port"] = instance.backendPort; - config["server"]["address"] = instance.backendAddress; - config["authentication"]["gameMasterPassword"] = sha256(instance.gameMasterPassword); - config["authentication"]["blueCommanderPassword"] = sha256(instance.blueCommanderPassword); - config["authentication"]["redCommanderPassword"] = sha256(instance.redCommanderPassword); - - fs.writeFile(path.join(folder, "Config", "olympus.json"), JSON.stringify(config, null, 4), (err) => { - if (err) { - console.log(`Error applying config in ${folder}: ${err}`) - rej(err); - } - else { - console.log(`Config succesfully applied in ${folder}`) - res(true); - } - }); - - } else { - rej("File does not exist") - } - res(true); - }); - return promise; -} - +/** Creates shortcuts both in the DCS Saved Games folder and on the desktop + * + */ async function installShortCuts(folder, name) { console.log(`Installing shortcuts for Olympus in ${folder}`); var promise = new Promise((res, rej) => { @@ -196,11 +186,52 @@ async function installShortCuts(folder, name) { return promise; } +/** Writes the configuration of an instance to the olympus.json file + * + */ +async function applyConfiguration(folder, instance) { + console.log(`Applying configuration to Olympus in ${folder}`); + var promise = new Promise((res, rej) => { + if (fs.existsSync(path.join(folder, "Config", "olympus.json"))) { + var config = JSON.parse(fs.readFileSync(path.join(folder, "Config", "olympus.json"))); + + config["client"]["port"] = instance.clientPort; + config["server"]["port"] = instance.backendPort; + config["server"]["address"] = instance.backendAddress; + config["authentication"]["gameMasterPassword"] = sha256(instance.gameMasterPassword); + config["authentication"]["blueCommanderPassword"] = sha256(instance.blueCommanderPassword); + config["authentication"]["redCommanderPassword"] = sha256(instance.redCommanderPassword); + + fs.writeFile(path.join(folder, "Config", "olympus.json"), JSON.stringify(config, null, 4), (err) => { + if (err) { + console.log(`Error applying config in ${folder}: ${err}`) + rej(err); + } + else { + console.log(`Config succesfully applied in ${folder}`) + res(true); + } + }); + + } else { + rej("File does not exist") + } + res(true); + }); + return promise; +} + +/** Deletes the Hooks script + * + */ async function deleteHooks(folder) { console.log(`Deleting hooks from ${folder}`); return deleteFile(path.join(folder, "Scripts", "Hooks", "OlympusHook.lua")); } +/** Deletes the Mod folder + * + */ async function deleteMod(folder) { console.log(`Deleting mod from ${folder}`); var promise = new Promise((res, rej) => { @@ -222,11 +253,17 @@ async function deleteMod(folder) { return promise; } +/** Deletes the olympus.json configuration file + * + */ async function deleteJSON(folder) { console.log(`Deleting JSON from ${folder}`); return deleteFile(path.join(folder, "Config", "olympus.json")); } +/** Deletes the shortcuts + * + */ async function deleteShortCuts(folder, name) { console.log(`Deleting ShortCuts from ${folder}`); var promise = new Promise((res, rej) => { diff --git a/manager/javascripts/manager.js b/manager/javascripts/manager.js index b130af89..567506f7 100644 --- a/manager/javascripts/manager.js +++ b/manager/javascripts/manager.js @@ -17,26 +17,31 @@ class Manager { } async start() { + /* Get the list of DCS instances */ var instances = await DCSInstance.getInstances(); + /* If there is only 1 DCS Instance and Olympus is not installed in it, go straight to the installation page (since there is nothing else to do) */ this.simplified = instances.length === 1 && !instances[0].installed; document.getElementById("loader").classList.add("hide"); + /* Check if there are corrupted or outdate instances */ if (instances.some((instance) => { return instance.installed && instance.error; })) { + /* Ask the user for confirmation */ showErrorPopup("One or more Olympus instances are corrupted or need updating. Press Close to fix this.", async () => { showWaitPopup("Please wait while your instances are being fixed.") fixInstances(instances.filter((instance) => { return instance.installed && instance.error; })).then( () => { location.reload() }, - () => { showErrorPopup("An error occurred while trying to fix you installations. Please reinstall Olympus manually"); } + () => { showErrorPopup("An error occurred while trying to fix your installations. Please reinstall Olympus manually."); } ) }) } + /* Check which buttons should be enabled */ const installEnabled = instances.some((instance) => { return !instance.installed; }); const updateEnabled = instances.some((instance) => { return instance.installed; }); const manageEnabled = instances.some((instance) => { return instance.installed; }); @@ -49,10 +54,12 @@ class Manager { updateEnabled: updateEnabled, manageEnabled: manageEnabled } + /* When the install button is clicked go the installation page */ menuPage.onInstallClicked = (e) => { menuPage.hide(); installationsPage.show(); } + /* When the update button is clicked go to the instances page in "update mode" (i.e. manage = false) */ menuPage.onUpdateClicked = (e) => { menuPage.hide(); instancesPage.options = { @@ -61,6 +68,7 @@ class Manager { } instancesPage.show(); } + /* When the manage button is clicked go to the instances page in "manage mode" (i.e. manage = true) */ menuPage.onManageClicked = (e) => { menuPage.hide(); instancesPage.options = { @@ -77,33 +85,37 @@ class Manager { instances: instances } installationsPage.setSelectedInstance = (activeInstance) => { - connectionsPage.options = { - ...connectionsPage.options, + /* Set the active options for the pages */ + const options = { instance: activeInstance, simplified: this.simplified, install: true } + connectionsPage.options = { + ...connectionsPage.options, + ...options + } passwordsPage.options = { ...passwordsPage.options, - instance: activeInstance, - simplified: this.simplified, - install: true + ...options } resultPage.options = { ...resultPage.options, - instance: activeInstance, - simplified: this.simplified, - install: true + ...options } + + /* Show the connections page */ installationsPage.hide(); connectionsPage.show(); connectionsPage.onBackClicked = (e) => { + /* Show the installation page */ connectionsPage.hide(); installationsPage.show(); } } installationsPage.onCancelClicked = (e) => { + /* Go back to the main menu */ installationsPage.hide(); menuPage.show(); } @@ -115,33 +127,37 @@ class Manager { instances: instances.filter((instance) => { return instance.installed; }) } instancesPage.setSelectedInstance = (activeInstance) => { - connectionsPage.options = { - ...connectionsPage.options, + /* Set the active options for the pages */ + const options = { instance: activeInstance, simplified: this.simplified, install: false } + connectionsPage.options = { + ...connectionsPage.options, + ...options + } passwordsPage.options = { ...passwordsPage.options, - instance: activeInstance, - simplified: this.simplified, - install: false + ...options } resultPage.options = { ...resultPage.options, - instance: activeInstance, - simplified: this.simplified, - install: false + ...options } + + /* Show the connections page */ instancesPage.hide(); connectionsPage.show(); connectionsPage.onBackClicked = (e) => { + /* Show the instances page */ connectionsPage.hide(); instancesPage.show(); } } instancesPage.onCancelClicked = (e) => { + /* Go back to the main menu */ instancesPage.hide(); menuPage.show(); } @@ -151,6 +167,7 @@ class Manager { connectionsPage.onNextClicked = async (e) => { let activeInstance = connectionsPage.options.instance; if (activeInstance) { + /* Check that the selected ports are free before proceeding */ if (await activeInstance.checkClientPort(activeInstance.clientPort) && await activeInstance.checkBackendPort(activeInstance.backendPort)) { connectionsPage.hide(); passwordsPage.show(); @@ -161,8 +178,8 @@ class Manager { showErrorPopup("An error has occurred, please restart the Olympus Manager.") } } - connectionsPage.onCancelClicked = (e) => { + /* Go back to the main menu */ connectionsPage.hide(); menuPage.show(); } @@ -170,6 +187,7 @@ class Manager { /* Passwords */ var passwordsPage = new PasswordsPage(); passwordsPage.onBackClicked = (e) => { + /* Go back to the connections page */ let activeInstance = connectionsPage.options.instance; if (activeInstance) { passwordsPage.hide(); @@ -181,6 +199,7 @@ class Manager { passwordsPage.onNextClicked = (e) => { let activeInstance = connectionsPage.options.instance; if (activeInstance) { + /* Check that all the passwords have been set */ if (activeInstance.gameMasterPassword === "" || activeInstance.blueCommanderPassword === "" || activeInstance.redCommanderPassword === "") { showErrorPopup("Please fill all the password inputs.") } @@ -197,6 +216,7 @@ class Manager { } passwordsPage.onCancelClicked = (e) => { + /* Go back to the main menu */ passwordsPage.hide(); menuPage.show(); } @@ -204,14 +224,17 @@ class Manager { /* Result */ var resultPage = new ResultPage(); resultPage.onBackClicked = (e) => { + /* Reload the page to apply changes */ resultPage.hide(); location.reload(); } resultPage.onCancelClicked = (e) => { + /* Reload the page to apply changes */ resultPage.hide(); location.reload(); } + /* Create all the HTML pages */ document.body.appendChild(menuPage.getElement()); document.body.appendChild(installationsPage.getElement()); document.body.appendChild(instancesPage.getElement()); @@ -219,28 +242,30 @@ class Manager { document.body.appendChild(passwordsPage.getElement()); document.body.appendChild(resultPage.getElement()); + /* In simplified mode we directly show the connections page */ if (this.simplified) { - connectionsPage.options = { - ...connectionsPage.options, + const options = { instance: instances[0], simplified: this.simplified, install: true } + connectionsPage.options = { + ...connectionsPage.options, + ...options + } passwordsPage.options = { ...passwordsPage.options, - instance: instances[0], - simplified: this.simplified, - install: true + ...options } resultPage.options = { ...resultPage.options, - instance: instances[0], - simplified: this.simplified, - install: true + ...options } + /* Show the connections page directly */ instancesPage.hide(); connectionsPage.show(); } else { + /* Show the main menu */ menuPage.show(); } } diff --git a/manager/javascripts/net.js b/manager/javascripts/net.js index 63116248..8248e2c9 100644 --- a/manager/javascripts/net.js +++ b/manager/javascripts/net.js @@ -1,5 +1,8 @@ const portfinder = require('portfinder') +/** Checks if a port is already in use + * + */ function checkPort(port, callback) { portfinder.getPort({ port: port, stopPort: port }, (err, res) => { if (err !== null) { @@ -11,6 +14,9 @@ function checkPort(port, callback) { }); } +/** Performs a fetch request, with a configurable timeout + * + */ async function fetchWithTimeout(resource, options = {}) { const { timeout = 8000 } = options; diff --git a/manager/javascripts/popup.js b/manager/javascripts/popup.js index 143adbd4..88c6e2b0 100644 --- a/manager/javascripts/popup.js +++ b/manager/javascripts/popup.js @@ -1,3 +1,5 @@ +// TODO: we can probably refactor this to be a bit cleaner + function showErrorPopup(message, onCloseCallback) { document.getElementById("grayout").classList.remove("hide"); document.getElementById("popup").classList.remove("hide"); diff --git a/manager/javascripts/preload.js b/manager/javascripts/preload.js index 96636d7d..baa2b9e6 100644 --- a/manager/javascripts/preload.js +++ b/manager/javascripts/preload.js @@ -13,6 +13,7 @@ const { Octokit } = require('octokit'); const VERSION = "{{OLYMPUS_VERSION_NUMBER}}"; function checkVersion() { + /* Check if we are running the latest version */ const request = new Request("https://raw.githubusercontent.com/Pax1601/DCSOlympus/main/version.json"); fetch(request).then((response) => { @@ -22,12 +23,15 @@ function checkVersion() { throw new Error("Error connecting to Github to retrieve latest version"); } }).then((res) => { + /* If we are running a development version of the script (not from a compiled package), skip version checking */ if (VERSION.includes("OLYMPUS_VERSION_NUMBER")) { console.log("Development build detected, skipping version checks...") } else { + /* Check if there is a newer version available */ var reg1 = res["version"].match(/\d+/g).map((str) => { return Number(str) }); var reg2 = VERSION.match(/\d+/g).map((str) => { return Number(str) }); + /* If a newer version is available update Olympus in Release mode */ if (reg1[0] > reg2[0] || (reg1[0] == reg2[0] && reg1[1] > reg2[1]) || (reg1[0] == reg2[0] && reg1[1] == reg2[1] && reg1[2] > reg2[2])) { console.log(`New version available: ${res["version"]}`); showConfirmPopup(`You are currently running DCS Olympus ${VERSION}, but ${res["version"]} is available. Do you want to update DCS Olympus automatically?