1 use crate::common::{Profile, RunCommon, RunTarget}; 2 use anyhow::{Result, bail}; 3 use clap::Parser; 4 use http::{Response, StatusCode}; 5 use std::convert::Infallible; 6 use std::net::SocketAddr; 7 use std::pin::Pin; 8 use std::task::{Context, Poll}; 9 use std::time::Instant; 10 use std::{ 11 path::PathBuf, 12 sync::{ 13 Arc, Mutex, 14 atomic::{AtomicBool, AtomicU64, Ordering}, 15 }, 16 time::Duration, 17 }; 18 use tokio::io::{self, AsyncWrite}; 19 use tokio::sync::Notify; 20 use wasmtime::component::{Component, Linker, ResourceTable}; 21 use wasmtime::{Engine, Store, StoreLimits, UpdateDeadline}; 22 use wasmtime_wasi::p2::{StreamError, StreamResult}; 23 use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; 24 use wasmtime_wasi_http::bindings::ProxyPre; 25 use wasmtime_wasi_http::bindings::http::types::{ErrorCode, Scheme}; 26 use wasmtime_wasi_http::io::TokioIo; 27 use wasmtime_wasi_http::{ 28 DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS, DEFAULT_OUTGOING_BODY_CHUNK_SIZE, WasiHttpCtx, 29 WasiHttpView, body::HyperOutgoingBody, 30 }; 31 32 #[cfg(feature = "wasi-config")] 33 use wasmtime_wasi_config::{WasiConfig, WasiConfigVariables}; 34 #[cfg(feature = "wasi-keyvalue")] 35 use wasmtime_wasi_keyvalue::{WasiKeyValue, WasiKeyValueCtx, WasiKeyValueCtxBuilder}; 36 #[cfg(feature = "wasi-nn")] 37 use wasmtime_wasi_nn::wit::WasiNnCtx; 38 39 struct Host { 40 table: wasmtime::component::ResourceTable, 41 ctx: WasiCtx, 42 http: WasiHttpCtx, 43 http_outgoing_body_buffer_chunks: Option<usize>, 44 http_outgoing_body_chunk_size: Option<usize>, 45 46 limits: StoreLimits, 47 48 #[cfg(feature = "wasi-nn")] 49 nn: Option<WasiNnCtx>, 50 51 #[cfg(feature = "wasi-config")] 52 wasi_config: Option<WasiConfigVariables>, 53 54 #[cfg(feature = "wasi-keyvalue")] 55 wasi_keyvalue: Option<WasiKeyValueCtx>, 56 57 #[cfg(feature = "profiling")] 58 guest_profiler: Option<Arc<wasmtime::GuestProfiler>>, 59 } 60 61 impl WasiView for Host { 62 fn ctx(&mut self) -> WasiCtxView<'_> { 63 WasiCtxView { 64 ctx: &mut self.ctx, 65 table: &mut self.table, 66 } 67 } 68 } 69 70 impl WasiHttpView for Host { 71 fn ctx(&mut self) -> &mut WasiHttpCtx { 72 &mut self.http 73 } 74 fn table(&mut self) -> &mut ResourceTable { 75 &mut self.table 76 } 77 78 fn outgoing_body_buffer_chunks(&mut self) -> usize { 79 self.http_outgoing_body_buffer_chunks 80 .unwrap_or_else(|| DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS) 81 } 82 83 fn outgoing_body_chunk_size(&mut self) -> usize { 84 self.http_outgoing_body_chunk_size 85 .unwrap_or_else(|| DEFAULT_OUTGOING_BODY_CHUNK_SIZE) 86 } 87 } 88 89 const DEFAULT_ADDR: std::net::SocketAddr = std::net::SocketAddr::new( 90 std::net::IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)), 91 8080, 92 ); 93 94 /// Runs a WebAssembly module 95 #[derive(Parser)] 96 pub struct ServeCommand { 97 #[command(flatten)] 98 run: RunCommon, 99 100 /// Socket address for the web server to bind to. 101 #[arg(long , value_name = "SOCKADDR", default_value_t = DEFAULT_ADDR)] 102 addr: SocketAddr, 103 104 /// Socket address where, when connected to, will initiate a graceful 105 /// shutdown. 106 /// 107 /// Note that graceful shutdown is also supported on ctrl-c. 108 #[arg(long, value_name = "SOCKADDR")] 109 shutdown_addr: Option<SocketAddr>, 110 111 /// Disable log prefixes of wasi-http handlers. 112 /// if unspecified, logs will be prefixed with 'stdout|stderr [{req_id}] :: ' 113 #[arg(long)] 114 no_logging_prefix: bool, 115 116 /// The WebAssembly component to run. 117 #[arg(value_name = "WASM", required = true)] 118 component: PathBuf, 119 } 120 121 impl ServeCommand { 122 /// Start a server to run the given wasi-http proxy component 123 pub fn execute(mut self) -> Result<()> { 124 self.run.common.init_logging()?; 125 126 // We force cli errors before starting to listen for connections so then 127 // we don't accidentally delay them to the first request. 128 129 if self.run.common.wasi.nn == Some(true) { 130 #[cfg(not(feature = "wasi-nn"))] 131 { 132 bail!("Cannot enable wasi-nn when the binary is not compiled with this feature."); 133 } 134 } 135 136 if self.run.common.wasi.threads == Some(true) { 137 bail!("wasi-threads does not support components yet") 138 } 139 140 // The serve command requires both wasi-http and the component model, so 141 // we enable those by default here. 142 if self.run.common.wasi.http.replace(true) == Some(false) { 143 bail!("wasi-http is required for the serve command, and must not be disabled"); 144 } 145 if self.run.common.wasm.component_model.replace(true) == Some(false) { 146 bail!("components are required for the serve command, and must not be disabled"); 147 } 148 149 let runtime = tokio::runtime::Builder::new_multi_thread() 150 .enable_time() 151 .enable_io() 152 .build()?; 153 154 runtime.block_on(self.serve())?; 155 156 Ok(()) 157 } 158 159 fn new_store(&self, engine: &Engine, req_id: u64) -> Result<Store<Host>> { 160 let mut builder = WasiCtxBuilder::new(); 161 self.run.configure_wasip2(&mut builder)?; 162 163 builder.env("REQUEST_ID", req_id.to_string()); 164 165 let stdout_prefix: String; 166 let stderr_prefix: String; 167 if self.no_logging_prefix { 168 stdout_prefix = "".to_string(); 169 stderr_prefix = "".to_string(); 170 } else { 171 stdout_prefix = format!("stdout [{req_id}] :: "); 172 stderr_prefix = format!("stderr [{req_id}] :: "); 173 } 174 builder.stdout(LogStream::new(stdout_prefix, Output::Stdout)); 175 builder.stderr(LogStream::new(stderr_prefix, Output::Stderr)); 176 177 let mut host = Host { 178 table: wasmtime::component::ResourceTable::new(), 179 ctx: builder.build(), 180 http: WasiHttpCtx::new(), 181 http_outgoing_body_buffer_chunks: self.run.common.wasi.http_outgoing_body_buffer_chunks, 182 http_outgoing_body_chunk_size: self.run.common.wasi.http_outgoing_body_chunk_size, 183 184 limits: StoreLimits::default(), 185 186 #[cfg(feature = "wasi-nn")] 187 nn: None, 188 #[cfg(feature = "wasi-config")] 189 wasi_config: None, 190 #[cfg(feature = "wasi-keyvalue")] 191 wasi_keyvalue: None, 192 #[cfg(feature = "profiling")] 193 guest_profiler: None, 194 }; 195 196 if self.run.common.wasi.nn == Some(true) { 197 #[cfg(feature = "wasi-nn")] 198 { 199 let graphs = self 200 .run 201 .common 202 .wasi 203 .nn_graph 204 .iter() 205 .map(|g| (g.format.clone(), g.dir.clone())) 206 .collect::<Vec<_>>(); 207 let (backends, registry) = wasmtime_wasi_nn::preload(&graphs)?; 208 host.nn.replace(WasiNnCtx::new(backends, registry)); 209 } 210 } 211 212 if self.run.common.wasi.config == Some(true) { 213 #[cfg(feature = "wasi-config")] 214 { 215 let vars = WasiConfigVariables::from_iter( 216 self.run 217 .common 218 .wasi 219 .config_var 220 .iter() 221 .map(|v| (v.key.clone(), v.value.clone())), 222 ); 223 host.wasi_config.replace(vars); 224 } 225 } 226 227 if self.run.common.wasi.keyvalue == Some(true) { 228 #[cfg(feature = "wasi-keyvalue")] 229 { 230 let ctx = WasiKeyValueCtxBuilder::new() 231 .in_memory_data( 232 self.run 233 .common 234 .wasi 235 .keyvalue_in_memory_data 236 .iter() 237 .map(|v| (v.key.clone(), v.value.clone())), 238 ) 239 .build(); 240 host.wasi_keyvalue.replace(ctx); 241 } 242 } 243 244 let mut store = Store::new(engine, host); 245 246 store.data_mut().limits = self.run.store_limits(); 247 store.limiter(|t| &mut t.limits); 248 249 // If fuel has been configured, we want to add the configured 250 // fuel amount to this store. 251 if let Some(fuel) = self.run.common.wasm.fuel { 252 store.set_fuel(fuel)?; 253 } 254 255 Ok(store) 256 } 257 258 fn add_to_linker(&self, linker: &mut Linker<Host>) -> Result<()> { 259 self.run.validate_p3_option()?; 260 let cli = self.run.validate_cli_enabled()?; 261 262 // Repurpose the `-Scli` flag of `wasmtime run` for `wasmtime serve` 263 // to serve as a signal to enable all WASI interfaces instead of just 264 // those in the `proxy` world. If `-Scli` is present then add all 265 // `command` APIs and then additionally add in the required HTTP APIs. 266 // 267 // If `-Scli` isn't passed then use the `add_to_linker_async` 268 // bindings which adds just those interfaces that the proxy interface 269 // uses. 270 if cli == Some(true) { 271 self.run.add_wasmtime_wasi_to_linker(linker)?; 272 wasmtime_wasi_http::add_only_http_to_linker_async(linker)?; 273 } else { 274 wasmtime_wasi_http::add_to_linker_async(linker)?; 275 } 276 277 if self.run.common.wasi.nn == Some(true) { 278 #[cfg(not(feature = "wasi-nn"))] 279 { 280 bail!("support for wasi-nn was disabled at compile time"); 281 } 282 #[cfg(feature = "wasi-nn")] 283 { 284 wasmtime_wasi_nn::wit::add_to_linker(linker, |h: &mut Host| { 285 let ctx = h.nn.as_mut().unwrap(); 286 wasmtime_wasi_nn::wit::WasiNnView::new(&mut h.table, ctx) 287 })?; 288 } 289 } 290 291 if self.run.common.wasi.config == Some(true) { 292 #[cfg(not(feature = "wasi-config"))] 293 { 294 bail!("support for wasi-config was disabled at compile time"); 295 } 296 #[cfg(feature = "wasi-config")] 297 { 298 wasmtime_wasi_config::add_to_linker(linker, |h| { 299 WasiConfig::from(h.wasi_config.as_ref().unwrap()) 300 })?; 301 } 302 } 303 304 if self.run.common.wasi.keyvalue == Some(true) { 305 #[cfg(not(feature = "wasi-keyvalue"))] 306 { 307 bail!("support for wasi-keyvalue was disabled at compile time"); 308 } 309 #[cfg(feature = "wasi-keyvalue")] 310 { 311 wasmtime_wasi_keyvalue::add_to_linker(linker, |h: &mut Host| { 312 WasiKeyValue::new(h.wasi_keyvalue.as_ref().unwrap(), &mut h.table) 313 })?; 314 } 315 } 316 317 if self.run.common.wasi.threads == Some(true) { 318 bail!("support for wasi-threads is not available with components"); 319 } 320 321 if self.run.common.wasi.http == Some(false) { 322 bail!("support for wasi-http must be enabled for `serve` subcommand"); 323 } 324 325 Ok(()) 326 } 327 328 async fn serve(mut self) -> Result<()> { 329 use hyper::server::conn::http1; 330 331 let mut config = self 332 .run 333 .common 334 .config(use_pooling_allocator_by_default().unwrap_or(None))?; 335 config.wasm_component_model(true); 336 config.async_support(true); 337 338 if self.run.common.wasm.timeout.is_some() { 339 config.epoch_interruption(true); 340 } 341 342 match self.run.profile { 343 Some(Profile::Native(s)) => { 344 config.profiler(s); 345 } 346 Some(Profile::Guest { .. }) => { 347 config.epoch_interruption(true); 348 } 349 None => {} 350 } 351 352 let engine = Engine::new(&config)?; 353 let mut linker = Linker::new(&engine); 354 355 self.add_to_linker(&mut linker)?; 356 357 let component = match self.run.load_module(&engine, &self.component)? { 358 RunTarget::Core(_) => bail!("The serve command currently requires a component"), 359 RunTarget::Component(c) => c, 360 }; 361 362 let instance = linker.instantiate_pre(&component)?; 363 let instance = ProxyPre::new(instance)?; 364 365 // Spawn background task(s) waiting for graceful shutdown signals. This 366 // always listens for ctrl-c but additionally can listen for a TCP 367 // connection to the specified address. 368 let shutdown = Arc::new(GracefulShutdown::default()); 369 tokio::task::spawn({ 370 let shutdown = shutdown.clone(); 371 async move { 372 tokio::signal::ctrl_c().await.unwrap(); 373 shutdown.requested.notify_one(); 374 } 375 }); 376 if let Some(addr) = self.shutdown_addr { 377 let listener = tokio::net::TcpListener::bind(addr).await?; 378 eprintln!( 379 "Listening for shutdown on tcp://{}/", 380 listener.local_addr()? 381 ); 382 let shutdown = shutdown.clone(); 383 tokio::task::spawn(async move { 384 let _ = listener.accept().await; 385 shutdown.requested.notify_one(); 386 }); 387 } 388 389 let socket = match &self.addr { 390 SocketAddr::V4(_) => tokio::net::TcpSocket::new_v4()?, 391 SocketAddr::V6(_) => tokio::net::TcpSocket::new_v6()?, 392 }; 393 // Conditionally enable `SO_REUSEADDR` depending on the current 394 // platform. On Unix we want this to be able to rebind an address in 395 // the `TIME_WAIT` state which can happen then a server is killed with 396 // active TCP connections and then restarted. On Windows though if 397 // `SO_REUSEADDR` is specified then it enables multiple applications to 398 // bind the port at the same time which is not something we want. Hence 399 // this is conditionally set based on the platform (and deviates from 400 // Tokio's default from always-on). 401 socket.set_reuseaddr(!cfg!(windows))?; 402 socket.bind(self.addr)?; 403 let listener = socket.listen(100)?; 404 405 eprintln!("Serving HTTP on http://{}/", listener.local_addr()?); 406 407 log::info!("Listening on {}", self.addr); 408 409 let handler = ProxyHandler::new(self, engine, instance); 410 411 loop { 412 // Wait for a socket, but also "race" against shutdown to break out 413 // of this loop. Once the graceful shutdown signal is received then 414 // this loop exits immediately. 415 let (stream, _) = tokio::select! { 416 _ = shutdown.requested.notified() => break, 417 v = listener.accept() => v?, 418 }; 419 let comp = component.clone(); 420 let stream = TokioIo::new(stream); 421 let h = handler.clone(); 422 let shutdown_guard = shutdown.clone().increment(); 423 tokio::task::spawn(async move { 424 if let Err(e) = http1::Builder::new() 425 .keep_alive(true) 426 .serve_connection( 427 stream, 428 hyper::service::service_fn(move |req| { 429 let comp = comp.clone(); 430 let h = h.clone(); 431 async move { 432 use http_body_util::{BodyExt, Full}; 433 fn to_errorcode(_: Infallible) -> ErrorCode { 434 unreachable!() 435 } 436 match handle_request(h, req, comp).await { 437 Ok(r) => Ok::<_, Infallible>(r), 438 Err(e) => { 439 eprintln!("error: {e:?}"); 440 let error_html = "\ 441 <!doctype html> 442 <html> 443 <head> 444 <title>500 Internal Server Error</title> 445 </head> 446 <body> 447 <center> 448 <h1>500 Internal Server Error</h1> 449 <hr> 450 wasmtime 451 </center> 452 </body> 453 </html>"; 454 Ok(Response::builder() 455 .status(StatusCode::INTERNAL_SERVER_ERROR) 456 .header("Content-Type", "text/html; charset=UTF-8") 457 .body( 458 Full::new(bytes::Bytes::from(error_html)) 459 .map_err(to_errorcode) 460 .boxed(), 461 ) 462 .unwrap()) 463 } 464 } 465 } 466 }), 467 ) 468 .await 469 { 470 eprintln!("error: {e:?}"); 471 } 472 drop(shutdown_guard); 473 }); 474 } 475 476 // Upon exiting the loop we'll no longer process any more incoming 477 // connections but there may still be outstanding connections 478 // processing in child tasks. If there are wait for those to complete 479 // before shutting down completely. Also enable short-circuiting this 480 // wait with a second ctrl-c signal. 481 if shutdown.close() { 482 return Ok(()); 483 } 484 eprintln!("Waiting for child tasks to exit, ctrl-c again to quit sooner..."); 485 tokio::select! { 486 _ = tokio::signal::ctrl_c() => {} 487 _ = shutdown.complete.notified() => {} 488 } 489 490 Ok(()) 491 } 492 } 493 494 /// Helper structure to manage graceful shutdown int he accept loop above. 495 #[derive(Default)] 496 struct GracefulShutdown { 497 /// Async notification that shutdown has been requested. 498 requested: Notify, 499 /// Async notification that shutdown has completed, signaled when 500 /// `notify_when_done` is `true` and `active_tasks` reaches 0. 501 complete: Notify, 502 /// Internal state related to what's in progress when shutdown is requested. 503 state: Mutex<GracefulShutdownState>, 504 } 505 506 #[derive(Default)] 507 struct GracefulShutdownState { 508 active_tasks: u32, 509 notify_when_done: bool, 510 } 511 512 impl GracefulShutdown { 513 /// Increments the number of active tasks and returns a guard indicating 514 fn increment(self: Arc<Self>) -> impl Drop { 515 struct Guard(Arc<GracefulShutdown>); 516 517 let mut state = self.state.lock().unwrap(); 518 assert!(!state.notify_when_done); 519 state.active_tasks += 1; 520 drop(state); 521 522 return Guard(self); 523 524 impl Drop for Guard { 525 fn drop(&mut self) { 526 let mut state = self.0.state.lock().unwrap(); 527 state.active_tasks -= 1; 528 if state.notify_when_done && state.active_tasks == 0 { 529 self.0.complete.notify_one(); 530 } 531 } 532 } 533 } 534 535 /// Flags this state as done spawning tasks and returns whether there are no 536 /// more child tasks remaining. 537 fn close(&self) -> bool { 538 let mut state = self.state.lock().unwrap(); 539 state.notify_when_done = true; 540 state.active_tasks == 0 541 } 542 } 543 544 /// When executing with a timeout enabled, this is how frequently epoch 545 /// interrupts will be executed to check for timeouts. If guest profiling 546 /// is enabled, the guest epoch period will be used. 547 const EPOCH_INTERRUPT_PERIOD: Duration = Duration::from_millis(50); 548 549 struct EpochThread { 550 shutdown: Arc<AtomicBool>, 551 handle: Option<std::thread::JoinHandle<()>>, 552 } 553 554 impl EpochThread { 555 fn spawn(interval: std::time::Duration, engine: Engine) -> Self { 556 let shutdown = Arc::new(AtomicBool::new(false)); 557 let handle = { 558 let shutdown = Arc::clone(&shutdown); 559 let handle = std::thread::spawn(move || { 560 while !shutdown.load(Ordering::Relaxed) { 561 std::thread::sleep(interval); 562 engine.increment_epoch(); 563 } 564 }); 565 Some(handle) 566 }; 567 568 EpochThread { shutdown, handle } 569 } 570 } 571 572 impl Drop for EpochThread { 573 fn drop(&mut self) { 574 if let Some(handle) = self.handle.take() { 575 self.shutdown.store(true, Ordering::Relaxed); 576 handle.join().unwrap(); 577 } 578 } 579 } 580 581 type WriteProfile = Box<dyn FnOnce(&mut Store<Host>) + Send>; 582 583 fn setup_epoch_handler( 584 cmd: &ServeCommand, 585 store: &mut Store<Host>, 586 component: Component, 587 ) -> Result<(WriteProfile, Option<EpochThread>)> { 588 // Profiling Enabled 589 if let Some(Profile::Guest { interval, path }) = &cmd.run.profile { 590 #[cfg(feature = "profiling")] 591 return setup_guest_profiler(cmd, store, path.clone(), *interval, component.clone()); 592 #[cfg(not(feature = "profiling"))] 593 { 594 let _ = (path, interval); 595 bail!("support for profiling disabled at compile time!"); 596 } 597 } 598 599 // Profiling disabled but there's a global request timeout 600 let epoch_thread = if let Some(timeout) = cmd.run.common.wasm.timeout { 601 let start = Instant::now(); 602 store.epoch_deadline_callback(move |_store| { 603 if start.elapsed() > timeout { 604 bail!("Timeout expired"); 605 } 606 Ok(UpdateDeadline::Continue(1)) 607 }); 608 store.set_epoch_deadline(1); 609 let engine = store.engine().clone(); 610 Some(EpochThread::spawn(EPOCH_INTERRUPT_PERIOD, engine)) 611 } else { 612 None 613 }; 614 615 Ok((Box::new(|_store| {}), epoch_thread)) 616 } 617 618 #[cfg(feature = "profiling")] 619 fn setup_guest_profiler( 620 cmd: &ServeCommand, 621 store: &mut Store<Host>, 622 path: String, 623 interval: Duration, 624 component: Component, 625 ) -> Result<(WriteProfile, Option<EpochThread>)> { 626 use wasmtime::{AsContext, GuestProfiler, StoreContext, StoreContextMut}; 627 628 let module_name = "<main>"; 629 630 store.data_mut().guest_profiler = Some(Arc::new(GuestProfiler::new_component( 631 module_name, 632 interval, 633 component, 634 std::iter::empty(), 635 ))); 636 637 fn sample( 638 mut store: StoreContextMut<Host>, 639 f: impl FnOnce(&mut GuestProfiler, StoreContext<Host>), 640 ) { 641 let mut profiler = store.data_mut().guest_profiler.take().unwrap(); 642 f( 643 Arc::get_mut(&mut profiler).expect("profiling doesn't support threads yet"), 644 store.as_context(), 645 ); 646 store.data_mut().guest_profiler = Some(profiler); 647 } 648 649 // Hostcall entry/exit, etc. 650 store.call_hook(|store, kind| { 651 sample(store, |profiler, store| profiler.call_hook(store, kind)); 652 Ok(()) 653 }); 654 655 let start = Instant::now(); 656 let timeout = cmd.run.common.wasm.timeout; 657 store.epoch_deadline_callback(move |store| { 658 sample(store, |profiler, store| { 659 profiler.sample(store, std::time::Duration::ZERO) 660 }); 661 662 // Originally epoch counting was used here; this is problematic in 663 // a lot of cases due to there being a lot of time (e.g. in hostcalls) 664 // when we are not expected to get sample hits. 665 if let Some(timeout) = timeout { 666 if start.elapsed() > timeout { 667 bail!("Timeout expired"); 668 } 669 } 670 671 Ok(UpdateDeadline::Continue(1)) 672 }); 673 674 store.set_epoch_deadline(1); 675 let engine = store.engine().clone(); 676 let epoch_thread = Some(EpochThread::spawn(interval, engine)); 677 678 let write_profile = Box::new(move |store: &mut Store<Host>| { 679 let profiler = Arc::try_unwrap(store.data_mut().guest_profiler.take().unwrap()) 680 .expect("profiling doesn't support threads yet"); 681 if let Err(e) = std::fs::File::create(&path) 682 .map_err(anyhow::Error::new) 683 .and_then(|output| profiler.finish(std::io::BufWriter::new(output))) 684 { 685 eprintln!("failed writing profile at {path}: {e:#}"); 686 } else { 687 eprintln!(); 688 eprintln!("Profile written to: {path}"); 689 eprintln!("View this profile at https://profiler.firefox.com/."); 690 } 691 }); 692 693 Ok((write_profile, epoch_thread)) 694 } 695 696 struct ProxyHandlerInner { 697 cmd: ServeCommand, 698 engine: Engine, 699 instance_pre: ProxyPre<Host>, 700 next_id: AtomicU64, 701 } 702 703 impl ProxyHandlerInner { 704 fn next_req_id(&self) -> u64 { 705 self.next_id.fetch_add(1, Ordering::Relaxed) 706 } 707 } 708 709 #[derive(Clone)] 710 struct ProxyHandler(Arc<ProxyHandlerInner>); 711 712 impl ProxyHandler { 713 fn new(cmd: ServeCommand, engine: Engine, instance_pre: ProxyPre<Host>) -> Self { 714 Self(Arc::new(ProxyHandlerInner { 715 cmd, 716 engine, 717 instance_pre, 718 next_id: AtomicU64::from(0), 719 })) 720 } 721 } 722 723 type Request = hyper::Request<hyper::body::Incoming>; 724 725 async fn handle_request( 726 ProxyHandler(inner): ProxyHandler, 727 req: Request, 728 component: Component, 729 ) -> Result<hyper::Response<HyperOutgoingBody>> { 730 let (sender, receiver) = tokio::sync::oneshot::channel(); 731 732 let req_id = inner.next_req_id(); 733 734 log::info!( 735 "Request {req_id} handling {} to {}", 736 req.method(), 737 req.uri() 738 ); 739 740 let mut store = inner.cmd.new_store(&inner.engine, req_id)?; 741 742 let req = store.data_mut().new_incoming_request(Scheme::Http, req)?; 743 let out = store.data_mut().new_response_outparam(sender)?; 744 let proxy = inner.instance_pre.instantiate_async(&mut store).await?; 745 746 let comp = component.clone(); 747 let task = tokio::task::spawn(async move { 748 let (write_profile, epoch_thread) = setup_epoch_handler(&inner.cmd, &mut store, comp)?; 749 750 if let Err(e) = proxy 751 .wasi_http_incoming_handler() 752 .call_handle(&mut store, req, out) 753 .await 754 { 755 log::error!("[{req_id}] :: {e:?}"); 756 return Err(e); 757 } 758 759 write_profile(&mut store); 760 drop(epoch_thread); 761 762 Ok(()) 763 }); 764 765 let result = match receiver.await { 766 Ok(Ok(resp)) => Ok(resp), 767 Ok(Err(e)) => Err(e.into()), 768 Err(_) => { 769 // An error in the receiver (`RecvError`) only indicates that the 770 // task exited before a response was sent (i.e., the sender was 771 // dropped); it does not describe the underlying cause of failure. 772 // Instead we retrieve and propagate the error from inside the task 773 // which should more clearly tell the user what went wrong. Note 774 // that we assume the task has already exited at this point so the 775 // `await` should resolve immediately. 776 let e = match task.await { 777 Ok(Ok(())) => { 778 bail!("guest never invoked `response-outparam::set` method") 779 } 780 Ok(Err(e)) => e, 781 Err(e) => e.into(), 782 }; 783 Err(e.context("guest never invoked `response-outparam::set` method")) 784 } 785 }; 786 787 result 788 } 789 790 #[derive(Clone)] 791 enum Output { 792 Stdout, 793 Stderr, 794 } 795 796 impl Output { 797 fn write_all(&self, buf: &[u8]) -> io::Result<()> { 798 use std::io::Write; 799 800 match self { 801 Output::Stdout => std::io::stdout().write_all(buf), 802 Output::Stderr => std::io::stderr().write_all(buf), 803 } 804 } 805 } 806 807 #[derive(Clone)] 808 struct LogStream { 809 output: Output, 810 state: Arc<LogStreamState>, 811 } 812 813 struct LogStreamState { 814 prefix: String, 815 needs_prefix_on_next_write: AtomicBool, 816 } 817 818 impl LogStream { 819 fn new(prefix: String, output: Output) -> LogStream { 820 LogStream { 821 output, 822 state: Arc::new(LogStreamState { 823 prefix, 824 needs_prefix_on_next_write: AtomicBool::new(true), 825 }), 826 } 827 } 828 829 fn write_all(&mut self, mut bytes: &[u8]) -> io::Result<()> { 830 while !bytes.is_empty() { 831 if self 832 .state 833 .needs_prefix_on_next_write 834 .load(Ordering::Relaxed) 835 { 836 self.output.write_all(self.state.prefix.as_bytes())?; 837 self.state 838 .needs_prefix_on_next_write 839 .store(false, Ordering::Relaxed); 840 } 841 match bytes.iter().position(|b| *b == b'\n') { 842 Some(i) => { 843 let (a, b) = bytes.split_at(i + 1); 844 bytes = b; 845 self.output.write_all(a)?; 846 self.state 847 .needs_prefix_on_next_write 848 .store(true, Ordering::Relaxed); 849 } 850 None => { 851 self.output.write_all(bytes)?; 852 break; 853 } 854 } 855 } 856 857 Ok(()) 858 } 859 } 860 861 impl wasmtime_wasi::cli::StdoutStream for LogStream { 862 fn p2_stream(&self) -> Box<dyn wasmtime_wasi::p2::OutputStream> { 863 Box::new(self.clone()) 864 } 865 fn async_stream(&self) -> Box<dyn AsyncWrite + Send + Sync> { 866 Box::new(self.clone()) 867 } 868 } 869 870 impl wasmtime_wasi::cli::IsTerminal for LogStream { 871 fn is_terminal(&self) -> bool { 872 match &self.output { 873 Output::Stdout => std::io::stdout().is_terminal(), 874 Output::Stderr => std::io::stderr().is_terminal(), 875 } 876 } 877 } 878 879 impl wasmtime_wasi::p2::OutputStream for LogStream { 880 fn write(&mut self, bytes: bytes::Bytes) -> StreamResult<()> { 881 self.write_all(&bytes) 882 .map_err(|e| StreamError::LastOperationFailed(e.into()))?; 883 Ok(()) 884 } 885 886 fn flush(&mut self) -> StreamResult<()> { 887 Ok(()) 888 } 889 890 fn check_write(&mut self) -> StreamResult<usize> { 891 Ok(1024 * 1024) 892 } 893 } 894 895 #[async_trait::async_trait] 896 impl wasmtime_wasi::p2::Pollable for LogStream { 897 async fn ready(&mut self) {} 898 } 899 900 impl AsyncWrite for LogStream { 901 fn poll_write( 902 mut self: Pin<&mut Self>, 903 _cx: &mut Context<'_>, 904 buf: &[u8], 905 ) -> Poll<io::Result<usize>> { 906 Poll::Ready(self.write_all(buf).map(|_| buf.len())) 907 } 908 fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> { 909 Poll::Ready(Ok(())) 910 } 911 fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> { 912 Poll::Ready(Ok(())) 913 } 914 } 915 916 /// The pooling allocator is tailor made for the `wasmtime serve` use case, so 917 /// try to use it when we can. The main cost of the pooling allocator, however, 918 /// is the virtual memory required to run it. Not all systems support the same 919 /// amount of virtual memory, for example some aarch64 and riscv64 configuration 920 /// only support 39 bits of virtual address space. 921 /// 922 /// The pooling allocator, by default, will request 1000 linear memories each 923 /// sized at 6G per linear memory. This is 6T of virtual memory which ends up 924 /// being about 42 bits of the address space. This exceeds the 39 bit limit of 925 /// some systems, so there the pooling allocator will fail by default. 926 /// 927 /// This function attempts to dynamically determine the hint for the pooling 928 /// allocator. This returns `Some(true)` if the pooling allocator should be used 929 /// by default, or `None` or an error otherwise. 930 /// 931 /// The method for testing this is to allocate a 0-sized 64-bit linear memory 932 /// with a maximum size that's N bits large where we force all memories to be 933 /// static. This should attempt to acquire N bits of the virtual address space. 934 /// If successful that should mean that the pooling allocator is OK to use, but 935 /// if it fails then the pooling allocator is not used and the normal mmap-based 936 /// implementation is used instead. 937 fn use_pooling_allocator_by_default() -> Result<Option<bool>> { 938 use wasmtime::{Config, Memory, MemoryType}; 939 const BITS_TO_TEST: u32 = 42; 940 let mut config = Config::new(); 941 config.wasm_memory64(true); 942 config.memory_reservation(1 << BITS_TO_TEST); 943 let engine = Engine::new(&config)?; 944 let mut store = Store::new(&engine, ()); 945 // NB: the maximum size is in wasm pages to take out the 16-bits of wasm 946 // page size here from the maximum size. 947 let ty = MemoryType::new64(0, Some(1 << (BITS_TO_TEST - 16))); 948 if Memory::new(&mut store, ty).is_ok() { 949 Ok(Some(true)) 950 } else { 951 Ok(None) 952 } 953 } 954