1use std::fmt;
25use std::io;
26use std::path::PathBuf;
27use std::time::{Duration, Instant};
28
29use protocol::message::Call;
30use protocol::session::{DisconnectReason, HandshakeError, HostConfig, Negotiated};
31use protocol::{Capabilities, PROTOCOL_VERSION};
32
33use super::connection::Connection;
34use super::jar::AgentJar;
35use super::launch::{
36 ConflictLine, Holder, JAR_MODE, PROCESS_NAME, PS_COMMAND, ReadyLine, RunningAgent, SocketName,
37 launch_command, parse_agents, parse_conflict, parse_ready, random_hex, remote_jar,
38};
39use super::records::{Record, Records};
40use crate::adb::{self, Device, ShellPacket};
41
42const SHUTDOWN_DEADLINE: Duration = Duration::from_secs(2);
44
45const CONFLICT_EXIT_WAIT: Duration = Duration::from_secs(3);
47
48#[derive(Clone, Debug, PartialEq, Eq)]
50pub struct DeployOptions {
51 pub force: bool,
54 pub idle_exit: Duration,
57 pub ready_timeout: Duration,
60 pub handshake_timeout: Duration,
62 pub exit_timeout: Duration,
65 pub agent_args: Vec<String>,
67}
68
69impl Default for DeployOptions {
70 fn default() -> DeployOptions {
71 DeployOptions {
72 force: false,
73 idle_exit: Duration::from_secs(30),
74 ready_timeout: Duration::from_secs(20),
75 handshake_timeout: Duration::from_secs(5),
76 exit_timeout: Duration::from_secs(3),
77 agent_args: Vec::new(),
78 }
79 }
80}
81
82#[derive(Clone, PartialEq, Eq)]
84pub struct AgentInfo {
85 pub serial: String,
86 pub socket: String,
87 pub pid: u32,
88 pub agent: String,
90 pub protocol: u16,
92 pub capabilities: Capabilities,
93 token: String,
94}
95
96impl fmt::Debug for AgentInfo {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 f.debug_struct("AgentInfo")
100 .field("serial", &self.serial)
101 .field("socket", &self.socket)
102 .field("pid", &self.pid)
103 .field("agent", &self.agent)
104 .field("protocol", &self.protocol)
105 .field("capabilities", &self.capabilities)
106 .finish()
107 }
108}
109
110impl AgentInfo {
111 #[cfg(test)]
113 pub(crate) fn new(
114 serial: &str,
115 socket: &str,
116 pid: u32,
117 negotiated: &Negotiated,
118 token: &str,
119 ) -> AgentInfo {
120 AgentInfo {
121 serial: serial.to_owned(),
122 socket: socket.to_owned(),
123 pid,
124 agent: negotiated.agent.clone(),
125 protocol: negotiated.version,
126 capabilities: negotiated.capabilities,
127 token: token.to_owned(),
128 }
129 }
130
131 pub fn token(&self) -> &str {
133 &self.token
134 }
135}
136
137#[derive(Clone, Debug, PartialEq, Eq)]
139pub enum Outcome {
140 Launched(Box<Launched>),
142 Reused,
144}
145
146#[derive(Clone, Debug, PartialEq, Eq)]
148pub struct Launched {
149 pub ready: ReadyLine,
150 pub replaced: Option<Replaced>,
152 pub took_over: Option<Holder>,
154}
155
156#[derive(Clone, Debug, PartialEq, Eq)]
158pub struct Replaced {
159 pub pid: u32,
160 pub socket: String,
161 pub reason: Incompatibility,
162 pub stopped: Stopped,
163}
164
165#[derive(Clone, Copy, Debug, PartialEq, Eq)]
167pub enum Stopped {
168 Exited,
170 Killed,
172 Gone,
174}
175
176#[derive(Clone, Debug, PartialEq, Eq)]
178pub enum Incompatibility {
179 Protocol { agent: Vec<u16>, host: Vec<u16> },
182 Version { reported: String, expected: String },
184 Unreachable(String),
187 Build { running: String, deploying: String },
190}
191
192impl fmt::Display for Incompatibility {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 match self {
195 Incompatibility::Protocol { agent, host } => write!(
196 f,
197 "it speaks protocol versions {agent:?} and this driver {host:?}"
198 ),
199 Incompatibility::Version { reported, expected } => {
200 write!(f, "it is {reported} and this driver deploys {expected}")
201 }
202 Incompatibility::Unreachable(why) => {
203 write!(f, "it does not answer as this host recorded it: {why}")
204 }
205 Incompatibility::Build { running, deploying } if running.is_empty() => write!(
206 f,
207 "it was recorded before builds were, and this driver deploys build {deploying}"
208 ),
209 Incompatibility::Build { running, deploying } => write!(
210 f,
211 "it is build {running} of the same version, and this driver deploys build \
212 {deploying}"
213 ),
214 }
215 }
216}
217
218#[derive(Clone, Debug, PartialEq, Eq)]
220pub enum Compatibility {
221 Compatible,
222 Incompatible(Incompatibility),
223}
224
225pub fn compatibility(
229 handshake: &Result<Negotiated, DisconnectReason>,
230 expected_agent: &str,
231) -> Compatibility {
232 match handshake {
233 Ok(negotiated) if negotiated.agent == expected_agent => Compatibility::Compatible,
234 Ok(negotiated) => Compatibility::Incompatible(Incompatibility::Version {
235 reported: negotiated.agent.clone(),
236 expected: expected_agent.to_owned(),
237 }),
238 Err(DisconnectReason::Handshake(HandshakeError::NoCommonVersion { host, agent })) => {
239 Compatibility::Incompatible(Incompatibility::Protocol {
240 agent: agent.clone(),
241 host: host.clone(),
242 })
243 }
244 Err(reason) => {
245 Compatibility::Incompatible(Incompatibility::Unreachable(reason.to_string()))
246 }
247 }
248}
249
250pub fn build_compatibility(recorded: &str, deploying: &str) -> Compatibility {
254 if !recorded.is_empty() && recorded == deploying {
255 Compatibility::Compatible
256 } else {
257 Compatibility::Incompatible(Incompatibility::Build {
258 running: recorded.to_owned(),
259 deploying: deploying.to_owned(),
260 })
261 }
262}
263
264#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
266pub struct Timings {
267 pub total: Duration,
268 pub listing: Duration,
270 pub replace: Option<Duration>,
272 pub push: Option<Duration>,
273 pub spawn_to_ready: Option<Duration>,
275 pub handshake: Duration,
277}
278
279#[derive(Clone, Debug, PartialEq, Eq)]
283pub enum AgentState {
284 Absent,
285 Running(RunningAgent),
287 Incompatible(RunningAgent),
289}
290
291pub fn agent_state(
295 agents: &[RunningAgent],
296 version: &str,
297 protocol: u16,
298 build: &str,
299) -> AgentState {
300 let current = agents.iter().find(|agent| {
301 agent
302 .name
303 .as_ref()
304 .is_some_and(|n| n.is(version, protocol, build))
305 });
306 match (current, agents.first()) {
307 (Some(agent), _) => AgentState::Running(agent.clone()),
308 (None, Some(agent)) => AgentState::Incompatible(agent.clone()),
309 (None, None) => AgentState::Absent,
310 }
311}
312
313#[derive(Clone, Debug, PartialEq, Eq)]
315pub struct Conflict {
316 pub holder: Option<Holder>,
319 pub detail: String,
322}
323
324impl Conflict {
325 pub fn foreign_agent(agent: &RunningAgent) -> Conflict {
327 let build = match &agent.name {
328 Some(name) => format!("version {} over protocol {}", name.version, name.protocol),
329 None => "a version its socket name does not say".to_owned(),
330 };
331 Conflict {
332 holder: Some(Holder {
333 name: PROCESS_NAME.to_owned(),
334 pid: Some(agent.pid),
335 }),
336 detail: format!(
337 "an agent this host did not start runs on the device (pid {}, socket {}, {build}); \
338 another daemon or an earlier run of this one may be using it, and it exits \
339 by itself once no host has been connected for its idle bound",
340 agent.pid, agent.socket
341 ),
342 }
343 }
344
345 pub fn message(&self) -> String {
347 let who = match &self.holder {
348 Some(holder) => format!("likely {holder}"),
349 None => "by a process the device does not name".to_owned(),
350 };
351 format!(
352 "another process holds UI automation, {who}: {}. Attach again with force to take \
353 it over, which stops that holder.",
354 self.detail
355 )
356 }
357
358 fn from_line(line: ConflictLine) -> Conflict {
359 Conflict {
360 holder: line.holder,
361 detail: line.message,
362 }
363 }
364}
365
366impl fmt::Display for Conflict {
367 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
368 f.write_str(&self.message())
369 }
370}
371
372#[derive(Debug)]
374pub enum DeployError {
375 Adb(adb::Error),
376 Conflict(Conflict),
378 Exited {
380 output: String,
381 exit: Option<u8>,
382 },
383 NoReadyLine {
385 waited: Duration,
386 output: String,
387 },
388 Handshake {
390 socket: String,
391 reason: DisconnectReason,
392 },
393 WrongVersion {
396 reported: String,
397 expected: String,
398 jar: Option<PathBuf>,
399 },
400 Records(io::Error),
402}
403
404impl fmt::Display for DeployError {
405 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
406 match self {
407 DeployError::Adb(error) => write!(f, "adb: {error}"),
408 DeployError::Conflict(conflict) => f.write_str(&conflict.message()),
409 DeployError::Exited { output, exit } => {
410 let status = exit.map_or_else(|| "unknown".to_owned(), |code| code.to_string());
411 write!(
412 f,
413 "the agent ended before it was ready (exit status {status}); its output: {output}"
414 )
415 }
416 DeployError::NoReadyLine { waited, output } => write!(
417 f,
418 "the agent printed no ready line within {:.1} s and was stopped; its output: {output}",
419 waited.as_secs_f64()
420 ),
421 DeployError::Handshake { socket, reason } => write!(
422 f,
423 "the agent listening on {socket} did not complete its handshake: {reason}"
424 ),
425 DeployError::WrongVersion {
426 reported,
427 expected,
428 jar,
429 } => {
430 let jar = jar
431 .as_ref()
432 .map_or_else(|| "in memory".to_owned(), |p| p.display().to_string());
433 write!(
434 f,
435 "the agent jar ({jar}) reports {reported}, but this driver deploys {expected}; \
436 rebuild it with agent-android/build.sh"
437 )
438 }
439 DeployError::Records(error) => write!(f, "cannot record the agent: {error}"),
440 }
441 }
442}
443
444impl std::error::Error for DeployError {}
445
446impl From<adb::Error> for DeployError {
447 fn from(error: adb::Error) -> DeployError {
448 DeployError::Adb(error)
449 }
450}
451
452pub fn push_jar(device: &Device, jar: &AgentJar, remote: &str) -> Result<(), adb::Error> {
454 device.push_bytes(jar.bytes(), remote, JAR_MODE)
455}
456
457enum Spawned {
459 Ready(ReadyLine),
460 Conflict(ConflictLine),
461 Exited { output: String, exit: Option<u8> },
462 Timeout { output: String },
463}
464
465#[derive(Default)]
467struct Transcript {
468 partial: Vec<u8>,
469 lines: Vec<String>,
470 stderr: String,
471}
472
473impl Transcript {
474 fn stdout(&mut self, data: &[u8]) -> Vec<String> {
476 self.partial.extend_from_slice(data);
477 let mut done = Vec::new();
478 while let Some(end) = self.partial.iter().position(|&b| b == b'\n') {
479 let line: Vec<u8> = self.partial.drain(..=end).collect();
480 let line = String::from_utf8_lossy(&line).trim_end().to_owned();
481 self.lines.push(line.clone());
482 done.push(line);
483 }
484 done
485 }
486
487 fn text(&self) -> String {
488 let mut text: Vec<String> = self.lines.iter().map(|l| format!("out: {l}")).collect();
489 if !self.partial.is_empty() {
490 text.push(format!("out: {}", String::from_utf8_lossy(&self.partial)));
491 }
492 text.extend(self.stderr.lines().map(|l| format!("err: {l}")));
493 text.join("\n")
494 }
495}
496
497#[derive(Clone, Debug)]
499pub struct Deployer {
500 device: Device,
501 jar: AgentJar,
502 records: Records,
503}
504
505pub struct Attached {
507 pub connection: Connection,
508 pub agent: AgentInfo,
509 pub outcome: Outcome,
510 pub timings: Timings,
511 deployer: Deployer,
512}
513
514impl fmt::Debug for Attached {
515 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
516 f.debug_struct("Attached")
517 .field("agent", &self.agent)
518 .field("outcome", &self.outcome)
519 .field("timings", &self.timings)
520 .finish_non_exhaustive()
521 }
522}
523
524impl Attached {
525 pub fn deployer(&self) -> &Deployer {
527 &self.deployer
528 }
529
530 pub fn stop(self, timeout: Duration) -> Result<Stopped, DeployError> {
533 let Attached {
534 connection,
535 agent,
536 deployer,
537 ..
538 } = self;
539 deployer.stop_agent(&connection, agent.pid, &agent.socket, timeout)
540 }
541
542 pub(crate) fn into_parts(self) -> (Connection, AgentInfo, Outcome, Deployer) {
544 (self.connection, self.agent, self.outcome, self.deployer)
545 }
546}
547
548impl Deployer {
549 pub fn new(device: Device, jar: AgentJar, records: Records) -> Deployer {
550 Deployer {
551 device,
552 jar,
553 records,
554 }
555 }
556
557 pub fn device(&self) -> &Device {
558 &self.device
559 }
560
561 pub fn jar(&self) -> &AgentJar {
562 &self.jar
563 }
564
565 pub fn records(&self) -> &Records {
566 &self.records
567 }
568
569 pub fn running(&self) -> Result<Vec<RunningAgent>, adb::Error> {
571 let listing = self.device.shell(PS_COMMAND)?;
572 Ok(parse_agents(&listing.stdout_text()))
573 }
574
575 pub fn state(&self) -> Result<AgentState, adb::Error> {
577 Ok(agent_state(
578 &self.running()?,
579 self.jar.version(),
580 PROTOCOL_VERSION,
581 &self.jar.build(),
582 ))
583 }
584
585 pub fn attach(&self, options: &DeployOptions) -> Result<Attached, DeployError> {
589 let started = Instant::now();
590 let agents = self.running()?;
591 let mut timings = Timings {
592 listing: started.elapsed(),
593 ..Timings::default()
594 };
595 let serial = self.device.serial();
596 let mut replaced = None;
597 let mut recorded_pid = None;
598 if let Some(record) = self.records.get(serial) {
599 let running = agents
600 .iter()
601 .any(|a| a.pid == record.pid && a.socket == record.socket);
602 if running {
603 recorded_pid = Some(record.pid);
604 let checking = Instant::now();
605 match self.check(&record, options) {
606 Ok((connection, negotiated)) => {
607 timings.handshake = checking.elapsed();
608 timings.total = started.elapsed();
609 return Ok(Attached {
610 agent: AgentInfo {
611 serial: serial.to_owned(),
612 socket: record.socket.clone(),
613 pid: record.pid,
614 agent: negotiated.agent,
615 protocol: negotiated.version,
616 capabilities: negotiated.capabilities,
617 token: record.token.clone(),
618 },
619 connection,
620 outcome: Outcome::Reused,
621 timings,
622 deployer: self.clone(),
623 });
624 }
625 Err((reason, connection)) => {
626 let stopped = self.replace(&record, &reason, connection, options)?;
627 timings.replace = Some(checking.elapsed());
628 replaced = Some(Replaced {
629 pid: record.pid,
630 socket: record.socket.clone(),
631 reason,
632 stopped,
633 });
634 }
635 }
636 }
637 self.records.remove(serial).map_err(DeployError::Records)?;
638 }
639 let foreign = agents.iter().find(|a| Some(a.pid) != recorded_pid);
640 if let Some(other) = foreign
641 && !options.force
642 {
643 return Err(DeployError::Conflict(Conflict::foreign_agent(other)));
644 }
645 self.launch(options, replaced, timings, started)
646 }
647
648 fn check(
650 &self,
651 record: &Record,
652 options: &DeployOptions,
653 ) -> Result<(Connection, Negotiated), (Incompatibility, Option<Connection>)> {
654 let unreachable = |why: String| (Incompatibility::Unreachable(why), None);
655 let stream = self
656 .device
657 .open_abstract(&record.socket)
658 .map_err(|e| unreachable(e.to_string()))?;
659 let connection = Connection::open(stream, self.host_config(&record.token))
660 .map_err(|e| unreachable(e.to_string()))?;
661 let handshake = connection.handshake(options.handshake_timeout);
662 match (compatibility(&handshake, &self.jar.agent_name()), handshake) {
663 (Compatibility::Compatible, Ok(negotiated)) => {
664 match build_compatibility(&record.build, &self.jar.build()) {
665 Compatibility::Compatible => Ok((connection, negotiated)),
666 Compatibility::Incompatible(reason) => Err((reason, Some(connection))),
667 }
668 }
669 (Compatibility::Incompatible(reason), _) => Err((reason, Some(connection))),
670 (Compatibility::Compatible, Err(reason)) => Err(unreachable(reason.to_string())),
671 }
672 }
673
674 pub(crate) fn stop_agent(
678 &self,
679 connection: &Connection,
680 pid: u32,
681 socket: &str,
682 timeout: Duration,
683 ) -> Result<Stopped, DeployError> {
684 let _ = connection.call(Call::Shutdown, SHUTDOWN_DEADLINE);
685 connection.close();
686 let stopped = self.finish(pid, socket, timeout)?;
687 self.forget(pid)?;
688 Ok(stopped)
689 }
690
691 fn replace(
695 &self,
696 record: &Record,
697 reason: &Incompatibility,
698 connection: Option<Connection>,
699 options: &DeployOptions,
700 ) -> Result<Stopped, DeployError> {
701 let answers = matches!(
702 reason,
703 Incompatibility::Version { .. } | Incompatibility::Build { .. }
704 );
705 if let (true, Some(connection)) = (answers, &connection) {
706 let _ = connection.call(Call::Shutdown, SHUTDOWN_DEADLINE);
707 }
708 drop(connection);
709 self.finish(record.pid, &record.socket, options.exit_timeout)
710 }
711
712 fn finish(&self, pid: u32, socket: &str, timeout: Duration) -> Result<Stopped, DeployError> {
715 if self.wait_gone(pid, socket, timeout)? {
716 return Ok(Stopped::Exited);
717 }
718 self.kill(pid, socket)?;
719 if self.wait_gone(pid, socket, Duration::from_secs(2))? {
720 Ok(Stopped::Killed)
721 } else {
722 Err(DeployError::Exited {
723 output: format!("agent {pid} on {socket} survived SIGKILL"),
724 exit: None,
725 })
726 }
727 }
728
729 fn wait_gone(&self, pid: u32, socket: &str, timeout: Duration) -> Result<bool, adb::Error> {
732 let polls = (timeout.as_millis() / 20).max(1);
733 let script = format!(
734 "i=0; while grep -qF {socket} /proc/{pid}/cmdline 2>/dev/null; do \
735 i=$((i+1)); [ $i -gt {polls} ] && exit 1; sleep 0.02; done; exit 0"
736 );
737 Ok(self.device.shell(&script)?.success())
738 }
739
740 fn kill(&self, pid: u32, socket: &str) -> Result<(), adb::Error> {
742 self.device.shell(&format!(
743 "grep -qF {socket} /proc/{pid}/cmdline 2>/dev/null && kill -9 {pid}"
744 ))?;
745 Ok(())
746 }
747
748 fn kill_socket(&self, socket: &str) -> Result<(), adb::Error> {
750 for agent in self.running()? {
751 if agent.socket == socket {
752 self.kill(agent.pid, socket)?;
753 }
754 }
755 Ok(())
756 }
757
758 fn forget(&self, pid: u32) -> Result<(), DeployError> {
760 let serial = self.device.serial();
761 if self.records.get(serial).is_some_and(|r| r.pid == pid) {
762 self.records.remove(serial).map_err(DeployError::Records)?;
763 }
764 Ok(())
765 }
766
767 fn host_config(&self, token: &str) -> HostConfig {
768 HostConfig {
769 token: Some(token.to_owned()),
770 ..HostConfig::default()
771 }
772 }
773
774 fn launch(
777 &self,
778 options: &DeployOptions,
779 replaced: Option<Replaced>,
780 mut timings: Timings,
781 started: Instant,
782 ) -> Result<Attached, DeployError> {
783 let socket = SocketName::generate(self.jar.version(), PROTOCOL_VERSION, &self.jar.build())
784 .to_string();
785 let token = random_hex(16);
786 let remote = remote_jar(&socket);
787 let pushing = Instant::now();
788 push_jar(&self.device, &self.jar, &remote)?;
789 timings.push = Some(pushing.elapsed());
790 let command = launch_command(
791 &remote,
792 &socket,
793 options.idle_exit,
794 options.force,
795 &options.agent_args,
796 );
797 let spawning = Instant::now();
798 let spawned = self.spawn(&command, &token, options.ready_timeout);
799 timings.spawn_to_ready = Some(spawning.elapsed());
800 let _ = self.device.shell(&format!("rm -f {remote}"));
802 let ready = match spawned? {
803 Spawned::Ready(ready) => ready,
804 Spawned::Conflict(line) => {
805 return Err(DeployError::Conflict(Conflict::from_line(line)));
806 }
807 Spawned::Exited { output, exit } => {
808 return Err(DeployError::Exited { output, exit });
809 }
810 Spawned::Timeout { output } => {
811 let _ = self.kill_socket(&socket);
812 return Err(DeployError::NoReadyLine {
813 waited: options.ready_timeout,
814 output,
815 });
816 }
817 };
818 let connecting = Instant::now();
819 let connection = self.device.open_abstract(&socket).and_then(|stream| {
820 Connection::open(stream, self.host_config(&token)).map_err(|source| adb::Error::Io {
821 request: format!("connect to the agent on {socket}"),
822 source,
823 })
824 });
825 let connection = match connection {
826 Ok(connection) => connection,
827 Err(error) => {
828 let _ = self.kill(ready.pid, &socket);
829 return Err(error.into());
830 }
831 };
832 let negotiated = match connection.handshake(options.handshake_timeout) {
833 Ok(negotiated) => negotiated,
834 Err(reason) => {
835 drop(connection);
836 let _ = self.kill(ready.pid, &socket);
837 return Err(DeployError::Handshake { socket, reason });
838 }
839 };
840 timings.handshake = connecting.elapsed();
841 let expected = self.jar.agent_name();
842 if negotiated.agent != expected {
843 let _ = connection.call(Call::Shutdown, SHUTDOWN_DEADLINE);
844 drop(connection);
845 let _ = self.finish(ready.pid, &socket, options.exit_timeout);
846 return Err(DeployError::WrongVersion {
847 reported: negotiated.agent,
848 expected,
849 jar: self.jar.path().map(PathBuf::from),
850 });
851 }
852 let agent = AgentInfo {
853 serial: self.device.serial().to_owned(),
854 socket: socket.clone(),
855 pid: ready.pid,
856 agent: negotiated.agent,
857 protocol: negotiated.version,
858 capabilities: negotiated.capabilities,
859 token,
860 };
861 self.records
862 .put(Record {
863 serial: agent.serial.clone(),
864 socket,
865 pid: agent.pid,
866 token: agent.token.clone(),
867 agent: agent.agent.clone(),
868 protocol: agent.protocol,
869 build: self.jar.build(),
870 })
871 .map_err(DeployError::Records)?;
872 timings.total = started.elapsed();
873 let took_over = ready.forced.clone();
874 Ok(Attached {
875 connection,
876 agent,
877 outcome: Outcome::Launched(Box::new(Launched {
878 ready,
879 replaced,
880 took_over,
881 })),
882 timings,
883 deployer: self.clone(),
884 })
885 }
886
887 fn spawn(&self, command: &str, token: &str, timeout: Duration) -> Result<Spawned, DeployError> {
891 let mut stream = self.device.shell_stream(command)?;
892 stream.write_stdin(format!("{token}\n").as_bytes())?;
893 stream.close_stdin()?;
894 let deadline = Instant::now() + timeout;
895 let mut transcript = Transcript::default();
896 let mut exit = None;
897 loop {
898 let left = deadline.saturating_duration_since(Instant::now());
899 if left.is_zero() {
900 return Ok(Spawned::Timeout {
901 output: transcript.text(),
902 });
903 }
904 stream.set_read_timeout(Some(left))?;
905 let packet = match stream.read_packet() {
906 Ok(packet) => packet,
907 Err(error) if error.is_timeout() => continue,
908 Err(error) => return Err(error.into()),
909 };
910 match packet {
911 Some(ShellPacket::Stdout(data)) => {
912 for line in transcript.stdout(&data) {
913 if let Some(ready) = parse_ready(&line) {
914 return Ok(Spawned::Ready(ready));
915 }
916 if let Some(conflict) = parse_conflict(&line) {
917 let _ = stream.set_read_timeout(Some(CONFLICT_EXIT_WAIT));
920 while let Ok(Some(_)) = stream.read_packet() {}
921 return Ok(Spawned::Conflict(conflict));
922 }
923 }
924 }
925 Some(ShellPacket::Stderr(data)) => {
926 transcript.stderr.push_str(&String::from_utf8_lossy(&data));
927 }
928 Some(ShellPacket::Exit(code)) => exit = Some(code),
929 None => {
930 return Ok(Spawned::Exited {
931 output: transcript.text(),
932 exit,
933 });
934 }
935 }
936 }
937 }
938
939 pub fn stop_recorded(&self, timeout: Duration) -> Result<Option<Stopped>, DeployError> {
943 let serial = self.device.serial();
944 let Some(record) = self.records.get(serial) else {
945 return Ok(None);
946 };
947 let running = self
948 .running()?
949 .into_iter()
950 .any(|a| a.pid == record.pid && a.socket == record.socket);
951 let stopped = if running {
952 if let Ok(stream) = self.device.open_abstract(&record.socket)
953 && let Ok(connection) = Connection::open(stream, self.host_config(&record.token))
954 && connection.handshake(timeout).is_ok()
955 {
956 let _ = connection.call(Call::Shutdown, SHUTDOWN_DEADLINE);
957 }
958 self.finish(record.pid, &record.socket, timeout)?
959 } else {
960 Stopped::Gone
961 };
962 self.records.remove(serial).map_err(DeployError::Records)?;
963 Ok(Some(stopped))
964 }
965}
966
967#[cfg(test)]
968mod tests {
969 use super::*;
970 use crate::agent::launch::SocketName;
971 use protocol::session::HandshakeError;
972
973 const EXPECTED: &str = "mobile-agent-android 0.1.0";
974
975 fn negotiated(agent: &str) -> Negotiated {
976 Negotiated {
977 version: 1,
978 max_frame: 1 << 24,
979 capabilities: Capabilities::TREE,
980 agent: agent.into(),
981 host_versions: vec![1],
982 agent_versions: vec![1],
983 }
984 }
985
986 fn running(pid: u32, socket: &str) -> RunningAgent {
987 RunningAgent {
988 pid,
989 socket: socket.into(),
990 name: SocketName::parse(socket),
991 }
992 }
993
994 #[test]
995 fn the_same_build_on_the_same_protocol_is_compatible() {
996 assert_eq!(
997 compatibility(&Ok(negotiated(EXPECTED)), EXPECTED),
998 Compatibility::Compatible
999 );
1000 }
1001
1002 #[test]
1005 fn an_agent_on_an_older_protocol_is_incompatible() {
1006 let refused = Err(DisconnectReason::Handshake(
1007 HandshakeError::NoCommonVersion {
1008 host: vec![1],
1009 agent: vec![0],
1010 },
1011 ));
1012 assert_eq!(
1013 compatibility(&refused, EXPECTED),
1014 Compatibility::Incompatible(Incompatibility::Protocol {
1015 agent: vec![0],
1016 host: vec![1],
1017 })
1018 );
1019 }
1020
1021 #[test]
1022 fn another_build_is_incompatible() {
1023 assert_eq!(
1024 compatibility(&Ok(negotiated("mobile-agent-android 0.0.9")), EXPECTED),
1025 Compatibility::Incompatible(Incompatibility::Version {
1026 reported: "mobile-agent-android 0.0.9".into(),
1027 expected: EXPECTED.into(),
1028 })
1029 );
1030 }
1031
1032 #[test]
1033 fn an_agent_that_does_not_answer_as_recorded_is_unreachable() {
1034 for reason in [
1035 DisconnectReason::Handshake(HandshakeError::TokenMismatch),
1036 DisconnectReason::Eof,
1037 DisconnectReason::Silent(Duration::from_secs(3)),
1038 ] {
1039 let text = reason.to_string();
1040 assert_eq!(
1041 compatibility(&Err(reason), EXPECTED),
1042 Compatibility::Incompatible(Incompatibility::Unreachable(text))
1043 );
1044 }
1045 }
1046
1047 #[test]
1048 fn incompatibilities_say_what_differs() {
1049 let protocol = Incompatibility::Protocol {
1050 agent: vec![0],
1051 host: vec![1],
1052 }
1053 .to_string();
1054 assert!(
1055 protocol.contains("[0]") && protocol.contains("[1]"),
1056 "{protocol}"
1057 );
1058 let version = Incompatibility::Version {
1059 reported: "mobile-agent-android 0.0.9".into(),
1060 expected: EXPECTED.into(),
1061 }
1062 .to_string();
1063 assert!(
1064 version.contains("0.0.9") && version.contains("0.1.0"),
1065 "{version}"
1066 );
1067 }
1068
1069 const BUILD: &str = "0123456789ab";
1070
1071 #[test]
1072 fn no_agent_process_is_absent() {
1073 assert_eq!(agent_state(&[], "0.1.0", 1, BUILD), AgentState::Absent);
1074 }
1075
1076 #[test]
1077 fn an_agent_named_for_this_build_is_running() {
1078 let agent = running(812, "mobile-agent-0.1.0-p1-0123456789ab-00ff");
1079 assert_eq!(
1080 agent_state(std::slice::from_ref(&agent), "0.1.0", 1, BUILD),
1081 AgentState::Running(agent)
1082 );
1083 }
1084
1085 #[test]
1086 fn another_version_protocol_build_or_an_unknown_name_is_incompatible() {
1087 for socket in [
1088 "mobile-agent-0.0.9-p1-0123456789ab-00ff",
1089 "mobile-agent-0.1.0-p2-0123456789ab-00ff",
1090 "mobile-agent-0.1.0-p1-ba9876543210-00ff",
1091 "mobile-agent-0.1.0-p1-00ff",
1092 "mobile-agent-3f9a1c2b7d4e",
1093 ] {
1094 let agent = running(812, socket);
1095 assert_eq!(
1096 agent_state(std::slice::from_ref(&agent), "0.1.0", 1, BUILD),
1097 AgentState::Incompatible(agent),
1098 "{socket}"
1099 );
1100 }
1101 }
1102
1103 #[test]
1104 fn a_compatible_agent_wins_over_an_incompatible_one() {
1105 let old = running(700, "mobile-agent-0.1.0-p1-ba9876543210-aa");
1106 let current = running(812, "mobile-agent-0.1.0-p1-0123456789ab-bb");
1107 assert_eq!(
1108 agent_state(&[old, current.clone()], "0.1.0", 1, BUILD),
1109 AgentState::Running(current)
1110 );
1111 }
1112
1113 #[test]
1117 fn a_recorded_agent_of_another_build_is_incompatible() {
1118 assert_eq!(build_compatibility(BUILD, BUILD), Compatibility::Compatible);
1119 assert_eq!(
1120 build_compatibility("ba9876543210", BUILD),
1121 Compatibility::Incompatible(Incompatibility::Build {
1122 running: "ba9876543210".into(),
1123 deploying: BUILD.into(),
1124 })
1125 );
1126 assert_eq!(
1127 build_compatibility("", BUILD),
1128 Compatibility::Incompatible(Incompatibility::Build {
1129 running: String::new(),
1130 deploying: BUILD.into(),
1131 })
1132 );
1133 let text = Incompatibility::Build {
1134 running: "ba9876543210".into(),
1135 deploying: BUILD.into(),
1136 }
1137 .to_string();
1138 assert!(
1139 text.contains("ba9876543210") && text.contains(BUILD),
1140 "{text}"
1141 );
1142 }
1143
1144 #[test]
1147 fn a_conflict_names_the_holder_and_the_force_option() {
1148 let conflict = Conflict {
1149 holder: Some(Holder {
1150 name: "uiautomator".into(),
1151 pid: Some(4702),
1152 }),
1153 detail: "UI automation is held by another process, likely uiautomator (pid 4702: \
1154 uiautomator events); stop it, or run the agent with --force to take the \
1155 role over"
1156 .into(),
1157 };
1158 let message = conflict.message();
1159 assert!(message.contains("uiautomator (pid 4702)"), "{message}");
1160 assert!(message.contains("force"), "{message}");
1161 assert!(
1162 DeployError::Conflict(conflict)
1163 .to_string()
1164 .contains("uiautomator")
1165 );
1166 }
1167
1168 #[test]
1169 fn an_agent_this_host_did_not_start_is_a_conflict_naming_it() {
1170 let conflict = Conflict::foreign_agent(&running(900, "mobile-agent-0.1.0-p1-cc"));
1171 assert_eq!(
1172 conflict.holder,
1173 Some(Holder {
1174 name: "mobile-agent".into(),
1175 pid: Some(900),
1176 })
1177 );
1178 let message = conflict.message();
1179 assert!(message.contains("mobile-agent-0.1.0-p1-cc"), "{message}");
1180 assert!(message.contains("force"), "{message}");
1181 }
1182
1183 #[test]
1184 fn deploy_errors_explain_themselves() {
1185 let exited = DeployError::Exited {
1186 output: "err: cannot listen on @s: Address already in use".into(),
1187 exit: Some(1),
1188 }
1189 .to_string();
1190 assert!(exited.contains("cannot listen"), "{exited}");
1191 let wrong = DeployError::WrongVersion {
1192 reported: "mobile-agent-android 0.0.9".into(),
1193 expected: EXPECTED.into(),
1194 jar: Some(PathBuf::from("/tmp/a.jar")),
1195 }
1196 .to_string();
1197 assert!(wrong.contains("agent-android/build.sh"), "{wrong}");
1198 assert!(wrong.contains("/tmp/a.jar"), "{wrong}");
1199 }
1200
1201 #[test]
1202 fn the_defaults_match_the_agents_own() {
1203 let options = DeployOptions::default();
1204 assert!(!options.force);
1205 assert_eq!(options.idle_exit, Duration::from_secs(30));
1206 assert!(options.ready_timeout >= Duration::from_secs(5));
1207 }
1208}