@@ -3,13 +3,9 @@ use std::process::Command;
33
44use std:: { error:: Error , thread} ;
55use std:: time:: Duration ;
6- use signal_hook:: { iterator:: Signals , consts:: SIGINT } ;
7-
8- use webbrowser;
6+ use signal_hook:: { iterator:: { Handle , Signals } , consts:: SIGINT } ;
97
108fn main ( ) {
11- setup_signal_handler ( ) . expect ( "Failed to set up signal handler" ) ;
12-
139 let args: Vec < String > = env:: args ( ) . collect ( ) ;
1410 println ! ( ">>> args {:?}" , args) ;
1511
@@ -18,140 +14,203 @@ fn main() {
1814 return ;
1915 }
2016
21- let port = args[ 1 ] . to_string ( ) ;
22- port. trim ( ) . parse :: < u32 > ( ) . expect ( "The port must be a number." ) ;
17+ let port = args[ 1 ] . trim ( ) ;
18+ port. parse :: < u32 > ( ) . expect ( "The port must be a number." ) ;
19+
20+ // Optional device serial number supplied as the third argument.
21+ let serial: Option < & str > = args. get ( 2 ) . map ( |s| s. as_str ( ) ) ;
2322
2423 // adb devices
2524 let dev_cnt = count_connected_devices ( ) ;
2625 if dev_cnt < 2 {
2726 println ! ( "Make sure your device is connected" ) ;
2827 return ;
29- } else if dev_cnt > 2 {
30- if args. len ( ) < 3 {
31- println ! ( "Multiple devices connected, please specify the target device serial number" ) ;
32- return ;
33- }
28+ } else if dev_cnt > 2 && serial. is_none ( ) {
29+ println ! ( "Multiple devices connected, please specify the target device serial number" ) ;
30+ return ;
3431 }
3532
3633 // apk path
37- let full_path = locate_apk_path ( ) ;
38- if full_path . is_empty ( ) {
39- return ;
40- }
34+ let full_path = match locate_apk_path ( serial ) {
35+ Some ( p ) => p ,
36+ None => return ,
37+ } ;
4138
4239 // forward
43- forward_connection ( & port) ;
44-
45- let handle = thread:: spawn ( || {
40+ forward_connection ( port, serial) ;
41+
42+ // Register the SIGINT handler after the forward is established so that
43+ // Ctrl-C always tears down the port forward before the process exits.
44+ let ( signal_handle, signals_close) =
45+ setup_signal_handler ( port. to_string ( ) , serial. map ( str:: to_string) )
46+ . expect ( "Failed to set up signal handler" ) ;
47+
48+ // Clone owned values for the worker thread.
49+ let port_owned = port. to_string ( ) ;
50+ let serial_owned = serial. map ( str:: to_string) ;
51+ let handle = thread:: spawn ( move || {
4652 thread:: sleep ( Duration :: from_secs ( 2 ) ) ;
4753
4854 println ! ( "Open the browser on the worker thread" ) ;
49- open_browser ( ) ;
55+ open_browser ( & port_owned , serial_owned . as_deref ( ) ) ;
5056 } ) ;
5157
52- startup_service_and_wait ( & port, full_path) ;
58+ startup_service_and_wait ( port, full_path, serial ) ;
5359
54- // unforward
55- unforward_connection ( & port) ;
60+ // On a normal (non-SIGINT) exit, close the signals iterator so the handler
61+ // thread can return without waiting for another signal.
62+ signals_close. close ( ) ;
63+ signal_handle. join ( ) . expect ( "Signal handler thread panicked" ) ;
5664
57- handle. join ( ) . unwrap ( ) ;
65+ handle. join ( ) . expect ( "Browser thread panicked" ) ;
5866 println ! ( "About to quit the app" ) ;
5967}
6068
61- fn serial_checked_command ( ) -> Command {
69+ /// Returns an `adb` [`Command`] pre-populated with the `-s <serial>` flag when
70+ /// a device serial number is provided.
71+ fn serial_checked_command ( serial : Option < & str > ) -> Command {
6272 let mut cmd = Command :: new ( "adb" ) ;
63- let args: Vec < String > = env:: args ( ) . collect ( ) ;
64- if args. len ( ) >= 3 {
65- cmd. args ( vec ! [ "-s" , & args[ 2 ] ] ) ;
73+ if let Some ( s) = serial {
74+ cmd. args ( [ "-s" , s] ) ;
6675 }
6776 cmd
6877}
6978
79+ /// Runs `adb devices` and returns the number of lines that contain the word
80+ /// `"device"` (including the header), which is a proxy for the number of
81+ /// recognised entries.
7082fn count_connected_devices ( ) -> usize {
71- let devices_result = Command :: new ( "adb" ) . arg ( "devices" ) . output ( ) ;
72- let s = devices_result. unwrap ( ) . stdout ;
73- let devices_out = std:: str:: from_utf8 ( & s) . unwrap ( ) ;
74-
75- println ! ( "{}" , format!( "\n Devices info : {}" , devices_out) ) ;
83+ let output = Command :: new ( "adb" )
84+ . arg ( "devices" )
85+ . output ( )
86+ . expect ( "Failed to run 'adb devices'" ) ;
87+ let devices_out =
88+ std:: str:: from_utf8 ( & output. stdout ) . expect ( "'adb devices' output is not valid UTF-8" ) ;
89+
90+ println ! ( "\n Devices info : {}" , devices_out) ;
7691 devices_out. matches ( "device" ) . count ( )
7792}
7893
79- fn locate_apk_path ( ) -> String {
80- let params = vec ! [ "shell" , "pm" , "path" , "com.rayworks.droidcast" ] ;
81- let path_result = serial_checked_command ( ) . args ( params) . output ( ) ;
82- let path = path_result. unwrap ( ) . stdout ;
83-
84- let mut raw_path = std:: str:: from_utf8 ( & path) . unwrap ( ) ;
85- raw_path = raw_path. split ( ":" ) . last ( ) . unwrap ( ) . trim ( ) ;
86- if raw_path. is_empty ( ) {
94+ /// Queries the package manager on the connected device for the APK path of
95+ /// `com.rayworks.droidcast`. Returns `Some(classpath_string)` on success, or
96+ /// `None` (after printing an error) when the package is not found.
97+ fn locate_apk_path ( serial : Option < & str > ) -> Option < String > {
98+ let params = [ "shell" , "pm" , "path" , "com.rayworks.droidcast" ] ;
99+ let output = serial_checked_command ( serial)
100+ . args ( params)
101+ . output ( )
102+ . expect ( "Failed to run 'pm path'" ) ;
103+
104+ let raw =
105+ std:: str:: from_utf8 ( & output. stdout ) . expect ( "'pm path' output is not valid UTF-8" ) ;
106+
107+ // `pm path` returns a line like `package:/data/app/…/base.apk`; we only
108+ // need the part after the first colon.
109+ let apk_path = raw
110+ . split_once ( ':' )
111+ . map ( |( _, after) | after. trim ( ) )
112+ . unwrap_or ( "" ) ;
113+
114+ if apk_path. is_empty ( ) {
87115 eprintln ! ( "Apk not found, have you installed it successfully?" ) ;
88- return "" . to_string ( ) ;
116+ return None ;
89117 }
90- let full_path = String :: from ( "CLASSPATH=" ) + & ( raw_path. to_string ( ) ) ;
118+
119+ let full_path = format ! ( "CLASSPATH={}" , apk_path) ;
91120 println ! ( "Path {}" , full_path) ;
92121
93- full_path
122+ Some ( full_path)
94123}
95124
96- fn startup_service_and_wait ( port : & String , full_path : String ) {
97- // app_process
98- let port_param = String :: from ( "--port=" ) + & port;
99- let params = vec ! [
125+ /// Spawns `app_process` via `adb shell` with the DroidCast main class and
126+ /// waits for the process to finish.
127+ fn startup_service_and_wait ( port : & str , full_path : String , serial : Option < & str > ) {
128+ let port_param = format ! ( "--port={}" , port) ;
129+ let params = [
100130 "shell" ,
101131 full_path. trim ( ) ,
102132 "app_process" ,
103133 "/" ,
104134 "com.rayworks.droidcast.Main" ,
105- port_param. as_str ( ) . trim( ) ,
135+ port_param. trim ( ) ,
106136 ] ;
107137 println ! ( "Params -> {:?}" , params) ;
108138
109- let result = serial_checked_command ( ) . args ( params)
139+ let result = serial_checked_command ( serial)
140+ . args ( params)
110141 . spawn ( )
111- . unwrap ( )
142+ . expect ( "Failed to spawn app_process" )
112143 . wait_with_output ( )
113- . expect ( "Failed to wait for a Child Process " ) ;
144+ . expect ( "Failed to wait for app_process " ) ;
114145
115146 println ! ( "status: {}" , result. status) ;
116147}
117148
118- fn forward_connection ( port : & String ) {
119- let grp = String :: from ( "tcp:" ) + & port;
120- let params_fwd = vec ! [ "forward" , & grp. trim( ) , & grp. trim( ) ] ;
149+ /// Forwards the local TCP port to the same port on the device using `adb forward`.
150+ fn forward_connection ( port : & str , serial : Option < & str > ) {
151+ let grp = format ! ( "tcp:{}" , port) ;
152+ let params_fwd = [ "forward" , grp. trim ( ) , grp. trim ( ) ] ;
121153 println ! ( "Params -> {:?}" , params_fwd) ;
122154
123- serial_checked_command ( ) . args ( params_fwd) . output ( ) . expect ( "Failed to forward the tcp connection" ) ;
155+ serial_checked_command ( serial)
156+ . args ( params_fwd)
157+ . output ( )
158+ . expect ( "Failed to forward the tcp connection" ) ;
124159}
125160
126- fn unforward_connection ( port : & String ) {
127- let tcp = format ! ( "tcp:{}" , & port) ;
128- let params_fwd = vec ! [ "forward" , "--remove" , & tcp] ;
161+ /// Removes the previously established port forward via `adb forward --remove`.
162+ fn unforward_connection ( port : & str , serial : Option < & str > ) {
163+ let tcp = format ! ( "tcp:{}" , port) ;
164+ let params_fwd = [ "forward" , "--remove" , & tcp] ;
129165
130- let status = serial_checked_command ( ) . args ( params_fwd) . output ( ) . unwrap ( ) . status ;
166+ let status = serial_checked_command ( serial)
167+ . args ( params_fwd)
168+ . output ( )
169+ . expect ( "Failed to run 'adb forward --remove'" )
170+ . status ;
131171 println ! ( "adb unforward action status : {:?}" , status) ;
132172}
133173
134- fn setup_signal_handler ( ) -> Result < ( ) , Box < dyn Error > > {
135- let mut signals = Signals :: new ( & [ SIGINT ] ) ?;
136-
137- thread:: spawn ( move || {
138- for sig in signals. forever ( ) {
174+ /// Installs a `SIGINT` handler that removes the active port forward when the
175+ /// user presses Ctrl-C. Returns the [`thread::JoinHandle`] for the background
176+ /// signal-handling thread together with a [`Handle`] that can be used to close
177+ /// the signals iterator (and thus allow the thread to return) on a clean exit.
178+ /// `port` and `serial` are moved into the thread so `unforward_connection` can
179+ /// be called from within the handler.
180+ fn setup_signal_handler (
181+ port : String ,
182+ serial : Option < String > ,
183+ ) -> Result < ( thread:: JoinHandle < ( ) > , Handle ) , Box < dyn Error > > {
184+ let mut signals = Signals :: new ( [ SIGINT ] ) ?;
185+ let handle = signals. handle ( ) ;
186+
187+ let join_handle = thread:: spawn ( move || {
188+ if let Some ( sig) = signals. forever ( ) . next ( ) {
139189 println ! ( "\n Received signal {:?}" , sig) ;
190+ unforward_connection ( & port, serial. as_deref ( ) ) ;
140191 }
141192 } ) ;
142193
143- Ok ( ( ) )
194+ Ok ( ( join_handle , handle ) )
144195}
145196
146- fn open_browser ( ) {
147- let args: Vec < String > = env:: args ( ) . collect ( ) ;
148-
149- let ip_param = vec ! [ "shell" , "ip route | awk '/wlan*/{ print $9 }'| tr -d '\n '" ] ;
150- let ip = serial_checked_command ( ) . args ( ip_param) . output ( ) . unwrap ( ) . stdout ;
151- let ip = std:: str:: from_utf8 ( & ip) . unwrap ( ) . to_string ( ) ;
152- println ! ( ">>> Share the url 'http://{}:{}/screenshot' to see the live screen" , ip, args[ 1 ] ) ;
153-
154- let url = format ! ( "http://localhost:{}/screenshot" , args[ 1 ] ) ;
197+ /// Retrieves the device's WLAN IP address, prints a shareable URL, and opens
198+ /// a local screenshot URL in the default browser.
199+ fn open_browser ( port : & str , serial : Option < & str > ) {
200+ let ip_param = [ "shell" , "ip route | awk '/wlan*/{ print $9 }'| tr -d '\n '" ] ;
201+ let ip_bytes = serial_checked_command ( serial)
202+ . args ( ip_param)
203+ . output ( )
204+ . expect ( "Failed to retrieve device IP address" )
205+ . stdout ;
206+ let ip = std:: str:: from_utf8 ( & ip_bytes)
207+ . expect ( "Device IP output is not valid UTF-8" ) ;
208+ println ! (
209+ ">>> Share the url 'http://{}:{}/screenshot' to see the live screen" ,
210+ ip, port
211+ ) ;
212+
213+ let url = format ! ( "http://localhost:{}/screenshot" , port) ;
155214 if webbrowser:: open ( & url) . is_err ( ) {
156215 println ! ( "Failed to open browser" ) ;
157216 }
0 commit comments