feat: added nightly link to manager to update without github account

added ability to force beta version
This commit is contained in:
Davide Passoni
2025-03-27 13:27:50 +01:00
parent c9b143b5e0
commit 55f75ff149
2 changed files with 251 additions and 223 deletions

View File

@@ -20,7 +20,8 @@ class Manager {
IP: undefined, IP: undefined,
logLocation: path.join(__dirname, "..", "manager.log"), logLocation: path.join(__dirname, "..", "manager.log"),
mode: 'basic', mode: 'basic',
state: 'IDLE' state: 'IDLE',
forceBeta: false
}; };
/* Manager pages */ /* Manager pages */

View File

@@ -1,303 +1,330 @@
const contextBridge = require('electron').contextBridge; const contextBridge = require("electron").contextBridge;
const ipcRenderer = require('electron').ipcRenderer; const ipcRenderer = require("electron").ipcRenderer;
const { exec, spawn } = require("child_process"); const { exec, spawn } = require("child_process");
const { showConfirmPopup, showWaitPopup, showErrorPopup } = require('./popup'); const { showConfirmPopup, showWaitPopup, showErrorPopup } = require("./popup");
const path = require('path'); const path = require("path");
const os = require('os'); const os = require("os");
const https = require('follow-redirects').https; const https = require("follow-redirects").https;
const fs = require('fs'); const fs = require("fs");
const AdmZip = require("adm-zip"); const AdmZip = require("adm-zip");
const { Octokit } = require('octokit'); const { Octokit } = require("octokit");
const { logger } = require("./filesystem"); const { logger } = require("./filesystem");
const { getManager } = require('./managerfactory'); const { getManager } = require("./managerfactory");
const { sleep } = require('./utils'); const { sleep } = require("./utils");
const VERSION = "{{OLYMPUS_VERSION_NUMBER}}"; const VERSION = "{{OLYMPUS_VERSION_NUMBER}}";
logger.log(`Running in ${__dirname}`); logger.log(`Running in ${__dirname}`);
function checkVersion() { function checkVersion(forceBeta) {
/* Check if we are running the latest version */ /* Check if we are running the latest version */
const request = new Request("https://raw.githubusercontent.com/Pax1601/DCSOlympus/main/version.json"); const request = new Request("https://raw.githubusercontent.com/Pax1601/DCSOlympus/main/version.json");
fetch(request).then((response) => { fetch(request)
if (response.status === 200) { .then((response) => {
return response.json(); if (response.status === 200) {
} else { return response.json();
throw new Error("Error connecting to Github to retrieve latest version"); } else {
} 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")) { .then((res) => {
logger.log("Development build detected, skipping version checks...") /* If we are running a development version of the script (not from a compiled package), skip version checking */
} else { if (VERSION.includes("OLYMPUS_VERSION_NUMBER")) {
/* Check if there is a newer version available */ logger.log("Development build detected, skipping version checks...");
var reg1 = res["version"].match(/\d+/g).map((str) => { return Number(str) }); } else {
var reg2 = VERSION.match(/\d+/g).map((str) => { return Number(str) }); /* 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 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])) { if (
logger.log(`New version available: ${res["version"]}`); reg1[0] > reg2[0] ||
showConfirmPopup(`<div class='main-message'>You are currently running DCS Olympus ${VERSION}, but ${res["version"]} is available. </div><div class='sub-message'> Do you want to update DCS Olympus automatically? </div> <div style="max-width: 100%; color: orange">Note: DCS and Olympus MUST be stopped before proceeding.</div>`, (reg1[0] == reg2[0] && reg1[1] > reg2[1]) ||
async () => { (reg1[0] == reg2[0] && reg1[1] == reg2[1] && reg1[2] > reg2[2])
/* Nested popup calls need to wait for animation to complete */ ) {
await sleep(300); logger.log(`New version available: ${res["version"]}`);
showConfirmPopup(
`<div class='main-message'>You are currently running DCS Olympus ${VERSION}, but ${res["version"]} is available. </div><div class='sub-message'> Do you want to update DCS Olympus automatically? </div> <div style="max-width: 100%; color: orange">Note: DCS and Olympus MUST be stopped before proceeding.</div>`,
async () => {
/* Nested popup calls need to wait for animation to complete */
await sleep(300);
updateOlympusRelease(); updateOlympusRelease();
}, () => { },
logger.log("Update canceled"); () => {
}) logger.log("Update canceled");
} }
/* If the current version is newer than the latest release, the user is probably a developer. Ask for a beta update */ );
else if (reg2[0] > reg1[0] || (reg2[0] == reg1[0] && reg2[1] > reg1[1]) || (reg2[0] == reg1[0] && reg2[1] == reg1[1] && reg2[2] > reg1[2])) { } else if (
logger.log(`Beta version detected: ${res["version"]} vs ${VERSION}`); /* If the current version is newer than the latest release, the user is probably a developer. Ask for a beta update */
reg2[0] > reg1[0] ||
(reg2[0] == reg1[0] && reg2[1] > reg1[1]) ||
(reg2[0] == reg1[0] && reg2[1] == reg1[1] && reg2[2] > reg1[2])
) {
logger.log(`Beta version detected: ${res["version"]} vs ${VERSION}`);
updateOlympusBeta();
}
else {
logger.log("Running latest version...");
if (forceBeta) {
logger.log(`Beta version forced: ${res["version"]} vs ${VERSION}`);
updateOlympusBeta(); updateOlympusBeta();
} }
} }
})
}
});
} }
/** Update Olympus in "beta" mode. The user will be provided with the option to update from a build artifact /** Update Olympus in "beta" mode. The user will be provided with the option to update from a build artifact
* *
*/ */
async function updateOlympusBeta() { async function updateOlympusBeta() {
/* Get a list of build artifacts */ /* Get a list of build artifacts */
const octokit = new Octokit({}); const octokit = new Octokit({});
const res = await octokit.request('GET /repos/{owner}/{repo}/actions/artifacts', { const res = await octokit.request("GET /repos/{owner}/{repo}/actions/artifacts", {
owner: 'Pax1601', owner: "Pax1601",
repo: 'DCSOlympus', repo: "DCSOlympus",
headers: { headers: {
'X-GitHub-Api-Version': '2022-11-28' "X-GitHub-Api-Version": "2022-11-28",
} },
}); });
const artifacts = res.data.artifacts; const artifacts = res.data.artifacts;
/* Select the newest artifact */
var artifact = artifacts.find((artifact) => { return artifact.name === "development_build_not_a_release" });
const date1 = new Date(artifact.updated_at); /* Select the newest artifact */
const date2 = fs.statSync(".").mtime; var artifact = artifacts.find((artifact) => {
if (date1 > date2) { return artifact.name === "development_build_not_a_release";
showConfirmPopup(`<div class='main-message'> Looks like you are running a beta version of Olympus!</div><div class='sub-message'> Latest beta artifact timestamp of: <b style="color: orange">${date1.toLocaleString()}</b> <br> Your installation timestamp: <b style="color: orange">${date2.toLocaleString()}</b> <br><br> Do you want to update to the newest beta version?</div>`, async () => { });
/* Nested popup calls need to wait for animation to complete */
await sleep(300);
/* Run the browser and download the artifact */ //TODO: try and directly download the file from code rather than using the browser const date1 = new Date(artifact.updated_at);
exec(`start https://github.com/Pax1601/DCSOlympus/actions/runs/${artifact.workflow_run.id}/artifacts/${artifact.id}`) const date2 = fs.statSync(".").mtime;
showConfirmPopup(`<div class='main-message'> A browser window was opened to download the beta artifact. </div><div class='sub-message'> Please wait for the download to complete, then press "Accept" and select the downloaded beta artifact.</div>`, if (date1 > date2) {
() => { showConfirmPopup(
/* Ask the user to select the downloaded file */ `<div class='main-message'> Looks like you are running a beta version of Olympus!</div><div class='sub-message'> Latest beta artifact timestamp of: <b style="color: orange">${date1.toLocaleString()}</b> <br> Your installation timestamp: <b style="color: orange">${date2.toLocaleString()}</b> <br><br> Do you want to update to the newest beta version?</div>`,
var input = document.createElement('input'); async () => {
input.type = 'file'; /* Nested popup calls need to wait for animation to complete */
input.click(); await sleep(300);
input.onchange = e => {
/* Run the update process */ /* Run the update process */
updateOlympus(e.target.files[0]) updateOlympus(
} "https://nightly.link/Pax1601/DCSOlympus/workflows/build_package/release-candidate/development_build_not_a_release.zip"
}, );
() => { },
logger.log("Update canceled"); () => {
}); logger.log("Update canceled");
}, }
() => { );
logger.log("Update canceled"); } else {
} logger.log("Build is latest");
) }
} else {
logger.log("Build is latest")
}
} }
/** Update Olympus to the lastest release /** Update Olympus to the lastest release
* *
*/ */
async function updateOlympusRelease() { async function updateOlympusRelease() {
const octokit = new Octokit({}) const octokit = new Octokit({});
const res = await octokit.request('GET /repos/{owner}/{repo}/releases/latest', { const res = await octokit.request("GET /repos/{owner}/{repo}/releases/latest", {
owner: 'Pax1601', owner: "Pax1601",
repo: 'DCSOlympus', repo: "DCSOlympus",
headers: { headers: {
'X-GitHub-Api-Version': '2022-11-28' "X-GitHub-Api-Version": "2022-11-28",
} },
}) });
/* Select the newest artifact */ /* Select the newest artifact */
var asset = res.data.assets.find((asset) => { return asset.name.includes("manager") }); var asset = res.data.assets.find((asset) => {
return asset.name.includes("manager");
});
/* Run the update process */ /* Run the update process */
updateOlympus(asset.browser_download_url) updateOlympus(asset.browser_download_url);
} }
function updateOlympus(location) { function updateOlympus(location) {
showWaitPopup("<div class='main-message'>Please wait while Olympus is being updated. </div><div class='sub-message'> The Manager will be closed and reopened automatically when updating is completed.</div>") showWaitPopup(
"<div class='main-message'>Please wait while Olympus is being updated. </div><div class='sub-message'> The Manager will be closed and reopened automatically when updating is completed.</div>"
);
/* If the location is a string, it is interpreted as a download url. Else, it is interpreted as a File (on disk)*/
if (typeof location === "string") {
logger.log(`Updating Olympus with package from ${location}`);
} else {
logger.log(`Updating Olympus with package from ${location.path}`);
}
let tmpDir;
const appPrefix = "dcs-olympus-";
try {
/* Create a temporary folder */
const folder = path.join(os.tmpdir(), appPrefix);
tmpDir = fs.mkdtempSync(folder);
/* If the location is a string, it is interpreted as a download url. Else, it is interpreted as a File (on disk)*/
if (typeof location === "string") { if (typeof location === "string") {
logger.log(`Updating Olympus with package from ${location}`) /* Download the file */
} else { const file = fs.createWriteStream(path.join(tmpDir, "temp.zip"));
logger.log(`Updating Olympus with package from ${location.path}`) logger.log(`Downloading update package in ${path.join(tmpDir, "temp.zip")}`);
} const request = https.get(location, (response) => {
if (response.statusCode === 200) {
response.pipe(file);
let tmpDir; /* Either on success or on error close the file stream */
const appPrefix = 'dcs-olympus-'; file.on("finish", () => {
try { file.close();
/* Create a temporary folder */ logger.log("Download completed");
const folder = path.join(os.tmpdir(), appPrefix);
tmpDir = fs.mkdtempSync(folder);
if (typeof location === "string") {
/* Download the file */
const file = fs.createWriteStream(path.join(tmpDir, "temp.zip"));
logger.log(`Downloading update package in ${path.join(tmpDir, "temp.zip")}`)
const request = https.get(location, (response) => {
if (response.statusCode === 200) {
response.pipe(file);
/* Either on success or on error close the file stream */
file.on("finish", () => {
file.close();
logger.log("Download completed");
/* Extract and copy the files */
extractAndCopy(tmpDir);
});
file.on("error", (err) => {
file.close();
logger.error(err);
throw Error(err);
})
} else {
failUpdate();
throw Error("Failed to download resource.")
}
});
} else {
/* Copy the archive to the temporary folder */
fs.copyFileSync(location.path, path.join(tmpDir, "temp.zip"));
/* Extract and copy the files */ /* Extract and copy the files */
extractAndCopy(tmpDir); extractAndCopy(tmpDir);
});
file.on("error", (err) => {
file.close();
logger.error(err);
throw Error(err);
});
} else {
failUpdate();
throw Error("Failed to download resource.");
} }
});
} else {
/* Copy the archive to the temporary folder */
fs.copyFileSync(location.path, path.join(tmpDir, "temp.zip"));
/* Extract and copy the files */
extractAndCopy(tmpDir);
} }
catch (err) { } catch (err) {
/* Show the failed update message */ /* Show the failed update message */
failUpdate(); failUpdate();
logger.error(err) logger.error(err);
} }
} }
/** Extract the contents of the zip and update Olympus by deleting all files from the current installation and copying the new files over. /** Extract the contents of the zip and update Olympus by deleting all files from the current installation and copying the new files over.
* *
*/ */
function extractAndCopy(folder) { function extractAndCopy(folder) {
/* Extract all the files */ /* Extract all the files */
const zip = new AdmZip(path.join(folder, "temp.zip")); const zip = new AdmZip(path.join(folder, "temp.zip"));
zip.extractAllTo(path.join(folder, "temp")); zip.extractAllTo(path.join(folder, "temp"));
/* Create a batch file that: /* Create a batch file that:
1) waits for 5 seconds to allow the old process to close; 1) waits for 5 seconds to allow the old process to close;
2) deletes the existing installation; 2) deletes the existing installation;
3) copies the new installation; 3) copies the new installation;
4) cds into the new installation; 4) cds into the new installation;
5) runs the installer.bat script */ 5) runs the installer.bat script */
fs.writeFileSync(path.join(folder, 'update.bat'), fs.writeFileSync(
`timeout /t 5 \necho D|xcopy /Y /S /E "${path.join(folder, "temp")}" "${path.join(__dirname, "..", "..")}" \ncd "${path.join(__dirname, "..", "..")}" \ncall installer.bat` path.join(folder, "update.bat"),
) `timeout /t 5 \necho D|xcopy /Y /S /E "${path.join(folder, "temp")}" "${path.join(
__dirname,
"..",
".."
)}" \ncd "${path.join(__dirname, "..", "..")}" \ncall installer.bat`
);
/* Launch the update script then close gracefully */ /* Launch the update script then close gracefully */
var proc = spawn('cmd.exe', ["/c", path.join(folder, 'update.bat')], { cwd: folder, shell: true, detached: true }); var proc = spawn("cmd.exe", ["/c", path.join(folder, "update.bat")], { cwd: folder, shell: true, detached: true });
proc.unref(); proc.unref();
ipcRenderer.send('window:close'); ipcRenderer.send("window:close");
} }
/** Something went wrong. Tell the user to update manually. /** Something went wrong. Tell the user to update manually.
* *
*/ */
function failUpdate() { function failUpdate() {
showErrorPopup(`<div class='main-message'>An error has occurred while updating Olympus. </div><div class='sub-message'> Please delete Olympus and update it manually. A browser window will open automatically on the download page. <br><br> You can find more info in ${path.join(__dirname, "..", "manager.log")}</div>`, () => { showErrorPopup(
exec(`start https://github.com/Pax1601/DCSOlympus/releases`, () => { `<div class='main-message'>An error has occurred while updating Olympus. </div><div class='sub-message'> Please delete Olympus and update it manually. A browser window will open automatically on the download page. <br><br> You can find more info in ${path.join(
ipcRenderer.send('window:close'); __dirname,
}) "..",
}) "manager.log"
)}</div>`,
() => {
exec(`start https://github.com/Pax1601/DCSOlympus/releases`, () => {
ipcRenderer.send("window:close");
});
}
);
} }
/* White-listed channels. */ /* White-listed channels. */
const ipc = { const ipc = {
'render': { render: {
/* From render to main. */ /* From render to main. */
'send': [ send: ["window:minimize", "window:maximize", "window:restore", "window:close"],
'window:minimize', /* From main to render. */
'window:maximize', receive: ["event:maximized", "event:unmaximized"],
'window:restore', /* From render to main and back again. */
'window:close' sendReceive: [],
], },
/* From main to render. */
'receive': [
'event:maximized',
'event:unmaximized'
],
/* From render to main and back again. */
'sendReceive': []
}
}; };
/* Exposed protected methods in the render process. */ /* Exposed protected methods in the render process. */
contextBridge.exposeInMainWorld( contextBridge.exposeInMainWorld(
/* Allowed 'ipcRenderer' methods. */ /* Allowed 'ipcRenderer' methods. */
'ipcRender', { "ipcRender",
{
/* From render to main. */ /* From render to main. */
send: (channel, args) => { send: (channel, args) => {
let validChannels = ipc.render.send; let validChannels = ipc.render.send;
if (validChannels.includes(channel)) { if (validChannels.includes(channel)) {
ipcRenderer.send(channel, args); ipcRenderer.send(channel, args);
} }
}, },
/* From main to render. */ /* From main to render. */
receive: (channel, listener) => { receive: (channel, listener) => {
let validChannels = ipc.render.receive; let validChannels = ipc.render.receive;
if (validChannels.includes(channel)) { if (validChannels.includes(channel)) {
/* Deliberately strip event as it includes `sender`. */ /* Deliberately strip event as it includes `sender`. */
ipcRenderer.on(channel, (event, ...args) => listener(...args)); ipcRenderer.on(channel, (event, ...args) => listener(...args));
} }
}, },
/* From render to main and back again. */ /* From render to main and back again. */
invoke: (channel, args) => { invoke: (channel, args) => {
let validChannels = ipc.render.sendReceive; let validChannels = ipc.render.sendReceive;
if (validChannels.includes(channel)) { if (validChannels.includes(channel)) {
return ipcRenderer.invoke(channel, args); return ipcRenderer.invoke(channel, args);
} }
} },
}); }
);
/* On content loaded */ /* On content loaded */
window.addEventListener('DOMContentLoaded', async () => { window.addEventListener("DOMContentLoaded", async () => {
document.getElementById("loader").classList.remove("hide"); document.getElementById("loader").classList.remove("hide");
await getManager().start(); await getManager().start();
await checkVersion();
}) await checkVersion(getManager().options.forceBeta);
});
window.addEventListener('resize', () => { window.addEventListener("resize", () => {
/* Compute the height of the content page */ /* Compute the height of the content page */
computePagesHeight(); computePagesHeight();
}) });
window.addEventListener('DOMContentLoaded', () => { window.addEventListener("DOMContentLoaded", () => {
/* Compute the height of the content page */ /* Compute the height of the content page */
computePagesHeight(); computePagesHeight();
}) });
document.addEventListener('managerStarted', () => { document.addEventListener("managerStarted", () => {
/* Compute the height of the content page */ /* Compute the height of the content page */
computePagesHeight(); computePagesHeight();
}) });
/** Computes the height of the content area /** Computes the height of the content area
* *
*/ */
function computePagesHeight() { function computePagesHeight() {
var pages = document.querySelectorAll(".manager-page"); var pages = document.querySelectorAll(".manager-page");
var titleBar = document.querySelector("#title-bar"); var titleBar = document.querySelector("#title-bar");
var header = document.querySelector("#header"); var header = document.querySelector("#header");
for (let i = 0; i < pages.length; i++) { for (let i = 0; i < pages.length; i++) {
pages[i].style.height = (window.innerHeight - (titleBar.clientHeight + header.clientHeight)) + "px"; pages[i].style.height = window.innerHeight - (titleBar.clientHeight + header.clientHeight) + "px";
} }
} }