diff --git a/.gitignore b/.gitignore index b7e695b..f8c4688 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ docs/ -out/ \ No newline at end of file +out/ +build.json diff --git a/forge.config.js b/forge.config.js index 6cc9c46..71d3f10 100644 --- a/forge.config.js +++ b/forge.config.js @@ -1,4 +1,22 @@ const fs = require("fs"); +const jszip = require("jszip"); +const crypto = require("crypto"); +const path = require("path"); + +const date = new Date().toISOString(); + +const generateBuildFile = (platform, arch, maker) => { + const package = fs.readFileSync(path.join(__dirname, "package.json"), "utf8"); + const hash = crypto.createHash("sha256").update(package); + const data = { + id: hash.digest("hex").slice(-6), + platform: platform, + arch: arch, + maker: maker, + date: date, + }; + return JSON.stringify(data, null, 2); +}; module.exports = { packagerConfig: { @@ -33,10 +51,24 @@ module.exports = { }, ], hooks: { + postPackage: async (config, results) => { + for (const outputPath of results.outputPaths) { + const [name, platform, arch] = path.basename(outputPath).split("-"); + const buildFile = path.join(outputPath, "resources", "app", "build.json"); + fs.writeFileSync(buildFile, generateBuildFile(platform, arch, "deb"), { encoding: "utf8" }); + } + }, postMake: async (config, results) => { for (const result of results) { const artifacts = []; - result.artifacts.forEach((artifact) => { + for (const artifact of result.artifacts) { + if (artifact.includes(".zip")) { + const [name, platform, arch] = path.basename(artifact).split("-"); + const buildFile = path.join(`${name}-${platform}-${arch}`, "resources", "app", "build.json"); + const zip = await jszip.loadAsync(fs.readFileSync(artifact)); + zip.file(buildFile, generateBuildFile(platform, arch, "zip")); + fs.writeFileSync(artifact, await zip.generateAsync({ type: "nodebuffer" })); + } if (artifact.includes("amd64")) { const renamed = artifact.replace("amd64", "x64"); fs.renameSync(artifact, renamed); @@ -44,7 +76,7 @@ module.exports = { } else { artifacts.push(artifact); } - }); + } result.artifacts = artifacts; } }, diff --git a/index.js b/index.js index 17709cb..d8995ac 100644 --- a/index.js +++ b/index.js @@ -7,6 +7,7 @@ const hardware = require("./js/hardware"); const webview = require("./js/webview"); const { app } = require("electron"); +global.APP = global.APP || {}; global.ARGS = global.ARGS || {}; if (!process.env.DISPLAY) { @@ -23,20 +24,85 @@ if (!process.env.DISPLAY) { * initialization tasks. */ app.whenReady().then(async () => { - const lock = app.requestSingleInstanceLock(); - if (!lock) { - console.error("TouchKio is already running"); + if (!(await initApp()) || !(await initArgs())) { + return; + } + + // Show used arguments + console.log(`Arguments: ${JSON.stringify(ARGS, null, 2)}`); + + // Chained init functions + const chained = [webview.init, hardware.init, integration.init]; + for (const init of chained) { + if (!(await init())) { + break; + } + } +}); + +/** + * Initializes the global app object. + * + * @returns {bool} Returns true if the initialization was successful. + */ +const initApp = async () => { + const packageJsonPath = path.join(app.getAppPath(), "package.json"); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); + + const buildJsonPath = path.join(app.getAppPath(), "build.json"); + const buildFileExists = fs.existsSync(buildJsonPath); + + // Set required app infos + APP.name = app.getName(); + APP.title = packageJson.title; + APP.version = app.getVersion(); + APP.path = app.getAppPath(); + APP.config = app.getPath("userData"); + + // Set additional update infos + APP.homepage = `https://github.com/${packageJson.author}/${packageJson.name}`; + APP.releases = { + url: `https://api.github.com/repos/${packageJson.author}/${packageJson.name}/releases`, + latest: null, + }; + APP.scripts = { + install: `https://raw.githubusercontent.com/${packageJson.author}/${packageJson.name}/main/install.sh`, + }; + + // Set additional build infos + APP.build = {}; + if (buildFileExists) { + APP.build = JSON.parse(fs.readFileSync(buildJsonPath, "utf8")); + } + + // Request single app instance lock + if (!app.requestSingleInstanceLock()) { + console.error(`${APP.title} is already running`); return app.quit(); } + + return true; +}; + +/** + * Initializes the global args object. + * + * @returns {bool} Returns true if the initialization was successful. + */ +const initArgs = async () => { let args = parseArgs(process); let argsProvided = !!Object.keys(args).length; - let argsFilePath = path.join(app.getPath("userData"), "Arguments.json"); + let argsFilePath = path.join(APP.config, "Arguments.json"); let argsFileExists = fs.existsSync(argsFilePath); // Show version and release info if ("help" in args || "version" in args) { - console.log(`${app.getName()}-v${app.getVersion()}, https://github.com/leukipp/touchkio`); + let build = ""; + if (APP.build.id) { + build = ` (${APP.build.id}), built on ${APP.build.date} (${APP.build.platform}-${APP.build.arch}-${APP.build.maker})`; + } + console.log(`${APP.name}-v${APP.version}${build}\n${APP.homepage}`); return app.quit(); } @@ -56,19 +122,10 @@ app.whenReady().then(async () => { if (!Array.isArray(args.web_url)) { args.web_url = args.web_url.split(",").map((url) => url.trim()); } - global.ARGS = args; + ARGS = args; - // Show used arguments - console.log(`Arguments: ${JSON.stringify(global.ARGS, null, 2)}`); - - // Chained init functions - const chained = [webview.init, hardware.init, integration.init]; - for (const init of chained) { - if (!(await init())) { - break; - } - } -}); + return true; +}; /** * Parses command-line arguments from the given process object. @@ -155,34 +212,41 @@ const promptArgs = async (proc) => { // Prompt questions and wait for the answers let args = {}; let ignore = []; - for (const { key, question, fallback } of prompts) { - if (key === "mqtt") { - const prompt = `${question} (${fallback}): `; - const answer = await read.question(prompt); - const value = (answer.trim() || fallback.match(/[YN]/)[0]).toLowerCase(); - if (!["y", "yes"].includes(value)) { - ignore = ignore.concat(["mqtt_url", "mqtt_user", "mqtt_password", "mqtt_discovery"]); - } - } else if (key === "check") { - const json = JSON.stringify(args, null, 2); - const prompt = `${question}\n${json}\n(${fallback}): `; - const answer = await read.question(prompt); - const value = (answer.trim() || fallback.match(/[YN]/)[0]).toLowerCase(); - if (!["y", "yes"].includes(value)) { - args = {}; - } - } else if (!ignore.includes(key)) { - const prompt = `${question} (${fallback}): `; - const answer = await read.question(prompt); - const value = answer.trim() || fallback; - if (key === "web_url") { - args[key] = value.split(",").map((v) => v.trim()); - } else { - args[key] = value; + try { + for (const { key, question, fallback } of prompts) { + if (key === "mqtt") { + const prompt = `${question} (${fallback}): `; + const answer = await read.question(prompt); + const value = (answer.trim() || fallback.match(/[YN]/)[0]).toLowerCase(); + if (!["y", "yes"].includes(value)) { + ignore = ignore.concat(["mqtt_url", "mqtt_user", "mqtt_password", "mqtt_discovery"]); + } + } else if (key === "check") { + const json = JSON.stringify(args, null, 2); + const prompt = `${question}\n${json}\n(${fallback}): `; + const answer = await read.question(prompt); + const value = (answer.trim() || fallback.match(/[YN]/)[0]).toLowerCase(); + if (!["y", "yes"].includes(value)) { + args = {}; + } + } else if (!ignore.includes(key)) { + const prompt = `${question} (${fallback}): `; + const answer = await read.question(prompt); + const value = answer.trim() || fallback; + if (key === "web_url") { + args[key] = value.split(",").map((v) => v.trim()); + } else { + args[key] = value; + } } } + } catch (error) { + console.error(`\n${error.message}`); + args = {}; + app.quit(); + } finally { + read.close(); } - read.close(); return args; }; @@ -223,7 +287,7 @@ const readArgs = (path) => { */ const encrypt = (value) => { const iv = crypto.randomBytes(16); - const key = crypto.scryptSync(hardware.getMachineId(), app.getName(), 32); + const key = crypto.scryptSync(hardware.getMachineId(), APP.name, 32); const cipher = crypto.createCipheriv("aes-256-cbc", key, iv); let encrypted = cipher.update(value, "utf8", "hex"); encrypted += cipher.final("hex"); @@ -239,7 +303,7 @@ const encrypt = (value) => { const decrypt = (value) => { const p = Buffer.from(value, "base64").toString("utf8").split(":"); const iv = Buffer.from(p.shift(), "hex"); - const key = crypto.scryptSync(hardware.getMachineId(), app.getName(), 32); + const key = crypto.scryptSync(hardware.getMachineId(), APP.name, 32); const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv); const buffer = Buffer.from(p.join(":"), "hex"); let decrypted = decipher.update(buffer, "binary", "utf8"); diff --git a/install.sh b/install.sh index 32ff9bf..a71cd72 100755 --- a/install.sh +++ b/install.sh @@ -1,5 +1,13 @@ #!/usr/bin/env bash +# Read arguments +ARG_UPDATE=false +for arg in "$@"; do + if [ "$arg" = "update" ]; then + ARG_UPDATE=true + fi +done + # Determine system architecture echo -e "Determining system architecture..." @@ -12,57 +20,39 @@ case "$(uname -m)" in ARCH="x64" ;; *) - echo "Architecture $(uname -m) running $BITS-bit operating system is not supported." - exit 1 + { echo "Architecture $(uname -m) running $BITS-bit operating system is not supported."; exit 1; } ;; esac -if [ "$BITS" -ne 64 ]; then - echo "Architecture $ARCH running $BITS-bit operating system is not supported." - exit 1 -fi - +[ "$BITS" -eq 64 ] || { echo "Architecture $ARCH running $BITS-bit operating system is not supported."; exit 1; } echo "Architecture $ARCH running $BITS-bit operating system is supported." # Download the latest .deb package echo -e "\nDownloading the latest release..." +TMP_DIR=$(mktemp -d) DEB_URL=$(wget -qO- https://api.github.com/repos/leukipp/touchkio/releases/latest | \ grep -o "\"browser_download_url\": \"[^\"]*_${ARCH}\.deb\"" | \ sed 's/"browser_download_url": "//;s/"//g') - -TMP_DIR=$(mktemp -d) DEB_PATH="${TMP_DIR}/$(basename "$DEB_URL")" chmod 755 "$TMP_DIR" -if [ -z "$DEB_URL" ]; then - echo "Download url for .deb file not found." - exit 1 -fi - -if ! wget --show-progress -q -O "$DEB_PATH" "$DEB_URL"; then - echo "Failed to download the .deb file." - exit 1 -fi +[ -z "$DEB_URL" ] && { echo "Download url for .deb file not found."; exit 1; } +wget --show-progress -q -O "$DEB_PATH" "$DEB_URL" || { echo "Failed to download the .deb file."; exit 1; } # Install the latest .deb package echo -e "\nInstalling the latest release..." -if ! command -v apt &> /dev/null; then - echo "Package manager apt was not found." - exit 1 -fi - -if ! sudo apt install "$DEB_PATH"; then - echo "Installation of .deb file failed." - exit 1 -fi +command -v apt &> /dev/null || { echo "Package manager apt was not found."; exit 1; } +sudo apt install -y "$DEB_PATH" || { echo "Installation of .deb file failed."; exit 1; } # Create the systemd user service echo -e "\nCreating systemd user service..." -SERVICE_FILE="$HOME/.config/systemd/user/touchkio.service" -mkdir -p "$(dirname "$SERVICE_FILE")" || { echo "Failed to create directory for $SERVICE_FILE"; exit 1; } +SERVICE_NAME="touchkio.service" +SERVICE_FILE="$HOME/.config/systemd/user/$SERVICE_NAME" +mkdir -p "$(dirname "$SERVICE_FILE")" || { echo "Failed to create directory for $SERVICE_FILE."; exit 1; } + SERVICE_CONTENT="[Unit] Description=TouchKio After=graphical.target @@ -76,20 +66,28 @@ RestartSec=5s [Install] WantedBy=default.target" +if $ARG_UPDATE; then + if systemctl --user --quiet is-active "${SERVICE_NAME}"; then + systemctl --user restart "${SERVICE_NAME}" + echo "Existing $SERVICE_NAME restarted." + else + echo "Existing $SERVICE_NAME not running, start touchkio manually." + fi + exit 0 +fi + SERVICE_CREATE=true if [ -f "$SERVICE_FILE" ]; then read -p "Service $SERVICE_FILE exists, overwrite? (y/N) " overwrite - if [[ ! ${overwrite:-n} == [Yy]* ]]; then - SERVICE_CREATE=false - fi + [[ ${overwrite:-n} == [Yy]* ]] || SERVICE_CREATE=false fi -if [ "$SERVICE_CREATE" = true ]; then - echo "$SERVICE_CONTENT" > "$SERVICE_FILE" || { echo "Failed to write to $SERVICE_FILE"; exit 1; } - systemctl --user enable "$(basename "$SERVICE_FILE")" || { echo "Failed to enable service $SERVICE_FILE"; exit 1; } - echo "Service $SERVICE_FILE enabled" +if $SERVICE_CREATE; then + echo "$SERVICE_CONTENT" > "$SERVICE_FILE" || { echo "Failed to write to $SERVICE_FILE."; exit 1; } + systemctl --user enable "$(basename "$SERVICE_FILE")" || { echo "Failed to enable service $SERVICE_FILE."; exit 1; } + echo "Service $SERVICE_FILE enabled." else - echo "Service $SERVICE_FILE not created" + echo "Service $SERVICE_FILE not created." fi # Export display variables @@ -97,20 +95,22 @@ echo -e "\nExporting display variables..." if [ -z "$DISPLAY" ]; then export DISPLAY=":0" - echo "DISPLAY was not set, defaulting to \"$DISPLAY\"" + echo "DISPLAY was not set, defaulting to \"$DISPLAY\"." else - echo "DISPLAY is set to \"$DISPLAY\"" + echo "DISPLAY is set to \"$DISPLAY\"." fi + if [ -z "$WAYLAND_DISPLAY" ]; then export WAYLAND_DISPLAY="wayland-0" - echo "WAYLAND_DISPLAY was not set, defaulting to \"$WAYLAND_DISPLAY\"" + echo "WAYLAND_DISPLAY was not set, defaulting to \"$WAYLAND_DISPLAY\"." else - echo "WAYLAND_DISPLAY is set to \"$WAYLAND_DISPLAY\"" + echo "WAYLAND_DISPLAY is set to \"$WAYLAND_DISPLAY\"." fi # Start the setup mode echo "" read -p "Start touchkio setup? (Y/n) " setup + if [[ ${setup:-y} == [Yy]* ]]; then echo "/usr/bin/touchkio --setup" /usr/bin/touchkio --setup @@ -118,3 +118,5 @@ else echo "/usr/bin/touchkio" /usr/bin/touchkio fi + +exit 0