77// for the protocol.
88//
99// Nothing about the hosted app is compiled in. The frontend is read off disk
10- // at runtime through a registered `copse://` scheme rather than baked into the
10+ // at runtime through a registered URI scheme rather than baked into the
1111// binary by `tauri_build`, which is what lets one published binary serve any
1212// app — and what lets this repository build it, on free public-runner minutes,
1313// for a private repository to consume.
@@ -29,7 +29,7 @@ type ServoRuntime = tauri_runtime_servo::Servo<tauri::EventLoopMessage>;
2929
3030/// Prefix marking protocol lines on the sidecar's stdout; everything else is
3131/// passed through as sidecar logging.
32- const PREFIX : & str = "@copse- tauri " ;
32+ const PREFIX : & str = "@tauri-shell " ;
3333
3434#[ derive( Deserialize ) ]
3535#[ serde( tag = "op" ) ]
@@ -71,7 +71,7 @@ fn parse_hex_color(value: &str) -> Option<Color> {
7171}
7272
7373fn window_label ( win_id : u64 ) -> String {
74- format ! ( "copse -{win_id}" )
74+ format ! ( "win -{win_id}" )
7575}
7676
7777fn send_window_event ( stdin : & SharedStdin , win_id : u64 , event : & str ) {
@@ -96,12 +96,12 @@ fn create_window(
9696 show : Option < bool > ,
9797 background_color : Option < String > ,
9898) {
99- // `copse ://localhost/<url>` rather than WebviewUrl::App: App resolves
99+ // `<scheme> ://localhost/<url>` rather than WebviewUrl::App: App resolves
100100 // against the store `tauri_build` embeds at compile time, which is exactly
101101 // the coupling this binary exists to avoid. The sidecar still sends the
102102 // same relative URL it always did — `index.html?winId=1&...` — so the
103103 // protocol is unchanged.
104- let app_url: tauri:: Url = match format ! ( "copse ://localhost/{url}" ) . parse ( ) {
104+ let app_url: tauri:: Url = match format ! ( "{} ://localhost/{url}" , scheme ( ) ) . parse ( ) {
105105 Ok ( parsed) => parsed,
106106 Err ( error) => {
107107 eprintln ! ( "[shell] window {win_id} has an unusable url '{url}': {error}" ) ;
@@ -113,7 +113,7 @@ fn create_window(
113113 window_label ( win_id) ,
114114 WebviewUrl :: CustomProtocol ( app_url) ,
115115 )
116- . title ( title. unwrap_or_else ( || "Copse " . to_string ( ) ) )
116+ . title ( title. unwrap_or_else ( || "App " . to_string ( ) ) )
117117 . inner_size ( width. unwrap_or ( 1200.0 ) , height. unwrap_or ( 800.0 ) )
118118 // The sidecar mirrors Electron's hidden-then-show pattern, but an
119119 // unmapped GTK window has no X11 handle yet and Servo needs one to
@@ -219,15 +219,44 @@ fn handle_sidecar_message(
219219 }
220220}
221221
222- /// The directory the `copse://` scheme serves.
222+ /// The URI scheme the frontend is served under, and therefore the page's
223+ /// origin.
223224///
224- /// `COPSE_FRONTEND_DIR` when set; otherwise `dist/renderer` relative to the
225+ /// A runtime parameter because the origin is the host application's business:
226+ /// it is what CSP `'self'` resolves to and what any origin-scoped storage is
227+ /// keyed by, so an app that wants its own should have it. Validated rather
228+ /// than trusted — a scheme with a `:` or a slash in it would silently produce
229+ /// a URL that resolves somewhere else entirely.
230+ fn scheme ( ) -> String {
231+ let configured = std:: env:: var ( "TAURI_SHELL_SCHEME" ) . unwrap_or_default ( ) ;
232+ let configured = configured. trim ( ) ;
233+ if configured. is_empty ( ) {
234+ return "app" . to_string ( ) ;
235+ }
236+ let valid = configured
237+ . chars ( )
238+ . next ( )
239+ . is_some_and ( |c| c. is_ascii_alphabetic ( ) )
240+ && configured
241+ . chars ( )
242+ . all ( |c| c. is_ascii_alphanumeric ( ) || c == '+' || c == '-' || c == '.' ) ;
243+ if valid {
244+ configured. to_string ( )
245+ } else {
246+ eprintln ! ( "[shell] ignoring unusable TAURI_SHELL_SCHEME '{configured}'; using 'app'" ) ;
247+ "app" . to_string ( )
248+ }
249+ }
250+
251+ /// The directory the configured scheme serves.
252+ ///
253+ /// `TAURI_SHELL_FRONTEND_DIR` when set; otherwise `dist/renderer` relative to the
225254/// working directory, which is the layout the sidecar's own build produces.
226255/// Deliberately not fatal when missing: the shell still starts, every request
227256/// 404s, and the log says which directory it looked in — which is a far
228257/// clearer failure than a blank window.
229258fn frontend_dir ( ) -> PathBuf {
230- if let Ok ( explicit) = std:: env:: var ( "COPSE_FRONTEND_DIR " ) {
259+ if let Ok ( explicit) = std:: env:: var ( "TAURI_SHELL_FRONTEND_DIR " ) {
231260 return PathBuf :: from ( explicit) ;
232261 }
233262 PathBuf :: from ( "dist/renderer" )
@@ -251,7 +280,7 @@ fn content_type_for(path: &str) -> &'static str {
251280 }
252281}
253282
254- /// The path a `copse ://localhost/<path>?<query>` request asks for, rejected if
283+ /// The path a `<scheme> ://localhost/<path>?<query>` request asks for, rejected if
255284/// it tries to climb out of the frontend directory.
256285///
257286/// The scheme is same-origin to the page, so anything the page can reach can
@@ -273,18 +302,18 @@ fn request_path(uri: &str) -> Option<String> {
273302
274303/// Locate `dist/sidecar/index.js`.
275304///
276- /// `COPSE_SIDECAR_ENTRY ` wins when set. Otherwise: the historical default is
305+ /// `TAURI_SHELL_SIDECAR_ENTRY ` wins when set. Otherwise: the historical default is
277306/// `../dist/sidecar/index.js`, which is *cwd*-relative and so only resolves
278307/// when the shell is started from inside `tauri-shell/` — which `cargo run`
279308/// does and a perf harness invoking the release binary by path does not. The
280309/// failure is a bare Node MODULE_NOT_FOUND naming a path nobody wrote, so try
281310/// the exe-relative location too and, when neither exists, say what was tried.
282311fn sidecar_entry ( ) -> std:: io:: Result < PathBuf > {
283- if let Ok ( explicit) = std:: env:: var ( "COPSE_SIDECAR_ENTRY " ) {
312+ if let Ok ( explicit) = std:: env:: var ( "TAURI_SHELL_SIDECAR_ENTRY " ) {
284313 return Ok ( PathBuf :: from ( explicit) ) ;
285314 }
286315 let mut candidates = vec ! [ PathBuf :: from( "../dist/sidecar/index.js" ) ] ;
287- // target/release/copse- tauri-shell → up three to the repo root.
316+ // target/release/tauri-shell → up three to the repo root.
288317 if let Ok ( exe) = std:: env:: current_exe ( ) {
289318 if let Some ( root) = exe
290319 . parent ( )
@@ -303,7 +332,7 @@ fn sidecar_entry() -> std::io::Result<PathBuf> {
303332 std:: io:: ErrorKind :: NotFound ,
304333 format ! (
305334 "no dist/sidecar/index.js (tried {}) — run `pnpm build && pnpm build:tauri`, \
306- or point COPSE_SIDECAR_ENTRY at it",
335+ or point TAURI_SHELL_SIDECAR_ENTRY at it",
307336 candidates
308337 . iter( )
309338 . map( |path| path. display( ) . to_string( ) )
@@ -313,12 +342,23 @@ fn sidecar_entry() -> std::io::Result<PathBuf> {
313342 ) )
314343}
315344
345+ /// How long to wait for the sidecar's first `create-window` before assuming
346+ /// the two ends disagree.
347+ ///
348+ /// The failure this catches is silent by construction: the sidecar decides
349+ /// whether a shell is attached by looking for `TAURI_SHELL=1`, and reads
350+ /// protocol lines by their prefix. Get either wrong — an old sidecar, a
351+ /// renamed variable — and it simply never asks for a window. Nothing errors;
352+ /// there is just no application. Worth a line in the log rather than a
353+ /// mystery.
354+ const FIRST_WINDOW_GRACE : std:: time:: Duration = std:: time:: Duration :: from_secs ( 15 ) ;
355+
316356fn spawn_sidecar ( handle : AppHandle < ServoRuntime > , alive : Arc < AtomicBool > ) -> std:: io:: Result < ( ) > {
317- let node = std:: env:: var ( "COPSE_SIDECAR_NODE " ) . unwrap_or_else ( |_| "node" . to_string ( ) ) ;
357+ let node = std:: env:: var ( "TAURI_SHELL_SIDECAR_NODE " ) . unwrap_or_else ( |_| "node" . to_string ( ) ) ;
318358 let entry = sidecar_entry ( ) ?;
319359 let mut child = Command :: new ( node)
320360 . arg ( entry)
321- . env ( "COPSE_TAURI_SHELL " , "1" )
361+ . env ( "TAURI_SHELL " , "1" )
322362 . stdin ( Stdio :: piped ( ) )
323363 . stdout ( Stdio :: piped ( ) )
324364 . spawn ( ) ?;
@@ -328,6 +368,20 @@ fn spawn_sidecar(handle: AppHandle<ServoRuntime>, alive: Arc<AtomicBool>) -> std
328368 ) ) ;
329369 let stdout = child. stdout . take ( ) . expect ( "sidecar stdout is piped" ) ;
330370
371+ let asked_for_a_window = Arc :: new ( AtomicBool :: new ( false ) ) ;
372+ let watchdog = asked_for_a_window. clone ( ) ;
373+ std:: thread:: spawn ( move || {
374+ std:: thread:: sleep ( FIRST_WINDOW_GRACE ) ;
375+ if !watchdog. load ( Ordering :: SeqCst ) {
376+ eprintln ! (
377+ "[shell] the sidecar has not asked for a window in {}s. It may not \
378+ have recognised this shell: it looks for TAURI_SHELL=1 in its \
379+ environment and for protocol lines prefixed '{PREFIX}'.",
380+ FIRST_WINDOW_GRACE . as_secs( )
381+ ) ;
382+ }
383+ } ) ;
384+
331385 std:: thread:: spawn ( move || {
332386 let reader = BufReader :: new ( stdout) ;
333387 for line in reader. lines ( ) {
@@ -337,7 +391,12 @@ fn spawn_sidecar(handle: AppHandle<ServoRuntime>, alive: Arc<AtomicBool>) -> std
337391 continue ;
338392 } ;
339393 match serde_json:: from_str :: < SidecarMessage > ( payload) {
340- Ok ( message) => handle_sidecar_message ( & handle, & stdin, message) ,
394+ Ok ( message) => {
395+ if matches ! ( message, SidecarMessage :: CreateWindow { .. } ) {
396+ asked_for_a_window. store ( true , Ordering :: SeqCst ) ;
397+ }
398+ handle_sidecar_message ( & handle, & stdin, message)
399+ }
341400 Err ( error) => eprintln ! ( "[shell] bad sidecar message: {error}: {payload}" ) ,
342401 }
343402 }
@@ -356,7 +415,11 @@ fn main() {
356415 let alive_for_setup = sidecar_alive. clone ( ) ;
357416
358417 let root = frontend_dir ( ) ;
359- println ! ( "[shell] serving copse://localhost/ from {}" , root. display( ) ) ;
418+ let scheme = scheme ( ) ;
419+ println ! (
420+ "[shell] serving {scheme}://localhost/ from {}" ,
421+ root. display( )
422+ ) ;
360423
361424 let app = tauri:: Builder :: < ServoRuntime > :: new ( )
362425 // Servo cannot read custom protocol request bodies; route Tauri's own
@@ -368,7 +431,7 @@ fn main() {
368431 // and counts as potentially trustworthy on the patched engine, so the
369432 // page keeps CSP 'self' and the secure-context APIs it would have had
370433 // on tauri://localhost.
371- . register_uri_scheme_protocol ( "copse" , move |_ctx, request| {
434+ . register_uri_scheme_protocol ( scheme , move |_ctx, request| {
372435 let uri = request. uri ( ) . to_string ( ) ;
373436 let Some ( path) = request_path ( & uri) else {
374437 eprintln ! ( "[shell] refused traversal in {uri}" ) ;
0 commit comments