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 #[arg(long, default_value = "127.0.0.1:8765")]
26 pub listen: SocketAddr,
27
28 #[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
68pub(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 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}