Skip to content

Commit 7779f8e

Browse files
Copilotrayworks
andauthored
refactor(script-rs): idiomatic Rust, better error handling, doc comments, SIGINT cleanup (#24)
* refactor(script-rs): idiomatic Rust, error handling, and doc comments * fix: call unforward_connection on SIGINT to clean up port forward * fix: remove redundant unforward call; signal thread returns after SIGINT with JoinHandle --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rayworks <1329281+rayworks@users.noreply.github.com>
1 parent 259ae87 commit 7779f8e

2 files changed

Lines changed: 136 additions & 78 deletions

File tree

script-rs/Cargo.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22
name = "script-rs"
33
version = "0.1.0"
44
edition = "2021"
5-
6-
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
5+
description = "CLI helper script for DroidCast – forwards ADB connections and opens the live-screen URL in a browser."
6+
repository = "https://github.com/rayworks/DroidCast"
77

88
[dependencies]
99
webbrowser = "1.0.3"
10-
1110
signal-hook = "0.3.17"

script-rs/src/main.rs

Lines changed: 134 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,9 @@ use std::process::Command;
33

44
use std::{error::Error, thread};
55
use 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

108
fn 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.
7082
fn 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!("\nDevices 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!("\nDevices 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!("\nReceived 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

Comments
 (0)