opendev_docker/
session.rs1use regex::Regex;
7use tracing::debug;
8
9use crate::errors::{DockerError, Result};
10use crate::models::{BashObservation, CheckMode};
11
12fn strip_ansi(s: &str) -> String {
14 let re = Regex::new(r"\x1B\[[\d;]*[A-Za-z]|\x1B[@-_][0-?]*[ -/]*[@-~]").unwrap();
15 re.replace_all(s, "").replace("\r\n", "\n")
16}
17
18pub struct DockerSession {
22 container_id: String,
23 name: String,
24 working_dir: Option<String>,
25}
26
27impl DockerSession {
28 pub fn new(container_id: &str, name: &str) -> Self {
30 Self {
31 container_id: container_id.to_string(),
32 name: name.to_string(),
33 working_dir: None,
34 }
35 }
36
37 pub fn set_working_dir(&mut self, dir: impl Into<String>) {
39 self.working_dir = Some(dir.into());
40 }
41
42 pub fn name(&self) -> &str {
44 &self.name
45 }
46
47 pub fn container_id(&self) -> &str {
49 &self.container_id
50 }
51
52 pub async fn exec_command(
54 &self,
55 command: &str,
56 timeout_secs: f64,
57 check: CheckMode,
58 ) -> Result<BashObservation> {
59 let mut args: Vec<String> = vec!["exec".into()];
60
61 if let Some(ref wd) = self.working_dir {
62 args.extend(["--workdir".into(), wd.clone()]);
63 }
64
65 args.push(self.container_id.clone());
66 args.extend(["bash".into(), "-c".into(), command.into()]);
67
68 debug!("docker exec in '{}': {}", self.name, command);
69
70 let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
71 let output = tokio::time::timeout(
72 std::time::Duration::from_secs_f64(timeout_secs),
73 tokio::process::Command::new("docker")
74 .args(&arg_refs)
75 .output(),
76 )
77 .await
78 .map_err(|_| DockerError::Timeout {
79 seconds: timeout_secs,
80 operation: format!("docker exec: {command}"),
81 })?
82 .map_err(|e| DockerError::CommandFailed {
83 message: format!("docker exec failed: {e}"),
84 stderr: String::new(),
85 })?;
86
87 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
88 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
89 let exit_code = output.status.code().unwrap_or(-1);
90
91 let combined = strip_ansi(&if stderr.is_empty() {
92 stdout
93 } else if stdout.is_empty() {
94 stderr
95 } else {
96 format!("{stdout}{stderr}")
97 });
98
99 let failure_reason = if exit_code != 0 {
100 Some(format!("Exit code {exit_code}"))
101 } else {
102 None
103 };
104
105 if check == CheckMode::Raise && exit_code != 0 {
106 return Err(DockerError::NonZeroExit {
107 exit_code,
108 command: command.to_string(),
109 output: combined,
110 });
111 }
112
113 Ok(BashObservation {
114 output: combined.trim().to_string(),
115 exit_code: if check == CheckMode::Ignore {
116 None
117 } else {
118 Some(exit_code)
119 },
120 failure_reason,
121 })
122 }
123
124 pub async fn copy_file_in(&self, host_path: &str, container_path: &str) -> Result<()> {
126 let dest = format!("{}:{}", self.container_id, container_path);
127 let output = tokio::process::Command::new("docker")
128 .args(["cp", host_path, &dest])
129 .output()
130 .await
131 .map_err(|e| DockerError::CommandFailed {
132 message: format!("docker cp failed: {e}"),
133 stderr: String::new(),
134 })?;
135
136 if !output.status.success() {
137 return Err(DockerError::CommandFailed {
138 message: "docker cp into container failed".into(),
139 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
140 });
141 }
142
143 Ok(())
144 }
145
146 pub async fn copy_file_out(&self, container_path: &str, host_path: &str) -> Result<()> {
148 let src = format!("{}:{}", self.container_id, container_path);
149 let output = tokio::process::Command::new("docker")
150 .args(["cp", &src, host_path])
151 .output()
152 .await
153 .map_err(|e| DockerError::CommandFailed {
154 message: format!("docker cp failed: {e}"),
155 stderr: String::new(),
156 })?;
157
158 if !output.status.success() {
159 return Err(DockerError::CommandFailed {
160 message: "docker cp from container failed".into(),
161 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
162 });
163 }
164
165 Ok(())
166 }
167
168 pub async fn interrupt(&self) -> Result<String> {
170 let obs = self
171 .exec_command("kill -INT -1 2>/dev/null; true", 5.0, CheckMode::Ignore)
172 .await?;
173 Ok(obs.output)
174 }
175}
176
177#[cfg(test)]
178#[path = "session_tests.rs"]
179mod tests;