aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakob Stendahl <jakob.stendahl@outlook.com>2021-10-21 01:48:47 +0200
committerJakob Stendahl <jakob.stendahl@outlook.com>2021-10-21 01:48:47 +0200
commit909265bad527a7c1e493b4e84d0e2be64114274c (patch)
treea27f1aa7728c8c382efff444cde5a2b355cef833
parentd12577a79300dee1fe7c8567fa71095e6c8b8a9d (diff)
downloadLuxcena-Neo-909265bad527a7c1e493b4e84d0e2be64114274c.tar.gz
Luxcena-Neo-909265bad527a7c1e493b4e84d0e2be64114274c.zip
:sparkles: Add self updater that actually does something (maybe)
-rw-r--r--src/SelfUpdater/index.js216
-rw-r--r--src/SocketIO/index.js22
-rw-r--r--src_frontend/App.svelte4
-rw-r--r--src_frontend/routes/Updating.svelte156
-rw-r--r--src_frontend/stores/socketStore.js5
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">&#0160;</div>
+ <div class="loading__element lett9">I</div>
+ <div class="loading__element lett10">S</div>
+ <div class="loading__element lett11">&#0160;</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");