1use std::fs::{self, OpenOptions};
18use std::io::{self, Read, Write};
19use std::net::{SocketAddr, TcpListener, TcpStream};
20use std::path::PathBuf;
21use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
22use std::sync::mpsc::{self, Receiver, RecvTimeoutError, SyncSender, TrySendError};
23use std::sync::{Arc, OnceLock};
24use std::thread::{self, JoinHandle};
25use std::time::{Duration, Instant};
26
27use serde::Serialize;
28use serde_json::{Value, json};
29
30const PROTOCOL_VERSION: u32 = 1;
31const MAX_REQUEST_BYTES: usize = 1024 * 1024;
32const COMMAND_TIMEOUT: Duration = Duration::from_secs(5);
33const MAX_PENDING_COMMANDS: usize = 64;
34const MAX_CONNECTIONS: usize = 64;
35
36#[derive(Default)]
37struct ActiveSession(AtomicU64);
38
39impl ActiveSession {
40 fn create(&self) -> io::Result<Option<String>> {
41 let value = loop {
42 let mut bytes = [0_u8; 8];
43 getrandom::fill(&mut bytes).map_err(io::Error::other)?;
44 let value = u64::from_ne_bytes(bytes);
45 if value != 0 {
46 break value;
47 }
48 };
49 match self
50 .0
51 .compare_exchange(0, value, Ordering::AcqRel, Ordering::Acquire)
52 {
53 Ok(_) => Ok(Some(format!("{value:016x}"))),
54 Err(_) => Ok(None),
55 }
56 }
57
58 fn matches(&self, supplied: &str) -> bool {
59 u64::from_str_radix(supplied, 16)
60 .ok()
61 .is_some_and(|value| value != 0 && self.0.load(Ordering::Acquire) == value)
62 }
63
64 fn clear(&self) {
65 self.0.store(0, Ordering::Release);
66 }
67}
68
69#[derive(Debug, Clone)]
71pub struct ServerConfig {
72 pub bind_address: SocketAddr,
74 pub descriptor_path: PathBuf,
76 pub renderer_revision: String,
78}
79
80#[derive(Clone, Default)]
93pub struct ServiceWaker(Arc<OnceLock<Box<dyn Fn() + Send + Sync>>>);
94
95impl ServiceWaker {
96 pub fn set(&self, wake: impl Fn() + Send + Sync + 'static) {
98 let _ = self.0.set(Box::new(wake));
99 }
100
101 fn wake(&self) {
102 if let Some(wake) = self.0.get() {
103 wake();
104 }
105 }
106}
107
108impl std::fmt::Debug for ServiceWaker {
109 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 formatter
111 .debug_struct("ServiceWaker")
112 .field("installed", &self.0.get().is_some())
113 .finish()
114 }
115}
116
117#[derive(Debug)]
119pub struct ControlRequest {
120 pub method: String,
121 pub path: String,
122 pub body: Value,
123 reply: SyncSender<ControlResponse>,
124}
125
126impl ControlRequest {
127 pub fn respond(self, response: ControlResponse) -> Result<(), ControlResponse> {
129 self.reply.send(response).map_err(|error| error.0)
130 }
131}
132
133#[derive(Debug)]
135pub enum ControlResponse {
136 Success(Value),
137 Error {
138 error: String,
139 message: String,
140 stacktrace: String,
141 },
142}
143
144impl ControlResponse {
145 pub fn unsupported(message: impl Into<String>) -> Self {
146 Self::Error {
147 error: "unsupported operation".into(),
148 message: message.into(),
149 stacktrace: String::new(),
150 }
151 }
152}
153
154#[derive(Debug, Serialize)]
155#[serde(rename_all = "camelCase")]
156struct Descriptor<'a> {
157 pid: u32,
158 address: String,
159 token: &'a str,
160 protocol_version: u32,
161 renderer: &'static str,
162 renderer_revision: &'a str,
163}
164
165pub struct DebugServer {
167 address: SocketAddr,
168 token: String,
169 descriptor_path: PathBuf,
170 shutdown: Arc<AtomicBool>,
171 waker: ServiceWaker,
172 thread: Option<JoinHandle<()>>,
173}
174
175impl DebugServer {
176 pub fn start(config: ServerConfig) -> io::Result<(Self, Receiver<ControlRequest>)> {
178 if !config.bind_address.ip().is_loopback() {
179 return Err(io::Error::new(
180 io::ErrorKind::InvalidInput,
181 "debug control must bind to a loopback address",
182 ));
183 }
184 let listener = TcpListener::bind(config.bind_address)?;
185 let address = listener.local_addr()?;
186 let token = random_hex(32)?;
187 let (command_tx, command_rx) = mpsc::sync_channel(MAX_PENDING_COMMANDS);
190 let shutdown = Arc::new(AtomicBool::new(false));
191 let waker = ServiceWaker::default();
192
193 let thread_shutdown = Arc::clone(&shutdown);
194 let thread_token = token.clone();
195 let thread_waker = waker.clone();
196 let thread = thread::Builder::new()
197 .name("blitz-debug-control".into())
198 .spawn(move || {
199 server_loop(
200 listener,
201 &thread_token,
202 command_tx,
203 &thread_waker,
204 thread_shutdown,
205 )
206 })?;
207
208 if let Err(error) = write_descriptor(&config, address, &token) {
209 shutdown.store(true, Ordering::Release);
210 let _ = TcpStream::connect(address);
211 let _ = thread.join();
212 return Err(error);
213 }
214
215 Ok((
216 Self {
217 address,
218 token,
219 descriptor_path: config.descriptor_path,
220 shutdown,
221 waker,
222 thread: Some(thread),
223 },
224 command_rx,
225 ))
226 }
227
228 pub fn waker(&self) -> ServiceWaker {
231 self.waker.clone()
232 }
233
234 pub fn address(&self) -> SocketAddr {
235 self.address
236 }
237
238 pub fn token(&self) -> &str {
239 &self.token
240 }
241
242 pub fn shutdown(mut self) {
243 self.stop();
244 }
245
246 fn stop(&mut self) {
247 self.shutdown.store(true, Ordering::Release);
248 let _ = TcpStream::connect(self.address);
249 if let Some(thread) = self.thread.take() {
250 let _ = thread.join();
251 }
252 let _ = fs::remove_file(&self.descriptor_path);
253 }
254}
255
256impl Drop for DebugServer {
257 fn drop(&mut self) {
258 self.stop();
259 }
260}
261
262fn random_hex(byte_len: usize) -> io::Result<String> {
263 let mut bytes = vec![0; byte_len];
264 getrandom::fill(&mut bytes).map_err(io::Error::other)?;
265 let mut output = String::with_capacity(byte_len * 2);
266 for byte in bytes {
267 use std::fmt::Write as _;
268 write!(output, "{byte:02x}").unwrap();
269 }
270 Ok(output)
271}
272
273fn write_descriptor(config: &ServerConfig, address: SocketAddr, token: &str) -> io::Result<()> {
274 let descriptor = Descriptor {
275 pid: std::process::id(),
276 address: address.to_string(),
277 token,
278 protocol_version: PROTOCOL_VERSION,
279 renderer: "blitz",
280 renderer_revision: &config.renderer_revision,
281 };
282 let bytes = serde_json::to_vec_pretty(&descriptor).map_err(io::Error::other)?;
283 if let Some(parent) = config.descriptor_path.parent() {
284 fs::create_dir_all(parent)?;
285 }
286 let temporary = config
287 .descriptor_path
288 .with_extension(format!("tmp-{}", random_hex(8)?));
289 let result = (|| {
290 let mut options = OpenOptions::new();
291 options.write(true).create_new(true);
292 #[cfg(unix)]
293 {
294 use std::os::unix::fs::OpenOptionsExt;
295 options.mode(0o600);
296 }
297 let mut file = options.open(&temporary)?;
298 file.write_all(&bytes)?;
299 file.sync_all()?;
300 fs::rename(&temporary, &config.descriptor_path)
301 })();
302 if result.is_err() {
303 let _ = fs::remove_file(&temporary);
304 }
305 result
306}
307
308fn server_loop(
309 listener: TcpListener,
310 token: &str,
311 command_tx: SyncSender<ControlRequest>,
312 waker: &ServiceWaker,
313 shutdown: Arc<AtomicBool>,
314) {
315 let active_session = Arc::new(ActiveSession::default());
316 let active_connections = Arc::new(AtomicUsize::new(0));
317 let token: Arc<str> = Arc::from(token);
318 for connection in listener.incoming() {
319 if shutdown.load(Ordering::Acquire) {
320 break;
321 }
322 match connection {
323 Ok(mut stream) => {
324 if active_connections.fetch_add(1, Ordering::AcqRel) >= MAX_CONNECTIONS {
325 active_connections.fetch_sub(1, Ordering::AcqRel);
326 let _ = write_response(
327 &mut stream,
328 webdriver_error("unknown error", "debug-control connection limit reached"),
329 );
330 continue;
331 }
332 let token = Arc::clone(&token);
333 let active_session = Arc::clone(&active_session);
334 let thread_connections = Arc::clone(&active_connections);
335 let command_tx = command_tx.clone();
336 let waker = waker.clone();
337 let spawned = thread::Builder::new()
338 .name("blitz-debug-connection".into())
339 .spawn(move || {
340 let _permit = ConnectionPermit(thread_connections);
341 let _ = stream.set_write_timeout(Some(COMMAND_TIMEOUT));
342 let response = match read_request(&mut stream) {
343 Ok(request) => {
344 route(request, &token, &active_session, &command_tx, &waker)
345 }
346 Err(error) => webdriver_error("invalid argument", error.to_string()),
347 };
348 let _ = write_response(&mut stream, response);
349 });
350 if spawned.is_err() {
351 active_connections.fetch_sub(1, Ordering::AcqRel);
352 }
353 }
354 Err(_) if shutdown.load(Ordering::Acquire) => break,
355 Err(_) => continue,
356 }
357 }
358}
359
360struct ConnectionPermit(Arc<AtomicUsize>);
361
362impl Drop for ConnectionPermit {
363 fn drop(&mut self) {
364 self.0.fetch_sub(1, Ordering::AcqRel);
365 }
366}
367
368#[derive(Debug)]
369struct HttpRequest {
370 method: String,
371 path: String,
372 body: Value,
373}
374
375fn read_request(stream: &mut TcpStream) -> io::Result<HttpRequest> {
376 read_request_within(stream, COMMAND_TIMEOUT)
377}
378
379fn read_request_within(stream: &mut TcpStream, within: Duration) -> io::Result<HttpRequest> {
380 let deadline = Instant::now() + within;
381 let mut bytes = Vec::with_capacity(4096);
382 let mut scan_from = 0;
383 let header_end = loop {
384 if bytes.len() >= MAX_REQUEST_BYTES {
385 return Err(io::Error::new(
386 io::ErrorKind::InvalidData,
387 "request is too large",
388 ));
389 }
390 let remaining = deadline.saturating_duration_since(Instant::now());
391 if remaining.is_zero() {
392 return Err(io::Error::new(io::ErrorKind::TimedOut, "request timed out"));
393 }
394 stream.set_read_timeout(Some(remaining))?;
395 let mut chunk = [0; 4096];
396 let allowed = (MAX_REQUEST_BYTES - bytes.len()).min(chunk.len());
397 let count = stream.read(&mut chunk[..allowed])?;
398 if count == 0 {
399 return Err(io::Error::new(
400 io::ErrorKind::UnexpectedEof,
401 "connection closed before headers",
402 ));
403 }
404 let prior_len = bytes.len();
405 bytes.extend_from_slice(&chunk[..count]);
406 scan_from = scan_from.min(prior_len.saturating_sub(3));
407 if let Some(index) = bytes[scan_from..]
408 .windows(4)
409 .position(|window| window == b"\r\n\r\n")
410 {
411 break scan_from + index + 4;
412 }
413 scan_from = bytes.len().saturating_sub(3);
414 };
415
416 let headers = std::str::from_utf8(&bytes[..header_end])
417 .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
418 let mut lines = headers.split("\r\n");
419 let mut request_line = lines.next().unwrap_or_default().split_whitespace();
420 let method = request_line.next().unwrap_or_default().to_string();
421 let path = request_line.next().unwrap_or_default().to_string();
422 if method.is_empty() || path.is_empty() {
423 return Err(io::Error::new(
424 io::ErrorKind::InvalidData,
425 "invalid request line",
426 ));
427 }
428 let header_fields: Vec<_> = lines.filter_map(|line| line.split_once(':')).collect();
429 let content_length = header_fields
430 .iter()
431 .copied()
432 .find(|(name, _)| name.eq_ignore_ascii_case("content-length"))
433 .map(|(_, value)| value.trim().parse::<usize>())
434 .transpose()
435 .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?
436 .unwrap_or(0);
437 if header_fields.iter().any(|(name, value)| {
438 name.eq_ignore_ascii_case("transfer-encoding")
439 && !value.trim().eq_ignore_ascii_case("identity")
440 }) {
441 return Err(io::Error::new(
442 io::ErrorKind::InvalidData,
443 "transfer-encoding is unsupported; send Content-Length",
444 ));
445 }
446 let body_end = header_end
447 .checked_add(content_length)
448 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "request size overflow"))?;
449 if body_end > MAX_REQUEST_BYTES {
450 return Err(io::Error::new(
451 io::ErrorKind::InvalidData,
452 "request is too large",
453 ));
454 }
455 while bytes.len() < body_end {
456 let deadline_remaining = deadline.saturating_duration_since(Instant::now());
457 if deadline_remaining.is_zero() {
458 return Err(io::Error::new(io::ErrorKind::TimedOut, "request timed out"));
459 }
460 stream.set_read_timeout(Some(deadline_remaining))?;
461 let remaining = body_end - bytes.len();
462 let mut chunk = vec![0; remaining.min(4096)];
463 let count = stream.read(&mut chunk)?;
464 if count == 0 {
465 return Err(io::Error::new(
466 io::ErrorKind::UnexpectedEof,
467 "connection closed before body",
468 ));
469 }
470 bytes.extend_from_slice(&chunk[..count]);
471 }
472 let body = if content_length == 0 {
473 Value::Null
474 } else {
475 serde_json::from_slice(&bytes[header_end..body_end])
476 .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?
477 };
478 Ok(HttpRequest { method, path, body })
479}
480
481fn route(
482 request: HttpRequest,
483 token: &str,
484 active_session: &ActiveSession,
485 command_tx: &SyncSender<ControlRequest>,
486 waker: &ServiceWaker,
487) -> Value {
488 if request.method == "GET" && request.path == "/status" {
489 return json!({"value": {
490 "ready": true,
491 "message": "Blitz debug control is ready",
492 "protocolVersion": PROTOCOL_VERSION,
493 }});
494 }
495
496 if request.method == "POST" && request.path == "/session" {
497 let supplied_token = request
498 .body
499 .pointer("/capabilities/alwaysMatch/blitz:token")
500 .and_then(Value::as_str);
501 if !token_matches(supplied_token, token) {
502 return webdriver_error("invalid argument", "invalid blitz:token capability");
503 }
504 let session_id = match active_session.create() {
505 Ok(Some(value)) => value,
506 Ok(None) => {
507 return webdriver_error("session not created", "only one session is supported");
508 }
509 Err(error) => return webdriver_error("unknown error", error.to_string()),
510 };
511 return json!({"value": {
512 "sessionId": session_id,
513 "capabilities": {
514 "browserName": "blitz",
515 "blitz:protocolVersion": PROTOCOL_VERSION,
516 }
517 }});
518 }
519
520 let Some((session_id, command_path)) = session_path(&request.path) else {
521 return webdriver_error("unknown command", "unknown debug-control route");
522 };
523 if !active_session.matches(session_id) {
524 return webdriver_error("invalid session id", "session is not active");
525 }
526 if request.method == "DELETE" && command_path.is_empty() {
527 active_session.clear();
528 return json!({"value": null});
529 }
530
531 let (reply_tx, reply_rx) = mpsc::sync_channel(1);
532 let control_request = ControlRequest {
533 method: request.method,
534 path: command_path.to_string(),
535 body: request.body,
536 reply: reply_tx,
537 };
538 match command_tx.try_send(control_request) {
539 Ok(()) => {}
540 Err(TrySendError::Full(_)) => {
541 return webdriver_error("unknown error", "renderer command queue is full");
542 }
543 Err(TrySendError::Disconnected(_)) => {
544 return webdriver_error("unknown error", "renderer command channel is closed");
545 }
546 }
547 waker.wake();
550 match reply_rx.recv_timeout(COMMAND_TIMEOUT) {
551 Ok(ControlResponse::Success(value)) => json!({"value": value}),
552 Ok(ControlResponse::Error {
553 error,
554 message,
555 stacktrace,
556 }) => json!({"value": {
557 "error": error,
558 "message": message,
559 "stacktrace": stacktrace,
560 }}),
561 Err(RecvTimeoutError::Timeout) => webdriver_error("timeout", "renderer command timed out"),
562 Err(RecvTimeoutError::Disconnected) => {
563 webdriver_error("unknown error", "renderer response channel is closed")
564 }
565 }
566}
567
568fn token_matches(supplied: Option<&str>, expected: &str) -> bool {
571 let supplied = supplied.unwrap_or_default().as_bytes();
572 let expected = expected.as_bytes();
573 let mut difference = supplied.len() ^ expected.len();
574 for (index, expected_byte) in expected.iter().enumerate() {
575 difference |= usize::from(*expected_byte ^ supplied.get(index).copied().unwrap_or(0));
576 }
577 difference == 0
578}
579
580fn session_path(path: &str) -> Option<(&str, &str)> {
581 let remainder = path.strip_prefix("/session/")?;
582 let (session_id, command) = remainder.split_once('/').unwrap_or((remainder, ""));
583 Some((session_id, command))
584}
585
586fn webdriver_error(error: &str, message: impl Into<String>) -> Value {
587 json!({"value": {
588 "error": error,
589 "message": message.into(),
590 "stacktrace": "",
591 }})
592}
593
594fn write_response(stream: &mut TcpStream, body: Value) -> io::Result<()> {
595 let bytes = serde_json::to_vec(&body).map_err(io::Error::other)?;
596 let status = match body.pointer("/value/error").and_then(Value::as_str) {
597 None => "200 OK",
598 Some("unknown command" | "invalid session id") => "404 Not Found",
599 Some("timeout" | "unknown error") => "500 Internal Server Error",
600 Some(_) => "400 Bad Request",
601 };
602 write!(
603 stream,
604 "HTTP/1.1 {status}\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
605 bytes.len()
606 )?;
607 stream.write_all(&bytes)
608}
609
610#[cfg(test)]
611mod tests {
612 use super::*;
613 use std::sync::Mutex;
614 use std::time::{SystemTime, UNIX_EPOCH};
615
616 #[test]
617 fn token_comparison_rejects_prefixes_suffixes_and_missing_values() {
618 assert!(token_matches(Some("012345"), "012345"));
619 assert!(!token_matches(Some("01234x"), "012345"));
620 assert!(!token_matches(Some("0123456"), "012345"));
621 assert!(!token_matches(Some("01234"), "012345"));
622 assert!(!token_matches(None, "012345"));
623 }
624
625 fn descriptor_path() -> PathBuf {
626 let nonce = SystemTime::now()
627 .duration_since(UNIX_EPOCH)
628 .unwrap()
629 .as_nanos();
630 std::env::temp_dir().join(format!("blitz-debug-{nonce}.json"))
631 }
632
633 fn request(address: SocketAddr, method: &str, path: &str, body: Value) -> Value {
634 let body = if body.is_null() {
635 Vec::new()
636 } else {
637 serde_json::to_vec(&body).unwrap()
638 };
639 let mut stream = TcpStream::connect(address).unwrap();
640 write!(
641 stream,
642 "{method} {path} HTTP/1.1\r\nHost: {address}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
643 body.len()
644 )
645 .unwrap();
646 stream.write_all(&body).unwrap();
647 let mut response = Vec::new();
648 stream.read_to_end(&mut response).unwrap();
649 let body_start = response
650 .windows(4)
651 .position(|window| window == b"\r\n\r\n")
652 .unwrap()
653 + 4;
654 serde_json::from_slice(&response[body_start..]).unwrap()
655 }
656
657 fn raw_request(address: SocketAddr, request: &str) -> Value {
658 let mut stream = TcpStream::connect(address).unwrap();
659 stream.write_all(request.as_bytes()).unwrap();
660 let mut response = Vec::new();
661 stream.read_to_end(&mut response).unwrap();
662 let body_start = response
663 .windows(4)
664 .position(|window| window == b"\r\n\r\n")
665 .unwrap()
666 + 4;
667 serde_json::from_slice(&response[body_start..]).unwrap()
668 }
669
670 fn create_session(address: SocketAddr, token: &str) -> String {
671 request(
672 address,
673 "POST",
674 "/session",
675 json!({"capabilities": {"alwaysMatch": {"blitz:token": token}}}),
676 )["value"]["sessionId"]
677 .as_str()
678 .unwrap()
679 .to_string()
680 }
681
682 #[test]
683 fn status_auth_session_command_and_reconnect() {
684 let descriptor = descriptor_path();
685 let (server, commands) = DebugServer::start(ServerConfig {
686 bind_address: (std::net::Ipv4Addr::LOCALHOST, 0).into(),
687 descriptor_path: descriptor.clone(),
688 renderer_revision: "test-revision".into(),
689 })
690 .unwrap();
691
692 let status = request(server.address(), "GET", "/status", Value::Null);
693 assert_eq!(status["value"]["ready"], true);
694 assert!(descriptor.exists());
695 #[cfg(unix)]
696 {
697 use std::os::unix::fs::PermissionsExt;
698 assert_eq!(
699 fs::metadata(&descriptor).unwrap().permissions().mode() & 0o777,
700 0o600
701 );
702 }
703
704 let rejected = request(
705 server.address(),
706 "POST",
707 "/session",
708 json!({"capabilities": {"alwaysMatch": {"blitz:token": "wrong"}}}),
709 );
710 assert_eq!(rejected["value"]["error"], "invalid argument");
711
712 let session = create_session(server.address(), server.token());
713 let duplicate = request(
714 server.address(),
715 "POST",
716 "/session",
717 json!({"capabilities": {"alwaysMatch": {"blitz:token": server.token()}}}),
718 );
719 assert_eq!(duplicate["value"]["error"], "session not created");
720 assert_eq!(
721 request(
722 server.address(),
723 "GET",
724 "/session/not-the-session/blitz/getDomSnapshot",
725 Value::Null,
726 )["value"]["error"],
727 "invalid session id"
728 );
729 assert_eq!(
730 request(server.address(), "GET", "/not-a-route", Value::Null)["value"]["error"],
731 "unknown command"
732 );
733 let address = server.address();
734 let command_path = format!("/session/{session}/blitz/getDomSnapshot");
735 let client = thread::spawn(move || request(address, "GET", &command_path, Value::Null));
736 let command = commands.recv_timeout(COMMAND_TIMEOUT).unwrap();
737 assert_eq!(command.method, "GET");
738 assert_eq!(command.path, "blitz/getDomSnapshot");
739 command
740 .respond(ControlResponse::Success(json!({"documentRevision": 7})))
741 .unwrap();
742 let response = client.join().unwrap();
743 assert_eq!(response["value"]["documentRevision"], 7);
744
745 let deleted = request(
746 server.address(),
747 "DELETE",
748 &format!("/session/{session}"),
749 Value::Null,
750 );
751 assert!(deleted["value"].is_null());
752 let second_session = create_session(server.address(), server.token());
753 assert_ne!(second_session, session);
754
755 server.shutdown();
756 assert!(!descriptor.exists());
757 }
758
759 #[test]
760 fn rejects_non_loopback_bind_address() {
761 let result = DebugServer::start(ServerConfig {
762 bind_address: ([0, 0, 0, 0], 0).into(),
763 descriptor_path: descriptor_path(),
764 renderer_revision: "test-revision".into(),
765 });
766 assert!(matches!(result, Err(error) if error.kind() == io::ErrorKind::InvalidInput));
767 }
768
769 #[test]
770 fn overflowing_content_length_is_rejected_without_killing_the_server() {
771 let (server, _commands) = DebugServer::start(ServerConfig {
772 bind_address: (std::net::Ipv4Addr::LOCALHOST, 0).into(),
773 descriptor_path: descriptor_path(),
774 renderer_revision: "test-revision".into(),
775 })
776 .unwrap();
777
778 let rejected = raw_request(
779 server.address(),
780 &format!(
781 "POST /session HTTP/1.1\r\nHost: localhost\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
782 usize::MAX
783 ),
784 );
785 assert_eq!(rejected["value"]["error"], "invalid argument");
786 let malformed_length = raw_request(
787 server.address(),
788 "POST /session HTTP/1.1\r\nHost: localhost\r\nContent-Length: nope\r\nConnection: close\r\n\r\n",
789 );
790 assert_eq!(malformed_length["value"]["error"], "invalid argument");
791 let malformed_json = raw_request(
792 server.address(),
793 "POST /session HTTP/1.1\r\nHost: localhost\r\nContent-Length: 1\r\nConnection: close\r\n\r\n{",
794 );
795 assert_eq!(malformed_json["value"]["error"], "invalid argument");
796 assert_eq!(
797 request(server.address(), "GET", "/status", Value::Null)["value"]["ready"],
798 true
799 );
800 server.shutdown();
801 }
802
803 #[test]
804 fn a_slow_client_does_not_block_other_connections_and_has_an_absolute_deadline() {
805 let (server, _commands) = DebugServer::start(ServerConfig {
806 bind_address: (std::net::Ipv4Addr::LOCALHOST, 0).into(),
807 descriptor_path: descriptor_path(),
808 renderer_revision: "test-revision".into(),
809 })
810 .unwrap();
811 let mut slow = TcpStream::connect(server.address()).unwrap();
812 slow.write_all(b"G").unwrap();
813 let started = Instant::now();
814 assert_eq!(
815 request(server.address(), "GET", "/status", Value::Null)["value"]["ready"],
816 true
817 );
818 assert!(started.elapsed() < Duration::from_millis(500));
819
820 let listener = TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0)).unwrap();
821 let address = listener.local_addr().unwrap();
822 let reader = thread::spawn(move || {
823 let (mut stream, _) = listener.accept().unwrap();
824 read_request_within(&mut stream, Duration::from_millis(40)).unwrap_err()
825 });
826 let mut drip = TcpStream::connect(address).unwrap();
827 drip.write_all(b"G").unwrap();
828 let error = reader.join().unwrap();
829 assert!(matches!(
830 error.kind(),
831 io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock
832 ));
833 server.shutdown();
834 }
835
836 fn snapshot_request() -> HttpRequest {
837 HttpRequest {
838 method: "GET".into(),
839 path: "/session/0000000000000001/blitz/getDomSnapshot".into(),
840 body: Value::Null,
841 }
842 }
843
844 #[test]
852 fn wakes_the_servicer_with_the_request_already_queued() {
853 let (command_tx, command_rx) = mpsc::sync_channel::<ControlRequest>(4);
854 let waker = ServiceWaker::default();
855 let queue = Mutex::new(command_rx);
856 waker.set(move || {
857 let queued = queue.lock().unwrap().try_recv().unwrap();
858 assert_eq!(queued.path, "blitz/getDomSnapshot");
859 queued
860 .respond(ControlResponse::Success(json!({"serviced": true})))
861 .unwrap();
862 });
863
864 let active_session = ActiveSession(AtomicU64::new(1));
865 let response = route(
866 snapshot_request(),
867 "token",
868 &active_session,
869 &command_tx,
870 &waker,
871 );
872
873 assert_eq!(response["value"]["serviced"], true);
874 }
875
876 #[test]
882 fn a_second_request_queues_behind_an_unserviced_one() {
883 let (command_tx, command_rx) = mpsc::sync_channel::<ControlRequest>(4);
884 let (reply_tx, _reply_rx) = mpsc::sync_channel(1);
885 command_tx
886 .send(ControlRequest {
887 method: "GET".into(),
888 path: "occupied".into(),
889 body: Value::Null,
890 reply: reply_tx,
891 })
892 .unwrap();
893
894 let waker = ServiceWaker::default();
895 let queue = Mutex::new(command_rx);
896 waker.set(move || {
897 let queue = queue.lock().unwrap();
898 assert_eq!(queue.try_recv().unwrap().path, "occupied");
899 queue
900 .try_recv()
901 .unwrap()
902 .respond(ControlResponse::Success(json!({"serviced": true})))
903 .unwrap();
904 });
905
906 let active_session = ActiveSession(AtomicU64::new(1));
907 let response = route(
908 snapshot_request(),
909 "token",
910 &active_session,
911 &command_tx,
912 &waker,
913 );
914
915 assert_eq!(response["value"]["serviced"], true);
916 }
917
918 #[test]
919 fn a_saturated_renderer_queue_fails_without_growing() {
920 let (command_tx, _command_rx) = mpsc::sync_channel::<ControlRequest>(1);
921 let (reply_tx, _reply_rx) = mpsc::sync_channel(1);
922 command_tx
923 .send(ControlRequest {
924 method: "GET".into(),
925 path: "occupied".into(),
926 body: Value::Null,
927 reply: reply_tx,
928 })
929 .unwrap();
930
931 let response = route(
932 snapshot_request(),
933 "token",
934 &ActiveSession(AtomicU64::new(1)),
935 &command_tx,
936 &ServiceWaker::default(),
937 );
938
939 assert_eq!(response["value"]["error"], "unknown error");
940 assert_eq!(
941 response["value"]["message"],
942 "renderer command queue is full"
943 );
944 }
945
946 #[test]
949 fn queues_requests_while_no_waker_is_installed() {
950 let (command_tx, command_rx) = mpsc::sync_channel::<ControlRequest>(4);
951 let waker = ServiceWaker::default();
952 let active_session = ActiveSession(AtomicU64::new(1));
953
954 let sender = command_tx.clone();
955 let servicer = thread::spawn(move || {
956 let queued = command_rx.recv_timeout(COMMAND_TIMEOUT).unwrap();
957 drop(sender);
958 queued
959 .respond(ControlResponse::Success(json!({"serviced": true})))
960 .unwrap();
961 });
962
963 let response = route(
964 snapshot_request(),
965 "token",
966 &active_session,
967 &command_tx,
968 &waker,
969 );
970
971 servicer.join().unwrap();
972 assert_eq!(response["value"]["serviced"], true);
973 }
974}