@@ -313,11 +313,38 @@ pub extern "system" fn Java_com_therealaleph_mhrv_Native_startDriveProxy(
313313 }
314314 } ;
315315
316+ // Validate the Drive backend SYNCHRONOUSLY before we hand a
317+ // handle back to Kotlin. Loads credentials.json, refreshes the
318+ // OAuth access token, and ensures the target folder exists —
319+ // any of which can fail (no token cached, expired refresh,
320+ // network unreachable, folder ID typo, ...). Without this
321+ // up-front validation, startDriveProxy would return a non-zero
322+ // handle even on credential failure, leaving Kotlin with a
323+ // dead service holding a zombie handle.
324+ //
325+ // The runtime is the same one the listener will run on, so
326+ // the HTTP/2 connection task spawned during `build_backend`
327+ // stays alive after we return.
328+ let backend = match rt. block_on ( crate :: drive_tunnel:: build_backend ( & config) ) {
329+ Ok ( b) => b,
330+ Err ( e) => {
331+ tracing:: error!( "android: drive backend init failed: {}" , e) ;
332+ // Drop the runtime explicitly — `build_backend` may have
333+ // spawned the HTTP/2 driver task that we no longer need.
334+ rt. shutdown_timeout ( std:: time:: Duration :: from_secs ( 1 ) ) ;
335+ return 0i64 ;
336+ }
337+ } ;
338+
316339 let ( tx, rx) = oneshot:: channel :: < ( ) > ( ) ;
317340 let cfg_for_task = config;
318341 rt. spawn ( async move {
319- if let Err ( e) =
320- crate :: drive_tunnel:: run_client_with_shutdown ( & cfg_for_task, rx) . await
342+ if let Err ( e) = crate :: drive_tunnel:: run_client_with_backend (
343+ & cfg_for_task,
344+ backend,
345+ rx,
346+ )
347+ . await
321348 {
322349 tracing:: error!( "android: drive client exited: {}" , e) ;
323350 }
@@ -386,52 +413,32 @@ pub extern "system" fn Java_com_therealaleph_mhrv_Native_driveCompleteAuth<'a>(
386413 config_json : JString ,
387414 code : JString ,
388415) -> jstring {
389- let result_json = safe (
390- r#"{"ok":false,"error":"panic"}"# . to_string ( ) ,
391- AssertUnwindSafe ( || {
392- install_logging_once ( ) ;
393- let json = jstring_to_string ( & mut env, & config_json) ;
394- let raw = jstring_to_string ( & mut env, & code) ;
395- if raw. trim ( ) . is_empty ( ) {
396- return r#"{"ok":false,"error":"empty code"}"# . to_string ( ) ;
397- }
398- let config: Config = match serde_json:: from_str ( & json) {
399- Ok ( c) => c,
400- Err ( e) => {
401- return format ! (
402- r#"{{"ok":false,"error":"bad config json: {}"}}"# ,
403- json_escape( & e. to_string( ) )
404- ) ;
405- }
406- } ;
407- let backend =
408- match crate :: google_drive:: GoogleDriveBackend :: from_config ( & config) {
409- Ok ( b) => b,
410- Err ( e) => {
411- return format ! (
412- r#"{{"ok":false,"error":"{}"}}"# ,
413- json_escape( & e. to_string( ) )
414- ) ;
415- }
416- } ;
417- let Some ( rt) = one_shot_runtime ( ) else {
418- return r#"{"ok":false,"error":"tokio init failed"}"# . to_string ( ) ;
419- } ;
420- match rt. block_on ( backend. apply_auth_code ( & raw ) ) {
421- Ok ( ( ) ) => {
422- let path = backend. token_path ( ) . display ( ) . to_string ( ) ;
423- format ! (
424- r#"{{"ok":true,"tokenPath":"{}"}}"# ,
425- json_escape( & path)
426- )
427- }
428- Err ( e) => format ! (
429- r#"{{"ok":false,"error":"{}"}}"# ,
430- json_escape( & e. to_string( ) )
431- ) ,
416+ let result_json = safe ( error_json ( "panic" ) , AssertUnwindSafe ( || {
417+ install_logging_once ( ) ;
418+ let json = jstring_to_string ( & mut env, & config_json) ;
419+ let raw = jstring_to_string ( & mut env, & code) ;
420+ if raw. trim ( ) . is_empty ( ) {
421+ return error_json ( "empty code" ) ;
422+ }
423+ let config: Config = match serde_json:: from_str ( & json) {
424+ Ok ( c) => c,
425+ Err ( e) => return error_json ( & format ! ( "bad config json: {}" , e) ) ,
426+ } ;
427+ let backend = match crate :: google_drive:: GoogleDriveBackend :: from_config ( & config) {
428+ Ok ( b) => b,
429+ Err ( e) => return error_json ( & e. to_string ( ) ) ,
430+ } ;
431+ let Some ( rt) = one_shot_runtime ( ) else {
432+ return error_json ( "tokio init failed" ) ;
433+ } ;
434+ match rt. block_on ( backend. apply_auth_code ( & raw ) ) {
435+ Ok ( ( ) ) => {
436+ let path = backend. token_path ( ) . display ( ) . to_string ( ) ;
437+ serde_json:: json!( { "ok" : true , "tokenPath" : path} ) . to_string ( )
432438 }
433- } ) ,
434- ) ;
439+ Err ( e) => error_json ( & e. to_string ( ) ) ,
440+ }
441+ } ) ) ;
435442 env. new_string ( result_json)
436443 . map ( |s| s. into_raw ( ) )
437444 . unwrap_or ( std:: ptr:: null_mut ( ) )
@@ -466,8 +473,12 @@ pub extern "system" fn Java_com_therealaleph_mhrv_Native_driveTokenPresent(
466473 } ) )
467474}
468475
469- fn json_escape ( s : & str ) -> String {
470- s. replace ( '\\' , "\\ \\ " ) . replace ( '"' , "\\ \" " )
476+ /// Build a `{"ok": false, "error": "<msg>"}` blob with proper JSON
477+ /// escaping. Wraps `serde_json::json!` so callers don't need to spell
478+ /// out the shape on every error site, and so a stray `\n` / `"` in an
479+ /// error message can't poison the parser on the Kotlin side.
480+ fn error_json ( msg : & str ) -> String {
481+ serde_json:: json!( { "ok" : false , "error" : msg} ) . to_string ( )
471482}
472483
473484/// `Native.stopProxy(long handle)` -> boolean. Idempotent: calling on an
@@ -583,50 +594,58 @@ pub extern "system" fn Java_com_therealaleph_mhrv_Native_checkUpdate<'a>(
583594 env : JNIEnv < ' a > ,
584595 _class : JClass ,
585596) -> jstring {
586- let result_json = safe (
587- r#"{"kind":"error","reason":"panic"}"# . to_string ( ) ,
588- AssertUnwindSafe ( || {
589- install_logging_once ( ) ;
590- let Some ( rt) = one_shot_runtime ( ) else {
591- return r#"{"kind":"error","reason":"tokio init failed"}"# . to_string ( ) ;
592- } ;
593- let outcome = rt. block_on ( crate :: update_check:: check (
594- crate :: update_check:: Route :: Direct ,
595- ) ) ;
596- update_check_to_json ( & outcome)
597- } ) ,
598- ) ;
597+ // Default-on-panic value uses the same `kind`/`reason` shape the
598+ // happy path produces so the Kotlin parser doesn't need a special
599+ // case for unwind crashes.
600+ let panic_default = serde_json:: json!( { "kind" : "error" , "reason" : "panic" } ) . to_string ( ) ;
601+ let result_json = safe ( panic_default, AssertUnwindSafe ( || {
602+ install_logging_once ( ) ;
603+ let Some ( rt) = one_shot_runtime ( ) else {
604+ return serde_json:: json!( { "kind" : "error" , "reason" : "tokio init failed" } )
605+ . to_string ( ) ;
606+ } ;
607+ let outcome =
608+ rt. block_on ( crate :: update_check:: check ( crate :: update_check:: Route :: Direct ) ) ;
609+ update_check_to_json ( & outcome)
610+ } ) ) ;
599611 env. new_string ( result_json)
600612 . map ( |s| s. into_raw ( ) )
601613 . unwrap_or ( std:: ptr:: null_mut ( ) )
602614}
603615
604616fn update_check_to_json ( u : & crate :: update_check:: UpdateCheck ) -> String {
605- // Hand-serialized to keep the JNI side free of serde derive noise on
606- // the inner enum (which would need `#[derive(Serialize)]`). Short
607- // enough that the hand-rolled version is simpler than pulling
608- // serde_json in here for one call.
609- fn esc ( s : & str ) -> String {
610- s. replace ( '\\' , "\\ \\ " ) . replace ( '"' , "\\ \" " )
611- }
612- match u {
613- crate :: update_check:: UpdateCheck :: UpToDate { current, latest } => format ! (
614- r#"{{"kind":"upToDate","current":"{}","latest":"{}"}}"# ,
615- esc( current) , esc( latest) ,
616- ) ,
617- crate :: update_check:: UpdateCheck :: UpdateAvailable { current, latest, release_url, .. } => format ! (
618- r#"{{"kind":"updateAvailable","current":"{}","latest":"{}","url":"{}"}}"# ,
619- esc( current) , esc( latest) , esc( release_url) ,
620- ) ,
621- crate :: update_check:: UpdateCheck :: Offline ( reason) => format ! (
622- r#"{{"kind":"offline","reason":"{}"}}"# ,
623- esc( reason) ,
624- ) ,
625- crate :: update_check:: UpdateCheck :: Error ( reason) => format ! (
626- r#"{{"kind":"error","reason":"{}"}}"# ,
627- esc( reason) ,
628- ) ,
629- }
617+ // serde_json::json! handles all the JSON escaping (control chars,
618+ // backslashes, embedded quotes, non-BMP code points) in one go;
619+ // the hand-rolled escaper that lived here only handled `\\` and
620+ // `"`, so a `\n` in an offline reason or release-note URL would
621+ // produce malformed JSON the Kotlin side couldn't parse.
622+ let value = match u {
623+ crate :: update_check:: UpdateCheck :: UpToDate { current, latest } => serde_json:: json!( {
624+ "kind" : "upToDate" ,
625+ "current" : current,
626+ "latest" : latest,
627+ } ) ,
628+ crate :: update_check:: UpdateCheck :: UpdateAvailable {
629+ current,
630+ latest,
631+ release_url,
632+ ..
633+ } => serde_json:: json!( {
634+ "kind" : "updateAvailable" ,
635+ "current" : current,
636+ "latest" : latest,
637+ "url" : release_url,
638+ } ) ,
639+ crate :: update_check:: UpdateCheck :: Offline ( reason) => serde_json:: json!( {
640+ "kind" : "offline" ,
641+ "reason" : reason,
642+ } ) ,
643+ crate :: update_check:: UpdateCheck :: Error ( reason) => serde_json:: json!( {
644+ "kind" : "error" ,
645+ "reason" : reason,
646+ } ) ,
647+ } ;
648+ value. to_string ( )
630649}
631650
632651/// `Native.testSni(googleIp, sni)` -> String. Returns a small JSON blob
@@ -639,21 +658,21 @@ pub extern "system" fn Java_com_therealaleph_mhrv_Native_testSni<'a>(
639658 google_ip : JString ,
640659 sni : JString ,
641660) -> jstring {
642- let result_json = safe ( r#"{"ok":false,"error":" panic"}"# . to_string ( ) , AssertUnwindSafe ( || {
661+ let result_json = safe ( error_json ( " panic") , AssertUnwindSafe ( || {
643662 install_logging_once ( ) ;
644663 let ip = jstring_to_string ( & mut env, & google_ip) ;
645664 let s = jstring_to_string ( & mut env, & sni) ;
646665 if ip. is_empty ( ) || s. is_empty ( ) {
647- return r#"{"ok":false,"error":" empty google_ip or sni"}"# . to_string ( ) ;
666+ return error_json ( " empty google_ip or sni") ;
648667 }
649668 let Some ( rt) = one_shot_runtime ( ) else {
650- return r#"{"ok":false,"error":" tokio init failed"}"# . to_string ( ) ;
669+ return error_json ( " tokio init failed") ;
651670 } ;
652671 let probe = rt. block_on ( crate :: scan_sni:: probe_one ( & ip, & s) ) ;
653672 match ( probe. latency_ms , probe. error ) {
654673 ( Some ( ms) , _) => {
655674 tracing:: info!( "sni_probe: {} via {} ok in {}ms" , s, ip, ms) ;
656- format ! ( r#"{{ "ok":true,"latencyMs":{}}}"# , ms)
675+ serde_json :: json! ( { "ok" : true , "latencyMs" : ms} ) . to_string ( )
657676 }
658677 ( None , Some ( e) ) => {
659678 // Surface the reason in logcat too — otherwise users see a
@@ -662,10 +681,9 @@ pub extern "system" fn Java_com_therealaleph_mhrv_Native_testSni<'a>(
662681 // - "connect: ..." -> TCP to google_ip:443 blocked
663682 // - "handshake: ..." -> TLS fail (cert, ALPN, etc.)
664683 tracing:: warn!( "sni_probe: {} via {} FAIL: {}" , s, ip, e) ;
665- let cleaned = e. replace ( '\\' , "\\ \\ " ) . replace ( '"' , "\\ \" " ) ;
666- format ! ( r#"{{"ok":false,"error":"{}"}}"# , cleaned)
684+ error_json ( & e)
667685 }
668- _ => r#"{"ok":false,"error":" unknown"}"# . to_string ( ) ,
686+ _ => error_json ( " unknown") ,
669687 }
670688 } ) ) ;
671689 env. new_string ( result_json) . map ( |s| s. into_raw ( ) ) . unwrap_or ( std:: ptr:: null_mut ( ) )
0 commit comments