1pub use rich_embed::{Command, ExitStatus, LocalPty, PtyHost};
7
8use std::io::{self, Write};
9use std::net::{
10 IpAddr, Ipv4Addr, Ipv6Addr, Shutdown, SocketAddr, TcpListener, TcpStream, ToSocketAddrs,
11};
12use std::sync::Arc;
13use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
14use std::thread::{self, JoinHandle};
15use std::time::{Duration, Instant};
16
17use intuituive::App;
18use tungstenite::WebSocket;
19use tungstenite::protocol::{Role, WebSocketConfig};
20
21use crate::http::{self, Head};
22use crate::{isolate, pty, session};
23
24pub use crate::isolate::{Isolation, SESSION_ENV};
25
26pub const XTERM_VERSION: &str = "6.0.0";
28
29pub const DEFAULT_MAX_SESSIONS: usize = 8;
31
32pub const DEFAULT_MAX_BUFFERED: usize = 1 << 20;
35
36const MAX_PENDING: usize = 64;
39const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
41const WRITE_TIMEOUT: Duration = Duration::from_secs(30);
43pub(crate) const MAX_MESSAGE: usize = 1 << 20;
45
46const PAGE: &str = include_str!("../assets/index.html");
47const PAGE_JS: &str = include_str!("../assets/app.js");
48const DOM_PAGE: &str = include_str!("../assets/dom.html");
49const DOM_JS: &str = include_str!("../assets/dom.js");
50const XTERM_JS: &str = include_str!("../assets/xterm/xterm.js");
51const XTERM_CSS: &str = include_str!("../assets/xterm/xterm.css");
52const FIT_JS: &str = include_str!("../assets/xterm/addon-fit.js");
53
54pub fn serve<A, F>(addr: A, app: F) -> io::Result<()>
62where
63 A: ToSocketAddrs,
64 F: Fn() -> App + Send + Sync + 'static,
65{
66 Server::bind(addr, app)?.run()
67}
68
69pub fn serve_command<A>(addr: A, command: impl Into<Command>) -> io::Result<()>
77where
78 A: ToSocketAddrs,
79{
80 Server::bind_command(addr, command)?.run()
81}
82
83#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
85#[non_exhaustive]
86pub enum Renderer {
87 #[default]
89 Xterm,
90 Dom,
94}
95
96impl Renderer {
97 pub fn name(self) -> &'static str {
99 match self {
100 Renderer::Xterm => "xterm",
101 Renderer::Dom => "dom",
102 }
103 }
104
105 pub fn parse(name: &str) -> Option<Renderer> {
107 match name {
108 "xterm" => Some(Renderer::Xterm),
109 "dom" => Some(Renderer::Dom),
110 _ => None,
111 }
112 }
113}
114
115enum Mode {
117 App(Box<dyn Fn() -> App + Send + Sync>),
118 Command(Command),
120 Host(Box<dyn Fn() -> Box<dyn PtyHost> + Send + Sync>),
122}
123
124pub struct Server {
127 listener: TcpListener,
128 shared: Shared,
129}
130
131struct Shared {
133 mode: Mode,
134 token: String,
135 max_sessions: usize,
136 max_buffered: usize,
137 renderer: Renderer,
138 compress: Option<usize>,
140 origins: Vec<String>,
141 title: String,
142 hosts: Vec<String>,
145 sessions: AtomicUsize,
147 pending: AtomicUsize,
149 programs: AtomicUsize,
151 stopping: AtomicBool,
152 isolation: Isolation,
154 spawn: isolate::Spawn,
155 bind: usize,
158}
159
160impl Server {
161 pub fn bind<A, F>(addr: A, app: F) -> io::Result<Server>
169 where
170 A: ToSocketAddrs,
171 F: Fn() -> App + Send + Sync + 'static,
172 {
173 let bind = isolate::next_bind();
174 if let Some(spec) = isolate::session_for(bind) {
175 std::process::exit(isolate::run_child(spec, app));
176 }
177 let mode = Mode::App(Box::new(app));
178 let mut server = if isolate::is_session_process() {
179 Server::with_mode(("127.0.0.1", 0), mode, "intuiTUIve".into())?
182 } else {
183 Server::with_mode(addr, mode, "intuiTUIve".into())?
184 };
185 server.shared.bind = bind;
186 Ok(server)
187 }
188
189 pub fn bind_command<A>(addr: A, command: impl Into<Command>) -> io::Result<Server>
193 where
194 A: ToSocketAddrs,
195 {
196 let command = command.into();
197 let title = command.program().to_string_lossy().into_owned();
198 Server::with_mode(addr, Mode::Command(command), title)
199 }
200
201 pub fn bind_host<A, F>(addr: A, host: F) -> io::Result<Server>
208 where
209 A: ToSocketAddrs,
210 F: Fn() -> Box<dyn PtyHost> + Send + Sync + 'static,
211 {
212 Server::with_mode(addr, Mode::Host(Box::new(host)), "terminal".into())
213 }
214
215 fn with_mode<A: ToSocketAddrs>(addr: A, mode: Mode, title: String) -> io::Result<Server> {
216 let listener = TcpListener::bind(addr)?;
217 let hosts = http::allowed_hosts(listener.local_addr()?);
218 Ok(Server {
219 listener,
220 shared: Shared {
221 mode,
222 token: random_token()?,
223 max_sessions: DEFAULT_MAX_SESSIONS,
224 max_buffered: DEFAULT_MAX_BUFFERED,
225 renderer: Renderer::Xterm,
226 compress: None,
227 origins: Vec::new(),
228 title,
229 hosts,
230 sessions: AtomicUsize::new(0),
231 pending: AtomicUsize::new(0),
232 programs: AtomicUsize::new(0),
233 stopping: AtomicBool::new(false),
234 isolation: Isolation::default(),
235 spawn: isolate::Spawn::default(),
236 bind: 0,
237 },
238 })
239 }
240
241 #[must_use]
246 pub fn isolation(mut self, isolation: Isolation) -> Server {
247 self.shared.isolation = isolation;
248 self
249 }
250
251 #[must_use]
256 pub fn session_command<P, I, S>(mut self, program: P, args: I) -> Server
257 where
258 P: Into<std::path::PathBuf>,
259 I: IntoIterator<Item = S>,
260 S: Into<std::ffi::OsString>,
261 {
262 let args = args.into_iter().map(Into::into).collect();
263 self.shared.spawn.command = Some((program.into(), args));
264 self
265 }
266
267 #[must_use]
273 pub fn session_memory(mut self, bytes: u64) -> Server {
274 self.shared.spawn.memory = Some(bytes);
275 self
276 }
277
278 #[must_use]
284 pub fn session_cpu(mut self, time: Duration) -> Server {
285 self.shared.spawn.cpu = Some(time);
286 self
287 }
288
289 #[must_use]
292 pub fn max_sessions(mut self, n: usize) -> Server {
293 self.shared.max_sessions = n;
294 self
295 }
296
297 #[must_use]
307 pub fn max_buffered(mut self, bytes: usize) -> Server {
308 self.shared.max_buffered = bytes.max(2);
309 self
310 }
311
312 #[must_use]
320 pub fn compress(mut self, from: Option<usize>) -> Server {
321 self.shared.compress = from;
322 self
323 }
324
325 #[must_use]
330 pub fn renderer(mut self, renderer: Renderer) -> Server {
331 self.shared.renderer = renderer;
332 self
333 }
334
335 #[must_use]
343 pub fn token(mut self, token: impl Into<String>) -> Server {
344 let token = token.into();
345 assert!(
346 !token.is_empty()
347 && token
348 .bytes()
349 .all(|b| b.is_ascii_alphanumeric() || b"-_.~".contains(&b)),
350 "a token is ASCII letters, digits, '-', '_', '.' and '~'"
351 );
352 self.shared.token = token;
353 self
354 }
355
356 #[must_use]
366 pub fn allow_host(mut self, host: impl AsRef<str>) -> Server {
367 let port = self.local_addr().port();
368 self.shared
369 .hosts
370 .extend(http::host_entries(host.as_ref(), port));
371 self
372 }
373
374 #[must_use]
378 pub fn allow_origin(mut self, origin: impl Into<String>) -> Server {
379 self.shared.origins.push(origin.into());
380 self
381 }
382
383 #[must_use]
386 pub fn title(mut self, title: impl Into<String>) -> Server {
387 self.shared.title = title.into();
388 self
389 }
390
391 pub fn local_addr(&self) -> SocketAddr {
393 self.listener
394 .local_addr()
395 .expect("a bound listener has an address")
396 }
397
398 pub fn access_token(&self) -> &str {
400 &self.shared.token
401 }
402
403 pub fn url(&self) -> String {
405 url_for(self.local_addr(), &self.shared.token)
406 }
407
408 pub fn run(self) -> io::Result<()> {
411 self.announce()?;
412 accept(self.listener, Arc::new(self.shared));
413 Ok(())
414 }
415
416 pub fn run_until(self, until: impl FnOnce()) -> io::Result<()> {
422 self.announce()?;
423 let handle = self.spawn()?;
424 until();
425 handle.stop();
426 Ok(())
427 }
428
429 fn announce(&self) -> io::Result<()> {
431 let local = self.local_addr();
432 let mut out = io::stdout().lock();
433 writeln!(out, "Serving {} at {}", self.shared.title, self.url())?;
434 out.flush()?;
435 drop(out);
436 if !local.ip().is_loopback() {
437 let what = match self.shared.mode {
438 Mode::App(_) => "the app",
439 Mode::Command(_) | Mode::Host(_) => "the program",
440 };
441 eprintln!(
442 "warning: listening on {local}, beyond this computer. Anyone who can reach it \
443 with the token can use {what}: there is no other authentication and no TLS. \
444 Put it behind a reverse proxy that has both."
445 );
446 }
447 Ok(())
448 }
449
450 pub fn spawn(self) -> io::Result<Handle> {
453 let local = self.local_addr();
454 let url = self.url();
455 let shared = Arc::new(self.shared);
456 let listener = self.listener;
457 let thread = {
458 let shared = shared.clone();
459 thread::Builder::new()
460 .name("rich-web accept".into())
461 .spawn(move || accept(listener, shared))?
462 };
463 Ok(Handle {
464 local,
465 url,
466 shared,
467 thread: Some(thread),
468 })
469 }
470}
471
472pub struct Handle {
475 local: SocketAddr,
476 url: String,
477 shared: Arc<Shared>,
478 thread: Option<JoinHandle<()>>,
479}
480
481impl Handle {
482 pub fn local_addr(&self) -> SocketAddr {
484 self.local
485 }
486
487 pub fn url(&self) -> &str {
489 &self.url
490 }
491
492 pub fn sessions(&self) -> usize {
494 self.shared.sessions.load(Ordering::SeqCst)
495 }
496
497 pub fn stop(mut self) {
504 self.shutdown();
505 }
506
507 fn shutdown(&mut self) {
508 self.shared.stopping.store(true, Ordering::SeqCst);
509 if let Some(accepting) = self.thread.take() {
510 let _ = TcpStream::connect_timeout(&reachable(self.local), Duration::from_secs(1));
512 let _ = accepting.join();
513 while self.shared.programs.load(Ordering::SeqCst) > 0 {
515 thread::sleep(Duration::from_millis(10));
516 }
517 }
518 }
519}
520
521impl Drop for Handle {
522 fn drop(&mut self) {
523 self.shutdown();
524 }
525}
526
527fn reachable(local: SocketAddr) -> SocketAddr {
530 let ip = match local.ip() {
531 IpAddr::V4(v4) if v4.is_unspecified() => IpAddr::V4(Ipv4Addr::LOCALHOST),
532 IpAddr::V6(v6) if v6.is_unspecified() => IpAddr::V6(Ipv6Addr::LOCALHOST),
533 ip => ip,
534 };
535 SocketAddr::new(ip, local.port())
536}
537
538fn url_for(local: SocketAddr, token: &str) -> String {
539 format!("http://{}/?token={token}", reachable(local))
540}
541
542pub(crate) fn random_token() -> io::Result<String> {
544 let mut bytes = [0u8; 16];
545 getrandom::fill(&mut bytes).map_err(|e| io::Error::other(format!("no random token: {e}")))?;
546 Ok(bytes.iter().map(|b| format!("{b:02x}")).collect())
547}
548
549fn accept(listener: TcpListener, shared: Arc<Shared>) {
550 for stream in listener.incoming() {
551 if shared.stopping.load(Ordering::SeqCst) {
552 break;
553 }
554 let Ok(stream) = stream else {
555 thread::sleep(Duration::from_millis(10));
557 continue;
558 };
559 if shared.pending.fetch_add(1, Ordering::SeqCst) >= MAX_PENDING {
560 shared.pending.fetch_sub(1, Ordering::SeqCst);
561 busy(stream);
562 continue;
563 }
564 let pending = Pending(shared.clone());
565 let spawned = thread::Builder::new()
566 .name("rich-web connection".into())
567 .spawn(move || connection(stream, pending));
568 if spawned.is_err() {
569 continue;
571 }
572 }
573}
574
575fn busy(mut stream: TcpStream) {
578 if stream.set_nonblocking(true).is_err() {
579 return;
580 }
581 let _ = io::Read::read(&mut stream, &mut [0u8; 4096]);
584 let _ = http::refuse(
585 &mut stream,
586 "503 Service Unavailable",
587 "Too many connections at once: try again in a moment.",
588 );
589 let _ = stream.shutdown(Shutdown::Write);
590}
591
592struct Pending(Arc<Shared>);
594
595impl Drop for Pending {
596 fn drop(&mut self) {
597 self.0.pending.fetch_sub(1, Ordering::SeqCst);
598 }
599}
600
601struct Slot(Arc<Shared>);
603
604impl Slot {
605 fn acquire(shared: &Arc<Shared>) -> Option<Slot> {
606 let mut n = shared.sessions.load(Ordering::SeqCst);
607 loop {
608 if n >= shared.max_sessions {
609 return None;
610 }
611 match shared
612 .sessions
613 .compare_exchange(n, n + 1, Ordering::SeqCst, Ordering::SeqCst)
614 {
615 Ok(_) => return Some(Slot(shared.clone())),
616 Err(now) => n = now,
617 }
618 }
619 }
620}
621
622impl Drop for Slot {
623 fn drop(&mut self) {
624 self.0.sessions.fetch_sub(1, Ordering::SeqCst);
625 }
626}
627
628fn connection(mut stream: TcpStream, pending: Pending) {
630 let _ = stream.set_nodelay(true);
631 if stream.set_write_timeout(Some(WRITE_TIMEOUT)).is_err() {
632 return;
633 }
634 let until = Instant::now() + HANDSHAKE_TIMEOUT;
636 let Ok(bytes) = http::read_head(&mut http::Deadline::new(&stream, until)) else {
637 return;
638 };
639 if pending.0.stopping.load(Ordering::SeqCst) {
641 return;
642 }
643 let Some(head) = Head::parse(&bytes) else {
644 let _ = http::refuse(&mut stream, "400 Bad Request", "Bad request");
645 return;
646 };
647 let shared = pending.0.clone();
648 if head.method != "GET" {
649 let _ = http::refuse(&mut stream, "405 Method Not Allowed", "Only GET");
650 return;
651 }
652 let js = "text/javascript; charset=utf-8";
653 let asset = match head.path.as_str() {
654 "/ws" => {
655 websocket(stream, &head, &shared, pending);
656 return;
657 }
658 "/" | "/index.html" => {
659 page(&mut stream, &head, &shared);
660 return;
661 }
662 "/app.js" => (js, PAGE_JS),
663 "/dom.js" => (js, DOM_JS),
664 "/xterm.js" => (js, XTERM_JS),
665 "/addon-fit.js" => (js, FIT_JS),
666 "/xterm.css" => ("text/css; charset=utf-8", XTERM_CSS),
667 _ => {
668 let _ = http::refuse(&mut stream, "404 Not Found", "Not found");
669 return;
670 }
671 };
672 let _ = http::respond(&mut stream, "200 OK", asset.0, &[], asset.1.as_bytes());
673}
674
675fn renderer_for(head: &Head, shared: &Shared) -> Renderer {
678 match shared.mode {
679 Mode::App(_) => head
680 .param("renderer")
681 .and_then(Renderer::parse)
682 .unwrap_or(shared.renderer),
683 Mode::Command(_) | Mode::Host(_) => Renderer::Xterm,
684 }
685}
686
687fn page(stream: &mut TcpStream, head: &Head, shared: &Shared) {
689 if !head
690 .param("token")
691 .is_some_and(|t| http::same_token(t, &shared.token))
692 {
693 let _ = http::refuse(
694 stream,
695 "403 Forbidden",
696 "Open the address the server printed when it started, with its token.",
697 );
698 return;
699 }
700 let mut connect = "'self'".to_string();
703 if let Some(host) = head.header("host").filter(|h| {
704 h.bytes()
705 .all(|b| b.is_ascii_alphanumeric() || b".-:[]".contains(&b))
706 }) {
707 connect.push_str(&format!(" ws://{host} wss://{host}"));
708 }
709 let policy = format!(
710 "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; \
711 img-src 'self' data:; font-src 'self' data:; connect-src {connect}; \
712 frame-ancestors 'none'; base-uri 'none'; form-action 'none'"
713 );
714 let template = match renderer_for(head, shared) {
715 Renderer::Xterm => PAGE,
716 Renderer::Dom => DOM_PAGE,
717 };
718 let mode = match shared.mode {
719 Mode::App(_) => "app",
720 Mode::Command(_) | Mode::Host(_) => "program",
721 };
722 let body = template
723 .replace("{{title}}", &http::escape_html(&shared.title))
724 .replace("{{mode}}", mode);
725 let _ = http::respond(
726 stream,
727 "200 OK",
728 "text/html; charset=utf-8",
729 &[
730 ("Content-Security-Policy", policy),
731 ("X-Frame-Options", "DENY".to_string()),
732 ],
733 body.as_bytes(),
734 );
735}
736
737fn check_upgrade(
740 head: &Head,
741 shared: &Shared,
742 local: Option<SocketAddr>,
743) -> Option<(&'static str, &'static str)> {
744 if http::websocket_key(head).is_none() {
745 return Some(("400 Bad Request", "Expected a WebSocket upgrade."));
746 }
747 if !head
748 .param("token")
749 .is_some_and(|t| http::same_token(t, &shared.token))
750 {
751 return Some(("403 Forbidden", "Wrong or missing token."));
752 }
753 let mut hosts = shared.hosts.clone();
754 hosts.extend(local.map(http::connection_hosts).unwrap_or_default());
755 if !http::origin_allowed(
756 head.header("origin"),
757 head.header("host"),
758 &hosts,
759 &shared.origins,
760 ) {
761 return Some(("403 Forbidden", "This origin may not open a session."));
762 }
763 None
764}
765
766fn websocket(mut stream: TcpStream, head: &Head, shared: &Arc<Shared>, pending: Pending) {
768 if let Some((status, why)) = check_upgrade(head, shared, stream.local_addr().ok()) {
769 let _ = http::refuse(&mut stream, status, why);
770 return;
771 }
772 let Some(_slot) = Slot::acquire(shared) else {
773 let _ = http::refuse(
774 &mut stream,
775 "503 Service Unavailable",
776 "Too many sessions: try again when one has ended.",
777 );
778 return;
779 };
780 let key = http::websocket_key(head).expect("checked");
781 let accept = tungstenite::handshake::derive_accept_key(key.as_bytes());
782 let answer = format!(
783 "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\
784 Sec-WebSocket-Accept: {accept}\r\n\r\n"
785 );
786 if stream.write_all(answer.as_bytes()).is_err() || stream.flush().is_err() {
787 return;
788 }
789 drop(pending);
790 let size = |name, default| {
791 head.param(name)
792 .and_then(|n| n.parse::<u16>().ok())
793 .filter(|n| (1..=session::MAX_CELLS).contains(n))
794 .unwrap_or(default)
795 };
796 let (columns, rows) = (size("cols", 80), size("rows", 24));
797 let z = session::Compress::negotiate(shared.compress, head.param("z") == Some("1"));
800 if let (Mode::App(_), Isolation::Processes) = (&shared.mode, shared.isolation) {
801 let session = isolate::Session {
802 spawn: &shared.spawn,
803 bind: shared.bind,
804 columns,
805 rows,
806 renderer: renderer_for(head, shared),
807 compress: z.0,
808 stopping: &shared.stopping,
809 running: &shared.programs,
810 };
811 isolate::run_parent(stream, session);
812 return;
813 }
814 let config = WebSocketConfig::default()
815 .max_message_size(Some(MAX_MESSAGE))
816 .max_frame_size(Some(MAX_MESSAGE));
817 let mut socket = WebSocket::from_raw_socket(stream, Role::Server, Some(config));
818 let (stopping, programs) = (&shared.stopping, &shared.programs);
819 let _ = match &shared.mode {
820 Mode::App(app) => {
821 let renderer = renderer_for(head, shared);
822 session::run(&mut socket, app(), columns, rows, stopping, renderer, z)
823 }
824 Mode::Command(command) => {
825 let (window, held) = pty::split(shared.max_buffered);
826 let host = LocalPty::new(command.clone()).backpressure(held);
827 let host = Box::new(host);
828 pty::run(
829 &mut socket,
830 host,
831 (columns, rows),
832 stopping,
833 programs,
834 window,
835 z,
836 )
837 }
838 Mode::Host(host) => {
839 let (window, _) = pty::split(shared.max_buffered);
840 let host = host();
841 pty::run(
842 &mut socket,
843 host,
844 (columns, rows),
845 stopping,
846 programs,
847 window,
848 z,
849 )
850 }
851 };
852}
853
854#[cfg(test)]
855mod tests {
856 use super::*;
857
858 #[test]
859 fn tokens_are_random_hex() {
860 let a = random_token().unwrap();
861 let b = random_token().unwrap();
862 assert_eq!(a.len(), 32);
863 assert!(a.bytes().all(|c| c.is_ascii_hexdigit()));
864 assert_ne!(a, b);
865 }
866
867 #[test]
868 fn urls_point_somewhere_reachable() {
869 let token = "t0k";
870 assert_eq!(
871 url_for("127.0.0.1:8080".parse().unwrap(), token),
872 "http://127.0.0.1:8080/?token=t0k"
873 );
874 assert_eq!(
875 url_for("0.0.0.0:80".parse().unwrap(), token),
876 "http://127.0.0.1:80/?token=t0k"
877 );
878 assert_eq!(
879 url_for("[::]:80".parse().unwrap(), token),
880 "http://[::1]:80/?token=t0k"
881 );
882 }
883
884 #[test]
885 #[should_panic(expected = "a token is")]
886 fn tokens_must_be_url_safe() {
887 let _ = Server::bind("127.0.0.1:0", || App::new(|| intuituive::label("x")))
888 .unwrap()
889 .token("has space");
890 }
891
892 #[test]
893 fn renderers_have_names() {
894 for renderer in [Renderer::Xterm, Renderer::Dom] {
895 assert_eq!(Renderer::parse(renderer.name()), Some(renderer));
896 }
897 assert_eq!(Renderer::parse("canvas"), None);
898 assert_eq!(Renderer::default(), Renderer::Xterm);
899 }
900
901 #[test]
902 fn connections_past_the_pending_cap_are_answered() {
903 use std::io::Read;
904
905 let handle = Server::bind("127.0.0.1:0", || App::new(|| intuituive::label("x")))
906 .unwrap()
907 .spawn()
908 .unwrap();
909 let addr = handle.local_addr();
910 let idle: Vec<TcpStream> = (0..MAX_PENDING)
912 .map(|_| TcpStream::connect(addr).unwrap())
913 .collect();
914 let start = Instant::now();
915 while handle.shared.pending.load(Ordering::SeqCst) < MAX_PENDING {
916 assert!(
917 start.elapsed() < Duration::from_secs(5),
918 "the idle connections"
919 );
920 thread::sleep(Duration::from_millis(10));
921 }
922 let mut late = TcpStream::connect(addr).unwrap();
924 late.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
925 let mut answer = String::new();
926 let _ = late.read_to_string(&mut answer);
927 assert!(answer.starts_with("HTTP/1.1 503 "), "{answer:?}");
928 drop(idle);
929 }
930
931 #[test]
932 fn a_command_is_titled_by_its_program() {
933 let server = Server::bind_command("127.0.0.1:0", ["htop", "-d", "10"]).unwrap();
934 assert_eq!(server.shared.title, "htop");
935 assert_eq!(server.shared.max_buffered, DEFAULT_MAX_BUFFERED);
936 }
937}