Skip to main content

kaizen/web/
mod.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! Local daemon web app: embedded UI plus WebSocket tool calls.
3
4mod assets;
5pub mod features;
6mod server;
7mod snapshot;
8mod token;
9pub mod tools;
10
11use crate::ipc::WebEndpoint;
12use anyhow::{Context, Result};
13use std::net::{IpAddr, Ipv4Addr, SocketAddr};
14use std::path::Path;
15use tokio::net::TcpListener;
16use tokio::task::JoinHandle;
17
18const DEFAULT_LISTEN: &str = "127.0.0.1:7878";
19
20pub async fn start(token_path: &Path) -> Result<(WebEndpoint, JoinHandle<()>)> {
21    let token = token::load_or_create_at(token_path)?;
22    let listener = bind_loopback().await?;
23    start_with_token(listener, token).await
24}
25
26pub async fn start_with_listener(listener: TcpListener) -> Result<(WebEndpoint, JoinHandle<()>)> {
27    start_with_token(listener, token::ephemeral()).await
28}
29
30pub async fn start_with_token(
31    listener: TcpListener,
32    token: String,
33) -> Result<(WebEndpoint, JoinHandle<()>)> {
34    let addr = listener.local_addr()?;
35    let endpoint = endpoint(addr, token);
36    let app = server::router(endpoint.token.clone());
37    let task = tokio::spawn(async move {
38        if let Err(err) = axum::serve(listener, app).await {
39            tracing::warn!(%err, "daemon web app stopped");
40        }
41    });
42    Ok((endpoint, task))
43}
44
45async fn bind_loopback() -> Result<TcpListener> {
46    match TcpListener::bind(DEFAULT_LISTEN).await {
47        Ok(listener) => Ok(listener),
48        Err(err) => bind_fallback()
49            .await
50            .with_context(|| format!("bind daemon web app at {DEFAULT_LISTEN}: {err}")),
51    }
52}
53
54async fn bind_fallback() -> Result<TcpListener> {
55    TcpListener::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0))
56        .await
57        .map_err(Into::into)
58}
59
60fn endpoint(addr: SocketAddr, token: String) -> WebEndpoint {
61    let public = public_addr(addr);
62    WebEndpoint {
63        listen: addr.to_string(),
64        url: format!("http://{public}/?token={token}"),
65        token,
66    }
67}
68
69fn public_addr(addr: SocketAddr) -> SocketAddr {
70    if addr.ip().is_unspecified() {
71        return SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), addr.port());
72    }
73    addr
74}