diff --git a/android/code/index.mjs b/android/code/index.mjs index 7ae0ad8..11531b5 100644 --- a/android/code/index.mjs +++ b/android/code/index.mjs @@ -1,12 +1,20 @@ import { WebSocketServer } from "ws"; import child_process from "child_process"; import fs from "fs"; +import { send_notification } from "./notifications.mjs" async function spawnPromise(program, args) { return new Promise((resolve, reject) => { + let output = ""; const process = child_process.spawn(program, args); - process.on("close", (_) => { - resolve(); + process.stdout.on('data', (data) => { + output += data; + }); + process.stderr.on('data', (data) => { + output += data; + }); + process.on("close", (code) => { + resolve({output, code}); }); }); } @@ -32,7 +40,8 @@ wss.on("connection", (ws) => { } else if (data === "home") { await spawnPromise("bash", ["/conf/home.sh"]); } else if (data === "install") { - await spawnPromise("bash", ["/conf/install.sh"]); + const res = await spawnPromise("bash", ["/conf/install.sh"]); + send_notification(res.code === 0, "Installing the application", res.output); } else if (data.includes("drag")) { const dataSplit = data.split(" "); diff --git a/android/code/notifications.mjs b/android/code/notifications.mjs new file mode 100644 index 0000000..200d5b2 --- /dev/null +++ b/android/code/notifications.mjs @@ -0,0 +1,36 @@ +import { WebSocket } from "ws"; +import { WebSocketServer } from "ws"; + +const notification_proxy = new WebSocketServer({ port: 3001 }); +let notification_subs = []; + +notification_proxy.on('connection', (ws) => { + notification_subs.push(ws); +}); + +export function send_notification(is_ok, context, message) +{ + let updated_subs = []; + + if (notification_subs.length === 0) { + console.log("WARNING: Got a notification, but nobody is subscribed"); + } + for (const sub of notification_subs) { + if (sub.readyState == WebSocket.CONNECTING) { + console.log("WARNING: Unable to forward a notification to client that is still connecting"); + updated_subs.push(sub); + } else { + try { + sub.send(JSON.stringify({ + is_ok, + context, + message + })); + updated_subs.push(sub); + } catch { + sub.close(); + console.log("WARNING: Fail to send a notification, closing the connection"); + } + } + } +} diff --git a/docker-compose.yaml b/docker-compose.yaml index 661ca4b..1505ce7 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -22,6 +22,7 @@ services: - 45457:45459 # This cannot change - 10001:10001 # api port - 3000:3000 # android server port + - 3001:3001 # Notifications server volumes: - $PWD/shared_buffer:/shared_buffer - $PWD/android/conf:/conf diff --git a/http_server/code/index.html b/http_server/code/index.html index 0780381..83ab631 100644 --- a/http_server/code/index.html +++ b/http_server/code/index.html @@ -128,10 +128,16 @@ #upload_form button:hover, #upload_form label:hover { background-color: #ddd; } + #notifications { + width: 40%; + margin-left: 60%; + position: absolute; + } -
+
+
+ diff --git a/http_server/code/index.mjs b/http_server/code/index.mjs index 32d82bf..1e5dc42 100644 --- a/http_server/code/index.mjs +++ b/http_server/code/index.mjs @@ -16,6 +16,8 @@ const device_size_y = 640; const app = express(); app.use(express.urlencoded({ extended: false })); +app.use(express.static('/code/dist')) + console.log("Waiting for full boot..."); await waitFullBoot(); console.log("Boot detected! activating endpoints"); @@ -25,10 +27,6 @@ app.get("/favicon.ico", function (req, res) { res.sendFile("/code/favicon.ico"); }); -app.get("/trafficLog.js", function (req, res) { - res.sendFile("/code/dist/trafficLog.js"); -}); - app.get("/htmx.js", function (req, res) { res.sendFile("/code/node_modules/htmx.org/dist/htmx.min.js"); }); @@ -69,10 +67,14 @@ app.post('/upload_apk', async function (req, res) { return res.status(400).send('No files were uploaded.'); } execSync("rm -rf /shared_buffer/*"); - console.log(req.files); - for (const [idx, file] of req.files.app.entries()) { - let uploadPath = '/shared_buffer/app' + idx + '.apk'; - await file.mv(uploadPath); + if (Array.isArray(req.files.app)) { + for (const [idx, file] of req.files.app.entries()) { + let uploadPath = '/shared_buffer/app' + idx + '.apk'; + await file.mv(uploadPath); + } + } else { + let uploadPath = '/shared_buffer/app' + 0 + '.apk'; + await req.files.app.mv(uploadPath); } android_websocket.send(`install`); res.send('Files uploaded!'); diff --git a/http_server/code/package.json b/http_server/code/package.json index ad975f7..4af3af4 100644 --- a/http_server/code/package.json +++ b/http_server/code/package.json @@ -1,6 +1,6 @@ { "scripts": { - "build": "esbuild --sourcemap --bundle src/trafficLog.jsx --outfile=dist/trafficLog.js --jsx-factory=h --jsx-fragment=Fragment" + "build": "esbuild --sourcemap --bundle src/trafficLog.jsx src/notifications.jsx --outdir=dist/ --jsx-factory=h --jsx-fragment=Fragment" }, "dependencies": { "express": "^4.18.2", diff --git a/http_server/code/src/notifications.jsx b/http_server/code/src/notifications.jsx new file mode 100644 index 0000000..9ba6110 --- /dev/null +++ b/http_server/code/src/notifications.jsx @@ -0,0 +1,69 @@ +import { render, Component } from "preact"; +import { useState } from "preact/hooks"; + +function rand_num() { + return Math.floor(Math.random() * Number.MAX_VALUE); +} + +class Notifications extends Component { + constructor() { + super(); + this.state = { notifications: [] }; + } + + remove_notification (id) { + const newNotifications = this.state.notifications.filter( + (notification) => notification.id !== id + ); + this.setState({ notifications: newNotifications }); + }; + + componentDidMount() { + // This should also be dynamic + this.connection = new WebSocket("ws://127.0.0.1:3001"); + + this.connection.onmessage = (msg) => { + let new_id = rand_num(); + this.setState({ + notifications: [ + { id: new_id, notification: JSON.parse(msg.data) }, + ...this.state.notifications, + ], + }); + // a 10 sec timeout + setTimeout(() => { + this.remove_notification(new_id) + }, 10000) + }; + } + + render() { + console.log("Render", this.state.notifications); + return this.state.notifications.map(({ id, notification }) => ( +
this.remove_notification(id)} + style={` + background-color: ${notification.is_ok ? "#66ff99" : "#ff5c33"}; + border-radius: 5px; + border-width: 2px; + border-style: solid; + border-color: ${notification.is_ok ? "#369648" : "#a23915"}; + padding: 5px; + margin-top: 2px; + margin-bottom: 2px; + `} + > +
+ {notification.context} +
+
+ {notification.message.split("\n").map((line) => ( +

{line}

+ ))} +
+
+ )); + } +} + +render(, document.getElementById("notifications"));