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