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