aboutsummaryrefslogtreecommitdiff
path: root/src/api/server.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/server.rs')
-rw-r--r--src/api/server.rs128
1 files changed, 104 insertions, 24 deletions
diff --git a/src/api/server.rs b/src/api/server.rs
index 513262bc..696e1b65 100644
--- a/src/api/server.rs
+++ b/src/api/server.rs
@@ -1,29 +1,34 @@
-use std::fs;
-use std::io::{BufRead, BufReader, Read, Write};
+use std::io::{self, Read, Write};
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
-use std::time::Duration;
+use std::time::{Duration, Instant};
use tracing::{debug, error, info, warn};
+#[cfg(test)]
+use std::fs;
+
use crate::api::schema::{
- ErrorBody, ErrorResponse, Method, Request, ResponseResult, SuccessResponse,
+ ErrorBody, ErrorResponse, Method, Request, ResponseResult, ServerCapabilities, SuccessResponse,
};
use crate::api::subscriptions::ActiveSubscription;
use crate::api::wait::wait_for_output;
use crate::api::{request_changes_ui, socket_path, ApiRequestMessage, ApiRequestSender, EventHub};
+use crate::ipc::{remove_socket_file_if_owned, socket_file_identity, SocketFileIdentity};
const SOCKET_PERMISSION_MODE: u32 = 0o600;
pub(super) const CONNECTION_POLL_INTERVAL: Duration = Duration::from_millis(100);
pub(super) const APP_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5);
const INITIAL_REQUEST_TIMEOUT: Duration = Duration::from_secs(5);
const STREAM_WRITE_TIMEOUT: Duration = Duration::from_secs(5);
+const MAX_INITIAL_REQUEST_BYTES: usize = 1024 * 1024;
pub struct ServerHandle {
_thread: std::thread::JoinHandle<()>,
path: PathBuf,
+ identity: SocketFileIdentity,
running: Arc<AtomicBool>,
}
@@ -31,7 +36,7 @@ impl Drop for ServerHandle {
fn drop(&mut self) {
self.running.store(false, Ordering::Relaxed);
- if let Err(err) = fs::remove_file(&self.path) {
+ if let Err(err) = self.remove_socket_file_if_owned() {
if err.kind() != std::io::ErrorKind::NotFound {
warn!(path = %self.path.display(), err = %err, "failed to remove api socket on shutdown");
}
@@ -39,15 +44,34 @@ impl Drop for ServerHandle {
}
}
+impl ServerHandle {
+ pub(crate) fn remove_socket_file_if_owned(&self) -> std::io::Result<()> {
+ remove_socket_file_if_owned(&self.path, self.identity)
+ }
+}
+
pub fn start_server(
api_tx: ApiRequestSender,
event_hub: EventHub,
) -> std::io::Result<ServerHandle> {
+ start_server_with_capabilities(
+ api_tx,
+ event_hub,
+ Some(ServerCapabilities { live_handoff: true }),
+ )
+}
+
+pub fn start_server_with_capabilities(
+ api_tx: ApiRequestSender,
+ event_hub: EventHub,
+ capabilities: Option<ServerCapabilities>,
+) -> std::io::Result<ServerHandle> {
let path = socket_path();
prepare_socket_path(&path)?;
let listener = UnixListener::bind(&path)?;
restrict_socket_permissions(&path)?;
+ let identity = socket_file_identity(&path)?;
info!(path = %path.display(), "api server listening");
let running = Arc::new(AtomicBool::new(true));
@@ -58,11 +82,16 @@ pub fn start_server(
Ok(stream) => {
let api_tx = api_tx.clone();
let event_hub = event_hub.clone();
+ let capabilities = capabilities.clone();
let connection_running = Arc::clone(&listener_running);
std::thread::spawn(move || {
- if let Err(err) =
- handle_connection(stream, &api_tx, &event_hub, &connection_running)
- {
+ if let Err(err) = handle_connection(
+ stream,
+ &api_tx,
+ &event_hub,
+ &connection_running,
+ capabilities,
+ ) {
warn!(err = %err, "api connection failed");
}
});
@@ -79,6 +108,7 @@ pub fn start_server(
Ok(ServerHandle {
_thread: thread,
path,
+ identity,
running,
})
}
@@ -101,20 +131,15 @@ fn handle_connection(
api_tx: &ApiRequestSender,
event_hub: &EventHub,
running: &Arc<AtomicBool>,
+ capabilities: Option<ServerCapabilities>,
) -> std::io::Result<()> {
- stream.set_read_timeout(Some(INITIAL_REQUEST_TIMEOUT))?;
- stream.set_write_timeout(Some(STREAM_WRITE_TIMEOUT))?;
-
- let mut line = String::new();
- {
- let mut reader = BufReader::new(&stream);
- let read = reader.read_line(&mut line)?;
- if read == 0 {
- return Ok(());
- }
+ if let Err(err) = stream.set_write_timeout(Some(STREAM_WRITE_TIMEOUT)) {
+ debug!(err = %err, "api connection write timeout unavailable");
}
- stream.set_read_timeout(None)?;
+ let Some(line) = read_initial_request_line(&mut stream)? else {
+ return Ok(());
+ };
let line = line.trim();
if line.is_empty() {
@@ -199,6 +224,7 @@ fn handle_connection(
method: method_body,
},
api_tx,
+ capabilities,
);
let result = write_text_line_allow_disconnect(&mut stream, &response);
match &result {
@@ -217,13 +243,18 @@ fn handle_connection(
}
}
-fn handle_request(request: Request, api_tx: &ApiRequestSender) -> String {
+fn handle_request(
+ request: Request,
+ api_tx: &ApiRequestSender,
+ capabilities: Option<ServerCapabilities>,
+) -> String {
match request.method {
Method::Ping(_) => serde_json::to_string(&SuccessResponse {
id: request.id,
result: ResponseResult::Pong {
version: env!("CARGO_PKG_VERSION").into(),
protocol: crate::protocol::PROTOCOL_VERSION,
+ capabilities,
},
})
.unwrap_or_else(|_| {
@@ -238,6 +269,7 @@ fn api_method_name(method: &Method) -> &'static str {
match method {
Method::Ping(_) => "ping",
Method::ServerStop(_) => "server.stop",
+ Method::ServerLiveHandoff(_) => "server.live_handoff",
Method::ServerReloadConfig(_) => "server.reload_config",
Method::WorkspaceCreate(_) => "workspace.create",
Method::WorkspaceList(_) => "workspace.list",
@@ -298,6 +330,52 @@ fn api_response_outcome(response: &str) -> &'static str {
}
}
+fn read_initial_request_line(stream: &mut UnixStream) -> std::io::Result<Option<String>> {
+ stream.set_nonblocking(true)?;
+ let deadline = Instant::now() + INITIAL_REQUEST_TIMEOUT;
+ let mut bytes = Vec::new();
+ let mut byte = [0u8; 1];
+
+ loop {
+ match stream.read(&mut byte) {
+ Ok(0) => {
+ stream.set_nonblocking(false)?;
+ return Ok(None);
+ }
+ Ok(_) => {
+ bytes.push(byte[0]);
+ if byte[0] == b'\n' {
+ stream.set_nonblocking(false)?;
+ return String::from_utf8(bytes)
+ .map(Some)
+ .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err));
+ }
+ if bytes.len() > MAX_INITIAL_REQUEST_BYTES {
+ stream.set_nonblocking(false)?;
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidData,
+ "api request line is too large",
+ ));
+ }
+ }
+ Err(err) if err.kind() == io::ErrorKind::WouldBlock => {
+ if Instant::now() >= deadline {
+ stream.set_nonblocking(false)?;
+ return Err(io::Error::new(
+ io::ErrorKind::TimedOut,
+ "timed out reading api request",
+ ));
+ }
+ std::thread::sleep(CONNECTION_POLL_INTERVAL);
+ }
+ Err(err) => {
+ stream.set_nonblocking(false)?;
+ return Err(err);
+ }
+ }
+ }
+}
+
fn stream_subscriptions(
mut stream: UnixStream,
request_id: String,
@@ -496,6 +574,7 @@ fn error_response_json(id: String, code: &str, message: String) -> String {
#[cfg(test)]
mod tests {
use super::*;
+ use std::io::{BufRead, BufReader};
use std::os::unix::fs::PermissionsExt;
use std::sync::{Mutex, OnceLock};
use tokio::sync::mpsc;
@@ -610,6 +689,7 @@ mod tests {
method: Method::Ping(crate::api::schema::PingParams::default()),
},
&tx,
+ Some(ServerCapabilities { live_handoff: true }),
);
let parsed: SuccessResponse = serde_json::from_str(&response).unwrap();
@@ -626,7 +706,7 @@ mod tests {
};
let request_for_thread = request.clone();
- let thread = std::thread::spawn(move || handle_request(request_for_thread, &tx));
+ let thread = std::thread::spawn(move || handle_request(request_for_thread, &tx, None));
let msg = rx.blocking_recv().unwrap();
assert_eq!(msg.request.id, "req_2");
@@ -692,7 +772,7 @@ mod tests {
let event_hub = EventHub::default();
let (done_tx, done_rx) = std::sync::mpsc::channel();
let server_thread = std::thread::spawn(move || {
- let result = handle_connection(server, &api_tx, &event_hub, &server_running);
+ let result = handle_connection(server, &api_tx, &event_hub, &server_running, None);
done_tx.send(result).unwrap();
});
@@ -724,7 +804,7 @@ mod tests {
let event_hub = EventHub::default();
let (done_tx, done_rx) = std::sync::mpsc::channel();
let server_thread = std::thread::spawn(move || {
- let result = handle_connection(server, &api_tx, &event_hub, &server_running);
+ let result = handle_connection(server, &api_tx, &event_hub, &server_running, None);
done_tx.send(result).unwrap();
});
@@ -756,7 +836,7 @@ mod tests {
let event_hub = EventHub::default();
let (done_tx, done_rx) = std::sync::mpsc::channel();
let server_thread = std::thread::spawn(move || {
- let result = handle_connection(server, &api_tx, &event_hub, &server_running);
+ let result = handle_connection(server, &api_tx, &event_hub, &server_running, None);
done_tx.send(result).unwrap();
});