1+ use crate :: desktop_event_transport:: DesktopEventStreamConfig ;
12use crate :: managed_node:: resolve_bundled_node_binary;
23use dirs:: home_dir;
34use parking_lot:: Mutex ;
@@ -185,12 +186,13 @@ fn kill_process_tree_windows(pid: u32, force: bool) -> bool {
185186}
186187fn navigate_main ( app : & AppHandle , url : & str ) {
187188 if let Some ( win) = app. webview_windows ( ) . get ( "main" ) {
188- let mut display = url. to_string ( ) ;
189+ let final_url = augment_launch_url ( url) ;
190+ let mut display = final_url. clone ( ) ;
189191 if let Some ( hash_index) = display. find ( '#' ) {
190192 display. replace_range ( hash_index + 1 .., "[REDACTED]" ) ;
191193 }
192194 log_line ( & format ! ( "navigating main to {display}" ) ) ;
193- if let Ok ( parsed) = Url :: parse ( url ) {
195+ if let Ok ( parsed) = Url :: parse ( & final_url ) {
194196 let _ = win. navigate ( parsed) ;
195197 } else {
196198 log_line ( "failed to parse URL for navigation" ) ;
@@ -200,6 +202,31 @@ fn navigate_main(app: &AppHandle, url: &str) {
200202 }
201203}
202204
205+ fn augment_launch_url ( base_url : & str ) -> String {
206+ let launch_query = std:: env:: var ( "CODENOMAD_UI_LAUNCH_QUERY" )
207+ . ok ( )
208+ . map ( |value| value. trim ( ) . to_string ( ) )
209+ . filter ( |value| !value. is_empty ( ) ) ;
210+
211+ let Some ( launch_query) = launch_query else {
212+ return base_url. to_string ( ) ;
213+ } ;
214+
215+ if base_url. contains ( '?' ) {
216+ return format ! (
217+ "{}&{}" ,
218+ base_url,
219+ launch_query. trim_start_matches( [ '?' , '#' ] )
220+ ) ;
221+ }
222+
223+ format ! (
224+ "{}?{}" ,
225+ base_url,
226+ launch_query. trim_start_matches( [ '?' , '#' ] )
227+ )
228+ }
229+
203230fn extract_cookie_value ( set_cookie : & str , name : & str ) -> Option < String > {
204231 let prefix = format ! ( "{name}=" ) ;
205232 let cookie_kv = set_cookie. split ( ';' ) . next ( ) ?. trim ( ) ;
@@ -298,6 +325,15 @@ fn generate_auth_cookie_name() -> String {
298325 format ! ( "{SESSION_COOKIE_NAME_PREFIX}_{pid}_{timestamp}" )
299326}
300327
328+ fn generate_transport_connection_id ( ) -> String {
329+ let ts = SystemTime :: now ( )
330+ . duration_since ( UNIX_EPOCH )
331+ . unwrap_or_default ( )
332+ . as_millis ( ) ;
333+ let tid = std:: thread:: current ( ) . id ( ) ;
334+ format ! ( "tauri-{}-{:?}" , ts, tid)
335+ }
336+
301337const DEFAULT_CONFIG_PATH : & str = "~/.config/codenomad/config.json" ;
302338
303339#[ derive( Debug , Deserialize ) ]
@@ -456,6 +492,8 @@ pub struct CliProcessManager {
456492 job : Arc < Mutex < Option < WindowsJobObject > > > ,
457493 ready : Arc < AtomicBool > ,
458494 bootstrap_token : Arc < Mutex < Option < String > > > ,
495+ session_cookie : Arc < Mutex < Option < String > > > ,
496+ auth_cookie_name : Arc < Mutex < Option < String > > > ,
459497}
460498
461499impl CliProcessManager {
@@ -467,6 +505,8 @@ impl CliProcessManager {
467505 job : Arc :: new ( Mutex :: new ( None ) ) ,
468506 ready : Arc :: new ( AtomicBool :: new ( false ) ) ,
469507 bootstrap_token : Arc :: new ( Mutex :: new ( None ) ) ,
508+ session_cookie : Arc :: new ( Mutex :: new ( None ) ) ,
509+ auth_cookie_name : Arc :: new ( Mutex :: new ( None ) ) ,
470510 }
471511 }
472512
@@ -475,6 +515,8 @@ impl CliProcessManager {
475515 self . stop ( ) ?;
476516 self . ready . store ( false , Ordering :: SeqCst ) ;
477517 * self . bootstrap_token . lock ( ) = None ;
518+ * self . session_cookie . lock ( ) = None ;
519+ * self . auth_cookie_name . lock ( ) = None ;
478520 {
479521 let mut status = self . status . lock ( ) ;
480522 status. state = CliState :: Starting ;
@@ -491,6 +533,8 @@ impl CliProcessManager {
491533 let job_arc = self . job . clone ( ) ;
492534 let ready_flag = self . ready . clone ( ) ;
493535 let token_arc = self . bootstrap_token . clone ( ) ;
536+ let session_cookie_arc = self . session_cookie . clone ( ) ;
537+ let auth_cookie_name_arc = self . auth_cookie_name . clone ( ) ;
494538 thread:: spawn ( move || {
495539 if let Err ( err) = Self :: spawn_cli (
496540 app. clone ( ) ,
@@ -500,6 +544,8 @@ impl CliProcessManager {
500544 job_arc,
501545 ready_flag,
502546 token_arc,
547+ session_cookie_arc,
548+ auth_cookie_name_arc,
503549 dev,
504550 ) {
505551 log_line ( & format ! ( "cli spawn failed: {err}" ) ) ;
@@ -594,6 +640,7 @@ impl CliProcessManager {
594640 status. port = None ;
595641 status. url = None ;
596642 status. error = None ;
643+ * self . session_cookie . lock ( ) = None ;
597644
598645 Ok ( ( ) )
599646 }
@@ -602,13 +649,35 @@ impl CliProcessManager {
602649 self . status . lock ( ) . clone ( )
603650 }
604651
652+ pub fn desktop_event_stream_config ( & self ) -> Option < DesktopEventStreamConfig > {
653+ let base_url = self . status . lock ( ) . url . clone ( ) ?;
654+ let events_url = format ! ( "{}/api/events" , base_url. trim_end_matches( '/' ) ) ;
655+ let client_id = format ! ( "tauri-{}" , std:: process:: id( ) ) ;
656+ let cookie_name = self
657+ . auth_cookie_name
658+ . lock ( )
659+ . clone ( )
660+ . unwrap_or_else ( || SESSION_COOKIE_NAME_PREFIX . to_string ( ) ) ;
661+
662+ Some ( DesktopEventStreamConfig {
663+ base_url,
664+ events_url,
665+ client_id,
666+ connection_id : generate_transport_connection_id ( ) ,
667+ cookie_name,
668+ session_cookie : self . session_cookie . lock ( ) . clone ( ) ,
669+ } )
670+ }
671+
605672 fn spawn_cli (
606673 app : AppHandle ,
607674 status : Arc < Mutex < CliStatus > > ,
608675 child_holder : Arc < Mutex < Option < Child > > > ,
609676 #[ cfg( windows) ] job_holder : Arc < Mutex < Option < WindowsJobObject > > > ,
610677 ready : Arc < AtomicBool > ,
611678 bootstrap_token : Arc < Mutex < Option < String > > > ,
679+ session_cookie : Arc < Mutex < Option < String > > > ,
680+ auth_cookie_name_holder : Arc < Mutex < Option < String > > > ,
612681 dev : bool ,
613682 ) -> anyhow:: Result < ( ) > {
614683 log_line ( "resolving CLI entry" ) ;
@@ -619,6 +688,7 @@ impl CliProcessManager {
619688 resolution. runner, resolution. entry, host
620689 ) ) ;
621690 let auth_cookie_name = Arc :: new ( generate_auth_cookie_name ( ) ) ;
691+ * auth_cookie_name_holder. lock ( ) = Some ( auth_cookie_name. as_str ( ) . to_string ( ) ) ;
622692 let args = resolution. build_args ( dev, & host, auth_cookie_name. as_str ( ) ) ;
623693 log_line ( & format ! ( "CLI args: {:?}" , args) ) ;
624694 if dev {
@@ -723,6 +793,7 @@ impl CliProcessManager {
723793 let app_clone = app. clone ( ) ;
724794 let ready_clone = ready. clone ( ) ;
725795 let token_clone = bootstrap_token. clone ( ) ;
796+ let session_cookie_clone = session_cookie. clone ( ) ;
726797 let auth_cookie_name_clone = auth_cookie_name. clone ( ) ;
727798
728799 thread:: spawn ( move || {
@@ -742,6 +813,7 @@ impl CliProcessManager {
742813 let status = status_clone. clone ( ) ;
743814 let ready = ready_clone. clone ( ) ;
744815 let token = token_clone. clone ( ) ;
816+ let session_cookie = session_cookie_clone. clone ( ) ;
745817 let auth_cookie_name = auth_cookie_name_clone. clone ( ) ;
746818 thread:: spawn ( move || {
747819 Self :: process_stream (
@@ -751,6 +823,7 @@ impl CliProcessManager {
751823 & status,
752824 & ready,
753825 & token,
826+ & session_cookie,
754827 auth_cookie_name. as_str ( ) ,
755828 ) ;
756829 } ) ;
@@ -761,6 +834,7 @@ impl CliProcessManager {
761834 let status = status_clone. clone ( ) ;
762835 let ready = ready_clone. clone ( ) ;
763836 let token = token_clone. clone ( ) ;
837+ let session_cookie = session_cookie_clone. clone ( ) ;
764838 let auth_cookie_name = auth_cookie_name_clone. clone ( ) ;
765839 thread:: spawn ( move || {
766840 Self :: process_stream (
@@ -770,6 +844,7 @@ impl CliProcessManager {
770844 & status,
771845 & ready,
772846 & token,
847+ & session_cookie,
773848 auth_cookie_name. as_str ( ) ,
774849 ) ;
775850 } ) ;
@@ -894,6 +969,7 @@ impl CliProcessManager {
894969 status : & Arc < Mutex < CliStatus > > ,
895970 ready : & Arc < AtomicBool > ,
896971 bootstrap_token : & Arc < Mutex < Option < String > > > ,
972+ session_cookie : & Arc < Mutex < Option < String > > > ,
897973 auth_cookie_name : & str ,
898974 ) {
899975 let mut buffer = String :: new ( ) ;
@@ -946,6 +1022,7 @@ impl CliProcessManager {
9461022 status,
9471023 ready,
9481024 bootstrap_token,
1025+ session_cookie,
9491026 auth_cookie_name,
9501027 url,
9511028 ) ;
@@ -963,6 +1040,7 @@ impl CliProcessManager {
9631040 status : & Arc < Mutex < CliStatus > > ,
9641041 ready : & Arc < AtomicBool > ,
9651042 bootstrap_token : & Arc < Mutex < Option < String > > > ,
1043+ session_cookie : & Arc < Mutex < Option < String > > > ,
9661044 auth_cookie_name : & str ,
9671045 base_url : String ,
9681046 ) {
@@ -995,6 +1073,7 @@ impl CliProcessManager {
9951073 log_line ( & format ! ( "failed to set session cookie: {err}" ) ) ;
9961074 navigate_main ( app, & format ! ( "{base_url}/login" ) ) ;
9971075 } else {
1076+ * session_cookie. lock ( ) = Some ( session_id. clone ( ) ) ;
9981077 navigate_main ( app, & base_url) ;
9991078 }
10001079 }
@@ -1215,31 +1294,37 @@ fn resolve_dev_entry(_app: &AppHandle) -> Option<String> {
12151294}
12161295
12171296fn resolve_prod_entry ( _app : & AppHandle ) -> Option < String > {
1297+ let base = workspace_root ( ) ;
1298+ let exe_dir = std:: env:: current_exe ( )
1299+ . ok ( )
1300+ . and_then ( |exe| exe. parent ( ) . map ( |dir| dir. to_path_buf ( ) ) ) ;
1301+
1302+ first_existing ( prod_entry_candidates ( exe_dir, base) )
1303+ }
1304+
1305+ fn prod_entry_candidates (
1306+ exe_dir : Option < PathBuf > ,
1307+ workspace : Option < PathBuf > ,
1308+ ) -> Vec < Option < PathBuf > > {
12181309 let mut candidates = Vec :: new ( ) ;
12191310
1220- if let Ok ( exe) = std:: env:: current_exe ( ) {
1221- if let Some ( dir) = exe. parent ( ) {
1222- candidates. push ( Some ( dir. join ( "resources/server/dist/bin.js" ) ) ) ;
1311+ if let Some ( dir) = exe_dir {
1312+ candidates. push ( Some ( dir. join ( "resources/server/dist/bin.js" ) ) ) ;
12231313
1224- let resources = dir. join ( "../Resources" ) ;
1225- candidates. push ( Some ( resources. join ( "server/dist/bin.js" ) ) ) ;
1226- candidates. push ( Some ( resources. join ( "resources/server/dist/bin.js" ) ) ) ;
1314+ let resources = dir. join ( "../Resources" ) ;
1315+ candidates. push ( Some ( resources. join ( "server/dist/bin.js" ) ) ) ;
1316+ candidates. push ( Some ( resources. join ( "resources/server/dist/bin.js" ) ) ) ;
12271317
1228- let linux_resource_roots = [ dir. join ( "../lib/CodeNomad" ) , dir. join ( "../lib/codenomad" ) ] ;
1229- for root in linux_resource_roots {
1230- candidates. push ( Some ( root. join ( "server/dist/bin.js" ) ) ) ;
1231- candidates. push ( Some ( root. join ( "resources/server/dist/bin.js" ) ) ) ;
1232- }
1318+ let linux_resource_roots = [ dir. join ( "../lib/CodeNomad" ) , dir. join ( "../lib/codenomad" ) ] ;
1319+ for root in linux_resource_roots {
1320+ candidates. push ( Some ( root. join ( "server/dist/bin.js" ) ) ) ;
1321+ candidates. push ( Some ( root. join ( "resources/server/dist/bin.js" ) ) ) ;
12331322 }
12341323 }
12351324
1236- let base = workspace_root ( ) ;
1237- candidates. push (
1238- base. as_ref ( )
1239- . map ( |p| p. join ( "packages/server/dist/bin.js" ) ) ,
1240- ) ;
1325+ candidates. push ( workspace. map ( |p| p. join ( "packages/server/dist/bin.js" ) ) ) ;
12411326
1242- first_existing ( candidates)
1327+ candidates
12431328}
12441329
12451330fn build_shell_command_string (
@@ -1355,3 +1440,53 @@ fn normalize_path(path: PathBuf) -> String {
13551440 rendered
13561441 }
13571442}
1443+
1444+ #[ cfg( test) ]
1445+ mod tests {
1446+ use super :: * ;
1447+ use std:: sync:: Mutex as StdMutex ;
1448+
1449+ static ENV_LOCK : StdMutex < ( ) > = StdMutex :: new ( ( ) ) ;
1450+
1451+ #[ test]
1452+ fn prod_entry_candidates_prefer_exe_relative_before_workspace_fallback ( ) {
1453+ let exe_dir = PathBuf :: from ( "/opt/codenomad/bin" ) ;
1454+ let workspace = PathBuf :: from ( "/workspace/codenomad" ) ;
1455+
1456+ let candidates = prod_entry_candidates ( Some ( exe_dir. clone ( ) ) , Some ( workspace. clone ( ) ) )
1457+ . into_iter ( )
1458+ . flatten ( )
1459+ . collect :: < Vec < _ > > ( ) ;
1460+
1461+ assert_eq ! (
1462+ candidates. first( ) ,
1463+ Some ( & exe_dir. join( "resources/server/dist/bin.js" ) )
1464+ ) ;
1465+ assert_eq ! (
1466+ candidates. last( ) ,
1467+ Some ( & workspace. join( "packages/server/dist/bin.js" ) )
1468+ ) ;
1469+ }
1470+
1471+ #[ test]
1472+ fn augment_launch_url_trims_leading_fragment_marker ( ) {
1473+ let _guard = ENV_LOCK . lock ( ) . expect ( "env lock poisoned" ) ;
1474+ std:: env:: set_var ( "CODENOMAD_UI_LAUNCH_QUERY" , "#debug=true" ) ;
1475+
1476+ let augmented = augment_launch_url ( "http://127.0.0.1:3000" ) ;
1477+
1478+ std:: env:: remove_var ( "CODENOMAD_UI_LAUNCH_QUERY" ) ;
1479+ assert_eq ! ( augmented, "http://127.0.0.1:3000?debug=true" ) ;
1480+ }
1481+
1482+ #[ test]
1483+ fn augment_launch_url_trims_fragment_marker_when_query_exists ( ) {
1484+ let _guard = ENV_LOCK . lock ( ) . expect ( "env lock poisoned" ) ;
1485+ std:: env:: set_var ( "CODENOMAD_UI_LAUNCH_QUERY" , "#debug=true" ) ;
1486+
1487+ let augmented = augment_launch_url ( "http://127.0.0.1:3000?existing=true" ) ;
1488+
1489+ std:: env:: remove_var ( "CODENOMAD_UI_LAUNCH_QUERY" ) ;
1490+ assert_eq ! ( augmented, "http://127.0.0.1:3000?existing=true&debug=true" ) ;
1491+ }
1492+ }
0 commit comments