diff options
| author | Danilo M. <danix@danix.xyz> | 2026-11-09 18:13:38 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-11-09 18:13:38 +0100 |
| commit | 4601ce03a1b41158f94e1e79edacf8540a89d15f (patch) | |
| tree | 74d00d8dc24a1a400a82f4096b2fc0895b575593 | |
| parent | 2c098221f4ed09d772df9bece5c71912f1c5e849 (diff) | |
| download | deck-it-4601ce03a1b41158f94e1e79edacf8540a89d15f.tar.gz deck-it-4601ce03a1b41158f94e1e79edacf8540a89d15f.zip | |
feat: launcher, adb tunnel, Slackware autostart
| -rwxr-xr-x | deck-it | 192 | ||||
| -rw-r--r-- | tests/test_launcher.py | 20 |
2 files changed, 212 insertions, 0 deletions
@@ -0,0 +1,192 @@ +#!/usr/bin/env bash +# Usage: deck-it start the dashboard + extended display over USB +# deck-it stop tear it all down +# deck-it env print MON/PORT/VNC_PORT for this config +# deck-it pair pair the tablet for Wi-Fi by scanning a QR code +# deck-it wifi start over Wi-Fi +# deck-it install start automatically when this tablet is plugged in (asks for sudo) +# deck-it uninstall remove the automatic start +set -u +DIR="$(dirname "$(readlink -f "$0")")" +SELF="$DIR/deck-it" +CONF="${XDG_CONFIG_HOME:-$HOME/.config}/deck-it" +VNC="$CONF/vnc" +RUN="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/deck-it" +RULE=/etc/udev/rules.d/70-deck-it.rules + +if [[ "${1:-}" == "env" ]]; then + (cd "$DIR" && exec python3 -m deckit env) +fi + +MON=TAB PORT=8765 VNC_PORT=5900 +env_out=$(cd "$DIR" && python3 -m deckit env 2>&1) && eval "$env_out" || echo "Config problem: $env_out" >&2 + +teardown() { + pkill -f "^bash $SELF watch" 2>/dev/null + pkill -x wayvnc + pkill -f '^python3 -m deckit' 2>/dev/null + hyprctl output remove "$MON" >/dev/null 2>&1 + local s + for s in $(adb devices | awk 'NR > 1 && $2 == "device" {print $1}'); do + timeout 5 adb -s "$s" reverse --remove-all 2>/dev/null + done + rm -f "$RUN/serial" +} + +tunnel() { + adb -s "$1" reverse "tcp:$VNC_PORT" "localfilesystem:$RUN/vnc.sock" >/dev/null && + adb -s "$1" reverse "tcp:$PORT" "localfilesystem:$RUN/http.sock" >/dev/null +} + +# The display stream needs a password over TLS; both live in a 0700 dir and AVNC gets them in the link. +vnc_setup() { + mkdir -p -m 700 "$VNC" + ( + umask 077 + [[ -s "$VNC/password" ]] || tr -dc A-Za-z0-9 </dev/urandom | head -c 24 > "$VNC/password" + [[ -s "$VNC/tls_cert.pem" ]] || openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \ + -days 3650 -subj /CN=deck-it -keyout "$VNC/tls_key.pem" -out "$VNC/tls_cert.pem" 2>/dev/null + printf 'enable_auth=true\nusername=deck-it\npassword=%s\nprivate_key_file=%s\ncertificate_file=%s\n' \ + "$(cat "$VNC/password")" "$VNC/tls_key.pem" "$VNC/tls_cert.pem" > "$VNC/config" + ) +} + +open_avnc() { + adb -s "$1" shell am start -a android.intent.action.VIEW \ + -d "'vnc://127.0.0.1:$VNC_PORT?VncUsername=deck-it&VncPassword=$(cat "$VNC/password")'" com.gaurav.avnc >/dev/null 2>&1 +} + +# A dropped Wi-Fi link can still report "device", and adb calls on it hang, so probe with a timeout. +alive() { + timeout 6 adb -s "$1" shell true >/dev/null 2>&1 +} + +wifi_connect() { + local addr + for addr in $(adb mdns services | awk '/_adb-tls-connect/ {print $NF}' | sort -u); do + timeout 5 adb connect "$addr" 2>/dev/null | grep -q '^connected\|already connected' && + alive "$addr" && { echo "$addr"; return 0; } + done + return 1 +} + +usb_present() { + [[ "$(timeout 3 adb -d get-state 2>/dev/null)" == device ]] +} + +# udev RUN= cannot carry a quoted shell command, so the rule calls a generated +# no-argument script that backgrounds the launcher and returns immediately. +install_udev() { + serial=$(timeout 5 adb -d get-serialno 2>/dev/null) + [[ "$serial" =~ ^[A-Za-z0-9]+$ ]] || { echo "Plug in the tablet with USB debugging allowed, then retry"; exit 1; } + mkdir -p "$CONF" + trigger="$CONF/trigger" + printf '#!/bin/sh\nsetsid "%s" start >/dev/null 2>&1 &\n' "$SELF" > "$trigger" + chmod 0755 "$trigger" + printf 'ACTION!="remove", SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{serial}=="%s", RUN+="%s"\n' \ + "$serial" "$trigger" | sudo tee "$RULE" >/dev/null || exit 1 + sudo udevadm control --reload + echo "Installed for tablet $serial. It starts when plugged in and stops when removed." + echo "To also start at login when the tablet is already connected, add to hypr-theme's autostart:" + echo " exec-once = $SELF" + exit 0 +} + +case "${1:-}" in +stop) + teardown + echo "Stopped" + exit 0 + ;; +teardown) + teardown + exit 0 + ;; +pair) + command -v qrencode >/dev/null || { echo "Install qrencode first"; exit 1; } + name="deck-it-$(tr -dc a-z0-9 </dev/urandom | head -c 8)" + pass=$(tr -dc 0-9 </dev/urandom | head -c 8) + echo "On the tablet: Developer options > Wireless debugging > Pair device with QR code, and scan this:" + echo + qrencode -t ansiutf8 -m 2 "WIFI:T:ADB;S:$name;P:$pass;;" + echo "Waiting for the scan (2 minutes)..." + for _ in {1..120}; do + addr=$(adb mdns services | awk -v n="$name" '$1 == n && /_adb-tls-pairing/ {print $NF; exit}') + if [[ -n "$addr" ]]; then + adb pair "$addr" "$pass" | grep -q 'Successfully paired' || { echo "Pairing failed"; exit 1; } + echo "Paired. Connecting..." + for _ in {1..10}; do addr=$(wifi_connect) && break; sleep 2; done + [[ -n "$addr" ]] && echo "Connected to $addr. Start with: deck-it wifi" || echo "Paired, but couldn't connect yet. Try: deck-it wifi" + exit 0 + fi + sleep 1 + done + echo "No scan detected. Is the tablet on the same Wi-Fi network as this PC?" + exit 1 + ;; +watch) + # Runs in the background in Wi-Fi mode: reconnect when the tablet drops, then restore the tunnels. + while sleep 5; do + serial=$(cat "$RUN/serial" 2>/dev/null) || exit 0 + alive "$serial" && timeout 6 adb -s "$serial" reverse --list 2>/dev/null | grep -q "tcp:$PORT" && continue + timeout 5 adb disconnect "$serial" >/dev/null 2>&1 + new=$(wifi_connect) || continue + tunnel "$new" && echo "$new" > "$RUN/serial" && open_avnc "$new" + done + ;; +install) + install_udev + ;; +uninstall) + if [[ -f "$RULE" ]]; then sudo rm -f "$RULE" && sudo udevadm control --reload; fi + rm -f "$CONF/trigger" + echo "Uninstalled" + exit 0 + ;; +wifi) + echo "Looking for the tablet on Wi-Fi..." + while :; do + [[ "${2:-}" == --wait ]] && usb_present && { echo "Tablet is on USB, leaving it to the USB start"; exit 0; } + ANDROID_SERIAL=$(wifi_connect) && break + [[ "${2:-}" == --wait ]] || { echo "Not found. Turn on Wireless debugging on the tablet, or pair first with: deck-it pair"; exit 1; } + sleep 10 + done + ;; +"") + echo "Waiting for tablet on USB (plug it in, allow USB debugging)..." + timeout 60 adb wait-for-usb-device || { echo "No tablet found"; exit 1; } + ANDROID_SERIAL=$(adb -d get-serialno) + ;; +*) + sed -n '2,8p' "$SELF" | sed 's/^# \{0,1\}//' + exit 1 + ;; +esac +export ANDROID_SERIAL + +for _ in {1..30}; do hyprctl monitors >/dev/null 2>&1 && break; sleep 1; done + +(cd "$DIR" && python3 -m deckit setup-display) || exit 1 + +# Both servers listen on sockets in a 0700 dir, so only this user and its adb tunnel can reach them. +mkdir -p -m 700 "$RUN" +vnc_setup +# wayvnc leaves its socket behind when killed and then refuses to bind, so clear it first. +pgrep -x wayvnc >/dev/null || { rm -f "$RUN/vnc.sock"; setsid wayvnc -C "$VNC/config" -u -o "$MON" "$RUN/vnc.sock" >/dev/null 2>&1 & } +pgrep -f '^python3 -m deckit' >/dev/null || (cd "$DIR" && setsid python3 -m deckit >server.log 2>&1 &) +sleep 1 + +tunnel "$ANDROID_SERIAL" || { echo "Could not set up the tunnel to the tablet"; exit 1; } +# Opening the page with the secret stores it as a cookie in the tablet's browser, which unlocks the API. +token="$CONF/token" +for _ in {1..10}; do [[ -s "$token" ]] && break; sleep 0.5; done +adb shell am start -a android.intent.action.VIEW -d "http://127.0.0.1:$PORT/?t=$(cat "$token")" >/dev/null 2>&1 +sleep 1 +open_avnc "$ANDROID_SERIAL" + +if [[ "${1:-}" == wifi ]]; then + echo "$ANDROID_SERIAL" > "$RUN/serial" + pgrep -f "^bash $SELF watch" >/dev/null || setsid bash "$SELF" watch >/dev/null 2>&1 & +fi + +echo "Ready. Display is in AVNC, dashboard at http://127.0.0.1:$PORT" diff --git a/tests/test_launcher.py b/tests/test_launcher.py new file mode 100644 index 0000000..bfcbdec --- /dev/null +++ b/tests/test_launcher.py @@ -0,0 +1,20 @@ +import subprocess +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent + + +class Launcher(unittest.TestCase): + def test_syntax(self): + self.assertEqual(subprocess.run(["bash", "-n", str(ROOT / "deck-it")]).returncode, 0) + + def test_env_prints_keys(self): + out = subprocess.run(["bash", str(ROOT / "deck-it"), "env"], + capture_output=True, text=True, cwd=ROOT).stdout + for key in ("MON=", "PORT=", "VNC_PORT="): + self.assertIn(key, out) + + +if __name__ == "__main__": + unittest.main() |
