1use crate::protocol::{DaemonInfo, Request, Response, PROTOCOL_VERSION};
11use crate::warm::WarmState;
12use pushkin_core::manifest::Manifest;
13use pushkin_core::pipeline::WriteRequest;
14use std::path::{Path, PathBuf};
15use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
16use tokio::net::{UnixListener, UnixStream};
17use tokio::task::JoinSet;
18
19pub const SOCKET_FILE: &str = ".pushkin/daemon.sock";
22
23const MAX_LINE_BYTES: usize = 16 * 1024 * 1024;
27
28#[derive(Debug, thiserror::Error)]
29pub enum ServerError {
30 #[error("daemon io failure: {0}")]
31 Io(#[from] std::io::Error),
32 #[error("daemon not running")]
33 NotRunning,
34 #[error("protocol failure: {0}")]
35 Protocol(String),
36}
37
38#[must_use]
39pub fn socket_path(repo_root: &Path) -> PathBuf {
40 repo_root.join(SOCKET_FILE)
41}
42
43pub fn serve(repo_root: &Path, manifest: Manifest) -> Result<(), ServerError> {
52 serve_at(&socket_path(repo_root), manifest, false)
53}
54
55pub fn serve_at(socket: &Path, manifest: Manifest, read_only: bool) -> Result<(), ServerError> {
64 let warm = WarmState::new(manifest);
65 let watch_root = socket
70 .parent()
71 .and_then(Path::parent)
72 .filter(|root| !root.as_os_str().is_empty())
73 .unwrap_or_else(|| Path::new("."));
74 let _watch_guard = warm.watch(watch_root).ok();
75 if let Some(parent) = socket.parent() {
76 std::fs::create_dir_all(parent)?;
77 }
78 if socket.exists() {
80 std::fs::remove_file(socket)?;
81 }
82
83 let runtime = tokio::runtime::Builder::new_current_thread()
84 .enable_io()
85 .enable_time()
86 .build()
87 .map_err(|e| ServerError::Protocol(format!("runtime build failed: {e}")))?;
88
89 let result = runtime.block_on(serve_inner(socket, &warm, read_only));
90 let _ = std::fs::remove_file(socket);
92 result
93}
94
95async fn serve_inner(socket: &Path, warm: &WarmState, read_only: bool) -> Result<(), ServerError> {
96 let listener = UnixListener::bind(socket)?;
97 let (shutdown_tx, mut shutdown_rx) = tokio::sync::mpsc::channel::<()>(1);
98 let mut connections: JoinSet<()> = JoinSet::new();
99
100 loop {
101 tokio::select! {
102 accepted = listener.accept() => {
103 let Ok((stream, _addr)) = accepted else { continue };
104 let warm = warm.share();
105 let shutdown_tx = shutdown_tx.clone();
106 connections.spawn(async move {
107 let _ = handle_connection(stream, &warm, read_only, &shutdown_tx).await;
110 });
111 }
112 _ = shutdown_rx.recv() => break,
113 Some(_) = connections.join_next(), if !connections.is_empty() => {}
115 }
116 }
117 while connections.join_next().await.is_some() {}
119 Ok(())
120}
121
122async fn handle_connection(
123 stream: UnixStream,
124 warm: &WarmState,
125 read_only: bool,
126 shutdown_tx: &tokio::sync::mpsc::Sender<()>,
127) -> Result<(), ServerError> {
128 let (read_half, mut write_half) = stream.into_split();
129 let mut lines = BufReader::with_capacity(64 * 1024, read_half).lines();
130
131 while let Ok(Some(line)) = lines.next_line().await {
132 if line.len() > MAX_LINE_BYTES {
133 let response = Response::Error {
134 message: "request exceeds size cap".to_owned(),
135 };
136 write_response(&mut write_half, &response).await?;
137 continue;
138 }
139 let response = match serde_json::from_str::<Request>(&line) {
140 Ok(request) => {
141 let response = respond(&request, warm, read_only);
142 let stop_serving = !read_only && matches!(request, Request::Shutdown { .. });
143 write_response(&mut write_half, &response).await?;
144 if stop_serving {
145 let _ = shutdown_tx.send(()).await;
146 return Ok(());
147 }
148 continue;
149 }
150 Err(error) => Response::Error {
151 message: format!("unrecognized request: {error}"),
152 },
153 };
154 write_response(&mut write_half, &response).await?;
155 }
156 Ok(())
157}
158
159async fn write_response(
160 write_half: &mut tokio::net::unix::OwnedWriteHalf,
161 response: &Response,
162) -> Result<(), ServerError> {
163 let mut payload = serde_json::to_string(response)
164 .map_err(|e| ServerError::Protocol(format!("response encode failed: {e}")))?;
165 payload.push('\n');
166 write_half.write_all(payload.as_bytes()).await?;
167 write_half.flush().await?;
168 Ok(())
169}
170
171fn respond(request: &Request, warm: &WarmState, read_only: bool) -> Response {
174 let v = match request {
175 Request::Check { v, .. } | Request::Ping { v } | Request::Shutdown { v } => *v,
176 };
177 if v != PROTOCOL_VERSION {
178 return Response::Error {
179 message: format!("protocol version {v} unsupported (daemon speaks {PROTOCOL_VERSION})"),
180 };
181 }
182 match request {
183 Request::Check {
184 file_path, content, ..
185 } => Response::Check {
186 result: warm.check(&WriteRequest {
187 file_path: file_path.clone(),
188 content: content.clone(),
189 }),
190 },
191 Request::Ping { .. } => Response::Pong {
192 info: DaemonInfo {
193 pid: std::process::id(),
194 version: env!("CARGO_PKG_VERSION").to_owned(),
195 read_only,
196 },
197 },
198 Request::Shutdown { .. } => {
199 if read_only {
200 Response::Error {
201 message: "read-only daemon: lifecycle mutations are refused over the wire \
202 (kill the process from the session that spawned it)"
203 .to_owned(),
204 }
205 } else {
206 Response::ShuttingDown
207 }
208 }
209 }
210}
211
212pub fn request(repo_root: &Path, request: &Request) -> Result<Response, ServerError> {
219 request_at(&socket_path(repo_root), request)
220}
221
222pub fn request_at(socket: &Path, request: &Request) -> Result<Response, ServerError> {
230 use std::io::{BufRead, BufReader as StdBufReader, Write};
231
232 let mut stream = match std::os::unix::net::UnixStream::connect(socket) {
233 Ok(stream) => stream,
234 Err(error) => {
235 return Err(match error.kind() {
236 std::io::ErrorKind::NotFound | std::io::ErrorKind::ConnectionRefused => {
237 ServerError::NotRunning
238 }
239 _ => ServerError::Io(error),
240 })
241 }
242 };
243 let mut payload = serde_json::to_string(request)
244 .map_err(|e| ServerError::Protocol(format!("request encode failed: {e}")))?;
245 payload.push('\n');
246 stream.write_all(payload.as_bytes())?;
247 stream.flush()?;
248
249 let mut line = String::new();
250 StdBufReader::new(&mut stream).read_line(&mut line)?;
251 if line.is_empty() {
252 return Err(ServerError::Protocol(
253 "daemon closed the connection without responding".to_owned(),
254 ));
255 }
256 serde_json::from_str(&line)
257 .map_err(|e| ServerError::Protocol(format!("unrecognized response: {e}")))
258}