From 1e588718a855ae2871a8841f6c6e621f49795454 Mon Sep 17 00:00:00 2001
From: "jakob.stendahl"
Date: Sat, 17 Dec 2022 21:31:41 +0100
Subject: Start moving to esm, work on updater
---
.npmignore | 14 +
app.js | 44 +--
bin/bashfuncs.sh | 96 ++++++
bin/build.sh | 12 -
bin/install.sh | 77 ++---
bin/luxcena-neo.service | 4 +-
bin/luxcena-neo.sh | 32 +-
bin/postinstall.sh | 13 +
bin/uninstall.sh | 7 +-
package-lock.json | 270 +++++++++++------
package.json | 23 +-
public/docs/Scripting/NeoBehaviour/index.html | 2 +-
public/docs/Scripting/Strip/index.html | 4 +-
public/docs/sitemap.xml | 24 +-
public/docs/sitemap.xml.gz | Bin 204 -> 205 bytes
runDev.cjs | 346 ++++++++++++++++++++++
runDev.js | 346 ----------------------
src/Logger/index.cjs | 79 +++++
src/Logger/index.js | 79 -----
src/NeoRuntimeManager/IPC.cjs | 199 +++++++++++++
src/NeoRuntimeManager/IPC.js | 199 -------------
src/NeoRuntimeManager/RuntimeProcess.cjs | 106 +++++++
src/NeoRuntimeManager/RuntimeProcess.js | 106 -------
src/NeoRuntimeManager/index.cjs | 339 +++++++++++++++++++++
src/NeoRuntimeManager/index.js | 339 ---------------------
src/SSLCert/index.cjs | 146 +++++++++
src/SSLCert/index.js | 144 ---------
src/SelfUpdater/index.js | 183 ++++++------
src/SocketIO/index.cjs | 409 ++++++++++++++++++++++++++
src/SocketIO/index.js | 409 --------------------------
src/UserData/index.cjs | 332 +++++++++++++++++++++
src/UserData/index.js | 332 ---------------------
32 files changed, 2473 insertions(+), 2242 deletions(-)
create mode 100644 .npmignore
create mode 100644 bin/bashfuncs.sh
delete mode 100644 bin/build.sh
create mode 100644 bin/postinstall.sh
create mode 100644 runDev.cjs
delete mode 100644 runDev.js
create mode 100644 src/Logger/index.cjs
delete mode 100644 src/Logger/index.js
create mode 100644 src/NeoRuntimeManager/IPC.cjs
delete mode 100644 src/NeoRuntimeManager/IPC.js
create mode 100644 src/NeoRuntimeManager/RuntimeProcess.cjs
delete mode 100644 src/NeoRuntimeManager/RuntimeProcess.js
create mode 100644 src/NeoRuntimeManager/index.cjs
delete mode 100644 src/NeoRuntimeManager/index.js
create mode 100644 src/SSLCert/index.cjs
delete mode 100644 src/SSLCert/index.js
create mode 100644 src/SocketIO/index.cjs
delete mode 100644 src/SocketIO/index.js
create mode 100644 src/UserData/index.cjs
delete mode 100644 src/UserData/index.js
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..9fd90f6
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,14 @@
+__pycache__
+*.pyc
+venv
+tmp
+
+*.log
+*.error
+
+state.json
+variables.json
+globvars.json
+
+/src_frontend/
+/docs/assets/
diff --git a/app.js b/app.js
index 044f68a..79a893b 100644
--- a/app.js
+++ b/app.js
@@ -1,8 +1,12 @@
-let fse = require("fs-extra");
-let events = require('events');
+import { existsSync, readFileSync } from 'fs';
+import events from 'node:events';
+import path from 'path';
+import {fileURLToPath} from 'url';
// Firstly we set up all globals, check that the usrData dir exists, if not, we run the setup
-global.__appdir = "/opt/luxcena-neo";
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+global.__appdir = __dirname;
global.__configdir = "/etc/luxcena-neo";
global.__datadir = "/var/luxcena-neo";
global.__logdir = "/var/log/luxcena-neo";
@@ -13,8 +17,8 @@ if ((process.argv.length >= 3) && (process.argv[2] == "dev")) {
global.__datadir = __dirname + "/tmp/userdata";
global.__logdir = __dirname + "/tmp/logs";
}
-if (!fse.existsSync(global.__appdir)) {
- console.log(`CRITICAL UserDir not found '${userDir}'! Exiting...`);
+if (!existsSync(global.__appdir)) {
+ console.log(`CRITICAL AppDir not found '${global.__appdir}'! Exiting...`);
process.exit(1);
}
@@ -22,29 +26,35 @@ if (!fse.existsSync(global.__appdir)) {
global.__event = new events.EventEmitter();
// Secondly we setup the logger,
-let logger = require("./src/Logger");
+import logger from './src/Logger/index.cjs'
logger.info("Starting Luxcena-Neo...");
let neoModules = {};
-neoModules.userData = require("./src/UserData")(neoModules);
-neoModules.SSLCert = require("./src/SSLCert")(neoModules);
-neoModules.selfUpdater = require("./src/SelfUpdater")(neoModules);
-neoModules.neoRuntimeManager = require("./src/NeoRuntimeManager")(neoModules);
+import UserData from './src/UserData/index.cjs';
+neoModules.userData = UserData(neoModules);
+import SSLCert from './src/SSLCert/index.cjs';
+neoModules.SSLCert = SSLCert(neoModules);
+import SelfUpdater from './src/SelfUpdater/index.js';
+neoModules.selfUpdater = SelfUpdater(neoModules);
+import NeoRuntimeManager from './src/NeoRuntimeManager/index.cjs';
+neoModules.neoRuntimeManager = NeoRuntimeManager(neoModules);
neoModules.neoRuntimeManager.mode.set(neoModules.userData.config.activeMode);
// All the domain-things are now setup, we are ready to run our main program...
-let express = require("express");
-let https = require("https");
+import express from 'express';
+import https from 'https';
let app = express();
let server = https.createServer({
- key: fse.readFileSync(__configdir + "/certs/privkey.pem"),
- cert: fse.readFileSync(__configdir + "/certs/cert.pem")
+ key: readFileSync(__configdir + "/certs/privkey.pem"),
+ cert: readFileSync(__configdir + "/certs/cert.pem")
},
app
);
-let io = require("socket.io")(server);
-require("./src/SocketIO")(neoModules, io);
+import {Server} from 'socket.io';
+let io = new Server(server);
+import SocketIO from './src/SocketIO/index.cjs'
+SocketIO(neoModules, io);
app.use("/", express.static(__appdir + "/public"));
server.listen(neoModules.userData.config.HTTP.port, () => {
@@ -78,7 +88,7 @@ function getNetworkAddress() {
}
return results[Object.keys(results)[0]][0]
}
-let http = require("http");
+import http from 'http';
function tryBroadcastSelf() {
if (neoModules.userData.config.DiscoveryServer.broadcastSelf) {
let address = neoModules.userData.config.DiscoveryServer.address;
diff --git a/bin/bashfuncs.sh b/bin/bashfuncs.sh
new file mode 100644
index 0000000..a1559a2
--- /dev/null
+++ b/bin/bashfuncs.sh
@@ -0,0 +1,96 @@
+#!/bin/bash
+
+function die() {
+ tput setaf 1
+ printf "\n\nInstall failed, successfull steps not reversed.\n"
+ tput sgr0
+ exit 1
+}
+
+
+function TPUT() {
+ if [ -t 1 ]; then
+ if [ "$1" = "tput" ]; then
+ shift
+ fi
+ tput $@
+ fi
+}
+
+function header() {
+ TPUT setaf 3
+ if [ -t 1 ]; then
+ printf "\n[ ] $1"
+ else
+ printf "\n- $1"
+ fi
+ TPUT sgr0
+}
+
+function commandError() {
+ trap - 1
+ cat $1
+ rm $1
+
+ TPUT setaf 1
+ printf "\n\nInstall failed.\n"
+ TPUT sgr0
+ TPUT cnorm
+ exit 1
+}
+
+function spinner() {
+ i=1
+ sp="/-\|"
+ while ps a | awk '{print $1}' | grep -q "$1"; do
+ TPUT cub $(tput cols)
+ TPUT cuf 1
+ printf "${sp:i++%${#sp}:1}"
+ TPUT cuf $(tput cols)
+ sleep 0.09
+ done
+
+ TPUT cub $(tput cols)
+ TPUT cuf 1
+}
+
+function execCommand() {
+ TPUT sc
+ TPUT setaf 4
+ if [ -t 1 ]; then
+ printf " ($1)"
+ else
+ printf "\n>> $1 "
+ fi
+ TPUT sgr0
+ log=$(mktemp)
+ bash -c "$1 > $log 2>&1" &
+
+ PID=$!
+
+ if [ -t 1 ]; then
+ spinner $PID
+ fi
+
+ wait $PID
+ commandSucc=$?
+ if [ $commandSucc -eq 0 ]; then
+ TPUT setaf 2
+ printf "✓"
+ TPUT sgr0
+ TPUT rc
+ TPUT el
+ else
+ TPUT setaf 1
+ printf "x"
+ TPUT sgr0
+ TPUT cuf $(tput cols)
+ printf "\n"
+ if [ $# -eq 1 ] || [ $2 -eq "0" ]; then
+ commandError $log
+ fi
+ fi
+ rm $log
+}
+
+TPUT civis
diff --git a/bin/build.sh b/bin/build.sh
deleted file mode 100644
index 6c10225..0000000
--- a/bin/build.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-echo "> Install node modules needed for build"
-NODE_ENV="development"
-npm i
-
-echo "> Make sure all python dependencies for build is installed"
-pip3 install mkdocs mkdocs-gitbook pygments pymdown-extensions
-
-echo "> Compile es6 and sass to bundles"
-npx webpack -p
-
-echo "> Generate html docs using sphinx"
-mkdocs build
diff --git a/bin/install.sh b/bin/install.sh
index d27c324..e9bd1aa 100755
--- a/bin/install.sh
+++ b/bin/install.sh
@@ -4,8 +4,8 @@ printf "\e[37mLuxcena-\e[31mn\e[32me\e[34mo\e[0m\n"
printf '\e[93m%s\e[0m' "-----------"
if [ "$EUID" -ne 0 ]; then
- echo "You need to run this script as root."
- echo "Try running with 'sudo ./bin/install.sh'"
+ echo "\nYou need to run this script as root."
+ echo "Try running with 'sudo $0/install.sh'"
exit 1
fi
@@ -38,7 +38,8 @@ function header() {
function commandError() {
trap - 1
- cat /tmp/luxcena-neo-update.log
+ cat $1
+ rm $1
TPUT setaf 1
printf "\n\nInstall failed.\n"
@@ -71,7 +72,8 @@ function execCommand() {
printf "\n>> $1 "
fi
TPUT sgr0
- bash -c "$1 > /tmp/luxcena-neo-update.log 2>&1" &
+ log=$(mktemp)
+ bash -c "$1 > $log 2>&1" &
PID=$!
@@ -94,9 +96,10 @@ function execCommand() {
TPUT cuf $(tput cols)
printf "\n"
if [ $# -eq 1 ] || [ $2 -eq "0" ]; then
- commandError
+ commandError $1
fi
fi
+ rm $log
}
TPUT civis
@@ -116,9 +119,6 @@ execCommand "usermod -a -G spi $username"
# First we make our directories
header "Making directories"
-[ -d "/opt/luxcena-neo/" ] && echo "\nSeems like luxcena-neo is already installed, please do update instead" && die
-execCommand "mkdir -p \"/opt/luxcena-neo\""
-execCommand "chown $username:$username \"/opt/luxcena-neo\""
execCommand "mkdir -p \"/var/luxcena-neo\""
execCommand "chown $username:$username \"/var/luxcena-neo\""
execCommand "mkdir -p \"/etc/luxcena-neo\""
@@ -126,56 +126,37 @@ execCommand "chown $username:$username \"/etc/luxcena-neo\""
execCommand "mkdir -p \"/var/log/luxcena-neo\""
execCommand "chown $username:$username \"/var/log/luxcena-neo\""
-# Choose branch to install
-TPUT cnorm
-printf '\n%s' "Which branch do you want to install (default: master)? "
-read BRANCH
-if [ -z "$BRANCH" ]; then
- BRANCH="master"
-fi
-TPUT civis
-
-# Get source code
-header "Fetch source code"
-execCommand "runuser -l $username -c \"git clone -b $BRANCH https://github.com/jakobst1n/luxcena-neo /opt/luxcena-neo/\""
-execCommand "chown -R lux-neo:lux-neo /opt/luxcena-neo"
-
# Install dependencies
header "Install dependencies"
if [ "$(uname -m)" = "armv6l" ]; then
- execCommand "wget https://unofficial-builds.nodejs.org/download/release/v14.10.0/node-v14.10.0-linux-armv6l.tar.gz"
- execCommand "tar -xzf node-v14.10.0-linux-armv6l.tar.gz"
- execCommand "sudo cp -r node-v14.10.0-linux-armv6l/* /usr/local"
- execCommand "rm -r node-v14.10.0-linux-armv6l"
- execCommand "rm node-v14.10.0-linux-armv6l.tar.gz"
+ execCommand "wget https://unofficial-builds.nodejs.org/download/release/v18.9.1/node-v18.9.1-linux-armv6l.tar.gz"
+ execCommand "tar -xzf node-v18.9.1-linux-armv6l.tar.gz"
+ execCommand "sudo cp -r node-v18.9.1-linux-armv6l/* /usr/local"
+ execCommand "rm -r node-v18.9.1-linux-armv6l"
+ execCommand "rm node-v18.9.1-linux-armv6l.tar.gz"
else
- execCommand "wget -qO- https://deb.nodesource.com/setup_14.x | bash -"
+ execCommand "wget -qO- https://deb.nodesource.com/setup_18.x | bash -"
execCommand "apt -q update"
execCommand "apt -qy install nodejs"
fi
+execCommand "apt -qy install jq curl"
execCommand "apt -qy install python3-pip"
execCommand "pip3 install virtualenv"
-execCommand "runuser -l 'lux-neo' -c \"export NODE_ENV=development; npm --prefix /opt/luxcena-neo install /opt/luxcena-neo\""
-
-# Create virtualenv
-header "Create python virtualenv and install dependencies"
-execCommand "rm -rf /opt/luxcena-neo/NeoRuntime/Runtime/venv"
-execCommand "virtualenv -p /usr/bin/python3 /opt/luxcena-neo/NeoRuntime/Runtime/venv"
-execCommand "source /opt/luxcena-neo/NeoRuntime/Runtime/venv/bin/activate && pip install rpi_ws281x"
-
-# Build the source code
-header "Build source code"
-execCommand "runuser -l 'lux-neo' -c \"npm --prefix /opt/luxcena-neo run build:frontend\""
-execCommand "runuser -l 'lux-neo' -c \"npm --prefix /opt/luxcena-neo run build:fontawesome\""
-execCommand "runuser -l 'lux-neo' -c \"npm --prefix /opt/luxcena-neo run build:dialog-polyfill\""
-
-# Install systemd service
-header "Install new systemd service"
-execCommand "cp /opt/luxcena-neo/bin/luxcena-neo.service /etc/systemd/system/luxcena-neo.service"
-execCommand "systemctl daemon-reload"
-execCommand "systemctl enable luxcena-neo"
-execCommand "systemctl start luxcena-neo"
+
+# Get package
+header "Download luxcena-neo"
+INSTALLDIR=$(getent passwd "$username" | cut -d: -f6)
+APIURL="https://api.github.com/repos/JakobST1n/luxcena-neo"
+REPOINFO=$(curl -s "$APIURL/releases/86402456" -H "Accept: application/vnd.github+json")
+TARBALL_NAME=$(echo "$REPOINFO" | jq '.assets[0].name')
+TARBALL_URL=$(echo "$REPOINFO" | jq '.assets[0].browser_download_url')
+execCommand "runuser -l $username -c \"curl -s -L -o $INSTALLDIR/$TARBALL_NAME $TARBALL_URL\""
+
+header "Install luxcena-neo"
+execCommand "runuser -l $username -c \"export NODE_ENV=production; npm --prefix $INSTALLDIR/luxcena-neo/ install $INSTALLDIR/$TARBALL_NAME \""
+execCommand "runuser -l $username -c \"rm $INSTALLDIR/$TARBALL_NAME\""
# Installation is done!
printf '\n\e[5m%s\e[0m\n' "🎉Luxcena-Neo is now installed🎉"
+echo "Run 'sudo $INSTALLDIR/luxcena-neo/node_modules/luxcena-neo/bin/luxcena-neo.sh'"
TPUT cnorm
diff --git a/bin/luxcena-neo.service b/bin/luxcena-neo.service
index b4115be..42d5e64 100644
--- a/bin/luxcena-neo.service
+++ b/bin/luxcena-neo.service
@@ -2,13 +2,13 @@
Description=Luxcena Neo
[Service]
-ExecStart=/opt/luxcena-neo/bin/luxcena-neo.sh
+ExecStart={{WD}}/bin/luxcena-neo.sh
Restart=always
RestartSec=10
Environment=PATH=/usr/bin:/usr/local/bin
Environment=NODE_ENV=development
-WorkingDirectory=/opt/luxcena-neo/
+WorkingDirectory={{WD}}
[Install]
WantedBy=multi-user.target
diff --git a/bin/luxcena-neo.sh b/bin/luxcena-neo.sh
index c9a6d97..9653d20 100755
--- a/bin/luxcena-neo.sh
+++ b/bin/luxcena-neo.sh
@@ -5,5 +5,33 @@
# the server needs root as well.
#runuser -l pi -c "export NODE_ENV=production; node ~/luxcena-neo-install/src/app.js"
-export NODE_ENV=development
-node /opt/luxcena-neo/app.js >> /var/log/luxcena-neo/service.log 2>&1
+
+set -o pipefail
+
+# Root directory of the installation
+BASEDIR=$(dirname $(dirname "$0"))
+SYSTEMD_SRC_FILE="$BASEDIR/bin/luxcena-neo.service"
+SYSTEMD_DEST_FILE="/etc/systemd/system/luxcena-neo.service"
+
+echo "Verifying that we are running the newest systemd service file"
+SYSTEMD_TMP=$(mktemp)
+sed "s|{{WD}}|$BASEDIR|" "$SYSTEMD_SRC_FILE" > "$SYSTEMD_TMP"
+
+if [[ -f "$SYSTEMD_DEST_FILE" ]] && cmp -s "$SYSTEMD_TMP" "$SYSTEMD_DEST_FILE"; then
+ echo "Newest service file installed."
+ rm "$SYSTEMD_TMP"
+else
+ echo "Serice file not up to date, attempting to update."
+ cp "$SYSTEMD_TMP" "$SYSTEMD_DEST_FILE"
+ rm "$SYSTEMD_TMP"
+ systemctl daemon-reload
+ systemctl enable luxcena-neo
+ systemctl restart luxcena-neo
+ echo "Service file updated, exiting with the hopes that the new file will automatically restart luxcena-neo."
+ exit 0
+fi
+
+echo "Starting luxcena-neo"
+export NODE_ENV=production
+node "$BASEDIR/app.js" >> /var/log/luxcena-neo/service.log 2>&1
+echo "Luxcena neo exited $?"
diff --git a/bin/postinstall.sh b/bin/postinstall.sh
new file mode 100644
index 0000000..85a77e7
--- /dev/null
+++ b/bin/postinstall.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+CWD=$PWD
+. "$CWD/bin/bashfuncs.sh"
+
+# Create virtualenv
+header "Create python virtualenv and install dependencies"
+execCommand "rm -rf $CWD/NeoRuntime/Runtime/venv"
+execCommand "pip3 install virtualenv"
+execCommand "virtualenv -p /usr/bin/python3 \"$CWD/NeoRuntime/Runtime/venv\""
+header "Attempting to install the rpi_ws281x library, if you want to actually control some leds you need this. Don't worry if not."
+execCommand "source \"$CWD/NeoRuntime/Runtime/venv/bin/activate\" && pip install rpi_ws281r" 1
+
diff --git a/bin/uninstall.sh b/bin/uninstall.sh
index ee8a5a2..c05d2f8 100755
--- a/bin/uninstall.sh
+++ b/bin/uninstall.sh
@@ -137,15 +137,16 @@ if [ $res -eq 1 ]; then
header "Uninstall luxcena-neo"
execCommand "rm -f /etc/systemd/system/luxcena-neo.service"
- execCommand "rm -rf /opt/luxcena-neo"
+ execCommand "systemctl reload-daemon"
+ execCommand "rm -rf /var/log/luxcena-neo"
TPUT setaf 2
printf "\nEverything should now be gone.\n"
- printf "/etc/luxcena-neo and /var/log/luxcena-neo is not removed.\n"
+ printf "/etc/luxcena-neo is not removed.\n"
TPUT sgr0
TPUT setaf 8
printf "Well, some dependencies still exists. Those are:\n"
printf " - packages (nodejs python3 python3-pip)\n"
TPUT sgr0
fi
-TPUT cnorm
\ No newline at end of file
+TPUT cnorm
diff --git a/package-lock.json b/package-lock.json
index 73af181..87d158a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,7 +12,7 @@
"express",
"fs-extra",
"ini",
- "request",
+ "node-fetch",
"socket.io"
],
"hasInstallScript": true,
@@ -23,9 +23,10 @@
"dependencies": {
"crypto-js": "^3.1.9-1",
"express": "^4.16.3",
- "fs-extra": "^8.1.0",
+ "fs-extra": "^11.1.0",
"ini": "^2.0.0",
- "request": "^2.88.0",
+ "node-fetch": "^3.3.0",
+ "request": "^2.88.2",
"socket.io": "^4.1.3"
},
"devDependencies": {
@@ -705,7 +706,6 @@
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "inBundle": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -788,7 +788,6 @@
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
- "inBundle": true,
"dependencies": {
"safer-buffer": "~2.1.0"
}
@@ -797,7 +796,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
- "inBundle": true,
"engines": {
"node": ">=0.8"
}
@@ -805,14 +803,12 @@
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
- "inBundle": true
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
"integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==",
- "inBundle": true,
"engines": {
"node": "*"
}
@@ -820,8 +816,7 @@
"node_modules/aws4": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
- "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
- "inBundle": true
+ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA=="
},
"node_modules/balanced-match": {
"version": "1.0.2",
@@ -842,7 +837,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
- "inBundle": true,
"dependencies": {
"tweetnacl": "^0.14.3"
}
@@ -1078,8 +1072,7 @@
"node_modules/caseless": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
- "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
- "inBundle": true
+ "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="
},
"node_modules/chalk": {
"version": "1.1.3",
@@ -1179,7 +1172,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
- "inBundle": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
@@ -1441,7 +1433,6 @@
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
- "inBundle": true,
"dependencies": {
"assert-plus": "^1.0.0"
},
@@ -1449,6 +1440,15 @@
"node": ">=0.10"
}
},
+ "node_modules/data-uri-to-buffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz",
+ "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==",
+ "inBundle": true,
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -1471,7 +1471,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
- "inBundle": true,
"engines": {
"node": ">=0.4.0"
}
@@ -1588,7 +1587,6 @@
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
"integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
- "inBundle": true,
"dependencies": {
"jsbn": "~0.1.0",
"safer-buffer": "^2.1.0"
@@ -1903,8 +1901,7 @@
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
- "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
- "inBundle": true
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"node_modules/extsprintf": {
"version": "1.3.0",
@@ -1912,20 +1909,40 @@
"integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==",
"engines": [
"node >=0.6.0"
- ],
- "inBundle": true
+ ]
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "inBundle": true
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
- "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
- "inBundle": true
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
+ },
+ "node_modules/fetch-blob": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
+ "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "inBundle": true,
+ "dependencies": {
+ "node-domexception": "^1.0.0",
+ "web-streams-polyfill": "^3.0.3"
+ },
+ "engines": {
+ "node": "^12.20 || >= 14.13"
+ }
},
"node_modules/fill-range": {
"version": "7.0.1",
@@ -1961,7 +1978,6 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
"integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==",
- "inBundle": true,
"engines": {
"node": "*"
}
@@ -1970,7 +1986,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
- "inBundle": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
@@ -1980,6 +1995,18 @@
"node": ">= 0.12"
}
},
+ "node_modules/formdata-polyfill": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
+ "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+ "inBundle": true,
+ "dependencies": {
+ "fetch-blob": "^3.1.2"
+ },
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1999,17 +2026,17 @@
}
},
"node_modules/fs-extra": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
- "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.0.tgz",
+ "integrity": "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==",
"inBundle": true,
"dependencies": {
"graceful-fs": "^4.2.0",
- "jsonfile": "^4.0.0",
- "universalify": "^0.1.0"
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
},
"engines": {
- "node": ">=6 <7 || >=8"
+ "node": ">=14.14"
}
},
"node_modules/fs.realpath": {
@@ -2065,7 +2092,6 @@
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
"integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
- "inBundle": true,
"dependencies": {
"assert-plus": "^1.0.0"
}
@@ -2118,7 +2144,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
"integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==",
- "inBundle": true,
"engines": {
"node": ">=4"
}
@@ -2128,7 +2153,6 @@
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz",
"integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
"deprecated": "this library is no longer supported",
- "inBundle": true,
"dependencies": {
"ajv": "^6.12.3",
"har-schema": "^2.0.0"
@@ -2208,7 +2232,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
"integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==",
- "inBundle": true,
"dependencies": {
"assert-plus": "^1.0.0",
"jsprim": "^1.2.2",
@@ -2394,8 +2417,7 @@
"node_modules/is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
- "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
- "inBundle": true
+ "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="
},
"node_modules/isarray": {
"version": "0.0.1",
@@ -2406,8 +2428,7 @@
"node_modules/isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
- "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==",
- "inBundle": true
+ "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g=="
},
"node_modules/jest-worker": {
"version": "26.6.2",
@@ -2444,32 +2465,31 @@
"node_modules/jsbn": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
- "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
- "inBundle": true
+ "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg=="
},
"node_modules/json-schema": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
- "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
- "inBundle": true
+ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "inBundle": true
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"node_modules/json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
- "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
- "inBundle": true
+ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="
},
"node_modules/jsonfile": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
- "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"inBundle": true,
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
@@ -2478,7 +2498,6 @@
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
"integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==",
- "inBundle": true,
"dependencies": {
"assert-plus": "1.0.0",
"extsprintf": "1.3.0",
@@ -2774,6 +2793,25 @@
"node": ">= 0.6"
}
},
+ "node_modules/node-domexception": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "github",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "inBundle": true,
+ "engines": {
+ "node": ">=10.5.0"
+ }
+ },
"node_modules/node-emoji": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz",
@@ -2783,6 +2821,24 @@
"lodash": "^4.17.21"
}
},
+ "node_modules/node-fetch": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.0.tgz",
+ "integrity": "sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==",
+ "inBundle": true,
+ "dependencies": {
+ "data-uri-to-buffer": "^4.0.0",
+ "fetch-blob": "^3.1.4",
+ "formdata-polyfill": "^4.0.10"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/node-fetch"
+ }
+ },
"node_modules/node-releases": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
@@ -2838,7 +2894,6 @@
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
- "inBundle": true,
"engines": {
"node": "*"
}
@@ -2967,8 +3022,7 @@
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
- "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
- "inBundle": true
+ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="
},
"node_modules/picocolors": {
"version": "1.0.0",
@@ -3638,14 +3692,12 @@
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
- "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
- "inBundle": true
+ "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="
},
"node_modules/punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
- "inBundle": true,
"engines": {
"node": ">=6"
}
@@ -3745,7 +3797,6 @@
"resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
"integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
"deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142",
- "inBundle": true,
"dependencies": {
"aws-sign2": "~0.7.0",
"aws4": "^1.8.0",
@@ -3776,7 +3827,6 @@
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
"integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
- "inBundle": true,
"engines": {
"node": ">=0.6"
}
@@ -4347,7 +4397,6 @@
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz",
"integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==",
- "inBundle": true,
"dependencies": {
"asn1": "~0.2.3",
"assert-plus": "^1.0.0",
@@ -4701,7 +4750,6 @@
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
- "inBundle": true,
"dependencies": {
"psl": "^1.1.28",
"punycode": "^2.1.1"
@@ -4714,7 +4762,6 @@
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
- "inBundle": true,
"dependencies": {
"safe-buffer": "^5.0.1"
},
@@ -4725,8 +4772,7 @@
"node_modules/tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
- "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
- "inBundle": true
+ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="
},
"node_modules/type-fest": {
"version": "1.4.0",
@@ -4754,12 +4800,12 @@
}
},
"node_modules/universalify": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
- "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
+ "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"inBundle": true,
"engines": {
- "node": ">= 4.0.0"
+ "node": ">= 10.0.0"
}
},
"node_modules/unpipe": {
@@ -4801,7 +4847,6 @@
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
- "inBundle": true,
"dependencies": {
"punycode": "^2.1.0"
}
@@ -4826,7 +4871,6 @@
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
- "inBundle": true,
"bin": {
"uuid": "bin/uuid"
}
@@ -4847,7 +4891,6 @@
"engines": [
"node >=0.6.0"
],
- "inBundle": true,
"dependencies": {
"assert-plus": "^1.0.0",
"core-util-is": "1.0.2",
@@ -4857,8 +4900,7 @@
"node_modules/verror/node_modules/core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
- "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
- "inBundle": true
+ "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="
},
"node_modules/w3c-keyname": {
"version": "2.2.6",
@@ -4866,6 +4908,15 @@
"integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==",
"dev": true
},
+ "node_modules/web-streams-polyfill": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
+ "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==",
+ "inBundle": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/wordwrap": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
@@ -6058,6 +6109,11 @@
"assert-plus": "^1.0.0"
}
},
+ "data-uri-to-buffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz",
+ "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA=="
+ },
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -6401,6 +6457,15 @@
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
},
+ "fetch-blob": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
+ "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
+ "requires": {
+ "node-domexception": "^1.0.0",
+ "web-streams-polyfill": "^3.0.3"
+ }
+ },
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -6439,6 +6504,14 @@
"mime-types": "^2.1.12"
}
},
+ "formdata-polyfill": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
+ "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+ "requires": {
+ "fetch-blob": "^3.1.2"
+ }
+ },
"forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -6450,13 +6523,13 @@
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="
},
"fs-extra": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
- "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.0.tgz",
+ "integrity": "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==",
"requires": {
"graceful-fs": "^4.2.0",
- "jsonfile": "^4.0.0",
- "universalify": "^0.1.0"
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
}
},
"fs.realpath": {
@@ -6803,11 +6876,12 @@
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="
},
"jsonfile": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
- "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"requires": {
- "graceful-fs": "^4.1.6"
+ "graceful-fs": "^4.1.6",
+ "universalify": "^2.0.0"
}
},
"jsprim": {
@@ -7028,6 +7102,11 @@
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
},
+ "node-domexception": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="
+ },
"node-emoji": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz",
@@ -7037,6 +7116,16 @@
"lodash": "^4.17.21"
}
},
+ "node-fetch": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.0.tgz",
+ "integrity": "sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==",
+ "requires": {
+ "data-uri-to-buffer": "^4.0.0",
+ "fetch-blob": "^3.1.4",
+ "formdata-polyfill": "^4.0.10"
+ }
+ },
"node-releases": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
@@ -8378,9 +8467,9 @@
}
},
"universalify": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
- "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
+ "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ=="
},
"unpipe": {
"version": "1.0.0",
@@ -8449,6 +8538,11 @@
"integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==",
"dev": true
},
+ "web-streams-polyfill": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
+ "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q=="
+ },
"wordwrap": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
diff --git a/package.json b/package.json
index c74a6a5..bb1fa5d 100644
--- a/package.json
+++ b/package.json
@@ -4,25 +4,32 @@
"description": "A all in one system for controlling addressable LEDs from a Raspberry Pi",
"scripts": {
"start": "node app.js",
- "dev": "node runDev.js",
-
+ "dev": "node runDev.cjs",
"dev:frontend": "rollup -c -w",
"build:frontend": "rollup -c",
"build:fontawesome": "mkdir -p public/assets/vendor/@fortawesome/fontawesome-free/webfonts && mkdir -p public/assets/vendor/@fortawesome/fontawesome-free/css && cp -a ./node_modules/@fortawesome/fontawesome-free/webfonts public/assets/vendor/@fortawesome/fontawesome-free/ && cp ./node_modules/@fortawesome/fontawesome-free/css/all.min.css public/assets/vendor/@fortawesome/fontawesome-free/css/all.min.css",
"build:dialog-polyfill": "mkdir -p public/assets/vendor/dialog-polyfill && cp ./node_modules/dialog-polyfill/dist/dialog-polyfill.css public/assets/vendor/dialog-polyfill/dialog-polyfill.css",
- "prebuild:docs": "pip3 install mkdocs mkdocs-gitbook pygments pymdown-extensions mkdocstrings mkdocstrings-python",
+ "prebuild:docs": "pip3 install mkdocs mkdocs-material pygments pymdown-extensions mkdocstrings mkdocstrings-python",
"build:docs": "mkdocs build",
"build": "npm run build:frontend && npm run build:fontawesome && npm run build:dialog-polyfill && npm run build:docs",
-
- "prepack": "npm run build"
+ "prepack": "npm run build",
+ "postinstall": "bash ./bin/postinstall.sh"
},
- "bundleDependencies": true,
+ "type": "module",
+ "bundleDependencies": [
+ "crypto-js",
+ "express",
+ "fs-extra",
+ "ini",
+ "node-fetch",
+ "socket.io"
+ ],
"dependencies": {
"crypto-js": "^3.1.9-1",
"express": "^4.16.3",
- "fs-extra": "^8.1.0",
+ "fs-extra": "^11.1.0",
"ini": "^2.0.0",
- "request": "^2.88.0",
+ "node-fetch": "^3.3.0",
"socket.io": "^4.1.3"
},
"devDependencies": {
diff --git a/public/docs/Scripting/NeoBehaviour/index.html b/public/docs/Scripting/NeoBehaviour/index.html
index 7d049d8..07bc222 100644
--- a/public/docs/Scripting/NeoBehaviour/index.html
+++ b/public/docs/Scripting/NeoBehaviour/index.html
@@ -1098,8 +1098,8 @@ When this method is called, variables can be declared using self.declare()
value
- property
writable
+ property
diff --git a/public/docs/Scripting/Strip/index.html b/public/docs/Scripting/Strip/index.html
index 4f24eda..d17627c 100644
--- a/public/docs/Scripting/Strip/index.html
+++ b/public/docs/Scripting/Strip/index.html
@@ -739,8 +739,8 @@ be setup in your module with the name strip
.
brightness
- property
writable
+ property
@@ -761,8 +761,8 @@ be setup in your module with the name strip
.
power_on
- property
writable
+ property
diff --git a/public/docs/sitemap.xml b/public/docs/sitemap.xml
index 45f675a..e71d331 100644
--- a/public/docs/sitemap.xml
+++ b/public/docs/sitemap.xml
@@ -2,62 +2,62 @@
None
- 2022-12-12
+ 2022-12-17
daily
None
- 2022-12-12
+ 2022-12-17
daily
None
- 2022-12-12
+ 2022-12-17
daily
None
- 2022-12-12
+ 2022-12-17
daily
None
- 2022-12-12
+ 2022-12-17
daily
None
- 2022-12-12
+ 2022-12-17
daily
None
- 2022-12-12
+ 2022-12-17
daily
None
- 2022-12-12
+ 2022-12-17
daily
None
- 2022-12-12
+ 2022-12-17
daily
None
- 2022-12-12
+ 2022-12-17
daily
None
- 2022-12-12
+ 2022-12-17
daily
None
- 2022-12-12
+ 2022-12-17
daily
\ No newline at end of file
diff --git a/public/docs/sitemap.xml.gz b/public/docs/sitemap.xml.gz
index bd43f17..e0dc09f 100644
Binary files a/public/docs/sitemap.xml.gz and b/public/docs/sitemap.xml.gz differ
diff --git a/runDev.cjs b/runDev.cjs
new file mode 100644
index 0000000..4fafa29
--- /dev/null
+++ b/runDev.cjs
@@ -0,0 +1,346 @@
+let fs = require("fs");
+let chokidar = require('chokidar');
+let blessed = require('blessed');
+let contrib = require('blessed-contrib');
+let colors = require('colors');
+let { spawn } = require("child_process");
+Tail = require('tail').Tail;
+
+
+/**
+ * CONFIG
+ */
+
+webpackLaunchCommand = ["npm", "run", "dev:frontend"];
+nodejsLaunchCommand = ["node", "app.js", `dev`];
+mkdocsLaunchCommand = ["mkdocs", "build"];
+
+nodejsFileWatcherPaths = [
+ "app.js",
+ "src/"
+];
+nodejsFileWatcherIgnore = [
+ ".log"
+];
+
+mkdocsFileWatcherPaths = [
+ "docs/",
+ "mkdocs.yml"
+];
+mkdocsFileWatcherIgnore = [
+
+];
+/*
+ * END OF CONFIG
+ */
+
+class watcher {
+
+ constructor(include, ignore, out, label, callback) {
+ this.include = include;
+ this.ignore = ignore;
+ this.out = out;
+ this.label = label;
+ this.callback = callback;
+
+ this.fswatcher = this.setup(() => {
+ this.ready()
+ })
+ }
+
+ setup(callback) {
+ return chokidar.watch(this.include).on("ready", () => {
+ callback();
+ })
+ }
+
+ ready() {
+ this.out.log(colors.magenta(this.label) + ": Watching files...");
+ this.fswatcher
+ .on("add", this.eventHandler.bind(this))
+ .on("change", this.eventHandler.bind(this))
+ .on("unlink", this.eventHandler.bind(this))
+ .on("addDir", this.eventHandler.bind(this))
+ .on("unlinkDir", this.eventHandler.bind(this));
+ }
+
+ eventHandler(path) {
+ for (let i=0; i < this.ignore.length; i++) {
+ if (path.includes(this.ignore[i])) {
+ this.out.log(colors.magenta(this.label) + ": " + colors.red("IGNORED") + ` ${path}`);
+ return;
+ }
+ }
+
+ this.out.log(colors.magenta(this.label) + `: ${path}`);
+ this.callback();
+ }
+
+ exit() {
+ this.fswatcher.close();
+ }
+
+}
+
+// This is obv. not good OOP, but it is easy...
+class runDevApp {
+
+ constructor(webpackLaunchCommand,
+ nodejsLaunchCommand,
+ mkdocsLaunchCommand,
+ nodejsFileWatcherPaths,
+ nodejsFileWatcherIgnore,
+ mkdocsFileWatcherPaths,
+ mkdocsFileWatcherIgnore
+ ) {
+ this.processList = [];
+ this.nodeRestarting = false;
+
+ this.ensureUserdirectories();
+ this.setupBlessed();
+
+ this.webpackProcessPID = this.spawnNewProcess(webpackLaunchCommand, this.webpackLog);
+ this.nodejsPID = this.spawnNewProcess(nodejsLaunchCommand, this.nodeLog);
+
+ this.docsWatcher = new watcher(
+ mkdocsFileWatcherPaths,
+ mkdocsFileWatcherIgnore,
+ this.fswatchLog, "DOCS",
+ () => {
+ this.spawnNewProcess(mkdocsLaunchCommand, this.mkdocsLog);
+ }
+ );
+
+ this.nodeWatcher = new watcher(
+ nodejsFileWatcherPaths,
+ nodejsFileWatcherIgnore,
+ this.fswatchLog, "NODE",
+ () => {
+ if (!this.nodeRestarting) {
+ this.nodeRestarting = true;
+
+ if (this.processList.hasOwnProperty(this.nodejsPID)) {
+ this.nodeLog.log("Restarting node...");
+ this.processList[this.nodejsPID][1].kill(1);
+ this.scriptLog.log(colors.magenta(this.nodejsPID) + ": " + colors.red("Kill sendt"));
+ } else {
+ this.nodeLog.log("Starting node...");
+ }
+
+ var exitWait = setInterval(() => {
+ if (!this.processList.hasOwnProperty(this.nodejsPID)) {
+ clearInterval(exitWait);
+ this.nodejsPID = this.spawnNewProcess(
+ nodejsLaunchCommand,
+ this.nodeLog
+ );
+ this.nodeRestarting = false;
+ }
+ }, 100);
+ this.scriptLog.log(colors.magenta(this.nodejsPID) + ": Waiting till exit");
+ }
+ });
+
+ }
+
+ ensureUserdirectories() {
+ // Generate all the temporary userdata-folder nececatty for the main node app
+ if (!fs.existsSync("./tmp")) {
+ fs.mkdirSync("./tmp")
+ }
+ if (!fs.existsSync("./tmp/userdata")) {
+ fs.mkdirSync("./tmp/userdata")
+ }
+ if (!fs.existsSync("./tmp")) {
+ fs.mkdirSync("./tmp/userdata")
+ }
+ }
+
+ setupBlessed() {
+ this.screen = blessed.screen();
+ this.grid = new contrib.grid({rows: 12, cols: 12, screen: this.screen});
+
+ this.logDefaultOptions = {
+ fg: "green",
+ selectedFg: "green",
+ height: '100%',
+ scrollable: true,
+ alwaysScroll: true,
+ scrollbar: {
+ ch: ' ',
+ inverse: true
+ },
+ mouse: true,
+ };
+
+ this.fswatchLog = this.grid.set(0, 2, 4, 3, blessed.log,
+ Object.assign({}, this.logDefaultOptions, {
+ label: 'Watcher',
+ }));
+ this.scriptLog = this.grid.set(2, 0, 6, 2, blessed.log,
+ Object.assign({}, this.logDefaultOptions, {
+ label: 'Actions',
+ }));
+ this.nodeLog = this.grid.set(6, 5, 6, 7, blessed.log,
+ Object.assign({}, this.logDefaultOptions, {
+ label: 'Node',
+ }));
+ this.mkdocsLog = this.grid.set(4, 2, 8, 3, blessed.log,
+ Object.assign({}, this.logDefaultOptions, {
+ label: 'MkDocs',
+ }));
+ this.webpackLog = this.grid.set(0, 5, 6, 7, blessed.log,
+ Object.assign({}, this.logDefaultOptions, {
+ label: 'Webpack',
+ border: {type: "line", fg: "yellow"}
+ }));
+ this.activeProcessesTable = this.grid.set(8, 0, 4, 2, contrib.table, {
+ keys: true,
+ fg: 'green',
+ selectedFg: 'black',
+ selectedBg: 'green',
+ interactive: true,
+ label: 'Active Processes',
+ width: '100%',
+ height: '100%',
+ border: {type: "line", fg: "cyan"},
+ columnSpacing: 2, //in chars
+ columnWidth: [6, 20] /*in chars*/
+ });
+ this.processCount = this.grid.set(0, 0, 2, 2, contrib.lcd, {
+ segmentWidth: 0.1,
+ segmentInterval: 0.06,
+ strokeWidth: 0.2,
+ elements: 3,
+ display: 0,
+ elementSpacing: 4,
+ elementPadding: 0,
+ color: "green",
+ label: "Process count"
+ });
+
+ this.activeProcessesTable.focus();
+
+ this.screen.key(['escape', 'q', 'C-c', "s"], function(ch, key) {
+ this.exit();
+ }.bind(this));
+ }
+
+ updateTableOfProcesses() {
+ let newTableData = [];
+ let that = this;
+
+ Object.keys(this.processList).forEach(function(key, index) {
+ newTableData.push( [key, that.processList[key][0]] );
+ }, this.processList);
+
+ this.activeProcessesTable.setData({
+ headers:[" PID", " Command"],
+ data: newTableData
+ });
+
+ let processN = Object.keys(this.processList).length;
+ if (processN > 2) {
+ this.processCount.setOptions({color: "yellow"});
+ } else if (processN < 1) {
+ this.processCount.setOptions({color: "green"});
+ } else {
+ this.processCount.setOptions({color: "blue"});
+ }
+ this.processCount.setDisplay(processN);
+
+ this.screen.render()
+ }
+
+ logProcessOutput(data, out) {
+ let lines = data.toString().split(/\r?\n/);
+ for (let i=0; i < lines.length; i++) {
+ out.log(lines[i].replace("\n", ""));
+ }
+ }
+
+ spawnNewProcess(args, out) {
+ // Spawn the new process with "unbuffer"
+ const proc = spawn("unbuffer", args, {
+ shell: true,
+ cwd: __dirname
+ });
+
+ proc.stdout.on("data", (data) => {
+ this.logProcessOutput(data, out)
+ });
+ proc.stderr.on("data", (data) => {
+ this.logProcessOutput(data, out)
+ });
+ proc.on("error", () => {
+ out.log(colors.red("Failed to start node..."));
+ });
+ proc.on("exit", (code) => {
+ out.log(colors.yellow("Childprocess unresponsive..."));
+ });
+ proc.on("close", (code) => {
+
+ if (code != undefined) {
+ out.log(colors.red("Process exited with code ") + colors.yellow(code.toString()));
+ this.scriptLog.log(colors.magenta(proc.pid) + ":" + colors.red(" Exited with ") + colors.yellow(code));
+ } else {
+ out.log(colors.red("Process exited without code"));
+ this.scriptLog.log(colors.magenta(proc.pid) + ":" + colors.red(" Exited no code"));
+ }
+
+ delete this.processList[proc.pid.toString()];
+ this.updateTableOfProcesses()
+ });
+
+ this.processList[proc.pid.toString()] = [args.join(" "), proc];
+ this.scriptLog.log(colors.magenta(proc.pid) + `: New process`);
+ process.stdout.write("\x07");
+
+ this.updateTableOfProcesses();
+ return proc.pid;
+ }
+
+ exit() {
+ // Stage one : Stop watchers
+ this.docsWatcher.exit();
+ this.nodeWatcher.exit();
+
+ // Stage two : Send kill signal to all child-processes
+ Object.keys(this.processList).forEach((key, index) => {
+ this.scriptLog.log(colors.magenta(key) + ":" + colors.red(" KILL SENDT"));
+ this.processList[key][1].kill(1);
+ }, this.processList);
+
+ // Stage three : wait a second before starting to check if all
+ // process' are dead.
+ setTimeout(() => {
+ var exitWait = setInterval(() => {
+ this.screen.render(); // Render each time to make sure updates are displayed
+ if (this.processList.length > 0) {
+ clearInterval(exitWait),
+ this.scriptLog.log("");
+ this.scriptLog.log("Process' dead");
+ this.scriptLog.log("Exiting...");
+ this.processCount.setOptions({color: "green"});
+ this.processCount.setDisplay("EXIT");
+ this.screen.render();
+ setTimeout(() => {
+ process.exit(0)
+ }, 3000);
+ }
+ }, 100);
+ }, 1000);
+ }
+
+}
+
+
+let app = new runDevApp(
+ webpackLaunchCommand,
+ nodejsLaunchCommand,
+ mkdocsLaunchCommand,
+ nodejsFileWatcherPaths,
+ nodejsFileWatcherIgnore,
+ mkdocsFileWatcherPaths,
+ mkdocsFileWatcherIgnore
+);
diff --git a/runDev.js b/runDev.js
deleted file mode 100644
index 4fafa29..0000000
--- a/runDev.js
+++ /dev/null
@@ -1,346 +0,0 @@
-let fs = require("fs");
-let chokidar = require('chokidar');
-let blessed = require('blessed');
-let contrib = require('blessed-contrib');
-let colors = require('colors');
-let { spawn } = require("child_process");
-Tail = require('tail').Tail;
-
-
-/**
- * CONFIG
- */
-
-webpackLaunchCommand = ["npm", "run", "dev:frontend"];
-nodejsLaunchCommand = ["node", "app.js", `dev`];
-mkdocsLaunchCommand = ["mkdocs", "build"];
-
-nodejsFileWatcherPaths = [
- "app.js",
- "src/"
-];
-nodejsFileWatcherIgnore = [
- ".log"
-];
-
-mkdocsFileWatcherPaths = [
- "docs/",
- "mkdocs.yml"
-];
-mkdocsFileWatcherIgnore = [
-
-];
-/*
- * END OF CONFIG
- */
-
-class watcher {
-
- constructor(include, ignore, out, label, callback) {
- this.include = include;
- this.ignore = ignore;
- this.out = out;
- this.label = label;
- this.callback = callback;
-
- this.fswatcher = this.setup(() => {
- this.ready()
- })
- }
-
- setup(callback) {
- return chokidar.watch(this.include).on("ready", () => {
- callback();
- })
- }
-
- ready() {
- this.out.log(colors.magenta(this.label) + ": Watching files...");
- this.fswatcher
- .on("add", this.eventHandler.bind(this))
- .on("change", this.eventHandler.bind(this))
- .on("unlink", this.eventHandler.bind(this))
- .on("addDir", this.eventHandler.bind(this))
- .on("unlinkDir", this.eventHandler.bind(this));
- }
-
- eventHandler(path) {
- for (let i=0; i < this.ignore.length; i++) {
- if (path.includes(this.ignore[i])) {
- this.out.log(colors.magenta(this.label) + ": " + colors.red("IGNORED") + ` ${path}`);
- return;
- }
- }
-
- this.out.log(colors.magenta(this.label) + `: ${path}`);
- this.callback();
- }
-
- exit() {
- this.fswatcher.close();
- }
-
-}
-
-// This is obv. not good OOP, but it is easy...
-class runDevApp {
-
- constructor(webpackLaunchCommand,
- nodejsLaunchCommand,
- mkdocsLaunchCommand,
- nodejsFileWatcherPaths,
- nodejsFileWatcherIgnore,
- mkdocsFileWatcherPaths,
- mkdocsFileWatcherIgnore
- ) {
- this.processList = [];
- this.nodeRestarting = false;
-
- this.ensureUserdirectories();
- this.setupBlessed();
-
- this.webpackProcessPID = this.spawnNewProcess(webpackLaunchCommand, this.webpackLog);
- this.nodejsPID = this.spawnNewProcess(nodejsLaunchCommand, this.nodeLog);
-
- this.docsWatcher = new watcher(
- mkdocsFileWatcherPaths,
- mkdocsFileWatcherIgnore,
- this.fswatchLog, "DOCS",
- () => {
- this.spawnNewProcess(mkdocsLaunchCommand, this.mkdocsLog);
- }
- );
-
- this.nodeWatcher = new watcher(
- nodejsFileWatcherPaths,
- nodejsFileWatcherIgnore,
- this.fswatchLog, "NODE",
- () => {
- if (!this.nodeRestarting) {
- this.nodeRestarting = true;
-
- if (this.processList.hasOwnProperty(this.nodejsPID)) {
- this.nodeLog.log("Restarting node...");
- this.processList[this.nodejsPID][1].kill(1);
- this.scriptLog.log(colors.magenta(this.nodejsPID) + ": " + colors.red("Kill sendt"));
- } else {
- this.nodeLog.log("Starting node...");
- }
-
- var exitWait = setInterval(() => {
- if (!this.processList.hasOwnProperty(this.nodejsPID)) {
- clearInterval(exitWait);
- this.nodejsPID = this.spawnNewProcess(
- nodejsLaunchCommand,
- this.nodeLog
- );
- this.nodeRestarting = false;
- }
- }, 100);
- this.scriptLog.log(colors.magenta(this.nodejsPID) + ": Waiting till exit");
- }
- });
-
- }
-
- ensureUserdirectories() {
- // Generate all the temporary userdata-folder nececatty for the main node app
- if (!fs.existsSync("./tmp")) {
- fs.mkdirSync("./tmp")
- }
- if (!fs.existsSync("./tmp/userdata")) {
- fs.mkdirSync("./tmp/userdata")
- }
- if (!fs.existsSync("./tmp")) {
- fs.mkdirSync("./tmp/userdata")
- }
- }
-
- setupBlessed() {
- this.screen = blessed.screen();
- this.grid = new contrib.grid({rows: 12, cols: 12, screen: this.screen});
-
- this.logDefaultOptions = {
- fg: "green",
- selectedFg: "green",
- height: '100%',
- scrollable: true,
- alwaysScroll: true,
- scrollbar: {
- ch: ' ',
- inverse: true
- },
- mouse: true,
- };
-
- this.fswatchLog = this.grid.set(0, 2, 4, 3, blessed.log,
- Object.assign({}, this.logDefaultOptions, {
- label: 'Watcher',
- }));
- this.scriptLog = this.grid.set(2, 0, 6, 2, blessed.log,
- Object.assign({}, this.logDefaultOptions, {
- label: 'Actions',
- }));
- this.nodeLog = this.grid.set(6, 5, 6, 7, blessed.log,
- Object.assign({}, this.logDefaultOptions, {
- label: 'Node',
- }));
- this.mkdocsLog = this.grid.set(4, 2, 8, 3, blessed.log,
- Object.assign({}, this.logDefaultOptions, {
- label: 'MkDocs',
- }));
- this.webpackLog = this.grid.set(0, 5, 6, 7, blessed.log,
- Object.assign({}, this.logDefaultOptions, {
- label: 'Webpack',
- border: {type: "line", fg: "yellow"}
- }));
- this.activeProcessesTable = this.grid.set(8, 0, 4, 2, contrib.table, {
- keys: true,
- fg: 'green',
- selectedFg: 'black',
- selectedBg: 'green',
- interactive: true,
- label: 'Active Processes',
- width: '100%',
- height: '100%',
- border: {type: "line", fg: "cyan"},
- columnSpacing: 2, //in chars
- columnWidth: [6, 20] /*in chars*/
- });
- this.processCount = this.grid.set(0, 0, 2, 2, contrib.lcd, {
- segmentWidth: 0.1,
- segmentInterval: 0.06,
- strokeWidth: 0.2,
- elements: 3,
- display: 0,
- elementSpacing: 4,
- elementPadding: 0,
- color: "green",
- label: "Process count"
- });
-
- this.activeProcessesTable.focus();
-
- this.screen.key(['escape', 'q', 'C-c', "s"], function(ch, key) {
- this.exit();
- }.bind(this));
- }
-
- updateTableOfProcesses() {
- let newTableData = [];
- let that = this;
-
- Object.keys(this.processList).forEach(function(key, index) {
- newTableData.push( [key, that.processList[key][0]] );
- }, this.processList);
-
- this.activeProcessesTable.setData({
- headers:[" PID", " Command"],
- data: newTableData
- });
-
- let processN = Object.keys(this.processList).length;
- if (processN > 2) {
- this.processCount.setOptions({color: "yellow"});
- } else if (processN < 1) {
- this.processCount.setOptions({color: "green"});
- } else {
- this.processCount.setOptions({color: "blue"});
- }
- this.processCount.setDisplay(processN);
-
- this.screen.render()
- }
-
- logProcessOutput(data, out) {
- let lines = data.toString().split(/\r?\n/);
- for (let i=0; i < lines.length; i++) {
- out.log(lines[i].replace("\n", ""));
- }
- }
-
- spawnNewProcess(args, out) {
- // Spawn the new process with "unbuffer"
- const proc = spawn("unbuffer", args, {
- shell: true,
- cwd: __dirname
- });
-
- proc.stdout.on("data", (data) => {
- this.logProcessOutput(data, out)
- });
- proc.stderr.on("data", (data) => {
- this.logProcessOutput(data, out)
- });
- proc.on("error", () => {
- out.log(colors.red("Failed to start node..."));
- });
- proc.on("exit", (code) => {
- out.log(colors.yellow("Childprocess unresponsive..."));
- });
- proc.on("close", (code) => {
-
- if (code != undefined) {
- out.log(colors.red("Process exited with code ") + colors.yellow(code.toString()));
- this.scriptLog.log(colors.magenta(proc.pid) + ":" + colors.red(" Exited with ") + colors.yellow(code));
- } else {
- out.log(colors.red("Process exited without code"));
- this.scriptLog.log(colors.magenta(proc.pid) + ":" + colors.red(" Exited no code"));
- }
-
- delete this.processList[proc.pid.toString()];
- this.updateTableOfProcesses()
- });
-
- this.processList[proc.pid.toString()] = [args.join(" "), proc];
- this.scriptLog.log(colors.magenta(proc.pid) + `: New process`);
- process.stdout.write("\x07");
-
- this.updateTableOfProcesses();
- return proc.pid;
- }
-
- exit() {
- // Stage one : Stop watchers
- this.docsWatcher.exit();
- this.nodeWatcher.exit();
-
- // Stage two : Send kill signal to all child-processes
- Object.keys(this.processList).forEach((key, index) => {
- this.scriptLog.log(colors.magenta(key) + ":" + colors.red(" KILL SENDT"));
- this.processList[key][1].kill(1);
- }, this.processList);
-
- // Stage three : wait a second before starting to check if all
- // process' are dead.
- setTimeout(() => {
- var exitWait = setInterval(() => {
- this.screen.render(); // Render each time to make sure updates are displayed
- if (this.processList.length > 0) {
- clearInterval(exitWait),
- this.scriptLog.log("");
- this.scriptLog.log("Process' dead");
- this.scriptLog.log("Exiting...");
- this.processCount.setOptions({color: "green"});
- this.processCount.setDisplay("EXIT");
- this.screen.render();
- setTimeout(() => {
- process.exit(0)
- }, 3000);
- }
- }, 100);
- }, 1000);
- }
-
-}
-
-
-let app = new runDevApp(
- webpackLaunchCommand,
- nodejsLaunchCommand,
- mkdocsLaunchCommand,
- nodejsFileWatcherPaths,
- nodejsFileWatcherIgnore,
- mkdocsFileWatcherPaths,
- mkdocsFileWatcherIgnore
-);
diff --git a/src/Logger/index.cjs b/src/Logger/index.cjs
new file mode 100644
index 0000000..2ee216a
--- /dev/null
+++ b/src/Logger/index.cjs
@@ -0,0 +1,79 @@
+let fse = require("fs-extra");
+
+const level = {
+ EMERG: "EMERGENCY",
+ ALERT: "ALERT",
+ CRIT: "CRITICAL",
+ ERROR: "ERROR",
+ WARNING: "WARNING",
+ NOTICE: "NOTICE",
+ INFO: "INFO",
+ DEBUG: "DEBUG",
+
+ ACCESS: ""
+};
+
+
+Object.defineProperty(String.prototype, "lPad", {
+ value: function lPad(len, chr="0") {
+ str = this;
+ var i = -1;
+ if (!chr && chr !== 0) chr = ' ';
+ len = len - this.length;
+ while (++i < len) {
+ str = chr + str;
+ }
+ return str;
+ },
+ writeable: true,
+ configurable: true
+});
+
+
+function getTimeStamp() {
+ let CDate = new Date();
+ let day = CDate.getDate().toString().lPad(2);
+ let month = (CDate.getMonth() + 1).toString().lPad(2); // +1 because js starts to count at 0
+ let year = CDate.getFullYear();
+ let hour = CDate.getHours().toString().lPad(2);
+ let min = CDate.getMinutes().toString().lPad(2);
+ let sec = CDate.getSeconds().toString().lPad(2);
+ let ms = Math.round(CDate.getMilliseconds() / 10).toString().lPad(2); // divide by 10 to make the last digit decimal, then round.
+
+ return `${day}.${month}.${year}-${hour}:${min}:${sec}.${ms}`;
+}
+
+
+function log(object, logLevel=level.DEBUG, file="/lux-neo.log") {
+ fse.ensureFileSync(__logdir + file);
+
+ let formattedLogString = `[${getTimeStamp()}] ${logLevel} ${object}`;
+ console.log(formattedLogString); // @TODO: This should probably be removed, used for dev currently
+
+
+ fse.appendFile(
+ __logdir + "/lux-neo.log",
+ formattedLogString + '\n'
+ ).catch(err => {
+ console.log("EMERGENCY Could not write to log-file 'lux-neo.log'...");
+ console.log("DEBUG FileWriteError: " + err)
+ });
+
+ if (__event != undefined) {
+ __event.emit("logger", logLevel, object);
+ }
+}
+
+module.exports = {
+ level,
+ log,
+ emerg: (object) => { log(object, level.EMERG); },
+ alert: (object) => { log(object, level.ALERT); },
+ crit: (object) => { log(object, level.CRIT); },
+ error: (object) => { log(object, level.ERROR); },
+ warning: (object) => { log(object, level.WARNING); },
+ notice: (object) => { log(object, level.NOTICE); },
+ info: (object) => { log(object, level.INFO); },
+ debug: (object) => { log(object, level.DEBUG); },
+ access: (object) => { log(object, level.ACCESS, file="/access.log"); }
+};
diff --git a/src/Logger/index.js b/src/Logger/index.js
deleted file mode 100644
index 2ee216a..0000000
--- a/src/Logger/index.js
+++ /dev/null
@@ -1,79 +0,0 @@
-let fse = require("fs-extra");
-
-const level = {
- EMERG: "EMERGENCY",
- ALERT: "ALERT",
- CRIT: "CRITICAL",
- ERROR: "ERROR",
- WARNING: "WARNING",
- NOTICE: "NOTICE",
- INFO: "INFO",
- DEBUG: "DEBUG",
-
- ACCESS: ""
-};
-
-
-Object.defineProperty(String.prototype, "lPad", {
- value: function lPad(len, chr="0") {
- str = this;
- var i = -1;
- if (!chr && chr !== 0) chr = ' ';
- len = len - this.length;
- while (++i < len) {
- str = chr + str;
- }
- return str;
- },
- writeable: true,
- configurable: true
-});
-
-
-function getTimeStamp() {
- let CDate = new Date();
- let day = CDate.getDate().toString().lPad(2);
- let month = (CDate.getMonth() + 1).toString().lPad(2); // +1 because js starts to count at 0
- let year = CDate.getFullYear();
- let hour = CDate.getHours().toString().lPad(2);
- let min = CDate.getMinutes().toString().lPad(2);
- let sec = CDate.getSeconds().toString().lPad(2);
- let ms = Math.round(CDate.getMilliseconds() / 10).toString().lPad(2); // divide by 10 to make the last digit decimal, then round.
-
- return `${day}.${month}.${year}-${hour}:${min}:${sec}.${ms}`;
-}
-
-
-function log(object, logLevel=level.DEBUG, file="/lux-neo.log") {
- fse.ensureFileSync(__logdir + file);
-
- let formattedLogString = `[${getTimeStamp()}] ${logLevel} ${object}`;
- console.log(formattedLogString); // @TODO: This should probably be removed, used for dev currently
-
-
- fse.appendFile(
- __logdir + "/lux-neo.log",
- formattedLogString + '\n'
- ).catch(err => {
- console.log("EMERGENCY Could not write to log-file 'lux-neo.log'...");
- console.log("DEBUG FileWriteError: " + err)
- });
-
- if (__event != undefined) {
- __event.emit("logger", logLevel, object);
- }
-}
-
-module.exports = {
- level,
- log,
- emerg: (object) => { log(object, level.EMERG); },
- alert: (object) => { log(object, level.ALERT); },
- crit: (object) => { log(object, level.CRIT); },
- error: (object) => { log(object, level.ERROR); },
- warning: (object) => { log(object, level.WARNING); },
- notice: (object) => { log(object, level.NOTICE); },
- info: (object) => { log(object, level.INFO); },
- debug: (object) => { log(object, level.DEBUG); },
- access: (object) => { log(object, level.ACCESS, file="/access.log"); }
-};
diff --git a/src/NeoRuntimeManager/IPC.cjs b/src/NeoRuntimeManager/IPC.cjs
new file mode 100644
index 0000000..0684418
--- /dev/null
+++ b/src/NeoRuntimeManager/IPC.cjs
@@ -0,0 +1,199 @@
+/**
+ * This module is used to communicate with a python NeoRuntime instance.
+ *
+ * @author jakobst1n.
+ * @since 3.10.2021
+ */
+
+const net = require("net");
+let logger = require("../Logger/index.cjs");
+
+/** @type {int} How long wait between each reconnection attempt */
+const RECONNECT_INTERVAL = 1000;
+/** @type {Object} ENUM-ish for command that can be sent to neoruntime */
+const COMMAND = Object.freeze({SET_GLOB : 0,
+ SET_VAR : 1,
+ SET_SEND_STRIP_BUF: 2});
+/** @type {Object} ENUM-ish for globvars */
+const GLOBVAR = Object.freeze({POWER_ON : 0,
+ BRIGHTNESS: 1});
+/** @type {Object} ENUM-ish for what type of data neoruntime sends */
+const DATATYPE = Object.freeze({STATES : 1,
+ STRIP_BUF: 2,
+ MATRIX: 3});
+
+/**
+ * class that will keep a active connection to a socket if possible, and
+ * automatically reconnect. It will emit events when data is received,
+ * and it will send commands to the process. */
+class IPC {
+
+ constructor(_socketFile, _eventEmitter) {
+ this.socketFile = _socketFile;
+ this.eventEmitter = _eventEmitter;
+
+ this.client;
+ this.connected = false;
+ this.reconnectInterval = false;
+
+ this.globvars = {};
+ this.variables = {};
+
+ this.reconnect();
+ }
+
+ /**
+ * If we are not already attempting to reconnect, this will start a
+ * interval that tries to reconnect. */
+ reconnect() {
+ if (this.reconnectInterval === false) {
+ this.reconnectInterval = setInterval(this.tryOpenSocketConnection.bind(this), RECONNECT_INTERVAL);
+ }
+ }
+
+ /**
+ * This will attempt to connect to the socket, and then setup all listeners
+ * if it succedes. */
+ tryOpenSocketConnection() {
+ // logger.info("Attempting to start IPC");
+
+ this.client = net.createConnection(this.socketFile)
+ .on('connect', () => {
+ clearInterval(this.reconnectInterval);
+ this.reconnectInterval = false;
+ logger.info("IPC Connected.");
+ })
+ .on("ready", () => {
+ this.connected = true;
+ })
+ .on('data', (data) => {
+ let json_data;
+ switch (data[0]) {
+ case DATATYPE.STATES:
+ try {
+ json_data = JSON.parse(data.toString("ascii", 1));
+ } catch (e) {
+ logger.warning("Could not parse json data from neoruntime");
+ return;
+ }
+
+ if (json_data.hasOwnProperty("globvars")) {
+ forEachDiff(json_data["globvars"], this.globvars, (key, newVal) => {
+ this.eventEmitter.emit("change", key, newVal);
+ });
+ this.globvars = json_data["globvars"];
+ }
+ if (json_data.hasOwnProperty("variables")) {
+ forEachDiff(json_data["variables"], this.variables, (key, newVal) => {
+ this.eventEmitter.emit("change", `variable/${key}`, newVal);
+ });
+ this.variables = json_data["variables"];
+ }
+ break;
+
+ case DATATYPE.MATRIX:
+ try {
+ json_data = JSON.parse(data.toString("ascii", 1));
+ } catch (e) {
+ logger.warning("Could not parse json data from neoruntime");
+ console.log(e);
+ }
+ this.eventEmitter.emit("matrix", json_data);
+ break;
+
+ case DATATYPE.STRIP_BUF:
+ this.eventEmitter.emit("strip_buffer", Array.from(data.values()).slice(1));
+ break;
+
+ default:
+ logger.info(data);
+ }
+
+ })
+ .on("timeout", () => {
+ logger.info("IPC Timeout");
+ })
+ .on("close", (hadError) => {
+ logger.info("IPC Close, hadError: ", hadError);
+ this.connected = false;
+ this.reconnect();
+ })
+ .on("end", () => {
+ logger.info("IPC End");
+ this.connected = false;
+ })
+ .on('error', (data) => {
+ logger.info('IPC Server not active.');
+ this.connected = false;
+ this.reconnect();
+ })
+ ;
+ }
+
+ /**
+ * Will send a command to the socket if we have a active connection,
+ * if not it will just drop the command. there is no queue implemented
+ * for such events. */
+ sendCommand(commandType, name, value) {
+ if (this.connected) {
+ let buf;
+
+ switch (commandType) {
+ case (COMMAND.SET_GLOB):
+ buf = Buffer.allocUnsafe(3);
+ buf[1] = name;
+ buf[2] = value;
+ break;
+
+ case (COMMAND.SET_VAR):
+ if (name.length > 32) { return {success: false, reason: "name too long", detail: "max size of name is 32 bytes"}; }
+ if (value.length > 93) { return {success: false, reason: "value too long", detail: "max size of value is 93 bytes"}; }
+ buf = Buffer.allocUnsafe(3 + name.length + value.length);
+ buf[1] = name.length;
+ buf[2] = value.length;
+ buf.write(name, 3, name.length, "ascii");
+ buf.write(value, 3+name.length, value.length, "ascii");
+ break;
+
+ case (COMMAND.SET_SEND_STRIP_BUF):
+ buf = Buffer.allocUnsafe(2);
+ buf[1] = (name) ? 1 : 0;
+ break;
+
+ default:
+ logger.warning(`IPC UNKNOWN COMMANDTYPE ${commandType}`)
+ return {success: false, reason: "ipc command unknown", detail: commandType};
+ }
+
+ buf[0] = commandType;
+ this.client.write(buf);
+ return {success: true}
+ }
+ return {success: false, reason: "socket not connected", detail: "This usually means the python script is not running"};
+ }
+
+}
+
+const isObject = v => v && typeof v === 'object';
+
+/**
+ * Will call callback on all the differences between the dicts
+ */
+function forEachDiff(dict1, dict2, callback) {
+ for (const key of new Set([...Object.keys(dict1), ...Object.keys(dict2)])) {
+ if (isObject(dict1[key]) && isObject(dict2[key])) {
+ if (dict1[key].value !== dict2[key].value) {
+ callback(key, dict1[key]);
+ }
+ } else if (dict1[key] !== dict2[key]) {
+ if (isObject(dict2[key]) && (dict1[key] == null)) {
+ dict2[key].value = null;
+ callback(key, dict2[key])
+ } else {
+ callback(key, dict1[key]);
+ }
+ }
+ }
+}
+
+module.exports = {IPC, COMMAND, GLOBVAR};
diff --git a/src/NeoRuntimeManager/IPC.js b/src/NeoRuntimeManager/IPC.js
deleted file mode 100644
index 56c8b5d..0000000
--- a/src/NeoRuntimeManager/IPC.js
+++ /dev/null
@@ -1,199 +0,0 @@
-/**
- * This module is used to communicate with a python NeoRuntime instance.
- *
- * @author jakobst1n.
- * @since 3.10.2021
- */
-
-const net = require("net");
-let logger = require(__appdir + "/src/Logger");
-
-/** @type {int} How long wait between each reconnection attempt */
-const RECONNECT_INTERVAL = 1000;
-/** @type {Object} ENUM-ish for command that can be sent to neoruntime */
-const COMMAND = Object.freeze({SET_GLOB : 0,
- SET_VAR : 1,
- SET_SEND_STRIP_BUF: 2});
-/** @type {Object} ENUM-ish for globvars */
-const GLOBVAR = Object.freeze({POWER_ON : 0,
- BRIGHTNESS: 1});
-/** @type {Object} ENUM-ish for what type of data neoruntime sends */
-const DATATYPE = Object.freeze({STATES : 1,
- STRIP_BUF: 2,
- MATRIX: 3});
-
-/**
- * class that will keep a active connection to a socket if possible, and
- * automatically reconnect. It will emit events when data is received,
- * and it will send commands to the process. */
-class IPC {
-
- constructor(_socketFile, _eventEmitter) {
- this.socketFile = _socketFile;
- this.eventEmitter = _eventEmitter;
-
- this.client;
- this.connected = false;
- this.reconnectInterval = false;
-
- this.globvars = {};
- this.variables = {};
-
- this.reconnect();
- }
-
- /**
- * If we are not already attempting to reconnect, this will start a
- * interval that tries to reconnect. */
- reconnect() {
- if (this.reconnectInterval === false) {
- this.reconnectInterval = setInterval(this.tryOpenSocketConnection.bind(this), RECONNECT_INTERVAL);
- }
- }
-
- /**
- * This will attempt to connect to the socket, and then setup all listeners
- * if it succedes. */
- tryOpenSocketConnection() {
- // logger.info("Attempting to start IPC");
-
- this.client = net.createConnection(this.socketFile)
- .on('connect', () => {
- clearInterval(this.reconnectInterval);
- this.reconnectInterval = false;
- logger.info("IPC Connected.");
- })
- .on("ready", () => {
- this.connected = true;
- })
- .on('data', (data) => {
- let json_data;
- switch (data[0]) {
- case DATATYPE.STATES:
- try {
- json_data = JSON.parse(data.toString("ascii", 1));
- } catch (e) {
- logger.warning("Could not parse json data from neoruntime");
- return;
- }
-
- if (json_data.hasOwnProperty("globvars")) {
- forEachDiff(json_data["globvars"], this.globvars, (key, newVal) => {
- this.eventEmitter.emit("change", key, newVal);
- });
- this.globvars = json_data["globvars"];
- }
- if (json_data.hasOwnProperty("variables")) {
- forEachDiff(json_data["variables"], this.variables, (key, newVal) => {
- this.eventEmitter.emit("change", `variable/${key}`, newVal);
- });
- this.variables = json_data["variables"];
- }
- break;
-
- case DATATYPE.MATRIX:
- try {
- json_data = JSON.parse(data.toString("ascii", 1));
- } catch (e) {
- logger.warning("Could not parse json data from neoruntime");
- console.log(e);
- }
- this.eventEmitter.emit("matrix", json_data);
- break;
-
- case DATATYPE.STRIP_BUF:
- this.eventEmitter.emit("strip_buffer", Array.from(data.values()).slice(1));
- break;
-
- default:
- logger.info(data);
- }
-
- })
- .on("timeout", () => {
- logger.info("IPC Timeout");
- })
- .on("close", (hadError) => {
- logger.info("IPC Close, hadError: ", hadError);
- this.connected = false;
- this.reconnect();
- })
- .on("end", () => {
- logger.info("IPC End");
- this.connected = false;
- })
- .on('error', (data) => {
- logger.info('IPC Server not active.');
- this.connected = false;
- this.reconnect();
- })
- ;
- }
-
- /**
- * Will send a command to the socket if we have a active connection,
- * if not it will just drop the command. there is no queue implemented
- * for such events. */
- sendCommand(commandType, name, value) {
- if (this.connected) {
- let buf;
-
- switch (commandType) {
- case (COMMAND.SET_GLOB):
- buf = Buffer.allocUnsafe(3);
- buf[1] = name;
- buf[2] = value;
- break;
-
- case (COMMAND.SET_VAR):
- if (name.length > 32) { return {success: false, reason: "name too long", detail: "max size of name is 32 bytes"}; }
- if (value.length > 93) { return {success: false, reason: "value too long", detail: "max size of value is 93 bytes"}; }
- buf = Buffer.allocUnsafe(3 + name.length + value.length);
- buf[1] = name.length;
- buf[2] = value.length;
- buf.write(name, 3, name.length, "ascii");
- buf.write(value, 3+name.length, value.length, "ascii");
- break;
-
- case (COMMAND.SET_SEND_STRIP_BUF):
- buf = Buffer.allocUnsafe(2);
- buf[1] = (name) ? 1 : 0;
- break;
-
- default:
- logger.warning(`IPC UNKNOWN COMMANDTYPE ${commandType}`)
- return {success: false, reason: "ipc command unknown", detail: commandType};
- }
-
- buf[0] = commandType;
- this.client.write(buf);
- return {success: true}
- }
- return {success: false, reason: "socket not connected", detail: "This usually means the python script is not running"};
- }
-
-}
-
-const isObject = v => v && typeof v === 'object';
-
-/**
- * Will call callback on all the differences between the dicts
- */
-function forEachDiff(dict1, dict2, callback) {
- for (const key of new Set([...Object.keys(dict1), ...Object.keys(dict2)])) {
- if (isObject(dict1[key]) && isObject(dict2[key])) {
- if (dict1[key].value !== dict2[key].value) {
- callback(key, dict1[key]);
- }
- } else if (dict1[key] !== dict2[key]) {
- if (isObject(dict2[key]) && (dict1[key] == null)) {
- dict2[key].value = null;
- callback(key, dict2[key])
- } else {
- callback(key, dict1[key]);
- }
- }
- }
-}
-
-module.exports = {IPC, COMMAND, GLOBVAR};
diff --git a/src/NeoRuntimeManager/RuntimeProcess.cjs b/src/NeoRuntimeManager/RuntimeProcess.cjs
new file mode 100644
index 0000000..be78fa9
--- /dev/null
+++ b/src/NeoRuntimeManager/RuntimeProcess.cjs
@@ -0,0 +1,106 @@
+let fs = require("fs-extra");
+let spawn = require("child_process");
+
+class RuntimeProcess {
+
+ constructor(_modePath, _eventEmitter) {
+ this.modePath = _modePath;
+ this.logfile = `${this.modePath}/mode.log`;
+ this.errfile = `${this.modePath}/mode.error`;
+
+ this.stdout = "";
+ this.stderr = "";
+
+ this.fl = false;
+ this.proc = null;
+
+ this.isRunning = false;
+ this.exitCode = null;
+
+ this.eventEmitter = _eventEmitter;
+ }
+
+ start() {
+ if (this.isRunning) {
+ console.log("PROCESS ALREADY RUNNING");
+ return {success: false, reason: "already running"};
+ }
+ this.isRunning = true;
+ this.proc = spawn.spawn(
+ // `${__appdir}/NeoRuntime/Runtime/venv/bin/python`,
+ "python3",
+ [
+ "-u", // This makes us able to get real-time output
+ `${__appdir}/NeoRuntime/Runtime/neo_runtime.py`,
+ `--strip-config="${__configdir}/strip.ini"`,
+ `--mode-path="${this.modePath}"`,
+ `--mode-entry=script`
+ ]
+ );
+
+ this.proc.on('error', (err) => {
+ console.log(err);
+ });
+
+ fs.ensureFileSync(this.logfile);
+ fs.ensureFileSync(this.errfile);
+ this.eventEmitter.emit("proc:start");
+
+ this.proc.stdout.on('data', (_stdout) => {
+ let stdout_str = _stdout.toString();
+ fs.appendFile(this.logfile, `[${timestamp()}]: ` + stdout_str);
+ this.eventEmitter.emit("proc:stdout", stdout_str);
+ });
+
+ this.proc.stdout.on('end', () => {
+ fs.appendFile(this.logfile, "\n");
+ });
+
+ this.proc.stderr.on('data', (_stderr) => {
+ let stderr_str = _stderr.toString();
+ fs.appendFile(this.errfile, `[${timestamp()}]: ` + stderr_str);
+ this.eventEmitter.emit("proc:stderr", stderr_str);
+ });
+
+ this.proc.stderr.on('end', () => {
+ fs.appendFile(this.logfile, "\n");
+ });
+
+ this.proc.on('close', (code) => {
+ if (code) {
+ fs.appendFile(this.logfile, `[${timestamp()}]: ` + "Script exited with code " + code.toString());
+ }
+ this.eventEmitter.emit("proc:exit", 0);
+ this.isRunning = false;
+ this.exitCode = code;
+ });
+
+ return {success: true};
+ }
+
+ stop(restart=false) {
+ try {
+ if (restart) {
+ this.proc.once("close", () => {
+ setTimeout(() => this.start(), 500);
+ });
+ }
+ this.proc.kill("SIGINT");
+ return {success: true}
+ } catch (err) {
+ console.log(err);
+ return {success:false, reason:err}
+ }
+ }
+}
+
+/**
+ * Creates and returns a timestamp that can be used in logfiles.
+ *
+ * @return {string} timestamp
+ */
+function timestamp() {
+ return (new Date()).toISOString();
+}
+
+module.exports = RuntimeProcess;
diff --git a/src/NeoRuntimeManager/RuntimeProcess.js b/src/NeoRuntimeManager/RuntimeProcess.js
deleted file mode 100644
index be78fa9..0000000
--- a/src/NeoRuntimeManager/RuntimeProcess.js
+++ /dev/null
@@ -1,106 +0,0 @@
-let fs = require("fs-extra");
-let spawn = require("child_process");
-
-class RuntimeProcess {
-
- constructor(_modePath, _eventEmitter) {
- this.modePath = _modePath;
- this.logfile = `${this.modePath}/mode.log`;
- this.errfile = `${this.modePath}/mode.error`;
-
- this.stdout = "";
- this.stderr = "";
-
- this.fl = false;
- this.proc = null;
-
- this.isRunning = false;
- this.exitCode = null;
-
- this.eventEmitter = _eventEmitter;
- }
-
- start() {
- if (this.isRunning) {
- console.log("PROCESS ALREADY RUNNING");
- return {success: false, reason: "already running"};
- }
- this.isRunning = true;
- this.proc = spawn.spawn(
- // `${__appdir}/NeoRuntime/Runtime/venv/bin/python`,
- "python3",
- [
- "-u", // This makes us able to get real-time output
- `${__appdir}/NeoRuntime/Runtime/neo_runtime.py`,
- `--strip-config="${__configdir}/strip.ini"`,
- `--mode-path="${this.modePath}"`,
- `--mode-entry=script`
- ]
- );
-
- this.proc.on('error', (err) => {
- console.log(err);
- });
-
- fs.ensureFileSync(this.logfile);
- fs.ensureFileSync(this.errfile);
- this.eventEmitter.emit("proc:start");
-
- this.proc.stdout.on('data', (_stdout) => {
- let stdout_str = _stdout.toString();
- fs.appendFile(this.logfile, `[${timestamp()}]: ` + stdout_str);
- this.eventEmitter.emit("proc:stdout", stdout_str);
- });
-
- this.proc.stdout.on('end', () => {
- fs.appendFile(this.logfile, "\n");
- });
-
- this.proc.stderr.on('data', (_stderr) => {
- let stderr_str = _stderr.toString();
- fs.appendFile(this.errfile, `[${timestamp()}]: ` + stderr_str);
- this.eventEmitter.emit("proc:stderr", stderr_str);
- });
-
- this.proc.stderr.on('end', () => {
- fs.appendFile(this.logfile, "\n");
- });
-
- this.proc.on('close', (code) => {
- if (code) {
- fs.appendFile(this.logfile, `[${timestamp()}]: ` + "Script exited with code " + code.toString());
- }
- this.eventEmitter.emit("proc:exit", 0);
- this.isRunning = false;
- this.exitCode = code;
- });
-
- return {success: true};
- }
-
- stop(restart=false) {
- try {
- if (restart) {
- this.proc.once("close", () => {
- setTimeout(() => this.start(), 500);
- });
- }
- this.proc.kill("SIGINT");
- return {success: true}
- } catch (err) {
- console.log(err);
- return {success:false, reason:err}
- }
- }
-}
-
-/**
- * Creates and returns a timestamp that can be used in logfiles.
- *
- * @return {string} timestamp
- */
-function timestamp() {
- return (new Date()).toISOString();
-}
-
-module.exports = RuntimeProcess;
diff --git a/src/NeoRuntimeManager/index.cjs b/src/NeoRuntimeManager/index.cjs
new file mode 100644
index 0000000..81bd7e8
--- /dev/null
+++ b/src/NeoRuntimeManager/index.cjs
@@ -0,0 +1,339 @@
+/**
+ * This module is used to execute and communicate with a python NeoRuntime instance.
+ *
+ * @author jakobst1n.
+ * @since 19.12.2019
+ */
+
+const fs = require("fs");
+const fsPromises = fs.promises;
+const RuntimeProcess = require("./RuntimeProcess.cjs");
+const IPC = require("./IPC.cjs");
+const logger = require("../Logger/index.cjs");
+const EventEmitter = require('events');
+
+/** @type {object} this should be a pointer to a object referencing all neoModules (see app.js) */
+let neoModules;
+
+/** @type {string} Currently active mode */
+let modeId = null;
+/** @type {int} Last exit code of a mode */
+let modeExitCode = 0;
+/** @type {RuntimeProcess} This is the current RuntimeProcess instance */
+let runtimeProcess = null;
+/** @type {IPC} The IPC instance, used to communicate with the script */
+let ipc = null;
+/** @type {EventEmitter} This is used to emit events when things change */
+const eventEmitter = new EventEmitter();
+/** @type {boolean} If this is true, we will not do things the usual way */
+let modeDebuggerActive = false;
+/** @type {string} Should be the modeId the debugger is attached to */
+let modeDebuggerId = null;
+/** @type {object} Handler for proc:start when debugger is active */
+let modeDebuggerProcStartHandler = null;
+/** @type {object} The last received matrix setup */
+let matrix = null;
+/** @type {object} intervall for sending current state */
+let debugModeStateEmitIntervall = null;
+
+eventEmitter.on("proc:exit", (code) => modeExitCode = code);
+eventEmitter.on("matrix", (_matrix) => matrix = _matrix);
+
+/**
+ * Check if a path id actually a mode (if it is a folder with a script.py file)
+ *
+ * @param {string} path - Path to check.
+ *
+ * @return {boolean} wether the path points to a valid mode.
+ */
+function isMode(path) {
+ if (!fs.existsSync(path)) { return false; }
+ let folderStat = fs.statSync(path);
+ if (!folderStat.isDirectory()) { return false; }
+ if (!fs.existsSync(path + "/script.py")) { return false; }
+ return true;
+}
+
+/**
+ * Get all ids of modes that can be set.
+ *
+ * @returns {array} All modeids
+ */
+function listModes() {
+ let modeDirs = [
+ ["builtin/", fs.readdirSync(__appdir + "/NeoRuntime/builtin")],
+ ["remote/", fs.readdirSync(__datadir + "/remoteCode")],
+ ["user/", fs.readdirSync(__datadir + "/userCode")]
+ ]
+ let validModes = [];
+ for (modeDir of modeDirs) {
+ for (modeName of modeDir[1]) {
+ let modeId = `${modeDir[0]}${modeName}`;
+ if (isMode(getModePath(modeId))) {
+ validModes.push(modeId);
+ }
+ }
+ }
+ return validModes;
+}
+
+/**
+ * Change mode, stop the old one and start the new one.
+ *
+ * @param {string} _modeId - Id of the mode to change to.
+ *
+ * @return {object} A standardform return object.
+ */
+function setMode(_modeId) {
+ if (modeDebuggerActive && (_modeId != modeDebuggerId)) {
+ return {success: false, reason: "debugger active", detail: "Cannot change mode when debugger is active."}
+ }
+ if (!isMode(getModePath(_modeId))) {
+ console.log(`Invalid mode "${_modeId}".`);
+ return {success: false, reason: "unknown modeId"};
+ }
+ logger.info(`Changing mode to "${_modeId}".`);
+
+ stopMode();
+
+ modeId = _modeId;
+ neoModules.userData.config.activeMode = modeId;
+ eventEmitter.emit("change", "mode", modeId);
+
+ runtimeProcess = new RuntimeProcess(getModePath(_modeId), eventEmitter);
+ startMode();
+
+ return {success: true}
+};
+
+/**
+ * Get current mode
+ *
+ * @return {string} current modeId
+ */
+function currentMode() {
+ return modeId;
+}
+
+/**
+ * Will attempt to stop current mode
+ *
+ * @return {object} A standardform return object.
+ */
+function stopMode(restart=false) {
+ if (modeRunning()) {
+ runtimeProcess.stop(restart);
+ }
+ return {success: true}
+};
+
+/**
+ * Will attempt to start current mode
+ *
+ * @return {object} A standardform return object.
+ */
+function startMode() {
+ if (runtimeProcess === null) {
+ return {success: false, reason: "no runtimeprocess", detail: "Runtimeprocess not set, did you mean to call setMode?"};
+ }
+ return runtimeProcess.start();
+};
+
+/**
+ * Will attempt to restart current mode
+ *
+ * @return {object} A standardform return object.
+ */
+function restartMode() {
+ return stopMode(true);
+};
+
+/**
+ * Checks if mode is running currently
+ *
+ * @return {boolean} if mode is running
+ */
+function modeRunning() {
+ if (runtimeProcess === null) { return false; }
+ return runtimeProcess.isRunning;
+};
+
+/**
+ * Get the full system path to a mode
+ *
+ * @param {string} modeId
+ *
+ * @return {string} Full path of mode
+ */
+function getModePath(modeId) {
+ let path = modeId.split("/");
+ let location = path.splice(0, 1).toString();
+ if (location === "user") { path = __datadir + "/userCode/" + path.join("/"); }
+ if (location === "remote") { path = __datadir + "/remoteCode/" + path.join("/"); }
+ if (location === "builtin") { path = __appdir + "/NeoRuntime/builtin/" + path.join("/"); }
+ return path;
+}
+
+/**
+ * Function that returns all globvars (brightness, power_on) as the values they
+ * had last time we heard from the python script.
+ *
+ * @return {object}
+ */
+function getGlobvars() {
+ if (!modeRunning()) { return {}; }
+ return ipc.globvars;
+}
+
+/**
+ * Sets value of a globvar power_on/brightness.
+ *
+ * @param {string} name - Name of the variable power_on/brightness
+ * @param {any} value - The value the variable should be set to
+ *
+ * @return {object} Standardform return object
+ */
+function setGlobvar(name, value) {
+ if (!modeRunning()) { return; }
+
+ switch(name) {
+ case "power_on":
+ return ipc.sendCommand(IPC.COMMAND.SET_GLOB, IPC.GLOBVAR.POWER_ON, (value) ? 1 : 0);
+ case "brightness":
+ return ipc.sendCommand(IPC.COMMAND.SET_GLOB, IPC.GLOBVAR.BRIGHTNESS, value);
+ default:
+ return {success: false, reason: "unknown globvar", detail: name};
+ }
+}
+
+/**
+ * Get all variables declared in mode
+ *
+ * @return {object}
+ */
+function getVariables() {
+ if (!modeRunning()) { return {}; }
+ return ipc.variables;
+}
+
+/**
+ * Sets value of a variable
+ *
+ * @param {string} name - Name of the variable
+ * @param {any} value - The value the variable should be set to
+ *
+ * @return {object} Standardform return object
+ */
+function setVariable(name, value) {
+ if (!modeRunning()) { return; }
+ return ipc.sendCommand(IPC.COMMAND.SET_VAR, name, value);
+}
+
+/**
+ * A function intented to be used in an interval to emit
+ * the current debug-state.
+ *
+ */
+function debugModeEmitState() {
+ eventEmitter.emit("debugger:state", {
+ mode: modeDebuggerId,
+ running: runtimeProcess.isRunning,
+ debugMode: modeDebuggerActive,
+ matrix: matrix
+ });
+}
+
+/**
+ * Start debugger for a mode
+ *
+ * @param {string} modeId - The mode to debug
+ *
+ * @return {object} Standardform return object
+ */
+function startDebugger(debuggerModeId) {
+ if (debuggerModeId.substr(0, 5) !== "user/") { return {success: false, reason: "not user mode"}; }
+ if (!isMode(getModePath(debuggerModeId))) { return {success: false, reason: "unknown modeId"}; }
+ if (modeDebuggerActive) { return {success: false, reason: "debugger already active"}; }
+ logger.info(`Starting debugger for ${debuggerModeId}`);
+
+ if (modeDebuggerProcStartHandler == null) {
+ modeDebuggerProcStartHandler = eventEmitter.on("proc:start", () => {
+ setTimeout(() => {
+ ipc.sendCommand(IPC.COMMAND.SET_SEND_STRIP_BUF, true);
+ }, 2000);
+ });
+ } else {
+ console.log(modeDebuggerProcStartHandler);
+ }
+
+ if (debugModeStateEmitIntervall == null) {
+ debugModeStateEmitIntervall = setInterval(debugModeEmitState, 1000);
+ }
+
+ modeDebuggerActive = true;
+ modeDebuggerId = debuggerModeId;
+ setTimeout(() => {
+ setMode(debuggerModeId);
+ }, 300);
+ return {success: true, code: fs.readFileSync(getModePath(debuggerModeId) + "/script.py").toString()}
+}
+
+/**
+ * Save mode
+ */
+function saveModeCode(_modeId, code) {
+ if (!modeDebuggerActive) { return {success: false, reason: "debugger not active"}; };
+ if (_modeId != modeDebuggerId) { return {success: false, reason: "modeid not the same as debuggermodeid"}; };
+ fs.writeFileSync(getModePath(`${modeDebuggerId}/script.py`), code);
+ return {success: true};
+}
+
+/**
+ * Stop the active debugger
+ *
+ * @return {object} Standardform return object
+ */
+function stopDebugger() {
+ if (!modeDebuggerActive) { return {success: true, detail: "No debugger active"} }
+ logger.info(`Stopping debugger`);
+ modeDebuggerActive = false;
+ eventEmitter.removeAllListeners("proc:start", modeDebuggerProcStartHandler);
+ modeDebuggerProcStartHandler = null;
+
+ clearInterval(debugModeStateEmitIntervall);
+ debugModeStateEmitIntervall = null;
+
+ ipc.sendCommand(IPC.COMMAND.SET_SEND_STRIP_BUF, false);
+ return {success: true}
+}
+
+module.exports = (_neoModules) => {
+ neoModules = _neoModules;
+ ipc = new IPC.IPC(neoModules.userData.config.neoRuntimeIPC.socketFile, eventEmitter);
+ return {
+ event: eventEmitter,
+ modes: listModes,
+ mode: {
+ current: currentMode,
+ set: (modeId) => setMode(modeId),
+ status: {
+ modeRunning: modeRunning(),
+ modeExitCode: modeExitCode
+ },
+ globvars: {
+ get: getGlobvars,
+ set: setGlobvar
+ },
+ variables: {
+ get: getVariables,
+ set: setVariable
+ }
+ },
+ getModePath,
+ isMode,
+ modeRunning,
+ startDebugger, stopDebugger, saveModeCode,
+ startMode, stopMode, restartMode,
+ matrix
+ }
+};
diff --git a/src/NeoRuntimeManager/index.js b/src/NeoRuntimeManager/index.js
deleted file mode 100644
index 5989f61..0000000
--- a/src/NeoRuntimeManager/index.js
+++ /dev/null
@@ -1,339 +0,0 @@
-/**
- * This module is used to execute and communicate with a python NeoRuntime instance.
- *
- * @author jakobst1n.
- * @since 19.12.2019
- */
-
-const fs = require("fs");
-const fsPromises = fs.promises;
-const RuntimeProcess = require("./RuntimeProcess");
-const IPC = require("./IPC");
-const logger = require(__appdir + "/src/Logger");
-const EventEmitter = require('events');
-
-/** @type {object} this should be a pointer to a object referencing all neoModules (see app.js) */
-let neoModules;
-
-/** @type {string} Currently active mode */
-let modeId = null;
-/** @type {int} Last exit code of a mode */
-let modeExitCode = 0;
-/** @type {RuntimeProcess} This is the current RuntimeProcess instance */
-let runtimeProcess = null;
-/** @type {IPC} The IPC instance, used to communicate with the script */
-let ipc = null;
-/** @type {EventEmitter} This is used to emit events when things change */
-const eventEmitter = new EventEmitter();
-/** @type {boolean} If this is true, we will not do things the usual way */
-let modeDebuggerActive = false;
-/** @type {string} Should be the modeId the debugger is attached to */
-let modeDebuggerId = null;
-/** @type {object} Handler for proc:start when debugger is active */
-let modeDebuggerProcStartHandler = null;
-/** @type {object} The last received matrix setup */
-let matrix = null;
-/** @type {object} intervall for sending current state */
-let debugModeStateEmitIntervall = null;
-
-eventEmitter.on("proc:exit", (code) => modeExitCode = code);
-eventEmitter.on("matrix", (_matrix) => matrix = _matrix);
-
-/**
- * Check if a path id actually a mode (if it is a folder with a script.py file)
- *
- * @param {string} path - Path to check.
- *
- * @return {boolean} wether the path points to a valid mode.
- */
-function isMode(path) {
- if (!fs.existsSync(path)) { return false; }
- let folderStat = fs.statSync(path);
- if (!folderStat.isDirectory()) { return false; }
- if (!fs.existsSync(path + "/script.py")) { return false; }
- return true;
-}
-
-/**
- * Get all ids of modes that can be set.
- *
- * @returns {array} All modeids
- */
-function listModes() {
- let modeDirs = [
- ["builtin/", fs.readdirSync(__appdir + "/NeoRuntime/builtin")],
- ["remote/", fs.readdirSync(__datadir + "/remoteCode")],
- ["user/", fs.readdirSync(__datadir + "/userCode")]
- ]
- let validModes = [];
- for (modeDir of modeDirs) {
- for (modeName of modeDir[1]) {
- let modeId = `${modeDir[0]}${modeName}`;
- if (isMode(getModePath(modeId))) {
- validModes.push(modeId);
- }
- }
- }
- return validModes;
-}
-
-/**
- * Change mode, stop the old one and start the new one.
- *
- * @param {string} _modeId - Id of the mode to change to.
- *
- * @return {object} A standardform return object.
- */
-function setMode(_modeId) {
- if (modeDebuggerActive && (_modeId != modeDebuggerId)) {
- return {success: false, reason: "debugger active", detail: "Cannot change mode when debugger is active."}
- }
- if (!isMode(getModePath(_modeId))) {
- console.log(`Invalid mode "${_modeId}".`);
- return {success: false, reason: "unknown modeId"};
- }
- logger.info(`Changing mode to "${_modeId}".`);
-
- stopMode();
-
- modeId = _modeId;
- neoModules.userData.config.activeMode = modeId;
- eventEmitter.emit("change", "mode", modeId);
-
- runtimeProcess = new RuntimeProcess(getModePath(_modeId), eventEmitter);
- startMode();
-
- return {success: true}
-};
-
-/**
- * Get current mode
- *
- * @return {string} current modeId
- */
-function currentMode() {
- return modeId;
-}
-
-/**
- * Will attempt to stop current mode
- *
- * @return {object} A standardform return object.
- */
-function stopMode(restart=false) {
- if (modeRunning()) {
- runtimeProcess.stop(restart);
- }
- return {success: true}
-};
-
-/**
- * Will attempt to start current mode
- *
- * @return {object} A standardform return object.
- */
-function startMode() {
- if (runtimeProcess === null) {
- return {success: false, reason: "no runtimeprocess", detail: "Runtimeprocess not set, did you mean to call setMode?"};
- }
- return runtimeProcess.start();
-};
-
-/**
- * Will attempt to restart current mode
- *
- * @return {object} A standardform return object.
- */
-function restartMode() {
- return stopMode(true);
-};
-
-/**
- * Checks if mode is running currently
- *
- * @return {boolean} if mode is running
- */
-function modeRunning() {
- if (runtimeProcess === null) { return false; }
- return runtimeProcess.isRunning;
-};
-
-/**
- * Get the full system path to a mode
- *
- * @param {string} modeId
- *
- * @return {string} Full path of mode
- */
-function getModePath(modeId) {
- let path = modeId.split("/");
- let location = path.splice(0, 1).toString();
- if (location === "user") { path = __datadir + "/userCode/" + path.join("/"); }
- if (location === "remote") { path = __datadir + "/remoteCode/" + path.join("/"); }
- if (location === "builtin") { path = __appdir + "/NeoRuntime/builtin/" + path.join("/"); }
- return path;
-}
-
-/**
- * Function that returns all globvars (brightness, power_on) as the values they
- * had last time we heard from the python script.
- *
- * @return {object}
- */
-function getGlobvars() {
- if (!modeRunning()) { return {}; }
- return ipc.globvars;
-}
-
-/**
- * Sets value of a globvar power_on/brightness.
- *
- * @param {string} name - Name of the variable power_on/brightness
- * @param {any} value - The value the variable should be set to
- *
- * @return {object} Standardform return object
- */
-function setGlobvar(name, value) {
- if (!modeRunning()) { return; }
-
- switch(name) {
- case "power_on":
- return ipc.sendCommand(IPC.COMMAND.SET_GLOB, IPC.GLOBVAR.POWER_ON, (value) ? 1 : 0);
- case "brightness":
- return ipc.sendCommand(IPC.COMMAND.SET_GLOB, IPC.GLOBVAR.BRIGHTNESS, value);
- default:
- return {success: false, reason: "unknown globvar", detail: name};
- }
-}
-
-/**
- * Get all variables declared in mode
- *
- * @return {object}
- */
-function getVariables() {
- if (!modeRunning()) { return {}; }
- return ipc.variables;
-}
-
-/**
- * Sets value of a variable
- *
- * @param {string} name - Name of the variable
- * @param {any} value - The value the variable should be set to
- *
- * @return {object} Standardform return object
- */
-function setVariable(name, value) {
- if (!modeRunning()) { return; }
- return ipc.sendCommand(IPC.COMMAND.SET_VAR, name, value);
-}
-
-/**
- * A function intented to be used in an interval to emit
- * the current debug-state.
- *
- */
-function debugModeEmitState() {
- eventEmitter.emit("debugger:state", {
- mode: modeDebuggerId,
- running: runtimeProcess.isRunning,
- debugMode: modeDebuggerActive,
- matrix: matrix
- });
-}
-
-/**
- * Start debugger for a mode
- *
- * @param {string} modeId - The mode to debug
- *
- * @return {object} Standardform return object
- */
-function startDebugger(debuggerModeId) {
- if (debuggerModeId.substr(0, 5) !== "user/") { return {success: false, reason: "not user mode"}; }
- if (!isMode(getModePath(debuggerModeId))) { return {success: false, reason: "unknown modeId"}; }
- if (modeDebuggerActive) { return {success: false, reason: "debugger already active"}; }
- logger.info(`Starting debugger for ${debuggerModeId}`);
-
- if (modeDebuggerProcStartHandler == null) {
- modeDebuggerProcStartHandler = eventEmitter.on("proc:start", () => {
- setTimeout(() => {
- ipc.sendCommand(IPC.COMMAND.SET_SEND_STRIP_BUF, true);
- }, 2000);
- });
- } else {
- console.log(modeDebuggerProcStartHandler);
- }
-
- if (debugModeStateEmitIntervall == null) {
- debugModeStateEmitIntervall = setInterval(debugModeEmitState, 1000);
- }
-
- modeDebuggerActive = true;
- modeDebuggerId = debuggerModeId;
- setTimeout(() => {
- setMode(debuggerModeId);
- }, 300);
- return {success: true, code: fs.readFileSync(getModePath(debuggerModeId) + "/script.py").toString()}
-}
-
-/**
- * Save mode
- */
-function saveModeCode(_modeId, code) {
- if (!modeDebuggerActive) { return {success: false, reason: "debugger not active"}; };
- if (_modeId != modeDebuggerId) { return {success: false, reason: "modeid not the same as debuggermodeid"}; };
- fs.writeFileSync(getModePath(`${modeDebuggerId}/script.py`), code);
- return {success: true};
-}
-
-/**
- * Stop the active debugger
- *
- * @return {object} Standardform return object
- */
-function stopDebugger() {
- if (!modeDebuggerActive) { return {success: true, detail: "No debugger active"} }
- logger.info(`Stopping debugger`);
- modeDebuggerActive = false;
- eventEmitter.removeAllListeners("proc:start", modeDebuggerProcStartHandler);
- modeDebuggerProcStartHandler = null;
-
- clearInterval(debugModeStateEmitIntervall);
- debugModeStateEmitIntervall = null;
-
- ipc.sendCommand(IPC.COMMAND.SET_SEND_STRIP_BUF, false);
- return {success: true}
-}
-
-module.exports = (_neoModules) => {
- neoModules = _neoModules;
- ipc = new IPC.IPC(neoModules.userData.config.neoRuntimeIPC.socketFile, eventEmitter);
- return {
- event: eventEmitter,
- modes: listModes,
- mode: {
- current: currentMode,
- set: (modeId) => setMode(modeId),
- status: {
- modeRunning: modeRunning(),
- modeExitCode: modeExitCode
- },
- globvars: {
- get: getGlobvars,
- set: setGlobvar
- },
- variables: {
- get: getVariables,
- set: setVariable
- }
- },
- getModePath,
- isMode,
- modeRunning,
- startDebugger, stopDebugger, saveModeCode,
- startMode, stopMode, restartMode,
- matrix
- }
-};
diff --git a/src/SSLCert/index.cjs b/src/SSLCert/index.cjs
new file mode 100644
index 0000000..05d9a32
--- /dev/null
+++ b/src/SSLCert/index.cjs
@@ -0,0 +1,146 @@
+/**
+ * Module that exports an instance of CertMon
+ * see class definition to see what it does.
+ *
+ * Requires global var '__datadir' to be set.
+ *
+ * @author jakobst1n.
+ * @since 14.16.2019
+ */
+let logger = require("../Logger/index.cjs");
+const fs = require("fs");
+const { execSync } = require("child_process");
+
+var neoModules;
+
+ /**
+ * This checks if the server has a valid certificate, if not,
+ * it will generate one.
+ */
+ class CertMon {
+
+ constructor(configPath, certPath, httpsConfig) {
+ this.certPath = __configdir + "/certs/";
+
+ let valid = this.checkValidity();
+ if (!valid) {
+ logger.notice("No valid certificate found, creating one now.");
+ this.generateCert();
+ }
+
+ let interval = setInterval(() => {
+ let certIsValid = this.checkValidity();
+ if (!valid) {
+ logger.crit("Certificate no longer valid, server should reboot to make a new one.");
+ }
+ }, 1440000); // Run once every day
+ }
+
+ checkValidity() {
+ let sslConfig = this.getConfig();
+ if (!sslConfig["certMade"]) {
+ logger.debug("'certMade' in config is false, assuming no valid certificate");
+ return false;
+ }
+ let expire = ((sslConfig["certExpire"] - Date.now()) / 86400000).toFixed(2);
+ if (expire > 0) {
+ logger.debug(`Certificate should be valid for ${expire} more days.`);
+ } else {
+ expire = Math.abs(expire);
+ logger.debug(`Certificate expired ${expire} days ago`);
+ return false;
+ }
+ return true;
+ }
+
+ getConfig() {
+ return neoModules.userData.config.SSLCert;
+ }
+
+ updateConfig(parameters) {
+ neoModules.userData.config.set(parameters);
+ }
+
+ generateCert() {
+ let certPath = this.certPath;
+ let config = this.getConfig();
+
+
+ // Create Root Certificate Autority
+ let res = openssl(
+ `genrsa ` +
+ `-out "${certPath}/root-CA.key.pem" ` +
+ `4096`
+ );
+
+ // Self sign the Root Certificate Autority
+ res = openssl(
+ `req ` +
+ `-x509 ` +
+ `-new ` +
+ `-nodes ` +
+ `-key "${certPath}/root-CA.key.pem" ` +
+ `-days 1024 ` +
+ `-out "${certPath}/root-CA.crt.pem" ` +
+ `-sha256 ` +
+ `-subj "/C=NO/ST=Oslo/L=Oslo/O=Luxcena Neo Self-Signing Authority/CN=${config.CN}"`
+ );
+
+ // Create a Device Certificate for each domain,
+ // such as example.com, *.example.com, awesome.example.com
+ // NOTE: You MUST match CN to the domain name or ip address you want to use
+ res = openssl(
+ `genrsa ` +
+ `-out "${certPath}/privkey.pem" ` +
+ `4096`
+ );
+
+ // Create a request from your Device, which your Root CA will sign
+ res = openssl(
+ `req ` +
+ `-new ` +
+ `-key "${certPath}/privkey.pem" ` +
+ `-out "${certPath}/csr.pem" ` +
+ `-subj "/C=NO/ST=Oslo/L=Oslo/O=Luxcena Neo Self-Signing Autohity/CN=${config.CN}"`
+ );
+
+ // Sign the request from Device with your Root CA
+ // -CAserial certs/ca/my-root-ca.srl
+ res = openssl(
+ `x509 ` +
+ `-req ` +
+ `-in "${certPath}/csr.pem" ` +
+ `-CA "${certPath}/root-CA.crt.pem" ` +
+ `-CAkey "${certPath}/root-CA.key.pem" ` +
+ `-CAcreateserial ` +
+ `-out "${certPath}/cert.pem" ` +
+ `-sha256 ` +
+ `-days 500`
+ );
+
+ let creationDate = Date.now();
+ config.certMade = true;
+ config.certDate = creationDate;
+ config.certExpire = creationDate + (500*86400000);
+ config.certCN = config.CN;
+
+ logger.info("Self-signed certificate created.");
+
+ }
+
+ }
+
+function openssl(command) {
+ try {
+ let stdout = execSync("openssl " + command);
+ return true
+ } catch (e) {
+ return false
+ }
+ }
+
+module.exports = (_neoModules) => {
+ neoModules = _neoModules;
+ return new CertMon();
+};
+
diff --git a/src/SSLCert/index.js b/src/SSLCert/index.js
deleted file mode 100644
index d235c9b..0000000
--- a/src/SSLCert/index.js
+++ /dev/null
@@ -1,144 +0,0 @@
-/**
- * Module that exports an instance of CertMon
- * see class definition to see what it does.
- *
- * Requires global var '__datadir' to be set.
- *
- * @author jakobst1n.
- * @since 14.16.2019
- */
- let logger = require(__appdir + "/src/Logger");
- const fs = require("fs");
- const { execSync } = require("child_process");
-
-var neoModules;
-
- /**
- * This checks if the server has a valid certificate, if not,
- * it will generate one.
- */
- class CertMon {
-
- constructor(configPath, certPath, httpsConfig) {
- this.certPath = __configdir + "/certs/";
-
- let valid = this.checkValidity();
- if (!valid) {
- logger.notice("No valid certificate found, creating one now.");
- this.generateCert();
- }
-
- let interval = setInterval(() => {
- let certIsValid = this.checkValidity();
- if (!valid) {
- logger.crit("Certificate no longer valid, server should reboot to make a new one.");
- }
- }, 1440000); // Run once every day
- }
-
- checkValidity() {
- let sslConfig = this.getConfig();
- if (!sslConfig["certMade"]) {
- logger.debug("'certMade' in config is false, assuming no valid certificate");
- return false;
- }
- let expire = ((sslConfig["certExpire"] - Date.now()) / 86400000).toFixed(2);
- if (expire > 0) {
- logger.debug(`Certificate should be valid for ${expire} more days.`);
- } else {
- expire = Math.abs(expire);
- logger.debug(`Certificate expired ${expire} days ago`);
- return false;
- }
- return true;
- }
-
- getConfig() {
- return neoModules.userData.config.SSLCert;
- }
-
- updateConfig(parameters) {
- neoModules.userData.config.set(parameters);
- }
-
- generateCert() {
- let certPath = this.certPath;
- let config = this.getConfig();
-
-
- // Create Root Certificate Autority
- let res = openssl(
- `genrsa ` +
- `-out "${certPath}/root-CA.key.pem" ` +
- `2048`
- );
-
- // Self sign the Root Certificate Autority
- res = openssl(
- `req ` +
- `-x509 ` +
- `-new ` +
- `-nodes ` +
- `-key "${certPath}/root-CA.key.pem" ` +
- `-days 1024 ` +
- `-out "${certPath}/root-CA.crt.pem" ` +
- `-subj "/C=NO/ST=Oslo/L=Oslo/O=Luxcena Neo Self-Signing Authority/CN=${config.CN}"`
- );
-
- // Create a Device Certificate for each domain,
- // such as example.com, *.example.com, awesome.example.com
- // NOTE: You MUST match CN to the domain name or ip address you want to use
- res = openssl(
- `genrsa ` +
- `-out "${certPath}/privkey.pem" ` +
- `2048`
- );
-
- // Create a request from your Device, which your Root CA will sign
- res = openssl(
- `req ` +
- `-new ` +
- `-key "${certPath}/privkey.pem" ` +
- `-out "${certPath}/csr.pem" ` +
- `-subj "/C=NO/ST=Oslo/L=Oslo/O=Luxcena Neo Self-Signing Autohity/CN=${config.CN}"`
- );
-
- // Sign the request from Device with your Root CA
- // -CAserial certs/ca/my-root-ca.srl
- res = openssl(
- `x509 ` +
- `-req ` +
- `-in "${certPath}/csr.pem" ` +
- `-CA "${certPath}/root-CA.crt.pem" ` +
- `-CAkey "${certPath}/root-CA.key.pem" ` +
- `-CAcreateserial ` +
- `-out "${certPath}/cert.pem" ` +
- `-days 500`
- );
-
- let creationDate = Date.now();
- config.certMade = true;
- config.certDate = creationDate;
- config.certExpire = creationDate + (500*86400000);
- config.certCN = config.CN;
-
- logger.info("Self-signed certificate created.");
-
- }
-
- }
-
-function openssl(command) {
- try {
- let stdout = execSync("openssl " + command);
- return true
- } catch (e) {
- return false
- }
- }
-
-module.exports = (_neoModules) => {
- neoModules = _neoModules;
- return new CertMon();
-};
-
diff --git a/src/SelfUpdater/index.js b/src/SelfUpdater/index.js
index cc7ce13..f54af51 100644
--- a/src/SelfUpdater/index.js
+++ b/src/SelfUpdater/index.js
@@ -1,13 +1,31 @@
-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");
+import { existsSync, readFileSync } from 'fs';
+import { ensureDirSync } from 'fs-extra';
+import { copyFile, rm } from 'fs/promises';
+import url from 'node:url';
+import { spawn } from 'child_process';
+import { EventEmitter } from 'events';
+import logger from '../Logger/index.cjs'
+import fetch from 'node-fetch';
let neoModules;
+/**
+ * Get the latest release from GitHub
+ */
+async function getLatestRelease() {
+ let res = await fetch("https://api.github.com/repos/jakobst1n/luxcena-neo/releases/86402456");
+
+ if (res.status !== 200) {
+ console.log(res.status);
+ this.remoteVersionNumber = "Unknown";
+ this.newVersion = false;
+ throw Error(`Could not get latest release (${res.status})...`);
+ }
+
+ return await res.json()
+}
+
+
/**
* This just tests if the current appdir is the "default" location
*/
@@ -20,17 +38,17 @@ function isInstalledInDefaultLocation() {
* it will add a number at the end if something already exists,
*/
function createUniqueDir(path, prefix) {
- fs.ensureDirSync(path);
+ ensureDirSync(path);
let fn = `${path}/${prefix}`;
let i = 0;
let cFn = fn;
while (true) {
- if (fs.existsSync(cFn)) {
+ if (existsSync(cFn)) {
i++;
cFn = `${fn}.${i}`;
continue;
}
- fs.ensureDirSync(cFn);
+ ensureDirSync(cFn);
return cFn;
}
}
@@ -107,56 +125,56 @@ class Updater {
this.backupdir = null;
this.backupcomplete = false;
- if (!isInstalledInDefaultLocation()) {
- return {success: false, reason: "not installed in default location", detail: __appdir};
- }
this.updating = true;
this.event.emit("start");
neoModules.neoRuntimeManager.stopMode();
try {
+ // Get info about the latest release
+ this.latestRelease = await getLatestRelease();
+
// Download update
- this.setStep("Downloading update (1/8)");
+ this.setStep("Downloading update (1/7)");
this.setCommand("Create updatedir");
this.updatedir = createUniqueDir("/tmp", "luxcena-neo.update");
+ this.setCommand("Download package");
await this.downloadUpdate(this.updatedir);
-
+
// Create backup
- this.setStep("Creating backup (2/8)");
+ this.setStep("Creating backup (2/7)");
this.setCommand("Create backupdir");
this.backupdir = createUniqueDir("/var/luxcena-neo/backups", "backup");
this.setCommand(`Copy ${__appdir} into ${this.backupdir}`);
- await fs.copy(__appdir, this.backupdir);
+ await copyFile(__appdir, this.backupdir);
this.backupcomplete = true;
- // Install update
- this.setStep("Installing update (3/8)");
- this.setCommand(`Copy ${this.updatedir} into /opt/luxcena-neo`);
- await fs.copy(this.updatedir, __appdir);
-
// Install dependencies
- this.setStep("Installing dependencies (4/8)");
+ this.setStep("Installing dependencies (3/7)");
await this.installDependencies();
-
- // Create python virtualenv
- this.setStep("Making virtualenv (5/8)");
- await this.makeVirtualenv();
-
- // Build source code
- this.setStep("Building source (6/8)");
- await this.build();
+ return
+
+ // Install package
+ this.setStep("Installing package (4/7)");
+ await this.installPackage(this.updatedir);
+
+ // Install update
+ this.setStep("Installing update (5/7)");
+ this.setCommand(`Copy ${this.updatedir} into ${__appdir}`);
+ await copyFile(this.updatedir, __appdir);
// Cleanup
- this.setStep("Cleaning up (7/8)");
+ this.setStep("Cleaning up (6/7)");
await this.cleanup();
// 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. (8/8)");
+ this.setStep("Stopping luxcena neo service in the hope that systemd will restart it. (7/7)");
this.setCommand("EXIT");
this.updating = false;
this.event.emit("end");
- process.exit(0);
+ setTimeout(() => {
+ process.exit(0);
+ }, 1000);
} catch (e) {
logger.crit(`Updater failed miserably...`);
@@ -174,7 +192,7 @@ class Updater {
if (this.backupcomplete && (this.backupdir != null)) {
this.setStep("Restoring backup");
this.setCommand(`Copy ${this.backupdir} into /opt/luxcena-neo`);
- await fs.copy(this.backupdir, __appdir);
+ await copyFile(this.backupdir, __appdir);
}
this.setStep("Cleaning up");
await this.cleanup();
@@ -201,9 +219,7 @@ class Updater {
* 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]);
+ await this.run(`curl`, ["-s", "-L", "-o", `${tmpdir}/${this.latestRelease["assets"][0]["name"]}`, this.latestRelease["assets"][0]["browser_download_url"]]);
}
async installDependencies() {
@@ -218,45 +234,24 @@ class Updater {
await this.run("rm", ["node-v14.10.0-linux-armv6l.tar.gz"]);
} else {
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`)) {
- await fs.remove(`${__appdir}/NeoRuntime/Runtime/venv`);
- }
-
- await this.run("virtualenv", ["-p", "/usr/bin/python3", `${__appdir}/NeoRuntime/Runtime/venv`]);
- await this.run("sh", ["-c", `. ${__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`]);
+ await this.run("apt", ["-qy", "install", "nodejs", "python3-pip"]);
+ await this.run("pip3", ["install", "virtualenv"]);
}
- async installSystemdService() {
- this.setCommand("Deleting old systemd service");
- await fs.remove("/etc/systemd/system/luxcena-neo.service");
- this.setCommand("Installing new systemd service");
- await fs.copy("/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"]);
+ async installPackage(tmpdir) {
+ await this.run("sh", ["-c", `export NODE_ENV=production; npm --prefix "${tmpdir}/luxcena-neo/" install "${tmpdir}/${this.latestRelease["assets"][0]["name"]}"`]);
}
async cleanup() {
if (this.updatedir != null) {
this.setCommand(`Removing temporary update files ${this.updatedir}`);
- await fs.remove(this.updatedir);
+ await rm(this.updatedir);
}
if (this.backupdir != null) {
this.setCommand(`Removing ${this.backupdir}, thinking everything went fine :)`);
- await fs.remove(this.backupdir);
+ await rm(this.backupdir);
}
}
@@ -265,10 +260,8 @@ class Updater {
class SelfUpdater {
constructor() {
- this.branch;
this.repoUrl;
this.localPackageJson;
- this.remotePackageJSON
this.localVersionNumber;
this.remoteVersionNumber;
this.newVersion = false;
@@ -281,37 +274,41 @@ class SelfUpdater {
this.updater = new Updater();
}
- async checkVersion() {
- this.localPackageJson = JSON.parse(fs.readFileSync(__appdir + "/package.json"));
+ async getCurrentVersionNumber() {
+ this.localPackageJson = JSON.parse(readFileSync(__appdir + "/package.json"));
this.localVersionNumber = this.localPackageJson["version"];
- this.branch = (await promiseSpawn(`git`, ["-C", __appdir, "rev-parse", "--abbrev-ref", "HEAD"])).out.replace("\n","");
- request.get(
- "https://raw.githubusercontent.com/JakobST1n/Luxcena-Neo/" + this.branch + "/package.json",
- (error, response, body) => {
- if (!error && (response.statusCode === 200)) {
- this.remotePackageJSON = JSON.parse(body);
- this.remoteVersionNumber = this.remotePackageJSON["version"];
- if (this.localVersionNumber != this.remoteVersionNumber) {
- logger.notice("A new version is available on \"" + this.branch + "\" (v" + this.remoteVersionNumber + ")");
- this.newVersion = true;
-
- } else {
- logger.info(`Running newest version (${this.localVersionNumber})`);
- this.newVersion = false;
- }
- } else {
- logger.notice("Could not find latest version! Please check you internet connection.");
- this.remotePackageJSON = null;
- this.remoteVersionNumber = "Unknown";
- this.newVersion = false;
- }
- }
- );
+ return this.localVersionNumber;
+ }
+
+ async getLatestVersionNumber() {
+ this.remoteVersionNumber = (await getLatestRelease())["tag_name"];
+ return this.remoteVersionNumber;
+ }
+
+ async checkVersion() {
+ let current_version;
+ let latest_version;
+ try {
+ current_version = await this.getCurrentVersionNumber();
+ latest_version = await this.getLatestVersionNumber();
+ } catch (err) {
+ logger.notice("Could not find latest version! Please check you internet connection.");
+ return;
+ }
+
+ if (current_version != latest_version) {
+ logger.notice(`A new version is available on (v${latest_version})`);
+ this.newVersion = true;
+
+ } else {
+ logger.info(`Running newest version (${current_version})`);
+ this.newVersion = false;
+ }
}
}
-module.exports = (_neoModules) => {
+export default function(_neoModules) {
neoModules = _neoModules;
return new SelfUpdater();
};
diff --git a/src/SocketIO/index.cjs b/src/SocketIO/index.cjs
new file mode 100644
index 0000000..6905a92
--- /dev/null
+++ b/src/SocketIO/index.cjs
@@ -0,0 +1,409 @@
+/**
+ * This module contains code for handling socketIO clients.
+ *
+ * There are two classes, one is a SocketIO controller module.
+ * The other one is a authorizedclient.
+ *
+ * @author jakobst1n.
+ * @since 19.12.2019
+ */
+
+let logger = require("../Logger/index.cjs");
+var exec = require('child_process').exec;
+var CryptoJS = require("crypto-js");
+let fs = require("fs");
+const { performance } = require("perf_hooks");
+
+let neoModules;
+
+const sanitizePath = (path) => path.match(/(user|remote|builtin\/[a-zA-Z0-9-_\/]{1,200})(\.[a-zA-Z0-9]{1,10})?/)[0];
+
+/**
+ * Create the open socketio namespace and setup all listeners.
+ *
+ * @param {io} socketio
+ */
+function createOpenSocketNamespace(io) {
+ const openNamespace = io.of("/open")
+
+ openNamespace.on("connection", (socket) => {
+ logger.access(`SOCKET:open Client (${socket.id}@${socket.handshake.headers.host}) connected.`);
+
+ socket.on("name:get", () => {
+ socket.emit("name", neoModules.userData.config.instanceName);
+ });
+ socket.on("mode:set", (modeId) => {
+ neoModules.neoRuntimeManager.mode.set(modeId);
+ });
+ socket.on("mode:get", () => {
+ socket.emit("mode", neoModules.neoRuntimeManager.mode.current());
+ });
+ socket.on("modelist:get", () => {
+ socket.emit("modelist", neoModules.neoRuntimeManager.modes())
+ });
+ socket.on("brightness:set", (brightness) => {
+ neoModules.neoRuntimeManager.mode.globvars.set("brightness", brightness);
+ });
+ socket.on("brightness:get", () => {
+ socket.emit("brightness", neoModules.neoRuntimeManager.mode.globvars.get().brightness);
+ });
+ socket.on("power:set", (power) => {
+ neoModules.neoRuntimeManager.mode.globvars.set("power_on", power);
+ });
+ socket.on("power:get", () => {
+ socket.emit("power", neoModules.neoRuntimeManager.mode.globvars.get().power_on);
+ });
+ socket.on("var:set", (name, value) => {
+ neoModules.neoRuntimeManager.mode.variables.set(name, value.toString());
+ });
+ socket.on("vars:get", () => {
+ socket.emit("vars", neoModules.neoRuntimeManager.mode.variables.get());
+ });
+ socket.on("modeinfo:get", () => {
+ socket.emit("modeinfo", {
+ mode: neoModules.neoRuntimeManager.mode.current(),
+ brightness: neoModules.neoRuntimeManager.mode.globvars.get().brightness,
+ power: neoModules.neoRuntimeManager.mode.globvars.get().power_on,
+ vars: neoModules.neoRuntimeManager.mode.variables.get()
+ });
+ });
+ socket.on("authenticate:user", (username, password, callback) => {
+ let user = neoModules.userData.user.get(username);
+ if (user == null) {
+ callback({success: false, reason: "Invalid username/password"})
+ logger.access(`SOCKET:open Client (${socket.id}@${socket.handshake.headers.host}) tried to log in as '${username}', wrong username and/or password.`);
+ return;
+ }
+
+ let providedHash = hashPassword(password, user.salt);
+ if (providedHash.hash == user.password) {
+ let token = createToken(socket);
+ while (session_tokens.hasOwnProperty(token)) {
+ token = createToken(socket);
+ }
+
+ session_tokens[token] = {
+ expire: (~~Date.now())+(2678400),
+ host: socket.handshake.headers.host,
+ user: {username: user.username}
+ };
+
+ callback({success: true, user: {username: username}, token: token})
+ logger.access(`SOCKET:open Client (${socket.id}@${socket.handshake.headers.host}) authenticated as user '${username}'`);
+ return;
+ }
+
+ callback({success: false, reason: "Invalid username/password"})
+ logger.access(`SOCKET:open Client (${socket.id}@${socket.handshake.headers.host}) tried to log in as '${username}', wrong username and/or password.`);
+ });
+
+ 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) => {
+ if (name == "modelist") {
+ openNamespace.emit("modelist", neoModules.neoRuntimeManager.modes());
+ } else if (["mode", "power_on", "brightness"].includes(name)) {
+ if (name == "power_on") { name = "power"; }
+ openNamespace.emit(name, value);
+ } else {
+ 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");
+ });
+}
+
+/**
+ * @type {object} This is the collection of valid session tokens.
+ */
+let session_tokens = {};
+
+/**
+ * Middleware that will stop the request if the client does not have a valid
+ * session token.
+ *
+ * @param {object} socket - The socket instance of the connected client
+ * @param {function} next - The callback to continue the middleware chain
+ */
+function authorize_middleware(socket, next) {
+ const token = socket.handshake.auth.token;
+
+ if (session_tokens.hasOwnProperty(token) &&
+ // session_tokens[token].host === socket.handshake.headers.host &&
+ session_tokens[token].expire > (~~(Date.now()))) {
+ socket.data.user = session_tokens[token].user;
+ next();
+ } else {
+ const err = new Error("not authorized");
+ err.data = { content: "invalid session token" };
+ next(err);
+ }
+}
+
+/**
+ * Create the open socketio namespace and setup all listeners.
+ * A valid session token is required to connect to this namespace.
+ *
+ * @param {io} socetio
+ */
+function createAuthorizedNamespace(io) {
+ const authorizedNamespace = io.of("/authed");
+ authorizedNamespace.use(authorize_middleware);
+ authorizedNamespace.on("connection", (socket) => {
+ logger.access(`SOCKET:authed Client (${socket.id}@${socket.handshake.headers.host}) connected.`);
+ let debuggerOpen = false;
+
+ socket.emit("user", socket.data.user);
+
+ /* InstanceName */
+ socket.on("name:set", (name, fn) => {
+ neoModules.userData.config.instanceName = name;
+ fn({success: true});
+ io.emit("name", neoModules.userData.config.instanceName);
+ });
+
+ /* UserData */
+ socket.on("mode:create", (name, template, fn) => {
+ fn(neoModules.userData.mode.create(name, template));
+ });
+ socket.on("mode:delete", (modeid, fn) => {
+ fn(neoModules.userData.mode.delete(modeid));
+ });
+
+ /* LED Config */
+ socket.on("led_config:get", () => {
+ socket.emit("led_config", neoModules.userData.strip.get());
+ });
+ socket.on("led_config:set", (config) => {
+ neoModules.userData.strip.set(config);
+ });
+
+ /* SelfUpdater */
+ socket.on("version:current_number", () => {
+ socket.emit("version:current_number", neoModules.selfUpdater.localVersionNumber);
+ });
+ socket.on("version:branch", (fn) => {
+ socket.emit("version:branch", neoModules.selfUpdater.branch);
+ });
+ socket.on("version:newest_number", (fn) => {
+ socket.emit("version:newest_number", neoModules.selfUpdater.remoteVersionNumber);
+ });
+ socket.on("version:check_for_update", (fn) => {
+ neoModules.selfUpdater.checkVersion().then(() => {
+ socket.emit("version:newest_number", neoModules.selfUpdater.remoteVersionNumber);
+ fn({success: true});
+ });
+ });
+ socket.on("system:update_version", () => {
+ neoModules.selfUpdater.updater.forceUpdate();
+ });
+
+ /* SSLCert */
+ socket.on("sslcert:info", (fn) => {
+ socket.emit("sslcert:info", {...neoModules.SSLCert.getConfig(), "isValid": neoModules.SSLCert.checkValidity()});
+ });
+ socket.on("sslcert:generate_new", (fn) => {
+ neoModules.SSLCert.generateCert();
+ fn({success: true});
+ });
+
+ /* System actions */
+ socket.on("restart:system", () => {
+ exec('shutdown -r now', function(error, stdout, stderr){ callback(stdout); });
+ });
+ socket.on("restart:service", () => {
+ let p = exec('systemctl restart luxcena-neo');
+ p.unref();
+ });
+
+ /* Users */
+ socket.on("users:get", () => {
+ socket.emit("users", neoModules.userData.users())
+ });
+ socket.on("user:delete", (username, fn) => {
+ if (username == socket.data.user.username) { fn({success: false, reason: "cannot delete logged in account"}); return; }
+ fn(neoModules.userData.user.delete(username));
+ socket.emit("users", neoModules.userData.users())
+ });
+ socket.on("user:changeusername", (oldusername, newusername, fn) => {
+ if (oldusername == socket.data.user.username) { fn({success: false, reason: "cannot change username of logged in account"}); return; }
+ let user = neoModules.userData.user.get(oldUserName);
+ if (user == null) { fn({success: false, reason: "unknown username", detail: oldusername}); return; }
+ user.username = newusername;
+ let res = neoModules.userData.user.save(user);
+ if (!res.success) { fn(res); return; }
+ res = neoModules.userData.user.delete(oldusername)
+ if (!res.success) { fn(res); return; }
+ fn({success: true});
+ socket.emit("users", neoModules.userData.users())
+ });
+ socket.on("user:newpassword", (username, newPassword, fn) => {
+ let user = neoModules.userData.user.get(username);
+ if (user == null) { fn({success: false, reason: "unknown username", detail: username}); return; }
+ let newHash = hashPassword(newPassword);
+ fn(neoModules.userData.user.save(username, newHash.salt, newHash.hash));
+ socket.emit("users", neoModules.userData.users())
+ });
+ socket.on("user:create", (username, newPassword, fn) => {
+ let user = neoModules.userData.user.get(username);
+ if (user != null) { fn({success: false, reason: "user already exists", detail: username}); return; }
+ if (username.length < 1) { fn({success: false, reason: "no username provided"}); return; }
+ let newHash = hashPassword(newPassword);
+ fn(neoModules.userData.user.save(username, newHash.salt, newHash.hash));
+ socket.emit("users", neoModules.userData.users())
+ });
+
+ /* Editor/debugger */
+ let onProcStart = () => socket.emit("editor:proc:start");
+ let onProcStop = (code) => socket.emit("editor:proc:exit", code);
+ let onProcStdout = (stdout) => socket.volatile.emit("editor:proc:stdout", stdout);
+ let onProcStderr = (stderr) => socket.volatile.emit("editor:proc:stderr", stderr);
+ let onDebuggerState = (state) => socket.volatile.emit("editor:debugger:state", state);
+ let closeDebugger = () => {
+ debuggerOpen = false;
+ neoModules.neoRuntimeManager.event.removeListener("proc:start", onProcStart);
+ neoModules.neoRuntimeManager.event.removeListener("proc:stop", onProcStop);
+ neoModules.neoRuntimeManager.event.removeListener("proc:stdout", onProcStdout);
+ neoModules.neoRuntimeManager.event.removeListener("proc:stderr", onProcStderr);
+ neoModules.neoRuntimeManager.event.removeListener("debugger:state", onDebuggerState);
+ return neoModules.neoRuntimeManager.stopDebugger();
+ };
+ socket.on("editor:open", (modeId, fn) => {
+ neoModules.neoRuntimeManager.event.on("proc:start", onProcStart);
+ neoModules.neoRuntimeManager.event.on("proc:exit", onProcStop);
+ neoModules.neoRuntimeManager.event.on("proc:stdout", onProcStdout);
+ neoModules.neoRuntimeManager.event.on("proc:stderr", onProcStderr);
+ neoModules.neoRuntimeManager.event.on("debugger:state", onDebuggerState);
+ let res = neoModules.neoRuntimeManager.startDebugger(modeId);
+ if (!res.success) { fn(res); return; }
+ logger.info(`Starting debugger for ${modeId}.`)
+ debuggerOpen = true;
+ fn({success: true})
+ socket.emit("editor:code", modeId, res.code);
+
+ if (neoModules.neoRuntimeManager.modeRunning()) {
+ socket.emit("editor:proc:start");
+ }
+ });
+ socket.on("editor:save", (modeId, code, fn) => {
+ if (!debuggerOpen) { fn({success: false, reason: "debugger not open"}); return; };
+ fn(neoModules.neoRuntimeManager.saveModeCode(modeId, code));
+ });
+ socket.on("editor:startmode", (fn) => {
+ if (neoModules.neoRuntimeManager.modeRunning()) {
+ fn({success: true});
+ socket.emit("editor:proc:start");
+ } else {
+ fn(neoModules.neoRuntimeManager.startMode());
+ }
+ });
+ socket.on("editor:stopmode", (fn) => {
+ fn(neoModules.neoRuntimeManager.stopMode());
+ });
+ socket.on("editor:restartmode", (fn) => {
+ fn(neoModules.neoRuntimeManager.restartMode());
+ });
+ socket.on("editor:close", (fn) => {
+ fn(closeDebugger());
+ logger.info("Stopped debugger");
+ });
+
+ /* Matrix and strip buffer */
+ socket.on("matrix:get", () => {
+ socket.emit("matrix", neoModules.neoRuntimeManager.matrix);
+ });
+
+ socket.on("disconnect", () => {
+ logger.access(`SOCKET:authed Client (${socket.id}@${socket.handshake.headers.host}) disconnected.`);
+ if (debuggerOpen) {
+ closeDebugger();
+ logger.info("Stopped debugger because client disconnected")
+ }
+ });
+ });
+
+ neoModules.neoRuntimeManager.event.on("matrix", (matrix) => {
+ authorizedNamespace.emit("matrix", matrix);
+ });
+ let lastStripBufferEmit = performance.now();
+ neoModules.neoRuntimeManager.event.on("strip_buffer", (strip_buffer) => {
+ if ((performance.now() - lastStripBufferEmit) > 50) {
+ authorizedNamespace.volatile.emit("strip_buffer", strip_buffer);
+ lastStripBufferEmit = performance.now();
+ } // We just drop packets
+ });
+ 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);
+ });
+}
+
+/**
+ * Protect
+ */
+function limitEmits(fn) {
+ let lastEmit = performance.now();
+
+ return {
+ }
+}
+
+/**
+ * Creates an access-token from the clients host-name and the current EPOCH.
+ *
+ * @param {client}
+ *
+ * @return {string} - The access-token.
+ */
+ function createToken(client) {
+ let time = Date.now().toString();
+ let host = client.handshake.headers.host;
+ return (CryptoJS.SHA256(`${host}${time}`).toString());
+}
+
+/**
+ * Create a new salt and hash from a password.
+ *
+ * @param {string} password - The password to hash.
+ * @param {string} salt - If set, this salt will be used, else a new salt is generated.
+ *
+ * @return {object} A object containing a password and a salt property.
+ */
+function hashPassword(password, salt = null) {
+ if (salt == null) {
+ salt = CryptoJS.lib.WordArray.random(128 / 2);
+ } else {
+ salt = CryptoJS.enc.Hex.parse(salt);
+ }
+ let hash = CryptoJS.PBKDF2(password, salt, {
+ keySize: 512 / 32,
+ iterations: 1000,
+ hasher: CryptoJS.algo.SHA512
+ });
+ return {hash: hash.toString(), salt: salt.toString()}
+}
+
+module.exports = (_neoModules, io) => {
+ neoModules = _neoModules;
+ return {
+ openNamespace: createOpenSocketNamespace(io),
+ authorizedNamespace: createAuthorizedNamespace(io)
+ }
+};
+
diff --git a/src/SocketIO/index.js b/src/SocketIO/index.js
deleted file mode 100644
index 675efc5..0000000
--- a/src/SocketIO/index.js
+++ /dev/null
@@ -1,409 +0,0 @@
-/**
- * This module contains code for handling socketIO clients.
- *
- * There are two classes, one is a SocketIO controller module.
- * The other one is a authorizedclient.
- *
- * @author jakobst1n.
- * @since 19.12.2019
- */
-
-let logger = require(__appdir + "/src/Logger");
-var exec = require('child_process').exec;
-var CryptoJS = require("crypto-js");
-let fs = require("fs");
-const { performance } = require("perf_hooks");
-
-let neoModules;
-
-const sanitizePath = (path) => path.match(/(user|remote|builtin\/[a-zA-Z0-9-_\/]{1,200})(\.[a-zA-Z0-9]{1,10})?/)[0];
-
-/**
- * Create the open socketio namespace and setup all listeners.
- *
- * @param {io} socketio
- */
-function createOpenSocketNamespace(io) {
- const openNamespace = io.of("/open")
-
- openNamespace.on("connection", (socket) => {
- logger.access(`SOCKET:open Client (${socket.id}@${socket.handshake.headers.host}) connected.`);
-
- socket.on("name:get", () => {
- socket.emit("name", neoModules.userData.config.instanceName);
- });
- socket.on("mode:set", (modeId) => {
- neoModules.neoRuntimeManager.mode.set(modeId);
- });
- socket.on("mode:get", () => {
- socket.emit("mode", neoModules.neoRuntimeManager.mode.current());
- });
- socket.on("modelist:get", () => {
- socket.emit("modelist", neoModules.neoRuntimeManager.modes())
- });
- socket.on("brightness:set", (brightness) => {
- neoModules.neoRuntimeManager.mode.globvars.set("brightness", brightness);
- });
- socket.on("brightness:get", () => {
- socket.emit("brightness", neoModules.neoRuntimeManager.mode.globvars.get().brightness);
- });
- socket.on("power:set", (power) => {
- neoModules.neoRuntimeManager.mode.globvars.set("power_on", power);
- });
- socket.on("power:get", () => {
- socket.emit("power", neoModules.neoRuntimeManager.mode.globvars.get().power_on);
- });
- socket.on("var:set", (name, value) => {
- neoModules.neoRuntimeManager.mode.variables.set(name, value.toString());
- });
- socket.on("vars:get", () => {
- socket.emit("vars", neoModules.neoRuntimeManager.mode.variables.get());
- });
- socket.on("modeinfo:get", () => {
- socket.emit("modeinfo", {
- mode: neoModules.neoRuntimeManager.mode.current(),
- brightness: neoModules.neoRuntimeManager.mode.globvars.get().brightness,
- power: neoModules.neoRuntimeManager.mode.globvars.get().power_on,
- vars: neoModules.neoRuntimeManager.mode.variables.get()
- });
- });
- socket.on("authenticate:user", (username, password, callback) => {
- let user = neoModules.userData.user.get(username);
- if (user == null) {
- callback({success: false, reason: "Invalid username/password"})
- logger.access(`SOCKET:open Client (${socket.id}@${socket.handshake.headers.host}) tried to log in as '${username}', wrong username and/or password.`);
- return;
- }
-
- let providedHash = hashPassword(password, user.salt);
- if (providedHash.hash == user.password) {
- let token = createToken(socket);
- while (session_tokens.hasOwnProperty(token)) {
- token = createToken(socket);
- }
-
- session_tokens[token] = {
- expire: (~~Date.now())+(2678400),
- host: socket.handshake.headers.host,
- user: {username: user.username}
- };
-
- callback({success: true, user: {username: username}, token: token})
- logger.access(`SOCKET:open Client (${socket.id}@${socket.handshake.headers.host}) authenticated as user '${username}'`);
- return;
- }
-
- callback({success: false, reason: "Invalid username/password"})
- logger.access(`SOCKET:open Client (${socket.id}@${socket.handshake.headers.host}) tried to log in as '${username}', wrong username and/or password.`);
- });
-
- 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) => {
- if (name == "modelist") {
- openNamespace.emit("modelist", neoModules.neoRuntimeManager.modes());
- } else if (["mode", "power_on", "brightness"].includes(name)) {
- if (name == "power_on") { name = "power"; }
- openNamespace.emit(name, value);
- } else {
- 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");
- });
-}
-
-/**
- * @type {object} This is the collection of valid session tokens.
- */
-let session_tokens = {};
-
-/**
- * Middleware that will stop the request if the client does not have a valid
- * session token.
- *
- * @param {object} socket - The socket instance of the connected client
- * @param {function} next - The callback to continue the middleware chain
- */
-function authorize_middleware(socket, next) {
- const token = socket.handshake.auth.token;
-
- if (session_tokens.hasOwnProperty(token) &&
- // session_tokens[token].host === socket.handshake.headers.host &&
- session_tokens[token].expire > (~~(Date.now()))) {
- socket.data.user = session_tokens[token].user;
- next();
- } else {
- const err = new Error("not authorized");
- err.data = { content: "invalid session token" };
- next(err);
- }
-}
-
-/**
- * Create the open socketio namespace and setup all listeners.
- * A valid session token is required to connect to this namespace.
- *
- * @param {io} socetio
- */
-function createAuthorizedNamespace(io) {
- const authorizedNamespace = io.of("/authed");
- authorizedNamespace.use(authorize_middleware);
- authorizedNamespace.on("connection", (socket) => {
- logger.access(`SOCKET:authed Client (${socket.id}@${socket.handshake.headers.host}) connected.`);
- let debuggerOpen = false;
-
- socket.emit("user", socket.data.user);
-
- /* InstanceName */
- socket.on("name:set", (name, fn) => {
- neoModules.userData.config.instanceName = name;
- fn({success: true});
- io.emit("name", neoModules.userData.config.instanceName);
- });
-
- /* UserData */
- socket.on("mode:create", (name, template, fn) => {
- fn(neoModules.userData.mode.create(name, template));
- });
- socket.on("mode:delete", (modeid, fn) => {
- fn(neoModules.userData.mode.delete(modeid));
- });
-
- /* LED Config */
- socket.on("led_config:get", () => {
- socket.emit("led_config", neoModules.userData.strip.get());
- });
- socket.on("led_config:set", (config) => {
- neoModules.userData.strip.set(config);
- });
-
- /* SelfUpdater */
- socket.on("version:current_number", () => {
- socket.emit("version:current_number", neoModules.selfUpdater.localVersionNumber);
- });
- socket.on("version:branch", (fn) => {
- socket.emit("version:branch", neoModules.selfUpdater.branch);
- });
- socket.on("version:newest_number", (fn) => {
- socket.emit("version:newest_number", neoModules.selfUpdater.remoteVersionNumber);
- });
- socket.on("version:check_for_update", (fn) => {
- neoModules.selfUpdater.checkVersion().then(() => {
- socket.emit("version:newest_number", neoModules.selfUpdater.remoteVersionNumber);
- fn({success: true});
- });
- });
- socket.on("system:update_version", () => {
- neoModules.selfUpdater.updater.forceUpdate();
- });
-
- /* SSLCert */
- socket.on("sslcert:info", (fn) => {
- socket.emit("sslcert:info", {...neoModules.SSLCert.getConfig(), "isValid": neoModules.SSLCert.checkValidity()});
- });
- socket.on("sslcert:generate_new", (fn) => {
- neoModules.SSLCert.generateCert();
- fn({success: true});
- });
-
- /* System actions */
- socket.on("restart:system", () => {
- exec('shutdown -r now', function(error, stdout, stderr){ callback(stdout); });
- });
- socket.on("restart:service", () => {
- let p = exec('systemctl restart luxcena-neo');
- p.unref();
- });
-
- /* Users */
- socket.on("users:get", () => {
- socket.emit("users", neoModules.userData.users())
- });
- socket.on("user:delete", (username, fn) => {
- if (username == socket.data.user.username) { fn({success: false, reason: "cannot delete logged in account"}); return; }
- fn(neoModules.userData.user.delete(username));
- socket.emit("users", neoModules.userData.users())
- });
- socket.on("user:changeusername", (oldusername, newusername, fn) => {
- if (oldusername == socket.data.user.username) { fn({success: false, reason: "cannot change username of logged in account"}); return; }
- let user = neoModules.userData.user.get(oldUserName);
- if (user == null) { fn({success: false, reason: "unknown username", detail: oldusername}); return; }
- user.username = newusername;
- let res = neoModules.userData.user.save(user);
- if (!res.success) { fn(res); return; }
- res = neoModules.userData.user.delete(oldusername)
- if (!res.success) { fn(res); return; }
- fn({success: true});
- socket.emit("users", neoModules.userData.users())
- });
- socket.on("user:newpassword", (username, newPassword, fn) => {
- let user = neoModules.userData.user.get(username);
- if (user == null) { fn({success: false, reason: "unknown username", detail: username}); return; }
- let newHash = hashPassword(newPassword);
- fn(neoModules.userData.user.save(username, newHash.salt, newHash.hash));
- socket.emit("users", neoModules.userData.users())
- });
- socket.on("user:create", (username, newPassword, fn) => {
- let user = neoModules.userData.user.get(username);
- if (user != null) { fn({success: false, reason: "user already exists", detail: username}); return; }
- if (username.length < 1) { fn({success: false, reason: "no username provided"}); return; }
- let newHash = hashPassword(newPassword);
- fn(neoModules.userData.user.save(username, newHash.salt, newHash.hash));
- socket.emit("users", neoModules.userData.users())
- });
-
- /* Editor/debugger */
- let onProcStart = () => socket.emit("editor:proc:start");
- let onProcStop = (code) => socket.emit("editor:proc:exit", code);
- let onProcStdout = (stdout) => socket.volatile.emit("editor:proc:stdout", stdout);
- let onProcStderr = (stderr) => socket.volatile.emit("editor:proc:stderr", stderr);
- let onDebuggerState = (state) => socket.volatile.emit("editor:debugger:state", state);
- let closeDebugger = () => {
- debuggerOpen = false;
- neoModules.neoRuntimeManager.event.removeListener("proc:start", onProcStart);
- neoModules.neoRuntimeManager.event.removeListener("proc:stop", onProcStop);
- neoModules.neoRuntimeManager.event.removeListener("proc:stdout", onProcStdout);
- neoModules.neoRuntimeManager.event.removeListener("proc:stderr", onProcStderr);
- neoModules.neoRuntimeManager.event.removeListener("debugger:state", onDebuggerState);
- return neoModules.neoRuntimeManager.stopDebugger();
- };
- socket.on("editor:open", (modeId, fn) => {
- neoModules.neoRuntimeManager.event.on("proc:start", onProcStart);
- neoModules.neoRuntimeManager.event.on("proc:exit", onProcStop);
- neoModules.neoRuntimeManager.event.on("proc:stdout", onProcStdout);
- neoModules.neoRuntimeManager.event.on("proc:stderr", onProcStderr);
- neoModules.neoRuntimeManager.event.on("debugger:state", onDebuggerState);
- let res = neoModules.neoRuntimeManager.startDebugger(modeId);
- if (!res.success) { fn(res); return; }
- logger.info(`Starting debugger for ${modeId}.`)
- debuggerOpen = true;
- fn({success: true})
- socket.emit("editor:code", modeId, res.code);
-
- if (neoModules.neoRuntimeManager.modeRunning()) {
- socket.emit("editor:proc:start");
- }
- });
- socket.on("editor:save", (modeId, code, fn) => {
- if (!debuggerOpen) { fn({success: false, reason: "debugger not open"}); return; };
- fn(neoModules.neoRuntimeManager.saveModeCode(modeId, code));
- });
- socket.on("editor:startmode", (fn) => {
- if (neoModules.neoRuntimeManager.modeRunning()) {
- fn({success: true});
- socket.emit("editor:proc:start");
- } else {
- fn(neoModules.neoRuntimeManager.startMode());
- }
- });
- socket.on("editor:stopmode", (fn) => {
- fn(neoModules.neoRuntimeManager.stopMode());
- });
- socket.on("editor:restartmode", (fn) => {
- fn(neoModules.neoRuntimeManager.restartMode());
- });
- socket.on("editor:close", (fn) => {
- fn(closeDebugger());
- logger.info("Stopped debugger");
- });
-
- /* Matrix and strip buffer */
- socket.on("matrix:get", () => {
- socket.emit("matrix", neoModules.neoRuntimeManager.matrix);
- });
-
- socket.on("disconnect", () => {
- logger.access(`SOCKET:authed Client (${socket.id}@${socket.handshake.headers.host}) disconnected.`);
- if (debuggerOpen) {
- closeDebugger();
- logger.info("Stopped debugger because client disconnected")
- }
- });
- });
-
- neoModules.neoRuntimeManager.event.on("matrix", (matrix) => {
- authorizedNamespace.emit("matrix", matrix);
- });
- let lastStripBufferEmit = performance.now();
- neoModules.neoRuntimeManager.event.on("strip_buffer", (strip_buffer) => {
- if ((performance.now() - lastStripBufferEmit) > 50) {
- authorizedNamespace.volatile.emit("strip_buffer", strip_buffer);
- lastStripBufferEmit = performance.now();
- } // We just drop packets
- });
- 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);
- });
-}
-
-/**
- * Protect
- */
-function limitEmits(fn) {
- let lastEmit = performance.now();
-
- return {
- }
-}
-
-/**
- * Creates an access-token from the clients host-name and the current EPOCH.
- *
- * @param {client}
- *
- * @return {string} - The access-token.
- */
- function createToken(client) {
- let time = Date.now().toString();
- let host = client.handshake.headers.host;
- return (CryptoJS.SHA256(`${host}${time}`).toString());
-}
-
-/**
- * Create a new salt and hash from a password.
- *
- * @param {string} password - The password to hash.
- * @param {string} salt - If set, this salt will be used, else a new salt is generated.
- *
- * @return {object} A object containing a password and a salt property.
- */
-function hashPassword(password, salt = null) {
- if (salt == null) {
- salt = CryptoJS.lib.WordArray.random(128 / 2);
- } else {
- salt = CryptoJS.enc.Hex.parse(salt);
- }
- let hash = CryptoJS.PBKDF2(password, salt, {
- keySize: 512 / 32,
- iterations: 1000,
- hasher: CryptoJS.algo.SHA512
- });
- return {hash: hash.toString(), salt: salt.toString()}
-}
-
-module.exports = (_neoModules, io) => {
- neoModules = _neoModules;
- return {
- openNamespace: createOpenSocketNamespace(io),
- authorizedNamespace: createAuthorizedNamespace(io)
- }
-};
-
diff --git a/src/UserData/index.cjs b/src/UserData/index.cjs
new file mode 100644
index 0000000..0c861b1
--- /dev/null
+++ b/src/UserData/index.cjs
@@ -0,0 +1,332 @@
+/**
+ * This module is the entry of UserData. This will ensure the user-dirs and all config-files.
+ * Also, it will
+ *
+ * @author jakobst1n.
+ * @since 19.12.2019
+ */
+
+let logger = require("../Logger/index.cjs");
+let fse = require("fs-extra");
+let ini = require('ini');
+
+let neoModules;
+
+/**
+ * This method will ensure that all required fields are in config.ini
+ */
+function ensureMainConfig() {
+ var config = ini.decode(fse.readFileSync(__configdir + "/config.ini", 'utf-8'))
+
+ if (config.instanceName == null) { config.instanceName = "neoStrip"; }
+ if (config.activeMode == null) { config.activeMode = "builtin/static"; }
+
+ if (config.HTTP == null) { config.HTTP = {}; }
+ if (config.HTTP.port == null) { config.HTTP.port = 443; }
+
+ if (config.SelfUpdater == null) { config.SelfUpdater = {}; }
+ if (config.SelfUpdater.checkVersionInterval == null) { config.SelfUpdater.checkVersionInterval = 1; }
+ if (config.SelfUpdater.automaticUpdate == null) { config.SelfUpdater.automaticUpdate = false; }
+
+ if (config.SSLCert == null) { config.SSLCert = {}; }
+ if (config.SSLCert.CN == null) { config.SSLCert.CN = "localhost"; }
+ if (config.SSLCert.certMade == null) { config.SSLCert.certMade = false; }
+ if (config.SSLCert.certDate == null) { config.SSLCert.certDate = 0; }
+ if (config.SSLCert.certExpire == null) { config.SSLCert.certExpire = 0; }
+ if (config.SSLCert.certCN == null) { config.SSLCert.certCN = ""; }
+
+ if (config.DiscoveryServer == null) { config.DiscoveryServer = {}; }
+ if (config.DiscoveryServer.address == null) { config.DiscoveryServer.address = "https://erj46s.deta.dev"; }
+ if (config.DiscoveryServer.broadcastSelf == null) { config.DiscoveryServer.broadcastSelf = false; }
+
+ if (config.neoRuntimeIPC == null) { config.neoRuntimeIPC = {}; }
+ if (config.neoRuntimeIPC.socketFile == null) { config.neoRuntimeIPC.socketFile = "/tmp/neo_runtime.sock"; }
+
+ fse.writeFileSync(__configdir + "/config.ini", ini.encode(config))
+}
+
+/**
+ * This method will ensure that all required fields are in config.ini
+ */
+function ensureStripConfig() {
+ var config = ini.decode(fse.readFileSync(__configdir + "/strip.ini", 'utf-8'))
+
+ if (config.DEFAULT == null) { config.DEFAULT = {}; }
+ if (config.DEFAULT.led_pin == null) { config.DEFAULT.led_pin = 18; }
+ if (config.DEFAULT.led_freq_hz == null) { config.DEFAULT.led_freq_hz = 80000; }
+ if (config.DEFAULT.led_dma == null) { config.DEFAULT.led_dma = 10; }
+ if (config.DEFAULT.led_invert == null) { config.DEFAULT.led_invert = false; }
+ if (config.DEFAULT.led_channel == null) { config.DEFAULT.led_channel = 0 }
+ if (config.DEFAULT.segments == null) { config.DEFAULT.segments = "50 50"; }
+ if (config.DEFAULT.matrix == null) { config.DEFAULT.matrix = "[[[0,false]],[[1,false]]]"; }
+
+ fse.writeFileSync(__configdir + "/strip.ini", ini.encode(config))
+}
+
+/**
+ * This method will make sure all files and folders needed for the app exists,
+ * it will also make sure all files contain all needed data.
+ */
+function init() {
+ // Generate all user-folders
+ logger.info("Ensuring all folder in UserDir exists...");
+
+ fse.ensureDirSync(__datadir + "/");
+ fse.ensureDirSync(__configdir);
+ fse.ensureDirSync(__configdir + "/certs");
+ fse.ensureDirSync(__datadir + "/userCode/");
+ fse.ensureDirSync(__datadir + "/remoteCode/");
+
+ // Generate config-files
+ if (!fse.existsSync(__configdir + "/config.ini")) {
+ fse.closeSync(fse.openSync(__configdir + "/config.ini", 'w'));
+ }
+ ensureMainConfig();
+
+ if (!fse.existsSync(__configdir + "/strip.ini")) {
+ fse.closeSync(fse.openSync(__configdir + "/strip.ini", 'w'));
+ }
+ ensureStripConfig();
+
+ if (!fse.existsSync(__configdir + "/users.ini")) {
+ fse.writeFileSync(__configdir + "/users.ini", ini.encode({
+ "neo": {
+ "password": "5adbc90fb4716fff62d9cf634837e22f29b011803ba29cee51f921b920fa941651737bd15d00dc72e4cbeee5e64e06ec99cc50ea917285a029797a98740cce0f",
+ "salt": "59b6de1040f3ae3c63de984ca5d61ef46f41dc6ecead3a9d5dab69f0bb3636aa49017e179b74dbcdb407f62bc139a7d55aa78fe2bbdd5327609ea124b2fa03b1"
+ }
+ }))
+ }
+};
+
+/**
+ * Recursive function which adds setters and getters to all properties
+ * in a nested object. This will make us able to save config values
+ * directly without doing anything other that `prop = value`.
+ *
+ * @param {object} config - The full config object.
+ * @param {string} configFile - The path of the configfile.
+ *
+ * @return {object} The config object with setters for values.
+ */
+ function withSetters(config, configFile) {
+ let outConfig = {};
+ function iter(inNode, outNode) {
+ for (const key of Object.keys(inNode)) {
+ if (typeof(inNode[key]) === "object") {
+ outNode[key] = {};
+ iter(inNode[key], outNode[key]);
+ } else {
+ outNode[`_${key}`] = inNode[key];
+ Object.defineProperty(outNode, key, {
+ get: function() { return this[`_${key}`]; },
+ set: function(value) {
+ this[`_${key}`] = value;
+ saveConfig(configFile, outConfig);
+ },
+ enumerable: true
+ });
+ }
+ }
+ }
+ iter(config, outConfig);
+ return outConfig
+}
+
+/**
+ * Returns a object with only the actual values and not setters, this is the
+ * inverse of withSetters.
+ *
+ * @param {object} config - The full config object.
+ *
+ * @return {object} The config object without setters.
+ */
+function withoutSetters(config) {
+ let out = {};
+ function iter(inNode, outNode) {
+ for (const key of Object.keys(inNode).filter(k => (k.substr(0, 1) != "_"))) {
+ if (typeof(inNode[key]) === "object") {
+ outNode[key] = {};
+ iter(inNode[key], outNode[key], out);
+ } else {
+ outNode[key] = inNode[`_${key}`];
+ }
+ }
+ }
+ iter(config, out);
+ return out;
+}
+
+/**
+ * Save config object, this will run stripSetters on the object it saves.
+ *
+ * @param {string} file - filename to save the config object to.
+ * @param {object} object - the config object to save.
+ */
+function saveConfig(file, object, removeSetters=true) {
+ if (removeSetters) {
+ object = withoutSetters(object);
+ }
+ fse.writeFileSync(file, ini.encode(object));
+}
+
+/**
+ * Reads a ini file and add setters to all properties
+ *
+ * @param {string} file - filename of file to read.
+ *
+ * @return {object} The config in the file.
+ */
+function getFullConfig(file, addSetters=true) {
+ let fullConfig = ini.decode(fse.readFileSync(file, "utf-8"));
+ if (addSetters) {
+ fullConfig = withSetters(fullConfig, file);
+ }
+ return fullConfig;
+}
+
+/**
+ * Save a user the user config file, this will append if a new user, and
+ * overwrite if it is a existsing user.
+ *
+ * @param {string} username - The username, case-insensitive.
+ * @param {string} salt - Salt used for password-checking.
+ * @param {string} password - Hashed password.
+ *
+ * @return {object} Standardform return object
+ */
+ function saveUser(username, salt, password) {
+ let config = ini.decode(fse.readFileSync(__configdir + "/users.ini", 'utf-8'))
+ config[username] = {}
+ config[username].salt = salt
+ config[username].password = password
+ fse.writeFileSync(__configdir + "/users.ini", ini.encode(config))
+ return {success: true}
+}
+
+/**
+ * Get a user, this will return null if no user is found.
+ *
+ * @return {object} with username, salt and hash properties.
+ */
+function getUser(username) {
+ let config = ini.decode(fse.readFileSync(__configdir + "/users.ini", 'utf-8'))
+ if (Object.prototype.hasOwnProperty.call(config, username)) {
+ return {...config[username], username: username}
+ }
+ return null;
+}
+
+/**
+ * Get all users
+ *
+ * @return {array} usernames
+ */
+function getUsers() {
+ let config = ini.decode(fse.readFileSync(__configdir + "/users.ini", "utf-8"));
+ let users = [];
+ for (const username of Object.keys(config)) {
+ users.push(username);
+ }
+ return users;
+}
+
+/**
+ * Delete a user
+ *
+ * @return {object} Standardform success object.
+ */
+function deleteUser(username) {
+ let config = ini.decode(fse.readFileSync(__configdir + "/users.ini", 'utf-8'))
+ if (config.length <= 1) { return {success: false, reason: "cannot delete only user"}; }
+ if (!Object.prototype.hasOwnProperty.call(config, username)) { return {success: false, reason: "user not found", detail: username}; }
+ delete config[username];
+ fse.writeFileSync(__configdir + "/users.ini", ini.encode(config));
+ return {success: true}
+}
+
+/**
+ * Create a new mode in the user directory.
+ *
+ * @param {string} name - The name of the file to use, a trailing number will
+ * be added if there are any conflicts.
+ * @param {string} template - Id of the template, builtin/static, template/base etc...
+ *
+ * @return {object} a standard convention result object.
+ */
+function createNewUserMode(name, template) {
+ source_script = null;
+ if ((template === "template/base") || (template === "") || (template == null)) {
+ source_script = __appdir + "/NeoRuntime/special/template_base/";
+ } else {
+ source_script = neoModules.neoRuntimeManager.getModePath(template);
+ }
+ if (!neoModules.neoRuntimeManager.isMode(source_script)) {
+ return {success: false, reason: "Source script not found"};
+ }
+
+ let newModeName = neoModules.neoRuntimeManager.getModePath(`user/${name}`);
+ let counter = 0;
+ while (neoModules.neoRuntimeManager.isMode(newModeName)) {
+ counter += 1;
+ newModeName = neoModules.neoRuntimeManager.getModePath(`user/${name}_${counter}`);
+ }
+
+ fse.ensureDirSync(newModeName);
+ fse.copySync(`${source_script}/script.py`, `${newModeName}/script.py`)
+ neoModules.neoRuntimeManager.event.emit("change", "modelist");
+ return {success: true};
+}
+
+/**
+ * Delete a user created mode
+ *
+ * @param {string} modeid - modeid to delete
+ *
+ * @return {object} a standard convention result object.
+ */
+function deleteUserMode(modeid) {
+ if (modeid.substr(0, 5) !== "user/") {
+ return {success: false, reason: "Not user mode"}
+ }
+ let modePath = neoModules.neoRuntimeManager.getModePath(modeid);
+ if (!neoModules.neoRuntimeManager.isMode(modePath)) {
+ return {success: false, reason: "Mode does not found"}
+ }
+ if (modeid === neoModules.neoRuntimeManager.mode.current()) {
+ return {success: false, reason: "Cannot delete currently active mode"}
+ }
+ fse.removeSync(modePath);
+ neoModules.neoRuntimeManager.event.emit("change", "modelist");
+ return {success: true}
+}
+
+module.exports = (_neoModules) => {
+ neoModules = _neoModules;
+ init();
+ return {
+ users: getUsers,
+ user: {
+ save: saveUser,
+ get: getUser,
+ delete: deleteUser
+ },
+ strip: {
+ get: () => {
+ let c = getFullConfig(`${__configdir}/strip.ini`, addSetters=false);
+ c.DEFAULT.matrix = JSON.parse(c.DEFAULT.matrix);
+ c.DEFAULT.segments = c.DEFAULT.segments.split(" ");
+ return c.DEFAULT;
+ },
+ set: (c) => {
+ c.segments = c.segments.join(" ");
+ c.matrix = JSON.stringify(c.matrix);
+ return saveConfig(`${__configdir}/strip.ini`, {DEFAULT: c}, removeSetters=false);
+ },
+ },
+ config: getFullConfig(`${__configdir}/config.ini`),
+ mode: {
+ create: createNewUserMode,
+ delete: deleteUserMode
+ }
+ }
+};
diff --git a/src/UserData/index.js b/src/UserData/index.js
deleted file mode 100644
index e442a79..0000000
--- a/src/UserData/index.js
+++ /dev/null
@@ -1,332 +0,0 @@
-/**
- * This module is the entry of UserData. This will ensure the user-dirs and all config-files.
- * Also, it will
- *
- * @author jakobst1n.
- * @since 19.12.2019
- */
-
-let logger = require(__appdir + "/src/Logger");
-let fse = require("fs-extra");
-let ini = require('ini');
-
-let neoModules;
-
-/**
- * This method will ensure that all required fields are in config.ini
- */
-function ensureMainConfig() {
- var config = ini.decode(fse.readFileSync(__configdir + "/config.ini", 'utf-8'))
-
- if (config.instanceName == null) { config.instanceName = "neoStrip"; }
- if (config.activeMode == null) { config.activeMode = "builtin/static"; }
-
- if (config.HTTP == null) { config.HTTP = {}; }
- if (config.HTTP.port == null) { config.HTTP.port = 443; }
-
- if (config.SelfUpdater == null) { config.SelfUpdater = {}; }
- if (config.SelfUpdater.checkVersionInterval == null) { config.SelfUpdater.checkVersionInterval = 1; }
- if (config.SelfUpdater.automaticUpdate == null) { config.SelfUpdater.automaticUpdate = false; }
-
- if (config.SSLCert == null) { config.SSLCert = {}; }
- if (config.SSLCert.CN == null) { config.SSLCert.CN = "localhost"; }
- if (config.SSLCert.certMade == null) { config.SSLCert.certMade = false; }
- if (config.SSLCert.certDate == null) { config.SSLCert.certDate = 0; }
- if (config.SSLCert.certExpire == null) { config.SSLCert.certExpire = 0; }
- if (config.SSLCert.certCN == null) { config.SSLCert.certCN = ""; }
-
- if (config.DiscoveryServer == null) { config.DiscoveryServer = {}; }
- if (config.DiscoveryServer.address == null) { config.DiscoveryServer.address = "https://erj46s.deta.dev"; }
- if (config.DiscoveryServer.broadcastSelf == null) { config.DiscoveryServer.broadcastSelf = false; }
-
- if (config.neoRuntimeIPC == null) { config.neoRuntimeIPC = {}; }
- if (config.neoRuntimeIPC.socketFile == null) { config.neoRuntimeIPC.socketFile = "/tmp/neo_runtime.sock"; }
-
- fse.writeFileSync(__configdir + "/config.ini", ini.encode(config))
-}
-
-/**
- * This method will ensure that all required fields are in config.ini
- */
-function ensureStripConfig() {
- var config = ini.decode(fse.readFileSync(__configdir + "/strip.ini", 'utf-8'))
-
- if (config.DEFAULT == null) { config.DEFAULT = {}; }
- if (config.DEFAULT.led_pin == null) { config.DEFAULT.led_pin = 18; }
- if (config.DEFAULT.led_freq_hz == null) { config.DEFAULT.led_freq_hz = 80000; }
- if (config.DEFAULT.led_dma == null) { config.DEFAULT.led_dma = 10; }
- if (config.DEFAULT.led_invert == null) { config.DEFAULT.led_invert = false; }
- if (config.DEFAULT.led_channel == null) { config.DEFAULT.led_channel = 0 }
- if (config.DEFAULT.segments == null) { config.DEFAULT.segments = "50 50"; }
- if (config.DEFAULT.matrix == null) { config.DEFAULT.matrix = "[[[0,false]],[[1,false]]]"; }
-
- fse.writeFileSync(__configdir + "/strip.ini", ini.encode(config))
-}
-
-/**
- * This method will make sure all files and folders needed for the app exists,
- * it will also make sure all files contain all needed data.
- */
-function init() {
- // Generate all user-folders
- logger.info("Ensuring all folder in UserDir exists...");
-
- fse.ensureDirSync(__datadir + "/");
- fse.ensureDirSync(__configdir);
- fse.ensureDirSync(__configdir + "/certs");
- fse.ensureDirSync(__datadir + "/userCode/");
- fse.ensureDirSync(__datadir + "/remoteCode/");
-
- // Generate config-files
- if (!fse.existsSync(__configdir + "/config.ini")) {
- fse.closeSync(fse.openSync(__configdir + "/config.ini", 'w'));
- }
- ensureMainConfig();
-
- if (!fse.existsSync(__configdir + "/strip.ini")) {
- fse.closeSync(fse.openSync(__configdir + "/strip.ini", 'w'));
- }
- ensureStripConfig();
-
- if (!fse.existsSync(__configdir + "/users.ini")) {
- fse.writeFileSync(__configdir + "/users.ini", ini.encode({
- "neo": {
- "password": "5adbc90fb4716fff62d9cf634837e22f29b011803ba29cee51f921b920fa941651737bd15d00dc72e4cbeee5e64e06ec99cc50ea917285a029797a98740cce0f",
- "salt": "59b6de1040f3ae3c63de984ca5d61ef46f41dc6ecead3a9d5dab69f0bb3636aa49017e179b74dbcdb407f62bc139a7d55aa78fe2bbdd5327609ea124b2fa03b1"
- }
- }))
- }
-};
-
-/**
- * Recursive function which adds setters and getters to all properties
- * in a nested object. This will make us able to save config values
- * directly without doing anything other that `prop = value`.
- *
- * @param {object} config - The full config object.
- * @param {string} configFile - The path of the configfile.
- *
- * @return {object} The config object with setters for values.
- */
- function withSetters(config, configFile) {
- let outConfig = {};
- function iter(inNode, outNode) {
- for (const key of Object.keys(inNode)) {
- if (typeof(inNode[key]) === "object") {
- outNode[key] = {};
- iter(inNode[key], outNode[key]);
- } else {
- outNode[`_${key}`] = inNode[key];
- Object.defineProperty(outNode, key, {
- get: function() { return this[`_${key}`]; },
- set: function(value) {
- this[`_${key}`] = value;
- saveConfig(configFile, outConfig);
- },
- enumerable: true
- });
- }
- }
- }
- iter(config, outConfig);
- return outConfig
-}
-
-/**
- * Returns a object with only the actual values and not setters, this is the
- * inverse of withSetters.
- *
- * @param {object} config - The full config object.
- *
- * @return {object} The config object without setters.
- */
-function withoutSetters(config) {
- let out = {};
- function iter(inNode, outNode) {
- for (const key of Object.keys(inNode).filter(k => (k.substr(0, 1) != "_"))) {
- if (typeof(inNode[key]) === "object") {
- outNode[key] = {};
- iter(inNode[key], outNode[key], out);
- } else {
- outNode[key] = inNode[`_${key}`];
- }
- }
- }
- iter(config, out);
- return out;
-}
-
-/**
- * Save config object, this will run stripSetters on the object it saves.
- *
- * @param {string} file - filename to save the config object to.
- * @param {object} object - the config object to save.
- */
-function saveConfig(file, object, removeSetters=true) {
- if (removeSetters) {
- object = withoutSetters(object);
- }
- fse.writeFileSync(file, ini.encode(object));
-}
-
-/**
- * Reads a ini file and add setters to all properties
- *
- * @param {string} file - filename of file to read.
- *
- * @return {object} The config in the file.
- */
-function getFullConfig(file, addSetters=true) {
- let fullConfig = ini.decode(fse.readFileSync(file, "utf-8"));
- if (addSetters) {
- fullConfig = withSetters(fullConfig, file);
- }
- return fullConfig;
-}
-
-/**
- * Save a user the user config file, this will append if a new user, and
- * overwrite if it is a existsing user.
- *
- * @param {string} username - The username, case-insensitive.
- * @param {string} salt - Salt used for password-checking.
- * @param {string} password - Hashed password.
- *
- * @return {object} Standardform return object
- */
- function saveUser(username, salt, password) {
- let config = ini.decode(fse.readFileSync(__configdir + "/users.ini", 'utf-8'))
- config[username] = {}
- config[username].salt = salt
- config[username].password = password
- fse.writeFileSync(__configdir + "/users.ini", ini.encode(config))
- return {success: true}
-}
-
-/**
- * Get a user, this will return null if no user is found.
- *
- * @return {object} with username, salt and hash properties.
- */
-function getUser(username) {
- let config = ini.decode(fse.readFileSync(__configdir + "/users.ini", 'utf-8'))
- if (Object.prototype.hasOwnProperty.call(config, username)) {
- return {...config[username], username: username}
- }
- return null;
-}
-
-/**
- * Get all users
- *
- * @return {array} usernames
- */
-function getUsers() {
- let config = ini.decode(fse.readFileSync(__configdir + "/users.ini", "utf-8"));
- let users = [];
- for (const username of Object.keys(config)) {
- users.push(username);
- }
- return users;
-}
-
-/**
- * Delete a user
- *
- * @return {object} Standardform success object.
- */
-function deleteUser(username) {
- let config = ini.decode(fse.readFileSync(__configdir + "/users.ini", 'utf-8'))
- if (config.length <= 1) { return {success: false, reason: "cannot delete only user"}; }
- if (!Object.prototype.hasOwnProperty.call(config, username)) { return {success: false, reason: "user not found", detail: username}; }
- delete config[username];
- fse.writeFileSync(__configdir + "/users.ini", ini.encode(config));
- return {success: true}
-}
-
-/**
- * Create a new mode in the user directory.
- *
- * @param {string} name - The name of the file to use, a trailing number will
- * be added if there are any conflicts.
- * @param {string} template - Id of the template, builtin/static, template/base etc...
- *
- * @return {object} a standard convention result object.
- */
-function createNewUserMode(name, template) {
- source_script = null;
- if ((template === "template/base") || (template === "") || (template == null)) {
- source_script = __appdir + "/NeoRuntime/special/template_base/";
- } else {
- source_script = neoModules.neoRuntimeManager.getModePath(template);
- }
- if (!neoModules.neoRuntimeManager.isMode(source_script)) {
- return {success: false, reason: "Source script not found"};
- }
-
- let newModeName = neoModules.neoRuntimeManager.getModePath(`user/${name}`);
- let counter = 0;
- while (neoModules.neoRuntimeManager.isMode(newModeName)) {
- counter += 1;
- newModeName = neoModules.neoRuntimeManager.getModePath(`user/${name}_${counter}`);
- }
-
- fse.ensureDirSync(newModeName);
- fse.copySync(`${source_script}/script.py`, `${newModeName}/script.py`)
- neoModules.neoRuntimeManager.event.emit("change", "modelist");
- return {success: true};
-}
-
-/**
- * Delete a user created mode
- *
- * @param {string} modeid - modeid to delete
- *
- * @return {object} a standard convention result object.
- */
-function deleteUserMode(modeid) {
- if (modeid.substr(0, 5) !== "user/") {
- return {success: false, reason: "Not user mode"}
- }
- let modePath = neoModules.neoRuntimeManager.getModePath(modeid);
- if (!neoModules.neoRuntimeManager.isMode(modePath)) {
- return {success: false, reason: "Mode does not found"}
- }
- if (modeid === neoModules.neoRuntimeManager.mode.current()) {
- return {success: false, reason: "Cannot delete currently active mode"}
- }
- fse.removeSync(modePath);
- neoModules.neoRuntimeManager.event.emit("change", "modelist");
- return {success: true}
-}
-
-module.exports = (_neoModules) => {
- neoModules = _neoModules;
- init();
- return {
- users: getUsers,
- user: {
- save: saveUser,
- get: getUser,
- delete: deleteUser
- },
- strip: {
- get: () => {
- let c = getFullConfig(`${__configdir}/strip.ini`, addSetters=false);
- c.DEFAULT.matrix = JSON.parse(c.DEFAULT.matrix);
- c.DEFAULT.segments = c.DEFAULT.segments.split(" ");
- return c.DEFAULT;
- },
- set: (c) => {
- c.segments = c.segments.join(" ");
- c.matrix = JSON.stringify(c.matrix);
- return saveConfig(`${__configdir}/strip.ini`, {DEFAULT: c}, removeSetters=false);
- },
- },
- config: getFullConfig(`${__configdir}/config.ini`),
- mode: {
- create: createNewUserMode,
- delete: deleteUserMode
- }
- }
-};
--
cgit v1.2.3