diff options
author | Jakob Stendahl <jakob.stendahl@outlook.com> | 2021-10-21 01:48:47 +0200 |
---|---|---|
committer | Jakob Stendahl <jakob.stendahl@outlook.com> | 2021-10-21 01:48:47 +0200 |
commit | 909265bad527a7c1e493b4e84d0e2be64114274c (patch) | |
tree | a27f1aa7728c8c382efff444cde5a2b355cef833 | |
parent | d12577a79300dee1fe7c8567fa71095e6c8b8a9d (diff) | |
download | Luxcena-Neo-909265bad527a7c1e493b4e84d0e2be64114274c.tar.gz Luxcena-Neo-909265bad527a7c1e493b4e84d0e2be64114274c.zip |
:sparkles: Add self updater that actually does something (maybe)
-rw-r--r-- | src/SelfUpdater/index.js | 216 | ||||
-rw-r--r-- | src/SocketIO/index.js | 22 | ||||
-rw-r--r-- | src_frontend/App.svelte | 4 | ||||
-rw-r--r-- | src_frontend/routes/Updating.svelte | 156 | ||||
-rw-r--r-- | src_frontend/stores/socketStore.js | 5 |
5 files changed, 393 insertions, 10 deletions
diff --git a/src/SelfUpdater/index.js b/src/SelfUpdater/index.js index f332e27..447da3f 100644 --- a/src/SelfUpdater/index.js +++ b/src/SelfUpdater/index.js @@ -1,11 +1,216 @@ let fs = require("fs-extra"); +const fsPromises = fs.promises; let url = require("url"); let request = require('request'); const spawn = require('child_process').spawn; +const EventEmitter = require('events') let logger = require(__appdir + "/src/Logger"); let neoModules; +/** + * This just tests if the current appdir is the "default" location + */ +function isInstalledInDefaultLocation() { + return (__appdir == "/opt/luxcena-neo"); +} + +/** + * Function that will create a new empty folder at the path, with the prefix + * it will add a number at the end if something already exists, + */ +function createUniqueDir(path, prefix) { + fs.ensureDirSync(path); + let fn = `${path}/${prefix}`; + let i = 0; + let cFn = fn; + while (true) { + if (fs.existsSync(cFn)) { + i++; + cFn = `${fn}.${i}`; + continue; + } + fs.ensureDirSync(cFn); + return cFn; + } +} + +class Updater { + + constructor() { + this.updating = false; + this.step = null; + this.command = null; + this.event = new EventEmitter(); + + this.updatelog = []; + } + + setStep(cStep) { + this.step = cStep; + this.event.emit("step", cStep); + this.updatelog.push(`- ${cStep}`); + logger.info(`Updater: ${cStep}`); + } + + setCommand(cCommand) { + this.command = cCommand; + this.event.emit("command", cCommand); + this.updatelog.push(`> ${cCommand}`); + logger.info(`Updater: (${cCommand})`); + } + + async forceUpdate() { + this.updatelog = []; + this.step = null; + this.command = null; + if (!isInstalledInDefaultLocation()) { + return {success: false, reason: "not installed in default location", detail: __appdir}; + } + this.updating = true; + this.event.emit("start"); + + neoModules.neoRuntimeManager.stopMode(); + let updatedir = null; + let backupdir = null; + try { + // Download update + this.setStep("Downloading update"); + this.setCommand("Create updatedir"); + updatedir = createUniqueDir("/tmp", "luxcena-neo.update"); + await this.downloadUpdate(updatedir); + + // Create backup + this.setStep("Creating backup"); + this.setCommand("Create backupdir"); + backupdir = createUniqueDir("/var/luxcena-neo/backups", "backup"); + this.setCommand(`Copy ${__appdir} into ${backupdir}`); + await fs.copySync(__appdir, backupdir); + + // Install update + this.setStep("Installing update"); + this.setCommand(`Copy ${updatedir} into /opt/luxcena-neo`); + await fs.copySync(updatedir, __appdir); + + // Install dependencies + this.setStep("Installing dependencies"); + await this.installDependencies(); + + // Create python virtualenv + this.setStep("Making virtualenv"); + await this.makeVirtualenv(); + + // Build source code + this.setStep("Building source"); + this.build(); + + // Restart self, systemd service restart policy will start us up again. + this.setStep("Stopping luxcena neo service in the hope that systemd will restart it."); + this.command("process.exit(0)"); + process.exit(0); + + } catch (e) { + logger.crit(`Updater failed miserably...`); + logger.crit(e); + + let logText; + if (e.hasOwnProperty("code") && e.hasOwnProperty("out") && e.hasOwnProperty("err")) { + logText = "Command failed with code " + e.code + "STDOUT: " + e.out + " STDERR: " + e.err; + } else { + logText = e.toString(); + } + this.updatelog.push(logText); + + // Restore here + + this.event.emit("error", this.updatelog); + neoModules.neoRuntimeManager.startMode(); + } + this.updating = false; + this.event.emit("end"); + } + + /** + * Spawn a new command, return a promise. + */ + run(cmd, opts) { + this.setCommand(`${cmd} ` + opts.join(" ")); + return new Promise(function(resolve, reject) { + let child = spawn(cmd, opts); + + let stdout = ""; + let stderr = ""; + + child.on('exit', (code, sig) => { + if (code == 0) { + resolve({ + code: code, + out: stdout, + err: stderr + }); + } else { + reject({ + code: code, + out: stdout, + err: stderr + }); + } + }); + child.stdout.on('data', data => { + stdout += data.toString(); + }); + child.stderr.on('data', data => { + stderr += data.toString(); + }); + }); + } + + /** + * Determine the currently used remote and branch, and download the newest commit + * into the temporary folder + */ + async downloadUpdate(tmpdir) { + let url = (await this.run(`git`, ["-C", __appdir, "remote", "get-url", "origin"])).out.replace("\n",""); + let branch = (await this.run(`git`, ["-C", __appdir, "rev-parse", "--abbrev-ref", "HEAD"])).out.replace("\n",""); + await this.run(`git`, ["clone", "-b", branch, url, tmpdir]); + } + + async installDependencies() { + // So, the server is running as root, that means we can just do this. + // we shouldn't, but, anyway. + await this.run("sh", ["-c", "wget -qO- https://deb.nodesource.com/setup_14.x | bash -"]); + await this.run("apt", ["-qy", "install", "nodejs", "python3-pip"]); + await this.run("pip3", ["install", "virtualenv"]); + await this.run("sh", ["-c", `export NODE_ENV=development; npm --prefix \"${__appdir}\" install \"${__appdir}\"`]); + } + + async makeVirtualenv() { + this.setCommand("Deleting old virtualenv"); + if (fs.existsSync(`${__appdir}/NeoRuntime/Runtime/venv`)) { + fs.unlinkSync(`${__appdir}/NeoRuntime/Runtime/venv`); + } + + await this.run("virtualenv", ["-p", "/usr/bin/python3", `${__appdir}/NeoRuntime/Runtime/venv`]); + await this.run("sh", ["-c", `source ${__appdir}/NeoRuntime/Runtime/venv/bin/activate && pip install rpi_ws281x`]); + } + + async build() { + await this.run("sh", ["-c", `npm --prefix \"${__appdir}\" run build:frontend`]); + await this.run("sh", ["-c", `npm --prefix \"${__appdir}\" run build:fontawesome`]); + await this.run("sh", ["-c", `npm --prefix \"${__appdir}\" run build:dialog-polyfill`]); + } + + async installSystemdService() { + this.setCommand("Deleting old systemd service"); + fs.unlinkSync("/etc/systemd/system/luxcena-neo.service"); + this.setCommand("Installing new systemd service"); + fs.copySync("/opt/luxcena-neo/bin/luxcena-neo.service", "/etc/systemd/system/luxcena-neo.service"); + await this.run("systemctl", ["daemon-reload"]); + await this.run("systemctl", ["enable", "luxcena-neo"]); + } + +} + class VersionChecker { constructor() { @@ -25,6 +230,7 @@ class VersionChecker { let newVersion = this.checkVersion(this.remotePackageJSON); }, this.checkFrequency); + this.updater = new Updater(); } checkVersion() { @@ -60,17 +266,9 @@ class VersionChecker { return false; } - doUpdate() { - spawn("luxcena-neo-cli.sh", ["update", ">>", "/tmp/luxcena-neo-update.log"], { - cwd: process.cwd(), - detached : true, - stdio: "inherit" - }); - } - } module.exports = (_neoModules) => { neoModules = _neoModules; return new VersionChecker(); -}; +};
\ No newline at end of file diff --git a/src/SocketIO/index.js b/src/SocketIO/index.js index ec1cedf..6e71dbe 100644 --- a/src/SocketIO/index.js +++ b/src/SocketIO/index.js @@ -99,6 +99,10 @@ function createOpenSocketNamespace(io) { socket.on("disconnect", () => { logger.access(`SOCKET:open Client (${socket.id}@${socket.handshake.headers.host}) disconnected.`); }); + + if (neoModules.selfUpdater.updater.updating) { + socket.emit("updater", "start"); + } }); neoModules.neoRuntimeManager.event.on("change", (name, value) => { @@ -111,6 +115,12 @@ function createOpenSocketNamespace(io) { openNamespace.emit("var", name, value); } }); + neoModules.selfUpdater.updater.event.on("start", () => { + openNamespace.emit("updater", "start"); + }); + neoModules.selfUpdater.updater.event.on("end", () => { + openNamespace.emit("updater", "end"); + }); } /** @@ -194,7 +204,7 @@ function createAuthorizedNamespace(io) { fn({success: true}); }); socket.on("system:update_version", () => { - neoModules.selfUpdater.doUpdate(); + neoModules.selfUpdater.updater.forceUpdate(); }); /* SSLCert */ @@ -307,6 +317,16 @@ function createAuthorizedNamespace(io) { } }); }); + + neoModules.selfUpdater.updater.event.on("step", (step) => { + authorizedNamespace.emit("updater:step", step); + }); + neoModules.selfUpdater.updater.event.on("command", (command) => { + authorizedNamespace.emit("updater:command", command); + }); + neoModules.selfUpdater.updater.event.on("error", (updateLog) => { + authorizedNamespace.emit("updater:error", updateLog); + }); } /** diff --git a/src_frontend/App.svelte b/src_frontend/App.svelte index e95f316..3f68718 100644 --- a/src_frontend/App.svelte +++ b/src_frontend/App.svelte @@ -3,6 +3,7 @@ import { wrap } from 'svelte-spa-router/wrap'; import MainRoute from "./routes/MainRoute.svelte"; import EditorRoute from "./routes/EditorRoute.svelte"; + import Updating from "./routes/Updating.svelte"; import LoginRoute from "./routes/LoginRoute.svelte"; import WidgetRoute from "./routes/WidgetRoute.svelte"; import UnknownRoute from "./routes/UnknownRoute.svelte"; @@ -16,6 +17,9 @@ main_router_routes.set("/editor/*", wrap({ component: EditorRoute })); + main_router_routes.set("/updating", wrap({ + component: Updating + })); main_router_routes.set("/login", wrap({ component: LoginRoute })); diff --git a/src_frontend/routes/Updating.svelte b/src_frontend/routes/Updating.svelte new file mode 100644 index 0000000..2e68310 --- /dev/null +++ b/src_frontend/routes/Updating.svelte @@ -0,0 +1,156 @@ +<script> + import { openSocket, authorizedSocket } from "../stores/socketStore"; + + let hasError = false; + let updateLog = ""; + let step = ""; + let command = ""; + + openSocket.on("updater", (event) => { + if (event == "end") { + if (!hasError) { + console.log("END UPDATE"); + window.location.href = "/"; + } + } + }); + authorizedSocket.on("updater:step", (_step) => { + step = _step; + }); + authorizedSocket.on("updater:command", (_command) => { + command = _command; + }); + authorizedSocket.on("updater:error", (_updateLog) => { + hasError = true; + updateLog = _updateLog.join("\n"); + }); + +</script> + +<style lang="scss"> + * { + margin: 0; + padding: 0; + border: 0; + box-sizing: border-box; + &:before, &:after { + box-sizing: inherit; + } + } + .wrapper { + background-color: #1fa2ed; + color: #fff; + height: 100vh; + } + + .start-screen { + display: flex; + justify-content: center; + flex-flow: nowrap column; + align-items: center; + min-height: 100vh; + } + .loading { + display: flex; + margin: 24px 0; + flex-wrap: wrap; + align-items: center; + justify-content: center; + margin: 0 5px 0 5px; + } + .loading__element { + font: normal 100 2rem/1 Roboto; + letter-spacing: .5em; + } + [class*="lett"] { + animation: bouncing 3s infinite ease; + } + + @for $i from 1 through 19 { + $delay: percentage($i); + .lett#{$i} { + animation-delay: $delay / 1000% + s; + } + } + + @keyframes bouncing { + 0%, 100% { + transform: scale3d(1,1,1); + } + 50% { + transform: scale3d(0,0,1); + } + } + + .current-event { + color: rgba(255, 255, 255, 0.53); + font: normal 100 1rem/1 Roboto; + letter-spacing: .1em; + width: 100%; + text-align: center; + margin-left: 10px; + margin-right: 10px; + margin-top: 5px; + .small { + font-size: 0.7rem; + margin-top: 5px; + } + } + + .update-log { display: none; } + + .has-error { + background-color: #6e2929; + .start-screen, .current-event { display: none; } + .update-log { + display: block; + position: absolute; + width: 90vw; + height: 70vh; + background-color: #282828; + border-radius: 15px; + padding: 15px; + overflow: auto; + margin-left: 5vw; + margin-top: 15vh; + box-sizing: border-box; + } + } + +</style> + +<div class="wrapper" class:has-error={hasError}> + <div class="start-screen"> + <div class="loading"> + <div class="loading__element lett1">L</div> + <div class="loading__element lett2">U</div> + <div class="loading__element lett3">X</div> + <div class="loading__element lett4">C</div> + <div class="loading__element lett5">E</div> + <div class="loading__element lett6">N</div> + <div class="loading__element lett7">A</div> + <div class="loading__element lett8"> </div> + <div class="loading__element lett9">I</div> + <div class="loading__element lett10">S</div> + <div class="loading__element lett11"> </div> + <div class="loading__element lett12">U</div> + <div class="loading__element lett13">P</div> + <div class="loading__element lett14">D</div> + <div class="loading__element lett15">A</div> + <div class="loading__element lett16">T</div> + <div class="loading__element lett17">I</div> + <div class="loading__element lett18">N</div> + <div class="loading__element lett19">G</div> + </div> + <div class="current-event"> + <p>{step}</p> + <p class="small">{command}</p> + </div> + </div> + <div class="update-log"> + Update failed (<a href="/">Go home</a>): + <pre> + {updateLog}; + </pre> + </div> +</div>
\ No newline at end of file diff --git a/src_frontend/stores/socketStore.js b/src_frontend/stores/socketStore.js index 328762a..b18ddca 100644 --- a/src_frontend/stores/socketStore.js +++ b/src_frontend/stores/socketStore.js @@ -19,6 +19,11 @@ openSocket.io.on("reconnect_attempt", () => { openSocket.io.on("reconnect", () => { openSocketReconnecting.set(false); }); +openSocket.on("updater", (state) => { + if (state == "start") { + replace("/updating"); + } +}); let storedSessionToken = localStorage.getItem("sessionToken"); |