88//! shared library and provides the browser/window layer.
99//!
1010//! The user's code uses `Deno.serve()` or `export default { fetch }`
11- //! to serve an HTTP app. The desktop runtime starts it on an in-process
12- //! memory channel and navigates the webview to `app://`, whose requests are
13- //! bridged into that channel by [`scheme_bridge`] — there is no TCP loopback.
14-
15- mod scheme_bridge;
11+ //! to serve an HTTP app. The desktop runtime starts it on a local port
12+ //! and navigates the webview to it.
1613
1714use std:: borrow:: Cow ;
1815use std:: collections:: HashMap ;
@@ -33,6 +30,7 @@ use deno_core::anyhow::bail;
3330use deno_core:: error:: AnyError ;
3431use deno_core:: serde_json;
3532use deno_core:: v8;
33+ use deno_lib:: util:: net:: allocate_random_port;
3634use deno_lib:: util:: result:: js_error_downcast_ref;
3735use deno_lib:: version:: otel_runtime_config;
3836use deno_runtime:: fmt_errors:: format_js_error;
@@ -1257,24 +1255,26 @@ laufey::main!(|| {
12571255
12581256 laufey:: set_js_namespace( "bindings" ) ;
12591257
1260- // Serve over an in-process memory channel — there is no TCP loopback for the
1261- // desktop app at all. No port allocation, no localhost exposure, no kernel
1262- // networking. The webview reaches the server through the `app://` scheme
1263- // handler registered below, which bridges each browser request into this
1264- // named in-memory channel.
1265- //
1266- // Publish DENO_SERVE_ADDRESS BEFORE the tokio runtime is built. Once the
1267- // runtime spins up its mio IO thread (and, optionally, the inspector server
1268- // thread), `setenv` is no longer thread-safe on glibc — Rust 1.81+ marks it
1269- // unsafe for that reason. We're still single-threaded up to here: the
1270- // worker-fork path has already returned, and the init calls above
1271- // (init_logging, mark_standalone, rustls install_default, set_js_namespace)
1272- // don't spawn threads.
1258+ // Allocate the desktop serve port and publish it via DENO_SERVE_ADDRESS
1259+ // BEFORE the tokio runtime is built. Once the runtime spins up its
1260+ // mio IO thread (and, optionally, the inspector server thread),
1261+ // `setenv` is no longer thread-safe on glibc — Rust 1.81+ marks it
1262+ // unsafe for that reason. We're still single-threaded up to here:
1263+ // the worker-fork path has already returned, and the init calls
1264+ // above (init_logging, mark_standalone, rustls install_default,
1265+ // set_js_namespace) don't spawn threads.
1266+ let desktop_serve_port = match allocate_random_port( ) {
1267+ Ok ( p) => p,
1268+ Err ( e) => {
1269+ log:: error!( "[desktop] failed to allocate serve port: {}" , e) ;
1270+ return ;
1271+ }
1272+ } ;
12731273 // SAFETY: see the block comment above — single-threaded at this point.
12741274 unsafe {
12751275 std:: env:: set_var(
12761276 "DENO_SERVE_ADDRESS" ,
1277- format!( "memory: {}" , scheme_bridge :: DESKTOP_SERVE_NAME ) ,
1277+ format!( "tcp:127.0.0.1: {}" , desktop_serve_port ) ,
12781278 ) ;
12791279 }
12801280
@@ -1320,7 +1320,7 @@ laufey::main!(|| {
13201320
13211321 rt. block_on( async {
13221322 log:: debug!( "[desktop] run_desktop starting" ) ;
1323- match run_desktop( update_rolled_back, data) . await {
1323+ match run_desktop( update_rolled_back, desktop_serve_port , data) . await {
13241324 Ok ( ( ) ) => log:: debug!( "[desktop] run_desktop completed OK" ) ,
13251325 Err ( error) => {
13261326 let is_js_error = js_error_downcast_ref( & error) . is_some( ) ;
@@ -1567,6 +1567,7 @@ fn find_section_in_dylib() -> Result<&'static [u8], AnyError> {
15671567
15681568async fn run_desktop (
15691569 update_rolled_back : bool ,
1570+ desktop_serve_port : u16 ,
15701571 data : denort:: binary:: StandaloneData ,
15711572) -> Result < ( ) , AnyError > {
15721573 // Make the error reporting URL available to the panic hook.
@@ -1634,9 +1635,9 @@ async fn run_desktop(
16341635 log:: debug!( "[desktop] inspector server bound on {addr}" ) ;
16351636 }
16361637
1637- // DENO_SERVE_ADDRESS (an in-process `memory:` channel) is published by
1638- // `laufey::main!` before the tokio runtime is built — see the comment there
1639- // for why we can't do it from here.
1638+ // DENO_SERVE_ADDRESS is published by `laufey::main!` before the
1639+ // tokio runtime is built — see the comment there for why we can't
1640+ // do it from here. `desktop_serve_port` is the port we put into it .
16401641
16411642 // Enable HMR if DENO_DESKTOP_HMR is set to a directory path
16421643 // (set by `deno compile --desktop --hmr`).
@@ -1714,10 +1715,8 @@ async fn run_desktop(
17141715
17151716 let run_opts = RunOptions {
17161717 auto_serve : true ,
1717- // The desktop app serves over an in-process memory channel
1718- // (DENO_SERVE_ADDRESS=memory:…), so there is no TCP serve port/host.
1719- serve_port : None ,
1720- serve_host : None ,
1718+ serve_port : Some ( desktop_serve_port) ,
1719+ serve_host : Some ( "127.0.0.1" . to_string ( ) ) ,
17211720 hmr_watch_dir : if is_framework_dev {
17221721 None
17231722 } else {
@@ -1795,6 +1794,7 @@ async fn run_desktop(
17951794 // Run the Deno runtime and Laufey event loop concurrently.
17961795 // We spawn the runtime first, wait for the server to be ready,
17971796 // then navigate the webview.
1797+ let url = format ! ( "http://127.0.0.1:{}" , desktop_serve_port) ;
17981798 log:: debug!( "[desktop] starting runtime and laufey event loop" ) ;
17991799 let run_fut =
18001800 denort:: run:: run_with_options ( Arc :: new ( sys. clone ( ) ) , sys, data, run_opts) ;
@@ -1838,29 +1838,42 @@ async fn run_desktop(
18381838 }
18391839 }
18401840
1841- // Register the app:// scheme handler now that we're running on the Deno
1842- // tokio runtime (the bridge spawns request tasks onto it), then wait for
1843- // Deno.serve to bind the in-process channel before navigating.
1844- scheme_bridge:: register ( ) ;
1845-
18461841 let id = initial_window_id_for_navigate. load ( Ordering :: Acquire ) ;
18471842 let mut server_ready = false ;
18481843 for i in 0 ..60 {
1849- if deno_net:: memory:: is_listening ( scheme_bridge:: DESKTOP_SERVE_NAME ) {
1850- log:: debug!(
1851- "[desktop] Server ready after {} attempts, navigating to {}" ,
1852- i + 1 ,
1853- scheme_bridge:: APP_URL
1844+ if let Ok ( mut stream) =
1845+ tokio:: net:: TcpStream :: connect ( ( "127.0.0.1" , desktop_serve_port) ) . await
1846+ {
1847+ let req = format ! (
1848+ "GET / HTTP/1.1\r \n Host: 127.0.0.1:{}\r \n Connection: close\r \n \r \n " ,
1849+ desktop_serve_port
18541850 ) ;
1855- server_ready = true ;
1856- break ;
1851+ if stream. write_all ( req. as_bytes ( ) ) . await . is_ok ( ) {
1852+ let mut buf = vec ! [ 0u8 ; 256 ] ;
1853+ if let Ok ( n) = stream. read ( & mut buf) . await {
1854+ let response = String :: from_utf8_lossy ( & buf[ ..n] ) ;
1855+ if response. starts_with ( "HTTP/1.1 2" )
1856+ || response. starts_with ( "HTTP/1.1 3" )
1857+ || response. starts_with ( "HTTP/1.0 2" )
1858+ || response. starts_with ( "HTTP/1.0 3" )
1859+ {
1860+ log:: debug!(
1861+ "[desktop] Server ready after {} attempts, navigating to {}" ,
1862+ i + 1 ,
1863+ & url
1864+ ) ;
1865+ server_ready = true ;
1866+ break ;
1867+ }
1868+ }
1869+ }
18571870 }
18581871 tokio:: time:: sleep ( std:: time:: Duration :: from_millis ( 250 ) ) . await ;
18591872 }
18601873 if !server_ready {
18611874 log:: warn!( "Server not ready after 15s, navigating anyway" ) ;
18621875 }
1863- laufey:: Window :: from_id ( id) . navigate ( scheme_bridge :: APP_URL ) ;
1876+ laufey:: Window :: from_id ( id) . navigate ( & url ) ;
18641877
18651878 // The window was created hidden and is normally revealed from its
18661879 // `on_page_load` handler the moment content paints. If that load never
0 commit comments