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