1use std::ffi::OsString;
40use std::io::{self, ErrorKind, Read, Write};
41use std::net::{Shutdown, TcpListener, TcpStream};
42use std::path::PathBuf;
43use std::process::{Child, Command, Stdio};
44use std::sync::Arc;
45use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
46use std::thread;
47use std::time::{Duration, Instant};
48
49use intuituive::App;
50use tungstenite::WebSocket;
51use tungstenite::protocol::{Role, WebSocketConfig};
52
53use crate::{Renderer, session};
54
55pub const SESSION_ENV: &str = "RS_RICH_WEB_SESSION";
58
59#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
61#[non_exhaustive]
62pub enum Isolation {
63 #[default]
67 Processes,
68 Threads,
72}
73
74const CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
77const HELLO_TIMEOUT: Duration = Duration::from_secs(2);
80const MAX_WAITING: usize = 64;
83const EXIT_WAIT: Duration = Duration::from_secs(1);
86const TURN: Duration = Duration::from_millis(50);
89
90#[derive(Clone, Debug, Default)]
92pub(crate) struct Spawn {
93 pub command: Option<(PathBuf, Vec<OsString>)>,
95 pub memory: Option<u64>,
97 pub cpu: Option<Duration>,
99}
100
101static BINDS: AtomicUsize = AtomicUsize::new(0);
104
105pub(crate) fn next_bind() -> usize {
107 BINDS.fetch_add(1, Ordering::SeqCst)
108}
109
110#[derive(Debug, PartialEq, Eq)]
112pub(crate) struct Spec {
113 port: u16,
114 parent: u32,
116 secret: String,
118 reply: String,
121 bind: usize,
122 columns: u16,
123 rows: u16,
124 renderer: Renderer,
125 memory: Option<u64>,
126 cpu: Option<u64>,
127 compress: Option<u64>,
129}
130
131impl Spec {
132 fn encode(&self) -> String {
133 let limit = |n: Option<u64>| n.map_or("-".to_string(), |n| n.to_string());
134 format!(
135 "3 {} {} {} {} {} {} {} {} {} {} {}",
136 self.port,
137 self.parent,
138 self.secret,
139 self.reply,
140 self.bind,
141 self.columns,
142 self.rows,
143 self.renderer.name(),
144 limit(self.memory),
145 limit(self.cpu),
146 limit(self.compress)
147 )
148 }
149
150 fn decode(text: &str) -> Option<Spec> {
151 let mut parts = text.split(' ');
152 if parts.next()? != "3" {
153 return None;
154 }
155 let mut next = || parts.next();
156 let limit = |text: &str| match text {
157 "-" => Some(None),
158 n => n.parse().ok().map(Some),
159 };
160 let spec = Spec {
161 port: next()?.parse().ok()?,
162 parent: next()?.parse().ok()?,
163 secret: next()?.to_string(),
164 reply: next()?.to_string(),
165 bind: next()?.parse().ok()?,
166 columns: next()?.parse().ok()?,
167 rows: next()?.parse().ok()?,
168 renderer: Renderer::parse(next()?)?,
169 memory: limit(next()?)?,
170 cpu: limit(next()?)?,
171 compress: limit(next()?)?,
172 };
173 next().is_none().then_some(spec)
174 }
175}
176
177pub(crate) fn session_for(bind: usize) -> Option<Spec> {
180 let spec = own_spec()?;
181 (spec.bind == bind).then_some(spec)
182}
183
184pub(crate) fn is_session_process() -> bool {
186 own_spec().is_some()
187}
188
189fn own_spec() -> Option<Spec> {
200 let spec = Spec::decode(&std::env::var(SESSION_ENV).ok()?)?;
201 #[cfg(unix)]
202 {
203 use rustix::process::{Pid, getpgrp, getpid, getppid};
204 if Pid::as_raw(getppid()) as u32 != spec.parent {
205 if getpgrp() == getpid() {
206 eprintln!("rich-web: the session's server has gone");
207 std::process::exit(1);
208 }
209 return None;
210 }
211 }
212 Some(spec)
213}
214
215pub(crate) fn run_child(spec: Spec, app: impl FnOnce() -> App) -> i32 {
218 #[cfg(windows)]
223 #[allow(unsafe_code)]
227 unsafe {
228 std::env::remove_var(SESSION_ENV)
229 };
230 #[cfg(unix)]
235 watch_server(spec.parent);
236 limit(spec.memory, spec.cpu);
237 let stream = match TcpStream::connect(("127.0.0.1", spec.port)) {
238 Ok(stream) => stream,
239 Err(error) => {
240 eprintln!("rich-web: the session could not reach its server: {error}");
241 return 1;
242 }
243 };
244 let _ = stream.set_nodelay(true);
245 let mut hello = stream.try_clone().expect("a socket clones");
246 if hello
247 .write_all(format!("{}\n", spec.secret).as_bytes())
248 .is_err()
249 {
250 return 1;
251 }
252 let _ = stream.set_read_timeout(Some(CONNECT_TIMEOUT));
255 let mut answer = vec![0u8; spec.reply.len() + 1];
256 if hello.read_exact(&mut answer).is_err()
257 || !crate::http::same_token(
258 std::str::from_utf8(&answer[..spec.reply.len()]).unwrap_or(""),
259 &spec.reply,
260 )
261 || answer[spec.reply.len()] != b'\n'
262 {
263 eprintln!("rich-web: the session's server did not answer");
264 return 1;
265 }
266 let _ = stream.set_read_timeout(None);
267 let config = WebSocketConfig::default()
268 .max_message_size(Some(crate::MAX_MESSAGE))
269 .max_frame_size(Some(crate::MAX_MESSAGE));
270 let mut socket = WebSocket::from_raw_socket(stream, Role::Server, Some(config));
271 let stopping = AtomicBool::new(false);
274 let code = match session::run(
275 &mut socket,
276 app(),
277 spec.columns,
278 spec.rows,
279 &stopping,
280 spec.renderer,
281 session::Compress(spec.compress.map(|n| n as usize)),
282 ) {
283 Ok(()) => 0,
284 Err(_) => 1,
285 };
286 #[cfg(unix)]
291 thread::sleep(ENDING);
292 code
293}
294
295#[cfg(unix)]
298const ENDING: Duration = Duration::from_millis(250);
299
300#[cfg(unix)]
302const WATCH: Duration = Duration::from_millis(50);
303
304#[cfg(unix)]
308fn watch_server(server: u32) {
309 use rustix::process::{Pid, Signal, getpid, getppid, kill_process_group};
310 thread::spawn(move || {
311 loop {
312 let parent = Pid::as_raw(getppid()) as u32;
313 if parent != server {
314 let _ = kill_process_group(getpid(), Signal::KILL);
315 std::process::exit(1);
316 }
317 thread::sleep(WATCH);
318 }
319 });
320}
321
322#[cfg(unix)]
324fn limits(
325 memory: Option<u64>,
326 cpu: Option<u64>,
327) -> impl Iterator<Item = (rustix::process::Resource, rustix::process::Rlimit)> {
328 use rustix::process::{Resource, Rlimit};
329 let memory = memory.filter(|_| !cfg!(target_vendor = "apple"));
331 [(Resource::As, memory), (Resource::Cpu, cpu)]
332 .into_iter()
333 .filter_map(|(resource, value)| {
334 let value = value?;
335 let limit = Rlimit {
336 current: Some(value),
337 maximum: Some(value),
338 };
339 Some((resource, limit))
340 })
341}
342
343#[cfg(unix)]
347fn limit(memory: Option<u64>, cpu: Option<u64>) {
348 for (resource, limit) in limits(memory, cpu) {
349 if let Err(error) = rustix::process::setrlimit(resource, limit) {
350 eprintln!("rich-web: the session's limit could not be set: {error}");
351 }
352 }
353}
354
355#[cfg(target_os = "linux")]
358fn limit_child(pid: u32, memory: Option<u64>, cpu: Option<u64>) -> io::Result<()> {
359 let pid = i32::try_from(pid)
360 .ok()
361 .and_then(rustix::process::Pid::from_raw)
362 .ok_or_else(|| io::Error::other("no such process"))?;
363 for (resource, limit) in limits(memory, cpu) {
364 rustix::process::prlimit(Some(pid), resource, limit)?;
365 }
366 Ok(())
367}
368
369#[cfg(not(unix))]
370fn limit(_memory: Option<u64>, _cpu: Option<u64>) {}
371
372struct Running<'a>(&'a AtomicUsize);
375
376impl Drop for Running<'_> {
377 fn drop(&mut self) {
378 self.0.fetch_sub(1, Ordering::SeqCst);
379 }
380}
381
382pub(crate) struct Session<'a> {
384 pub spawn: &'a Spawn,
385 pub bind: usize,
386 pub columns: u16,
387 pub rows: u16,
388 pub renderer: Renderer,
389 pub compress: Option<usize>,
390 pub stopping: &'a AtomicBool,
391 pub running: &'a AtomicUsize,
392}
393
394pub(crate) fn run_parent(browser: TcpStream, session: Session<'_>) {
397 session.running.fetch_add(1, Ordering::SeqCst);
398 let _counted = Running(session.running);
399 if session.stopping.load(Ordering::SeqCst) {
400 return goodbye(browser, "The server stopped.");
401 }
402 let started = match start(&session) {
403 Ok(started) => started,
404 Err(error) => {
405 return goodbye(
406 browser,
407 &format!("The session's process could not start: {error}."),
408 );
409 }
410 };
411 let (mut child, tree, listener, secrets) = started;
412 let memory = session.spawn.memory;
413 let child_stream = match connected(&listener, &secrets, &mut child, memory, session.stopping) {
414 Ok(stream) => stream,
415 Err(why) => {
416 end(&mut child, tree);
417 return goodbye(browser, &why);
418 }
419 };
420 drop(listener);
421 let relayed = relay(&browser, &child_stream, session.stopping);
422 let _ = child_stream.shutdown(Shutdown::Both);
426 let status = end(&mut child, tree);
427 let reason = match relayed.ended {
430 _ if !relayed.can_close => None,
431 _ if session.stopping.load(Ordering::SeqCst) => Some("The server stopped.".to_string()),
432 Ended::Page => None,
433 Ended::Child => Some(match status {
434 Some(status) if status.success() => "The session ended.".to_string(),
435 Some(status) => format!("The session's process ended unexpectedly ({status})."),
436 None => "The session's process ended unexpectedly.".to_string(),
437 }),
438 };
439 match reason {
440 Some(reason) => goodbye(browser, &reason),
441 None => {
442 let _ = browser.shutdown(Shutdown::Both);
443 }
444 }
445}
446
447fn start(session: &Session<'_>) -> io::Result<(Child, Tree, TcpListener, Secrets)> {
450 let listener = TcpListener::bind(("127.0.0.1", 0))?;
451 let secrets = Secrets {
452 secret: crate::random_token()?,
453 reply: crate::random_token()?,
454 };
455 let spec = Spec {
456 port: listener.local_addr()?.port(),
457 parent: std::process::id(),
458 secret: secrets.secret.clone(),
459 reply: secrets.reply.clone(),
460 bind: session.bind,
461 columns: session.columns,
462 rows: session.rows,
463 renderer: session.renderer,
464 memory: session.spawn.memory,
465 cpu: session.spawn.cpu.map(|cpu| cpu.as_secs().max(1)),
466 compress: session.compress.map(|n| n as u64),
467 };
468 let (program, args) = match &session.spawn.command {
469 Some((program, args)) => (program.clone(), args.clone()),
470 None => (
471 std::env::current_exe()?,
472 std::env::args_os().skip(1).collect(),
473 ),
474 };
475 let mut command = Command::new(program);
476 command
477 .args(args)
478 .env(SESSION_ENV, spec.encode())
479 .stdin(Stdio::null());
480 #[cfg(unix)]
482 std::os::unix::process::CommandExt::process_group(&mut command, 0);
483 let mut child = command.spawn()?;
484 #[cfg(target_os = "linux")]
485 if let Err(error) = limit_child(child.id(), spec.memory, spec.cpu)
486 .and_then(|()| within_memory(child.id(), spec.memory))
487 {
488 let _ = child.kill();
489 let _ = child.wait();
490 return Err(io::Error::other(format!(
491 "its limits could not be set: {error}"
492 )));
493 }
494 let tree = match Tree::new(&child) {
497 Ok(tree) => tree,
498 Err(error) => {
499 let _ = child.kill();
500 let _ = child.wait();
501 return Err(io::Error::other(format!(
502 "it could not be put in a job object: {error}"
503 )));
504 }
505 };
506 listener.set_nonblocking(true)?;
507 Ok((child, tree, listener, secrets))
508}
509
510struct Tree {
515 #[cfg(unix)]
516 leader: u32,
517 #[cfg(windows)]
518 _job: win32job::Job,
519}
520
521impl Tree {
522 #[cfg(unix)]
524 fn new(child: &Child) -> io::Result<Tree> {
525 Ok(Tree { leader: child.id() })
526 }
527
528 #[cfg(windows)]
532 fn new(child: &Child) -> io::Result<Tree> {
533 use std::os::windows::io::AsRawHandle;
534 let mut limits = win32job::ExtendedLimitInfo::new();
535 limits.limit_kill_on_job_close();
536 let job = win32job::Job::create_with_limit_info(&limits)?;
537 job.assign_process(child.as_raw_handle() as isize)?;
538 Ok(Tree { _job: job })
539 }
540
541 #[cfg(not(any(unix, windows)))]
542 fn new(_child: &Child) -> io::Result<Tree> {
543 Ok(Tree {})
544 }
545
546 fn kill(self) {
548 #[cfg(unix)]
549 kill_group(self.leader);
550 }
552}
553
554struct Secrets {
556 secret: String,
557 reply: String,
558}
559
560#[cfg(target_os = "linux")]
564fn within_memory(pid: u32, memory: Option<u64>) -> io::Result<()> {
565 let Some(memory) = memory else {
566 return Ok(());
567 };
568 let status = std::fs::read_to_string(format!("/proc/{pid}/status"))?;
569 let peak = status
570 .lines()
571 .find_map(|line| line.strip_prefix("VmPeak:"))
572 .and_then(|kb| kb.trim().trim_end_matches("kB").trim().parse::<u64>().ok())
573 .unwrap_or(0)
574 * 1024;
575 if peak > memory {
576 return Err(io::Error::other(format!(
577 "it took {peak} bytes before they were set"
578 )));
579 }
580 Ok(())
581}
582
583fn connected(
593 listener: &TcpListener,
594 secrets: &Secrets,
595 child: &mut Child,
596 memory: Option<u64>,
597 stopping: &AtomicBool,
598) -> Result<TcpStream, String> {
599 let until = Instant::now() + CONNECT_TIMEOUT;
600 let mut waiting: Vec<(TcpStream, Vec<u8>, Instant)> = Vec::new();
601 loop {
602 if stopping.load(Ordering::SeqCst) {
603 return Err("The server stopped.".into());
604 }
605 if let Ok(Some(status)) = child.try_wait() {
606 return Err(format!(
607 "The session's process ended before it started the app ({status}): \
608 it must reach the same serve() call as the server."
609 ));
610 }
611 let now = Instant::now();
612 if now > until {
613 return Err("The session's process did not start the app in time.".into());
614 }
615 while let Ok((stream, _)) = listener.accept() {
616 if waiting.len() < MAX_WAITING && stream.set_nonblocking(true).is_ok() {
617 waiting.push((stream, Vec::new(), now + HELLO_TIMEOUT));
618 }
619 }
620 let mut found = None;
621 waiting.retain_mut(|(stream, line, by)| {
622 if found.is_some() {
623 return true;
624 }
625 match hello(stream, line, &secrets.secret) {
626 Some(true) => {
627 found = stream.try_clone().ok();
628 false
629 }
630 Some(false) => false,
631 None => now < *by,
632 }
633 });
634 if let Some(mut stream) = found {
635 #[cfg(not(target_os = "linux"))]
636 let _ = memory;
637 #[cfg(target_os = "linux")]
638 if let Err(error) = within_memory(child.id(), memory) {
639 return Err(format!(
640 "The session's process could not start: its limits could not be set: {error}."
641 ));
642 }
643 let answered = stream
644 .set_nonblocking(false)
645 .and_then(|()| stream.write_all(format!("{}\n", secrets.reply).as_bytes()));
646 if answered.is_ok() {
647 let _ = stream.set_nodelay(true);
648 return Ok(stream);
649 }
650 continue;
651 }
652 thread::sleep(Duration::from_millis(10));
653 }
654}
655
656fn hello(stream: &TcpStream, line: &mut Vec<u8>, secret: &str) -> Option<bool> {
661 let mut byte = [0u8];
662 loop {
663 match (&*stream).read(&mut byte) {
664 Ok(1) if byte[0] == b'\n' => {
665 return Some(
666 std::str::from_utf8(line)
667 .is_ok_and(|line| crate::http::same_token(line, secret)),
668 );
669 }
670 Ok(1) if line.len() < secret.len() => line.push(byte[0]),
671 Err(e) if e.kind() == ErrorKind::WouldBlock => return None,
672 Err(e) if e.kind() == ErrorKind::Interrupted => {}
673 _ => return Some(false),
674 }
675 }
676}
677
678#[derive(Debug, PartialEq, Eq)]
680enum Ended {
681 Page,
683 Child,
685}
686
687struct Relayed {
689 ended: Ended,
690 can_close: bool,
693}
694
695fn relay(browser: &TcpStream, child: &TcpStream, stopping: &AtomicBool) -> Relayed {
697 let failed = Relayed {
698 ended: Ended::Page,
699 can_close: true,
700 };
701 let done = Arc::new(AtomicBool::new(false));
702 let (Ok(page_in), Ok(child_out)) = (browser.try_clone(), child.try_clone()) else {
703 return failed;
704 };
705 let _ = page_in.set_read_timeout(Some(TURN));
706 let _ = child.set_read_timeout(Some(TURN));
707 let _ = child.set_write_timeout(Some(TURN));
711 let upstream = {
713 let done = done.clone();
714 thread::Builder::new()
715 .name("rich-web relay".into())
716 .spawn(move || {
717 pump(page_in, child_out, &done, &mut |_| {}, true);
718 done.store(true, Ordering::SeqCst);
719 })
720 };
721 let mut frames = Frames::default();
724 let (Ok(child_in), Ok(page_out)) = (child.try_clone(), browser.try_clone()) else {
725 done.store(true, Ordering::SeqCst);
726 return failed;
727 };
728 let child_ended = {
729 let stop = StopOr(&done, stopping);
730 pump_until(
731 child_in,
732 page_out,
733 &stop,
734 &mut |bytes| frames.feed(bytes),
735 false,
736 )
737 };
738 done.store(true, Ordering::SeqCst);
739 if let Ok(upstream) = upstream {
740 let _ = upstream.join();
741 }
742 Relayed {
743 ended: if child_ended {
744 Ended::Child
745 } else {
746 Ended::Page
747 },
748 can_close: frames.at_boundary() && !frames.closed,
749 }
750}
751
752struct StopOr<'a>(&'a AtomicBool, &'a AtomicBool);
754
755impl StopOr<'_> {
756 fn is_set(&self) -> bool {
757 self.0.load(Ordering::SeqCst) || self.1.load(Ordering::SeqCst)
758 }
759}
760
761fn pump(
763 from: TcpStream,
764 to: TcpStream,
765 done: &AtomicBool,
766 seen: &mut dyn FnMut(&[u8]),
767 patient: bool,
768) {
769 let never = AtomicBool::new(false);
770 pump_until(from, to, &StopOr(done, &never), seen, patient);
771}
772
773fn pump_until(
777 mut from: TcpStream,
778 mut to: TcpStream,
779 stop: &StopOr<'_>,
780 seen: &mut dyn FnMut(&[u8]),
781 patient: bool,
782) -> bool {
783 let mut buffer = vec![0u8; 64 * 1024];
784 loop {
785 if stop.is_set() {
786 return false;
787 }
788 match from.read(&mut buffer) {
789 Ok(0) => return true,
790 Ok(n) => {
791 seen(&buffer[..n]);
792 if !write_until(&mut to, &buffer[..n], stop, patient) {
793 return false;
794 }
795 }
796 Err(e) if matches!(e.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) => {}
797 Err(e) if e.kind() == ErrorKind::Interrupted => {}
798 Err(_) => return true,
799 }
800 }
801}
802
803fn write_until(to: &mut TcpStream, mut bytes: &[u8], stop: &StopOr<'_>, patient: bool) -> bool {
806 while !bytes.is_empty() {
807 match to.write(bytes) {
808 Ok(0) => return false,
809 Ok(n) => bytes = &bytes[n..],
810 Err(e) if e.kind() == ErrorKind::Interrupted => {}
811 Err(e)
812 if patient && matches!(e.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) =>
813 {
814 if stop.is_set() {
815 return false;
816 }
817 }
818 Err(_) => return false,
819 }
820 }
821 true
822}
823
824#[derive(Default)]
827struct Frames {
828 header: Vec<u8>,
830 left: u64,
832 closed: bool,
833}
834
835impl Frames {
836 fn at_boundary(&self) -> bool {
837 self.header.is_empty() && self.left == 0
838 }
839
840 fn feed(&mut self, mut bytes: &[u8]) {
841 while !bytes.is_empty() {
842 if self.left > 0 {
843 let n = (self.left.min(bytes.len() as u64)) as usize;
844 self.left -= n as u64;
845 bytes = &bytes[n..];
846 continue;
847 }
848 self.header.push(bytes[0]);
849 bytes = &bytes[1..];
850 if let Some((length, opcode)) = header_done(&self.header) {
851 if opcode == 0x8 {
852 self.closed = true;
853 }
854 self.header.clear();
855 self.left = length;
856 }
857 }
858 }
859}
860
861fn header_done(header: &[u8]) -> Option<(u64, u8)> {
864 if header.len() < 2 {
865 return None;
866 }
867 let opcode = header[0] & 0x0f;
868 let masked = header[1] & 0x80 != 0;
869 let short = u64::from(header[1] & 0x7f);
870 let (extended, length) = match short {
871 126 => (2, None),
872 127 => (8, None),
873 n => (0, Some(n)),
874 };
875 let mask = if masked { 4 } else { 0 };
876 if header.len() < 2 + extended + mask {
877 return None;
878 }
879 let length = length.unwrap_or_else(|| {
880 header[2..2 + extended]
881 .iter()
882 .fold(0u64, |n, b| (n << 8) | u64::from(*b))
883 });
884 Some((length, opcode))
885}
886
887fn end(child: &mut Child, tree: Tree) -> Option<std::process::ExitStatus> {
891 let until = Instant::now() + EXIT_WAIT;
892 while Instant::now() < until {
893 match child.try_wait() {
894 Ok(Some(status)) => {
895 tree.kill();
896 return Some(status);
897 }
898 Ok(None) => thread::sleep(Duration::from_millis(10)),
899 Err(_) => break,
900 }
901 }
902 tree.kill();
903 let _ = child.kill();
904 let _ = child.wait();
905 None
906}
907
908#[cfg(unix)]
910fn kill_group(leader: u32) {
911 use rustix::process::{Pid, Signal, kill_process_group};
912 if let Some(pid) = i32::try_from(leader).ok().and_then(Pid::from_raw) {
913 let _ = kill_process_group(pid, Signal::KILL);
914 }
915}
916
917fn goodbye(mut browser: TcpStream, reason: &str) {
920 let mut end = reason.len().min(123);
921 while !reason.is_char_boundary(end) {
922 end -= 1;
923 }
924 let mut payload = 1000u16.to_be_bytes().to_vec();
925 payload.extend_from_slice(&reason.as_bytes()[..end]);
926 let mut frame = vec![0x88, payload.len() as u8];
927 frame.extend_from_slice(&payload);
928 let _ = browser.write_all(&frame);
929 let _ = browser.flush();
930 let _ = browser.set_read_timeout(Some(Duration::from_millis(200)));
933 let _ = browser.read(&mut [0u8; 256]);
934 let _ = browser.shutdown(Shutdown::Both);
935}
936
937#[cfg(test)]
938mod tests {
939 use super::*;
940
941 #[test]
942 fn specs_round_trip() {
943 let spec = Spec {
944 port: 4321,
945 parent: 99,
946 secret: "abc".into(),
947 reply: "def".into(),
948 bind: 2,
949 columns: 80,
950 rows: 24,
951 renderer: Renderer::Dom,
952 memory: Some(1 << 28),
953 cpu: None,
954 compress: Some(1024),
955 };
956 assert_eq!(Spec::decode(&spec.encode()), Some(spec));
957 assert_eq!(Spec::decode("1 1 a 0 80 24 xterm - -"), None);
959 assert_eq!(Spec::decode("3 1 9 a b 0 80 24 xterm - -"), None);
960 assert_eq!(Spec::decode("2 1 9 a b 0 80 24 xterm - -"), None);
961 assert_eq!(Spec::decode("3 1 9 a b 0 80 24 canvas - - -"), None);
962 }
963
964 #[test]
965 fn frames_are_followed_across_reads() {
966 let mut frames = Frames::default();
967 frames.feed(&[0x81, 0x03, b'a']);
969 assert!(!frames.at_boundary());
970 frames.feed(b"bc");
971 assert!(frames.at_boundary());
972 let mut big = vec![0x82, 126, 0x01, 0x2c];
974 big.extend(vec![0u8; 300]);
975 frames.feed(&big[..3]);
976 assert!(!frames.at_boundary());
977 frames.feed(&big[3..]);
978 assert!(frames.at_boundary());
979 assert!(!frames.closed);
980 frames.feed(&[0x88, 0x02, 0x03, 0xe8]);
981 assert!(frames.closed && frames.at_boundary());
982 }
983
984 #[test]
985 fn a_write_to_a_process_that_stopped_reading_gives_up_when_told() {
986 let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap();
987 let mut writer = TcpStream::connect(listener.local_addr().unwrap()).unwrap();
988 let (_reader, _) = listener.accept().unwrap();
990 writer
991 .set_write_timeout(Some(Duration::from_millis(20)))
992 .unwrap();
993 let stopping = Arc::new(AtomicBool::new(false));
994 let never = AtomicBool::new(false);
995 let stopper = {
996 let stopping = stopping.clone();
997 thread::spawn(move || {
998 thread::sleep(Duration::from_millis(300));
999 stopping.store(true, Ordering::SeqCst);
1000 })
1001 };
1002 let started = Instant::now();
1003 let big = vec![0u8; 64 << 20];
1004 let stop = StopOr(&never, &stopping);
1005 assert!(!write_until(&mut writer, &big, &stop, true));
1006 assert!(started.elapsed() < Duration::from_secs(5));
1007 stopper.join().unwrap();
1008 }
1009
1010 #[test]
1011 fn a_masked_header_counts_its_key() {
1012 assert_eq!(header_done(&[0x81, 0x85]), None);
1013 assert_eq!(header_done(&[0x81, 0x85, 1, 2, 3]), None);
1014 assert_eq!(header_done(&[0x81, 0x85, 1, 2, 3, 4]), Some((5, 1)));
1015 }
1016}