1use crate::protocol::{ClientMessage, PatchOp, ServerMessage, PROTOCOL_ID};
7use crate::source::RunSource;
8use anyhow::{Context, Result};
9use futures_util::{SinkExt, StreamExt};
10use std::collections::HashMap;
11use std::path::PathBuf;
12use std::sync::Arc;
13use tokio::net::{TcpListener, TcpStream};
14use tokio::sync::{broadcast, Mutex};
15use tokio_tungstenite::tungstenite::Message;
16
17#[derive(Clone, Debug)]
19enum Update {
20 Runs(Vec<serde_json::Value>),
21 Patch {
22 run_id: String,
23 revision: u64,
24 patch: Vec<PatchOp>,
25 },
26}
27
28pub struct ServeOptions {
29 pub database_path: PathBuf,
30 pub bind: String,
31}
32
33pub async fn serve(options: ServeOptions) -> Result<()> {
34 let listener = TcpListener::bind(&options.bind)
35 .await
36 .with_context(|| format!("binding {}", options.bind))?;
37 eprintln!(
38 "piw serve: watching {} on ws://{}/ws",
39 options.database_path.display(),
40 listener.local_addr()?
41 );
42 serve_on(listener, options.database_path).await
43}
44
45pub async fn serve_on(listener: TcpListener, database_path: PathBuf) -> Result<()> {
47 let local = listener.local_addr()?;
52 if !local.ip().is_loopback() {
53 anyhow::bail!(
54 "refusing to serve on non-loopback address {local}: the live replay \
55 protocol is unauthenticated; bind to 127.0.0.1 and use an SSH tunnel \
56 for remote access"
57 );
58 }
59 let source = Arc::new(Mutex::new(RunSource::new(&database_path)));
60 let (updates_tx, _) = broadcast::channel::<Update>(256);
61
62 {
65 let source = Arc::clone(&source);
66 let updates_tx = updates_tx.clone();
67 tokio::spawn(async move {
68 loop {
69 tokio::time::sleep(std::time::Duration::from_millis(250)).await;
70 let outcome = source.lock().await.refresh_all();
71 for (run_id, revision, patch) in outcome.patches {
72 let _ = updates_tx.send(Update::Patch {
73 run_id,
74 revision,
75 patch,
76 });
77 }
78 if outcome.listing_changed {
79 let _ = updates_tx.send(Update::Runs(source.lock().await.summaries()));
80 }
81 }
82 });
83 }
84
85 loop {
86 let (stream, _addr) = listener.accept().await?;
87 let source = Arc::clone(&source);
88 let updates_rx = updates_tx.subscribe();
89 tokio::spawn(async move {
90 let _ = handle_connection(stream, source, updates_rx).await;
91 });
92 }
93}
94
95async fn send(
96 sink: &mut (impl SinkExt<Message, Error = tokio_tungstenite::tungstenite::Error> + Unpin),
97 message: &ServerMessage,
98) -> Result<()> {
99 let text = serde_json::to_string(message)?;
100 sink.send(Message::Text(text.into())).await?;
101 Ok(())
102}
103
104#[allow(clippy::result_large_err)]
107fn reject_browser_origins(
108 request: &tokio_tungstenite::tungstenite::handshake::server::Request,
109 response: tokio_tungstenite::tungstenite::handshake::server::Response,
110) -> Result<
111 tokio_tungstenite::tungstenite::handshake::server::Response,
112 tokio_tungstenite::tungstenite::handshake::server::ErrorResponse,
113> {
114 if request.headers().contains_key("origin") {
115 let mut rejection = tokio_tungstenite::tungstenite::handshake::server::ErrorResponse::new(
116 Some("browser origins are not allowed".to_string()),
117 );
118 *rejection.status_mut() = tokio_tungstenite::tungstenite::http::StatusCode::FORBIDDEN;
119 return Err(rejection);
120 }
121 Ok(response)
122}
123
124async fn handle_connection(
125 stream: TcpStream,
126 source: Arc<Mutex<RunSource>>,
127 mut updates_rx: broadcast::Receiver<Update>,
128) -> Result<()> {
129 let ws = tokio_tungstenite::accept_hdr_async(stream, reject_browser_origins).await?;
134 let (mut sink, mut reads) = ws.split();
135 send(
136 &mut sink,
137 &ServerMessage::Hello {
138 protocol: PROTOCOL_ID.to_string(),
139 },
140 )
141 .await?;
142
143 let mut watching_runs = false;
144 let mut watched: HashMap<String, u64> = HashMap::new();
148
149 loop {
150 tokio::select! {
151 incoming = reads.next() => {
152 let Some(incoming) = incoming else { break };
153 let message = match incoming {
154 Ok(Message::Text(text)) => text,
155 Ok(Message::Close(_)) => break,
156 Ok(_) => continue,
157 Err(_) => break,
158 };
159 let Ok(request) = serde_json::from_str::<ClientMessage>(&message) else {
160 continue;
162 };
163 match request {
164 ClientMessage::WatchRuns => {
165 watching_runs = true;
166 let runs = source.lock().await.summaries();
167 send(&mut sink, &ServerMessage::Runs { runs }).await?;
168 }
169 ClientMessage::WatchRun { run_id } => {
170 let snapshot = {
173 let source = source.lock().await;
174 source.get(&run_id).map(|entry| (entry.revision, entry.view()))
175 };
176 match snapshot {
177 Some((revision, view)) => {
178 watched.insert(run_id.clone(), revision);
179 send(&mut sink, &ServerMessage::RunSnapshot {
180 run_id,
181 revision,
182 view,
183 }).await?;
184 }
185 None => {
186 send(&mut sink, &ServerMessage::Error {
187 message: format!("unknown run {run_id}"),
188 run_id: Some(run_id),
189 }).await?;
190 }
191 }
192 }
193 ClientMessage::UnwatchRun { run_id } => {
194 watched.remove(&run_id);
195 }
196 ClientMessage::FetchArtifact { run_id, path } => {
197 send(&mut sink, &ServerMessage::Error {
198 message: format!("artifact {path} not available; values are stored in SQLite"),
199 run_id: Some(run_id),
200 }).await?;
201 }
202 }
203 }
204 update = updates_rx.recv() => {
205 match update {
206 Ok(Update::Runs(runs)) => {
207 if watching_runs {
208 send(&mut sink, &ServerMessage::Runs { runs }).await?;
209 }
210 }
211 Ok(Update::Patch { run_id, revision, patch }) => {
212 let Some(&last) = watched.get(&run_id) else { continue };
213 if revision == last + 1 {
214 watched.insert(run_id.clone(), revision);
215 send(&mut sink, &ServerMessage::RunPatch { run_id, revision, patch }).await?;
216 } else if revision > last {
217 let snapshot = {
219 let source = source.lock().await;
220 source.get(&run_id).map(|entry| (entry.revision, entry.view()))
221 };
222 if let Some((revision, view)) = snapshot {
223 watched.insert(run_id.clone(), revision);
224 send(&mut sink, &ServerMessage::RunSnapshot { run_id, revision, view }).await?;
225 }
226 }
227 }
228 Err(broadcast::error::RecvError::Lagged(_)) => {
229 let run_ids: Vec<String> = watched.keys().cloned().collect();
231 for run_id in run_ids {
232 let snapshot = {
233 let source = source.lock().await;
234 source.get(&run_id).map(|entry| (entry.revision, entry.view()))
235 };
236 if let Some((revision, view)) = snapshot {
237 watched.insert(run_id.clone(), revision);
238 send(&mut sink, &ServerMessage::RunSnapshot { run_id, revision, view }).await?;
239 }
240 }
241 }
242 Err(broadcast::error::RecvError::Closed) => break,
243 }
244 }
245 }
246 }
247 Ok(())
248}