Skip to main content

aether_cli/acp/
server.rs

1use super::state::{AcpState, ClientGuard};
2use super::{AcpArgs, AcpRunError, create_acp_state};
3use acp_utils::websocket::WebSocketTransport;
4use std::fs::canonicalize;
5use std::future::Future;
6use std::io;
7use std::net::SocketAddr;
8use std::path::PathBuf;
9use std::sync::Arc;
10use thiserror::Error;
11use tokio::net::{TcpListener, TcpStream};
12#[cfg(unix)]
13use tokio::signal::unix::{SignalKind, signal};
14use tokio::task::JoinSet;
15use tokio_tungstenite::tungstenite::{
16    handshake::server::{ErrorResponse, Request},
17    http::StatusCode,
18};
19use tokio_util::sync::CancellationToken;
20use tracing::{info, warn};
21
22#[derive(clap::Args, Debug)]
23pub struct ServerArgs {
24    /// Address to listen on. The raw server is unauthenticated; use a private network or authenticating proxy.
25    #[arg(long, default_value = "127.0.0.1:8765")]
26    pub listen: SocketAddr,
27
28    /// Server workspace used to resolve settings and as the default session directory.
29    #[arg(short = 'C', long, default_value = ".")]
30    pub cwd: PathBuf,
31
32    #[command(flatten)]
33    pub acp: AcpArgs,
34}
35
36#[derive(Debug, Error)]
37pub enum ServerRunError {
38    #[error("Invalid server workspace {path}: {source}")]
39    Workspace { path: PathBuf, source: io::Error },
40    #[error(transparent)]
41    Initialization(#[from] AcpRunError),
42    #[error("Failed to bind ACP listener at {address}: {source}")]
43    Bind { address: SocketAddr, source: io::Error },
44    #[error("ACP listener failed: {0}")]
45    Accept(#[source] io::Error),
46    #[error("Failed to listen for shutdown signals: {0}")]
47    Signal(#[source] io::Error),
48}
49
50pub async fn run_server(args: ServerArgs) -> Result<(), ServerRunError> {
51    let cwd = canonicalize(&args.cwd)
52        .and_then(|cwd| {
53            if cwd.is_dir() {
54                Ok(cwd)
55            } else {
56                Err(io::Error::new(io::ErrorKind::NotADirectory, "workspace must be a directory"))
57            }
58        })
59        .map_err(|source| ServerRunError::Workspace { path: args.cwd, source })?;
60
61    let state = Arc::new(create_acp_state(args.acp, &cwd)?);
62    info!(address = %args.listen, cwd = %cwd.display(), "Starting Aether ACP WebSocket server");
63    let result = serve(state.clone(), args.listen).await;
64    state.shutdown_all().await;
65    result
66}
67
68/// Owns networking, not session lifetime. Drop closes the listener and aborts
69/// connection tasks; explicit shutdown also waits for output detachment.
70pub(crate) struct AcpServer {
71    listener: TcpListener,
72    state: Arc<AcpState>,
73    connections: JoinSet<()>,
74    stop: CancellationToken,
75}
76
77impl AcpServer {
78    pub(crate) async fn bind(address: SocketAddr, state: Arc<AcpState>) -> Result<Self, ServerRunError> {
79        let listener = TcpListener::bind(address).await.map_err(|source| ServerRunError::Bind { address, source })?;
80        Ok(Self { listener, stop: state.stop_token().child_token(), state, connections: JoinSet::new() })
81    }
82
83    #[cfg(any(test, feature = "testing"))]
84    pub(crate) fn local_addr(&self) -> io::Result<SocketAddr> {
85        self.listener.local_addr()
86    }
87
88    pub(crate) async fn run_until(
89        mut self,
90        stop: impl Future<Output = Result<(), ServerRunError>>,
91    ) -> Result<(), ServerRunError> {
92        let result = tokio::select! {
93            result = self.run() => result,
94            result = stop => result,
95        };
96        self.shutdown().await;
97        result
98    }
99
100    async fn run(&mut self) -> Result<(), ServerRunError> {
101        loop {
102            tokio::select! {
103                biased;
104                () = self.stop.cancelled() => return Ok(()),
105                result = self.connections.join_next(), if !self.connections.is_empty() => {
106                    if let Some(Err(error)) = result {
107                        warn!(%error, "ACP connection task failed");
108                    }
109                }
110                accepted = self.listener.accept() => {
111                    let (socket, peer) = accepted.map_err(ServerRunError::Accept)?;
112                    self.accept(socket, peer);
113                }
114            }
115        }
116    }
117
118    /// Close admission and join all networking, leaving the host running.
119    pub(crate) async fn shutdown(self) {
120        self.stop.cancel();
121        drop(self.listener);
122        let mut connections = self.connections;
123        while let Some(result) = connections.join_next().await {
124            if let Err(error) = result
125                && !error.is_cancelled()
126            {
127                warn!(%error, "ACP connection task failed during shutdown");
128            }
129        }
130    }
131
132    fn accept(&mut self, socket: TcpStream, peer: SocketAddr) {
133        let stop = self.stop.clone();
134        let guard = self.state.try_attach();
135        self.connections.spawn(serve_connection(socket, peer, guard, stop));
136    }
137}
138
139#[expect(clippy::result_large_err, reason = "tungstenite's handshake callback requires an HTTP error response")]
140async fn serve_connection(socket: TcpStream, peer: SocketAddr, guard: Option<ClientGuard>, stop: CancellationToken) {
141    let handshake = tokio::select! {
142        biased;
143        () = stop.cancelled() => {
144            if let Some(guard) = guard {
145                guard.release().await;
146            }
147            return;
148        },
149        result = tokio_tungstenite::accept_hdr_async(socket, |_request: &Request, response| {
150            if guard.is_some() {
151                Ok(response)
152            } else {
153                let mut error = ErrorResponse::new(Some("client already attached".to_string()));
154                *error.status_mut() = StatusCode::CONFLICT;
155                Err(error)
156            }
157        }) => result,
158    };
159    let socket = match handshake {
160        Ok(socket) => socket,
161        Err(error) => {
162            warn!(%peer, %error, "ACP WebSocket handshake failed");
163            if let Some(guard) = guard {
164                guard.release().await;
165            }
166            return;
167        }
168    };
169    if let Err(error) =
170        guard.expect("successful handshake owns the client").serve(WebSocketTransport::new(socket), stop).await
171    {
172        warn!(%peer, %error, "ACP connection failed");
173    }
174}
175
176async fn serve(state: Arc<AcpState>, listen: SocketAddr) -> Result<(), ServerRunError> {
177    AcpServer::bind(listen, state)
178        .await?
179        .run_until(async { shutdown_signal().await.map_err(ServerRunError::Signal) })
180        .await
181}
182
183async fn shutdown_signal() -> io::Result<()> {
184    #[cfg(unix)]
185    {
186        let mut terminate = signal(SignalKind::terminate())?;
187        tokio::select! {
188            result = tokio::signal::ctrl_c() => result,
189            _ = terminate.recv() => Ok(()),
190        }
191    }
192    #[cfg(not(unix))]
193    tokio::signal::ctrl_c().await
194}