@@ -95,10 +95,16 @@ enum Cmd {
9595 PollStats ,
9696 /// Probe a single SNI against the given google_ip. Result is written
9797 /// into UiState::sni_probe keyed by the SNI string.
98- TestSni { google_ip : String , sni : String } ,
98+ TestSni {
99+ google_ip : String ,
100+ sni : String ,
101+ } ,
99102 /// Probe a batch of SNI names. Results appear in UiState::sni_probe one
100103 /// by one as each probe finishes.
101- TestAllSni { google_ip : String , snis : Vec < String > } ,
104+ TestAllSni {
105+ google_ip : String ,
106+ snis : Vec < String > ,
107+ } ,
102108}
103109
104110struct App {
@@ -213,7 +219,10 @@ fn sni_pool_for_form(user: Option<&[String]>, front_domain: &str) -> Vec<SniRow>
213219 if !user_clean. is_empty ( ) {
214220 return user_clean
215221 . into_iter ( )
216- . map ( |name| SniRow { name, enabled : true } )
222+ . map ( |name| SniRow {
223+ name,
224+ enabled : true ,
225+ } )
217226 . collect ( ) ;
218227 }
219228 // Default: primary + the other Google-edge subdomains, primary first,
@@ -223,11 +232,17 @@ fn sni_pool_for_form(user: Option<&[String]>, front_domain: &str) -> Vec<SniRow>
223232 let mut out = Vec :: new ( ) ;
224233 if !primary. is_empty ( ) {
225234 seen. insert ( primary. clone ( ) ) ;
226- out. push ( SniRow { name : primary, enabled : true } ) ;
235+ out. push ( SniRow {
236+ name : primary,
237+ enabled : true ,
238+ } ) ;
227239 }
228240 for s in DEFAULT_GOOGLE_SNI_POOL {
229241 if seen. insert ( s. to_string ( ) ) {
230- out. push ( SniRow { name : ( * s) . to_string ( ) , enabled : true } ) ;
242+ out. push ( SniRow {
243+ name : ( * s) . to_string ( ) ,
244+ enabled : true ,
245+ } ) ;
231246 }
232247 }
233248 out
@@ -281,7 +296,11 @@ impl FormState {
281296 enable_batching : false ,
282297 upstream_socks5 : {
283298 let v = self . upstream_socks5 . trim ( ) ;
284- if v. is_empty ( ) { None } else { Some ( v. to_string ( ) ) }
299+ if v. is_empty ( ) {
300+ None
301+ } else {
302+ Some ( v. to_string ( ) )
303+ }
285304 } ,
286305 parallel_relay : self . parallel_relay ,
287306 sni_hosts : {
@@ -296,7 +315,11 @@ impl FormState {
296315 // If the user's pool is empty/all-off we still save as None so
297316 // the backend falls back to sensible defaults instead of dying
298317 // on an empty pool.
299- if active. is_empty ( ) { None } else { Some ( active) }
318+ if active. is_empty ( ) {
319+ None
320+ } else {
321+ Some ( active)
322+ }
300323 } ,
301324 } )
302325 }
@@ -307,8 +330,7 @@ fn save_config(cfg: &Config) -> Result<PathBuf, String> {
307330 if let Some ( parent) = path. parent ( ) {
308331 std:: fs:: create_dir_all ( parent) . map_err ( |e| e. to_string ( ) ) ?;
309332 }
310- let json = serde_json:: to_string_pretty ( & ConfigWire :: from ( cfg) )
311- . map_err ( |e| e. to_string ( ) ) ?;
333+ let json = serde_json:: to_string_pretty ( & ConfigWire :: from ( cfg) ) . map_err ( |e| e. to_string ( ) ) ?;
312334 std:: fs:: write ( & path, json) . map_err ( |e| e. to_string ( ) ) ?;
313335 Ok ( path)
314336}
@@ -368,7 +390,10 @@ impl<'a> From<&'a Config> for ConfigWire<'a> {
368390 hosts : & c. hosts ,
369391 upstream_socks5 : c. upstream_socks5 . as_deref ( ) ,
370392 parallel_relay : c. parallel_relay ,
371- sni_hosts : c. sni_hosts . as_ref ( ) . map ( |v| v. iter ( ) . map ( String :: as_str) . collect ( ) ) ,
393+ sni_hosts : c
394+ . sni_hosts
395+ . as_ref ( )
396+ . map ( |v| v. iter ( ) . map ( String :: as_str) . collect ( ) ) ,
372397 }
373398 }
374399}
@@ -766,14 +791,13 @@ impl App {
766791 } ) ;
767792 }
768793 }
769- if ui. button ( "Keep working only" ) . on_hover_text (
770- "Uncheck every SNI that didn't pass the last probe."
771- ) . clicked ( ) {
794+ if ui
795+ . button ( "Keep working only" )
796+ . on_hover_text ( "Uncheck every SNI that didn't pass the last probe." )
797+ . clicked ( )
798+ {
772799 for row in & mut self . form . sni_pool {
773- let ok = matches ! (
774- probe_map. get( & row. name) ,
775- Some ( SniProbeState :: Ok ( _) )
776- ) ;
800+ let ok = matches ! ( probe_map. get( & row. name) , Some ( SniProbeState :: Ok ( _) ) ) ;
777801 row. enabled = ok;
778802 }
779803 }
@@ -785,13 +809,20 @@ impl App {
785809 if ui. button ( "Clear status" ) . clicked ( ) {
786810 self . shared . state . lock ( ) . unwrap ( ) . sni_probe . clear ( ) ;
787811 }
788- if ui. button ( "Reset to defaults" ) . on_hover_text (
789- "Replace the list with the built-in Google SNI pool. Custom entries \
790- are dropped."
791- ) . clicked ( ) {
812+ if ui
813+ . button ( "Reset to defaults" )
814+ . on_hover_text (
815+ "Replace the list with the built-in Google SNI pool. Custom entries \
816+ are dropped.",
817+ )
818+ . clicked ( )
819+ {
792820 self . form . sni_pool = DEFAULT_GOOGLE_SNI_POOL
793821 . iter ( )
794- . map ( |s| SniRow { name : ( * s) . to_string ( ) , enabled : true } )
822+ . map ( |s| SniRow {
823+ name : ( * s) . to_string ( ) ,
824+ enabled : true ,
825+ } )
795826 . collect ( ) ;
796827 self . shared . state . lock ( ) . unwrap ( ) . sni_probe . clear ( ) ;
797828 }
@@ -804,51 +835,55 @@ impl App {
804835 let mut test_name: Option < String > = None ;
805836 const STATUS_W : f32 = 150.0 ;
806837 const NAME_W : f32 = 230.0 ;
807- egui:: ScrollArea :: vertical ( ) . max_height ( 280.0 ) . show ( ui, |ui| {
808- for ( i, row) in self . form . sni_pool . iter_mut ( ) . enumerate ( ) {
809- ui. horizontal ( |ui| {
810- ui. checkbox ( & mut row. enabled , "" ) ;
811- ui. add (
812- egui:: TextEdit :: singleline ( & mut row. name )
813- . desired_width ( NAME_W )
814- . font ( egui:: TextStyle :: Monospace ) ,
815- ) ;
816- let status_txt = match probe_map. get ( & row. name ) {
817- Some ( SniProbeState :: Ok ( ms) ) => {
818- egui:: RichText :: new ( format ! ( "ok {} ms" , ms) )
819- . color ( egui:: Color32 :: from_rgb ( 80 , 180 , 100 ) )
820- . monospace ( )
821- }
822- Some ( SniProbeState :: Failed ( e) ) => {
823- let short = if e. len ( ) > 22 { & e[ ..22 ] } else { e } ;
824- egui:: RichText :: new ( format ! ( "fail {}" , short) )
825- . color ( egui:: Color32 :: from_rgb ( 220 , 110 , 110 ) )
826- . monospace ( )
827- }
828- Some ( SniProbeState :: InFlight ) => {
829- egui:: RichText :: new ( "testing…" )
838+ egui:: ScrollArea :: vertical ( )
839+ . max_height ( 280.0 )
840+ . show ( ui, |ui| {
841+ for ( i, row) in self . form . sni_pool . iter_mut ( ) . enumerate ( ) {
842+ ui. horizontal ( |ui| {
843+ ui. checkbox ( & mut row. enabled , "" ) ;
844+ ui. add (
845+ egui:: TextEdit :: singleline ( & mut row. name )
846+ . desired_width ( NAME_W )
847+ . font ( egui:: TextStyle :: Monospace ) ,
848+ ) ;
849+ let status_txt = match probe_map. get ( & row. name ) {
850+ Some ( SniProbeState :: Ok ( ms) ) => {
851+ egui:: RichText :: new ( format ! ( "ok {} ms" , ms) )
852+ . color ( egui:: Color32 :: from_rgb ( 80 , 180 , 100 ) )
853+ . monospace ( )
854+ }
855+ Some ( SniProbeState :: Failed ( e) ) => {
856+ let short = if e. len ( ) > 22 { & e[ ..22 ] } else { e } ;
857+ egui:: RichText :: new ( format ! ( "fail {}" , short) )
858+ . color ( egui:: Color32 :: from_rgb ( 220 , 110 , 110 ) )
859+ . monospace ( )
860+ }
861+ Some ( SniProbeState :: InFlight ) => {
862+ egui:: RichText :: new ( "testing…" )
863+ . color ( egui:: Color32 :: GRAY )
864+ . monospace ( )
865+ }
866+ None => egui:: RichText :: new ( "untested" )
830867 . color ( egui:: Color32 :: GRAY )
831- . monospace ( )
868+ . monospace ( ) ,
869+ } ;
870+ ui. add_sized (
871+ [ STATUS_W , 18.0 ] ,
872+ egui:: Label :: new ( status_txt) . truncate ( ) ,
873+ ) ;
874+ if ui. small_button ( "Test" ) . clicked ( ) {
875+ test_name = Some ( row. name . clone ( ) ) ;
832876 }
833- None => {
834- egui:: RichText :: new ( "untested" )
835- . color ( egui:: Color32 :: GRAY )
836- . monospace ( )
877+ if ui
878+ . small_button ( "remove" )
879+ . on_hover_text ( "Remove this row" )
880+ . clicked ( )
881+ {
882+ to_remove = Some ( i) ;
837883 }
838- } ;
839- ui. add_sized ( [ STATUS_W , 18.0 ] , egui:: Label :: new ( status_txt) . truncate ( ) ) ;
840- if ui. small_button ( "Test" ) . clicked ( ) {
841- test_name = Some ( row. name . clone ( ) ) ;
842- }
843- if ui. small_button ( "remove" )
844- . on_hover_text ( "Remove this row" )
845- . clicked ( )
846- {
847- to_remove = Some ( i) ;
848- }
849- } ) ;
850- }
851- } ) ;
884+ } ) ;
885+ }
886+ } ) ;
852887
853888 if let Some ( name) = test_name {
854889 let name = name. trim ( ) . to_string ( ) ;
@@ -930,12 +965,16 @@ fn fmt_bytes(b: u64) -> String {
930965fn background_thread ( shared : Arc < Shared > , rx : Receiver < Cmd > ) {
931966 let rt = Runtime :: new ( ) . expect ( "failed to create tokio runtime" ) ;
932967
933- let mut active: Option < ( JoinHandle < ( ) > , Arc < AsyncMutex < Option < Arc < DomainFronter > > > > ) > = None ;
968+ let mut active: Option < (
969+ JoinHandle < ( ) > ,
970+ Arc < AsyncMutex < Option < Arc < DomainFronter > > > > ,
971+ tokio:: sync:: oneshot:: Sender < ( ) > ,
972+ ) > = None ;
934973
935974 loop {
936975 match rx. recv_timeout ( Duration :: from_millis ( 250 ) ) {
937976 Ok ( Cmd :: PollStats ) => {
938- if let Some ( ( _, fronter_slot) ) = & active {
977+ if let Some ( ( _, fronter_slot, _ ) ) = & active {
939978 let slot = fronter_slot. clone ( ) ;
940979 let shared = shared. clone ( ) ;
941980 rt. spawn ( async move {
@@ -950,6 +989,7 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
950989 } ) ;
951990 }
952991 }
992+ // In background_thread function, modify the Cmd::Start handler:
953993 Ok ( Cmd :: Start ( cfg) ) => {
954994 if active. is_some ( ) {
955995 push_log ( & shared, "[ui] already running" ) ;
@@ -961,6 +1001,8 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
9611001 Arc :: new ( AsyncMutex :: new ( None ) ) ;
9621002 let fronter_slot2 = fronter_slot. clone ( ) ;
9631003
1004+ let ( shutdown_tx, shutdown_rx) = tokio:: sync:: oneshot:: channel ( ) ;
1005+
9641006 let handle = rt. spawn ( async move {
9651007 let base = data_dir:: data_dir ( ) ;
9661008 let mitm = match MitmCertManager :: new_in ( & base) {
@@ -986,27 +1028,49 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
9861028 s. running = true ;
9871029 s. started_at = Some ( Instant :: now ( ) ) ;
9881030 }
989- push_log ( & shared2, & format ! (
990- "[ui] listening HTTP {}:{} SOCKS5 {}:{}" ,
991- cfg. listen_host, cfg. listen_port,
992- cfg. listen_host, cfg. socks5_port. unwrap_or( cfg. listen_port + 1 )
993- ) ) ;
994- let _ = server. run ( ) . await ;
1031+ push_log (
1032+ & shared2,
1033+ & format ! (
1034+ "[ui] listening HTTP {}:{} SOCKS5 {}:{}" ,
1035+ cfg. listen_host,
1036+ cfg. listen_port,
1037+ cfg. listen_host,
1038+ cfg. socks5_port. unwrap_or( cfg. listen_port + 1 )
1039+ ) ,
1040+ ) ;
1041+
1042+ let _ = server. run ( shutdown_rx) . await ;
1043+
9951044 shared2. state . lock ( ) . unwrap ( ) . running = false ;
9961045 push_log ( & shared2, "[ui] proxy stopped" ) ;
9971046 } ) ;
9981047
999- active = Some ( ( handle, fronter_slot) ) ;
1048+ active = Some ( ( handle, fronter_slot, shutdown_tx ) ) ;
10001049 }
1050+
10011051 Ok ( Cmd :: Stop ) => {
1002- if let Some ( ( handle, _) ) = active. take ( ) {
1003- handle. abort ( ) ;
1052+ if let Some ( ( handle, _, shutdown_tx) ) = active. take ( ) {
1053+ push_log ( & shared, "[ui] stop requested" ) ;
1054+ let _ = shutdown_tx. send ( ( ) ) ;
1055+
1056+ // Give the proxy 2 seconds to shut down gracefully
1057+ rt. block_on ( async {
1058+ tokio:: select! {
1059+ _ = handle => {
1060+ push_log( & shared, "[ui] proxy stopped gracefully" ) ;
1061+ }
1062+ _ = tokio:: time:: sleep( tokio:: time:: Duration :: from_secs( 2 ) ) => {
1063+ push_log( & shared, "[ui] shutdown timeout, forcing abort" ) ;
1064+ }
1065+ }
1066+ } ) ;
1067+
10041068 shared. state . lock ( ) . unwrap ( ) . running = false ;
10051069 shared. state . lock ( ) . unwrap ( ) . started_at = None ;
10061070 shared. state . lock ( ) . unwrap ( ) . last_stats = None ;
1007- push_log ( & shared, "[ui] stop requested" ) ;
10081071 }
10091072 }
1073+
10101074 Ok ( Cmd :: Test ( cfg) ) => {
10111075 let shared2 = shared. clone ( ) ;
10121076 push_log ( & shared, "[ui] running test..." ) ;
@@ -1018,7 +1082,10 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
10181082 } else {
10191083 "Test failed — see terminal for details." . into ( )
10201084 } ;
1021- push_log ( & shared2, & format ! ( "[ui] test result: {}" , if ok { "pass" } else { "fail" } ) ) ;
1085+ push_log (
1086+ & shared2,
1087+ & format ! ( "[ui] test result: {}" , if ok { "pass" } else { "fail" } ) ,
1088+ ) ;
10221089 // Also run ip scan on demand (cheap).
10231090 let _ = scan_ips:: run ( & cfg) . await ;
10241091 } ) ;
@@ -1055,7 +1122,9 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
10551122 let result = scan_sni:: probe_one ( & google_ip, & sni) . await ;
10561123 let state = match result. latency_ms {
10571124 Some ( ms) => SniProbeState :: Ok ( ms) ,
1058- None => SniProbeState :: Failed ( result. error . unwrap_or_else ( || "failed" . into ( ) ) ) ,
1125+ None => {
1126+ SniProbeState :: Failed ( result. error . unwrap_or_else ( || "failed" . into ( ) ) )
1127+ }
10591128 } ;
10601129 shared2. state . lock ( ) . unwrap ( ) . sni_probe . insert ( sni, state) ;
10611130 } ) ;
@@ -1074,7 +1143,9 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
10741143 for ( sni, r) in results {
10751144 let state = match r. latency_ms {
10761145 Some ( ms) => SniProbeState :: Ok ( ms) ,
1077- None => SniProbeState :: Failed ( r. error . unwrap_or_else ( || "failed" . into ( ) ) ) ,
1146+ None => {
1147+ SniProbeState :: Failed ( r. error . unwrap_or_else ( || "failed" . into ( ) ) )
1148+ }
10781149 } ;
10791150 st. sni_probe . insert ( sni, state) ;
10801151 }
@@ -1093,7 +1164,7 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
10931164 }
10941165
10951166 // Clean up finished task.
1096- if let Some ( ( handle, _) ) = & active {
1167+ if let Some ( ( handle, _, _ ) ) = & active {
10971168 if handle. is_finished ( ) {
10981169 active = None ;
10991170 shared. state . lock ( ) . unwrap ( ) . running = false ;
@@ -1105,7 +1176,9 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
11051176fn push_log ( shared : & Shared , msg : & str ) {
11061177 let line = format ! (
11071178 "{} {}" ,
1108- time:: OffsetDateTime :: now_utc( ) . format( & time:: format_description:: well_known:: Iso8601 :: DEFAULT ) . unwrap_or_default( ) ,
1179+ time:: OffsetDateTime :: now_utc( )
1180+ . format( & time:: format_description:: well_known:: Iso8601 :: DEFAULT )
1181+ . unwrap_or_default( ) ,
11091182 msg
11101183 ) ;
11111184 let mut s = shared. state . lock ( ) . unwrap ( ) ;
0 commit comments