fix: first-boot container creation, remote input relay, ISO packages
Critical first-boot fixes (root cause: ALL 25 containers failed on install): - Fix image-versions.sh sourcing: multi-path fallback for /opt/archipelago/scripts/ - Fix --add-host host-gateway: resolve actual gateway IP (podman 4.3 compat) - Fix disk size detection: check /var/lib/archipelago not / (was forcing prune on 428GB disk) - Fix Bitcoin health check: expand $RPC vars at creation, not inside container - Add --network-alias to all containers (aardvark-dns reliability) - Add --network-alias to backend RPC install handler ISO build: - Add apache2-utils for htpasswd (Fedimint gateway password hashing) Remote input: - Add broadcast relay channel for companion app → browser input forwarding - Add /ws/remote-relay WebSocket endpoint - Android: NES controller improvements, server connect flow updates Container images: - Fix lnd-ui Dockerfile: listen on 8080, run as root user (rootless compat) - Fix bitcoin-ui, electrs-ui Dockerfiles: root user for rootless podman Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ use crate::state::StateManager;
|
||||
use anyhow::Result;
|
||||
use hyper::{Method, Request, Response, StatusCode};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::broadcast;
|
||||
use tracing::debug;
|
||||
|
||||
/// Build an HTTP response without unwrap. Falls back to a plain 500 if builder fails.
|
||||
@@ -32,6 +33,8 @@ pub struct ApiHandler {
|
||||
state_manager: Arc<StateManager>,
|
||||
metrics_store: Arc<MetricsStore>,
|
||||
session_store: SessionStore,
|
||||
/// Broadcast channel for relaying companion app input to remote browsers.
|
||||
input_relay_tx: broadcast::Sender<String>,
|
||||
}
|
||||
|
||||
impl ApiHandler {
|
||||
@@ -50,6 +53,7 @@ impl ApiHandler {
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
let (input_relay_tx, _) = broadcast::channel(64);
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
@@ -57,6 +61,7 @@ impl ApiHandler {
|
||||
state_manager,
|
||||
metrics_store,
|
||||
session_store,
|
||||
input_relay_tx,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -147,7 +152,16 @@ impl ApiHandler {
|
||||
tracing::warn!("401 WebSocket /ws/remote-input — session invalid or missing");
|
||||
return Ok(Self::unauthorized());
|
||||
}
|
||||
return Self::handle_remote_input(req).await;
|
||||
return Self::handle_remote_input(req, self.input_relay_tx.clone()).await;
|
||||
}
|
||||
|
||||
// Remote relay WebSocket — browser receives companion input events
|
||||
if method == Method::GET && path == "/ws/remote-relay" {
|
||||
if !self.is_authenticated(req.headers()).await {
|
||||
tracing::warn!("401 WebSocket /ws/remote-relay — session invalid or missing");
|
||||
return Ok(Self::unauthorized());
|
||||
}
|
||||
return Self::handle_remote_relay(req, self.input_relay_tx.subscribe()).await;
|
||||
}
|
||||
|
||||
// Convert body to bytes for non-WS routes
|
||||
|
||||
@@ -5,6 +5,7 @@ use hyper_ws_listener::WsStream;
|
||||
use serde::Deserialize;
|
||||
use std::time::Instant;
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
@@ -121,6 +122,7 @@ async fn handle_input(msg: &str) -> Result<Option<String>> {
|
||||
impl ApiHandler {
|
||||
pub(super) async fn handle_remote_input(
|
||||
req: Request<hyper::Body>,
|
||||
relay_tx: broadcast::Sender<String>,
|
||||
) -> Result<Response<hyper::Body>> {
|
||||
let (response, ws_fut_opt) = hyper_ws_listener::create_ws(req)
|
||||
.map_err(|e| anyhow::anyhow!("WebSocket upgrade failed: {}", e))?;
|
||||
@@ -183,6 +185,9 @@ impl ApiHandler {
|
||||
continue; // silently drop
|
||||
}
|
||||
|
||||
// Relay to connected browsers (best-effort, ignore if no receivers)
|
||||
let _ = relay_tx.send(text.clone());
|
||||
|
||||
match handle_input(&text).await {
|
||||
Ok(Some(reply)) => {
|
||||
let _ = tx.send(Message::Text(reply)).await;
|
||||
@@ -220,4 +225,88 @@ impl ApiHandler {
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Browser relay — receives input events from the broadcast channel and forwards to the browser.
|
||||
pub(super) async fn handle_remote_relay(
|
||||
req: Request<hyper::Body>,
|
||||
mut relay_rx: broadcast::Receiver<String>,
|
||||
) -> Result<Response<hyper::Body>> {
|
||||
let (response, ws_fut_opt) = hyper_ws_listener::create_ws(req)
|
||||
.map_err(|e| anyhow::anyhow!("WebSocket upgrade failed: {}", e))?;
|
||||
|
||||
if let Some(ws_fut) = ws_fut_opt {
|
||||
tokio::spawn(async move {
|
||||
let ws_stream: WsStream = match ws_fut.await {
|
||||
Ok(Ok(s)) => s,
|
||||
Ok(Err(e)) => {
|
||||
debug!("Remote relay WS handshake failed: {}", e);
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Remote relay WS task join failed: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
info!("Remote relay browser connected");
|
||||
let (mut tx, mut rx) = ws_stream.split();
|
||||
|
||||
let _ = tx.send(Message::Text(r#"{"t":"ok"}"#.to_string())).await;
|
||||
|
||||
let ping_interval = tokio::time::interval(tokio::time::Duration::from_secs(30));
|
||||
tokio::pin!(ping_interval);
|
||||
let mut last_activity = Instant::now();
|
||||
const INACTIVITY_TIMEOUT: u64 = 300;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = ping_interval.tick() => {
|
||||
if last_activity.elapsed().as_secs() >= INACTIVITY_TIMEOUT {
|
||||
info!("Remote relay inactive, closing");
|
||||
let _ = tx.send(Message::Close(None)).await;
|
||||
break;
|
||||
}
|
||||
if tx.send(Message::Ping(vec![])).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
input = relay_rx.recv() => {
|
||||
match input {
|
||||
Ok(text) => {
|
||||
if tx.send(Message::Text(text)).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(n)) => {
|
||||
debug!("Remote relay lagged, skipped {} messages", n);
|
||||
}
|
||||
Err(broadcast::error::RecvError::Closed) => break,
|
||||
}
|
||||
}
|
||||
msg = rx.next() => {
|
||||
match msg {
|
||||
Some(Ok(Message::Pong(_))) | Some(Ok(Message::Text(_))) => {
|
||||
last_activity = Instant::now();
|
||||
}
|
||||
Some(Ok(Message::Ping(data))) => {
|
||||
last_activity = Instant::now();
|
||||
let _ = tx.send(Message::Pong(data)).await;
|
||||
}
|
||||
Some(Ok(Message::Close(_))) | None => break,
|
||||
Some(Ok(_)) => { last_activity = Instant::now(); }
|
||||
Some(Err(e)) => {
|
||||
debug!("Remote relay stream error: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("Remote relay browser disconnected");
|
||||
});
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,8 @@ impl RpcHandler {
|
||||
];
|
||||
|
||||
let is_tailscale = package_id == "tailscale";
|
||||
// Explicit DNS alias for aardvark-dns (must outlive run_args)
|
||||
let network_alias_flag = format!("--network-alias={}", container_name);
|
||||
|
||||
// Network mode
|
||||
if is_tailscale {
|
||||
@@ -182,6 +184,7 @@ impl RpcHandler {
|
||||
.await;
|
||||
if net_check.map(|s| s.success()).unwrap_or(false) {
|
||||
run_args.push("--network=archy-net");
|
||||
run_args.push(&network_alias_flag);
|
||||
} else {
|
||||
tracing::error!(
|
||||
"archy-net network does not exist — {} will use default network. \
|
||||
|
||||
Reference in New Issue
Block a user