diff --git a/README.md b/README.md index 2f01478..d936365 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,16 @@ Volumio-spezifisch: /home/volumio/phoenix.log ``` +Die Volumio-spezifischen Units, der eingeschränkte Update-Helfer und die +zugehörige sudo-Regel werden einmalig als root installiert: + +```bash +sudo bash /home/volumio/Phoenix/scripts/install_volumio_services.sh +``` + +Der Installer verändert keine Debian-/Analyzer-Units und startet laufende +Dienste nicht automatisch neu. + Wenn `toggle-ext` genutzt werden soll, sollte es also unter diesem Pfad liegen: ```text diff --git a/etc/sudoers.d/phoenix-update-volumio b/etc/sudoers.d/phoenix-update-volumio new file mode 100644 index 0000000..77f055f --- /dev/null +++ b/etc/sudoers.d/phoenix-update-volumio @@ -0,0 +1 @@ +volumio ALL=(root) NOPASSWD: /usr/bin/systemctl --no-block start phoenix-update-volumio.service diff --git a/etc/systemd/system/phoenix-update-volumio.service b/etc/systemd/system/phoenix-update-volumio.service new file mode 100644 index 0000000..691ef32 --- /dev/null +++ b/etc/systemd/system/phoenix-update-volumio.service @@ -0,0 +1,15 @@ +[Unit] +Description=Apply staged Phoenix update on Volumio +After=local-fs.target + +[Service] +Type=oneshot +User=root +Group=root +TimeoutStartSec=30min +Nice=10 +IOSchedulingClass=idle +Environment=PHOENIX_INSTALL_PROFILE=volumio +Environment=PHOENIX_UPDATE_DETACHED=1 +Environment=PHOENIX_ROOT_OVERRIDE=/home/volumio/Phoenix +ExecStart=/bin/bash /usr/local/libexec/phoenix/restart_phoenix_services.sh diff --git a/scripts/install_volumio_services.sh b/scripts/install_volumio_services.sh new file mode 100755 index 0000000..1427fed --- /dev/null +++ b/scripts/install_volumio_services.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +set -euo pipefail + +if [ "$(id -u)" -ne 0 ]; then + echo "run this installer as root" >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +PHOENIX_ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)" +SYSTEMD_SOURCE="$PHOENIX_ROOT/etc/systemd/system" +SUDOERS_SOURCE="$PHOENIX_ROOT/etc/sudoers.d/phoenix-update-volumio" +LIBEXEC_DIR="/usr/local/libexec/phoenix" + +required_files=( + "$SYSTEMD_SOURCE/analyzer-kiosk-volumio.service" + "$SYSTEMD_SOURCE/analyzer-web-volumio.service" + "$SYSTEMD_SOURCE/phoenix-update-volumio.service" + "$SUDOERS_SOURCE" + "$PHOENIX_ROOT/scripts/restart_phoenix_services.sh" +) + +for path in "${required_files[@]}"; do + if [ ! -f "$path" ]; then + echo "required Volumio integration file missing: $path" >&2 + exit 1 + fi +done + +if ! command -v visudo >/dev/null 2>&1; then + echo "visudo not found; refusing to install sudo policy" >&2 + exit 1 +fi +visudo -cf "$SUDOERS_SOURCE" + +install -d -o root -g root -m 0755 "$LIBEXEC_DIR" +install -o root -g root -m 0755 \ + "$PHOENIX_ROOT/scripts/restart_phoenix_services.sh" \ + "$LIBEXEC_DIR/restart_phoenix_services.sh" +install -o root -g root -m 0644 \ + "$SYSTEMD_SOURCE/analyzer-kiosk-volumio.service" \ + /etc/systemd/system/analyzer-kiosk-volumio.service +install -o root -g root -m 0644 \ + "$SYSTEMD_SOURCE/analyzer-web-volumio.service" \ + /etc/systemd/system/analyzer-web-volumio.service +install -o root -g root -m 0644 \ + "$SYSTEMD_SOURCE/phoenix-update-volumio.service" \ + /etc/systemd/system/phoenix-update-volumio.service +install -o root -g root -m 0440 \ + "$SUDOERS_SOURCE" \ + /etc/sudoers.d/phoenix-update-volumio + +systemctl daemon-reload +systemctl enable analyzer-web-volumio.service analyzer-kiosk-volumio.service + +echo "Volumio integration installed" +echo "Existing services were not restarted" diff --git a/scripts/restart_phoenix_services.sh b/scripts/restart_phoenix_services.sh index 6af857d..9c4422f 100644 --- a/scripts/restart_phoenix_services.sh +++ b/scripts/restart_phoenix_services.sh @@ -3,7 +3,12 @@ set -euo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" -PHOENIX_ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)" +PHOENIX_ROOT="${PHOENIX_ROOT_OVERRIDE:-}" +if [ -z "$PHOENIX_ROOT" ]; then + PHOENIX_ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)" +else + PHOENIX_ROOT="$(cd "$PHOENIX_ROOT" && pwd -P)" +fi PHOENIX_PARENT="$(cd "$PHOENIX_ROOT/.." && pwd -P)" PHOENIX_NAME="$(basename "$PHOENIX_ROOT")" STAGE_DIR="$PHOENIX_PARENT/${PHOENIX_NAME}.update" @@ -14,6 +19,10 @@ LOG_FILE="$PHOENIX_PARENT/${PHOENIX_NAME}.update.log" exec >>"$LOG_FILE" 2>&1 echo "=== $(date): restart_phoenix_services.sh START ===" +is_volumio_install() { + [ "${PHOENIX_INSTALL_PROFILE:-}" = "volumio" ] || [ "$PHOENIX_ROOT" = "/home/volumio/Phoenix" ] +} + restart_if_present() { local unit="$1" if ! command -v systemctl >/dev/null 2>&1; then @@ -61,6 +70,19 @@ run_systemd_run() { /usr/bin/env PHOENIX_UPDATE_DETACHED=1 /bin/bash "$0" } +start_volumio_update_service() { + local unit="phoenix-update-volumio.service" + if [ "$(id -u)" -eq 0 ]; then + /usr/bin/systemctl --no-block start "$unit" + return $? + fi + if ! command -v sudo >/dev/null 2>&1; then + echo "sudo not found; cannot start $unit as non-root" >&2 + return 1 + fi + sudo -n /usr/bin/systemctl --no-block start "$unit" +} + health_ok() { if ! command -v curl >/dev/null 2>&1; then return 0 @@ -144,7 +166,56 @@ wait_for_candidate_health() { return 1 } +build_volumio_candidate() { + local phoenix_dir="${1:-$STAGE_DIR}" + local cargo_bin="" + local cargo_dir="" + local candidate + + if ! needs_phoenix_build "$phoenix_dir"; then + echo "Volumio candidate already contains a runnable Phoenix binary" + return 0 + fi + + for candidate in /root/.cargo/bin/cargo /home/volumio/.cargo/bin/cargo /usr/local/bin/cargo /usr/bin/cargo; do + if [ -x "$candidate" ]; then + cargo_bin="$candidate" + break + fi + done + if [ -z "$cargo_bin" ]; then + echo "cargo not found; cannot build staged Volumio update" >&2 + return 1 + fi + cargo_dir="$(dirname "$cargo_bin")" + + echo "building staged Volumio update before stopping the running kiosk" + if ! ( + cd "$phoenix_dir" + PATH="$cargo_dir:/root/.cargo/bin:/home/volumio/.cargo/bin:/usr/local/bin:/usr/bin:/bin" \ + CARGO_TERM_COLOR=never "$cargo_bin" build --release --locked + ); then + if [ -d "$phoenix_dir/target" ]; then + chown -R volumio:volumio "$phoenix_dir/target" || true + fi + echo "staged Volumio build failed; running installation was left untouched" >&2 + return 1 + fi + chown -R volumio:volumio "$phoenix_dir/target" + if [ ! -x "$phoenix_dir/target/release/phoenix" ] || [ ! -s "$phoenix_dir/target/release/phoenix" ]; then + echo "staged Volumio build did not produce a runnable Phoenix binary" >&2 + return 1 + fi + echo "staged Volumio build completed" +} + start_all_services() { + if is_volumio_install; then + restart_if_present analyzer-web-volumio.service + clear_browser_asset_caches + restart_if_present analyzer-kiosk-volumio.service + return + fi restart_if_present phoenix.service restart_if_present analyzer-web.service restart_if_present analyzer-web-volumio.service @@ -154,6 +225,11 @@ start_all_services() { } stop_all_services() { + if is_volumio_install; then + stop_if_present analyzer-kiosk-volumio.service + stop_if_present analyzer-web-volumio.service + return + fi stop_if_present analyzer-kiosk.service stop_if_present analyzer-kiosk-volumio.service stop_if_present analyzer-web.service @@ -180,6 +256,16 @@ rollback_to_backup() { } if [ "${PHOENIX_UPDATE_DETACHED:-0}" != "1" ]; then + if is_volumio_install; then + echo "handing Volumio update to phoenix-update-volumio.service" + if start_volumio_update_service; then + echo "Volumio update service accepted" + exit 0 + fi + echo "failed to start phoenix-update-volumio.service" >&2 + echo "run scripts/install_volumio_services.sh once as root" >&2 + exit 1 + fi if ! command -v systemd-run >/dev/null 2>&1; then echo "systemd-run not found; cannot safely detach updater" >&2 exit 1 @@ -202,6 +288,10 @@ if [ ! -f "$STAGE_DIR/Cargo.toml" ] || [ ! -d "$STAGE_DIR/src" ] || [ ! -d "$STA exit 1 fi +if is_volumio_install; then + build_volumio_candidate "$STAGE_DIR" +fi + stop_all_services rm -rf "$BACKUP_DIR" @@ -222,4 +312,11 @@ if ! wait_for_candidate_health "$PHOENIX_ROOT" "$health_attempts" "$health_delay exit 1 fi +if is_volumio_install && [ "$(id -u)" -eq 0 ] && [ -f "$PHOENIX_ROOT/scripts/restart_phoenix_services.sh" ]; then + install -o root -g root -m 0755 \ + "$PHOENIX_ROOT/scripts/restart_phoenix_services.sh" \ + /usr/local/libexec/phoenix/restart_phoenix_services.sh + echo "Volumio privileged update helper refreshed" +fi + echo "=== $(date): restart_phoenix_services.sh OK ===" diff --git a/session_volumio.sh b/session_volumio.sh index 397314e..17bbb9c 100644 --- a/session_volumio.sh +++ b/session_volumio.sh @@ -165,10 +165,6 @@ else chmod 700 "$CHROME_USER_DATA_DIR" fi -pkill -9 -x chromium || true -pkill -9 -f "chromium.*--kiosk" || true -sleep 0.3 - EXTENSIONS=() EXT_VK_RESOLVED="$(resolve_extension_dir "$EXT_VK" \ "$PHOENIX_DIR/VirtualKeyboard" \ @@ -216,21 +212,18 @@ CHROME_CMD=( --disable-features=Translate,TranslateUI --memory-pressure-off --max-active-webgl-contexts=1 - "$ANALYZER_URL" - "$VOLUMIO_URL" ) if [ -n "$EXTENSION_ARG" ]; then CHROME_CMD+=(--load-extension="$EXTENSION_ARG") fi +# Start with the lightweight Analyzer page. The tab-toggle extension creates +# exactly one inactive Volumio tab after Chromium is ready. +CHROME_CMD+=("$ANALYZER_URL") + sudo -u "$KIOSK_USER" "${CHROME_CMD[@]}" >>"$LOG" 2>&1 & CHROME_PID=$! -sleep 2 -if command -v xdotool >/dev/null 2>&1; then - xdotool key Ctrl+t type "$VOLUMIO_URL" key Return || true -fi - wait "$CHROME_PID" echo "=== $(date): session_volumio.sh END ===" diff --git a/toggle-ext/bg.js b/toggle-ext/bg.js index e9cc7c3..597c193 100644 --- a/toggle-ext/bg.js +++ b/toggle-ext/bg.js @@ -1,16 +1,45 @@ -// Aktiviert den bestehenden Tab mit Ziel-URL (ohne Reload). Falls nicht offen, wird er geöffnet. +const ANALYZER_URL = 'http://localhost:8088/'; +const VOLUMIO_URL = 'http://localhost:3000/'; + +function matchingTabs(tabs, url) { + return tabs.filter(tab => tab.url && tab.url.startsWith(url)); +} + +// Keep one Analyzer and one Volumio tab. Chromium starts on Analyzer, so +// Volumio can load in the background without delaying the first visible UI. +function ensureDualTabs() { + chrome.tabs.query({ currentWindow: true }, tabs => { + const analyzerTabs = matchingTabs(tabs, ANALYZER_URL); + const volumioTabs = matchingTabs(tabs, VOLUMIO_URL); + const duplicates = [...analyzerTabs.slice(1), ...volumioTabs.slice(1)] + .map(tab => tab.id) + .filter(id => Number.isInteger(id)); + + if (duplicates.length > 0) chrome.tabs.remove(duplicates); + if (analyzerTabs.length === 0) chrome.tabs.create({ url: ANALYZER_URL, active: true }); + if (volumioTabs.length === 0) chrome.tabs.create({ url: VOLUMIO_URL, active: false }); + }); +} + +// Activate an existing application tab without reloading it. function activateOrCreate(url) { - chrome.tabs.query({}, tabs => { - const t = tabs.find(tt => tt.url && tt.url.startsWith(url)); - if (t) { chrome.tabs.update(t.id, {active: true}); } - else { chrome.tabs.create({url}); } + chrome.tabs.query({ currentWindow: true }, tabs => { + const tab = matchingTabs(tabs, url)[0]; + if (tab) chrome.tabs.update(tab.id, { active: true }); + else chrome.tabs.create({ url, active: true }); }); } chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { - if (msg && msg.cmd === 'toggle') { - if (msg.from === 'volumio') activateOrCreate('http://localhost:8088/'); - else activateOrCreate('http://localhost:3000/'); - sendResponse({ok:true}); + if (!msg) return; + if (msg.cmd === 'bootstrap') { + ensureDualTabs(); + sendResponse({ ok: true }); + return; + } + if (msg.cmd === 'toggle') { + if (msg.from === 'volumio') activateOrCreate(ANALYZER_URL); + else activateOrCreate(VOLUMIO_URL); + sendResponse({ ok: true }); } }); diff --git a/toggle-ext/content.js b/toggle-ext/content.js index 88aeac6..36e459c 100644 --- a/toggle-ext/content.js +++ b/toggle-ext/content.js @@ -1 +1,40 @@ -(function(){try{const e="3000"===location.port,t=document.createElement("button");function o(){chrome.runtime.sendMessage({cmd:"toggle",from:e?"volumio":"analyzer"},(()=>{}))}t.textContent=e?"Analyzer":"Volumio",Object.assign(t.style,{position:"fixed",right:"10px",top:"10px",zIndex:999999,font:"bold 14px ui-monospace,monospace",padding:"6px 10px",background:"#0b0b12",color:"#ddd",border:"1px solid #333",borderRadius:"8px",cursor:"pointer",opacity:"0.85"}),t.onmouseenter=()=>t.style.opacity="1",t.onmouseleave=()=>t.style.opacity="0.85",t.onclick=o,document.body.appendChild(t),window.addEventListener("keydown",(t=>{"t"===t.key.toLowerCase()&&(t.preventDefault(),o())}),{capture:!0})}catch(e){}})(); +(function () { + try { + const isVolumio = location.port === '3000'; + const button = document.createElement('button'); + const toggle = () => { + chrome.runtime.sendMessage( + { cmd: 'toggle', from: isVolumio ? 'volumio' : 'analyzer' }, + () => {}, + ); + }; + + chrome.runtime.sendMessage({ cmd: 'bootstrap' }, () => {}); + + button.textContent = isVolumio ? 'Analyzer' : 'Volumio'; + Object.assign(button.style, { + position: 'fixed', + right: '10px', + top: '10px', + zIndex: 999999, + font: 'bold 14px ui-monospace,monospace', + padding: '6px 10px', + background: '#0b0b12', + color: '#ddd', + border: '1px solid #333', + borderRadius: '8px', + cursor: 'pointer', + opacity: '0.85', + }); + button.onmouseenter = () => { button.style.opacity = '1'; }; + button.onmouseleave = () => { button.style.opacity = '0.85'; }; + button.onclick = toggle; + document.body.appendChild(button); + window.addEventListener('keydown', event => { + if (event.key.toLowerCase() === 't') { + event.preventDefault(); + toggle(); + } + }, { capture: true }); + } catch (_) {} +})();