1use crate::default_network;
2use std::{error::Error, fmt, path::Path, process::Command};
3
4#[derive(Clone, Debug, Eq, PartialEq)]
9pub struct DfxRawOutput {
10 pub success: bool,
11 pub status: String,
12 pub stdout: Vec<u8>,
13 pub stderr: Vec<u8>,
14}
15
16#[derive(Debug)]
21pub enum DfxCommandError {
22 Io(std::io::Error),
23 Failed { command: String, stderr: String },
24 SnapshotIdUnavailable { output: String },
25}
26
27impl fmt::Display for DfxCommandError {
28 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
30 match self {
31 Self::Io(err) => write!(formatter, "{err}"),
32 Self::Failed { command, stderr } => {
33 write!(formatter, "dfx command failed: {command}\n{stderr}")
34 }
35 Self::SnapshotIdUnavailable { output } => {
36 write!(
37 formatter,
38 "could not parse snapshot id from dfx output: {output}"
39 )
40 }
41 }
42 }
43}
44
45impl Error for DfxCommandError {
46 fn source(&self) -> Option<&(dyn Error + 'static)> {
48 match self {
49 Self::Io(err) => Some(err),
50 Self::Failed { .. } | Self::SnapshotIdUnavailable { .. } => None,
51 }
52 }
53}
54
55impl From<std::io::Error> for DfxCommandError {
56 fn from(err: std::io::Error) -> Self {
58 Self::Io(err)
59 }
60}
61
62#[derive(Clone, Debug, Eq, PartialEq)]
67pub struct Dfx {
68 executable: String,
69 network: Option<String>,
70}
71
72impl Dfx {
73 #[must_use]
75 pub fn new(executable: impl Into<String>, network: Option<String>) -> Self {
76 Self {
77 executable: executable.into(),
78 network,
79 }
80 }
81
82 #[must_use]
84 pub fn network(&self) -> Option<&str> {
85 self.network.as_deref()
86 }
87
88 #[must_use]
90 pub fn command(&self) -> Command {
91 Command::new(&self.executable)
92 }
93
94 #[must_use]
96 pub fn command_in(&self, cwd: &Path) -> Command {
97 let mut command = self.command();
98 command.current_dir(cwd);
99 command
100 }
101
102 #[must_use]
104 pub fn canister_command(&self) -> Command {
105 let mut command = self.command();
106 command.arg("canister");
107 add_network_args(&mut command, self.network());
108 command
109 }
110
111 pub fn ping(&self) -> Result<(), DfxCommandError> {
113 let mut command = self.command();
114 command.arg("ping");
115 let network = self.network().map_or_else(default_network, str::to_string);
116 command.arg(network);
117 run_status(&mut command)
118 }
119
120 pub fn canister_id_optional(&self, name: &str) -> Result<Option<String>, DfxCommandError> {
122 let mut command = self.canister_command();
123 command.args(["id", name]);
124 match run_output(&mut command) {
125 Ok(output) => Ok(Some(output)),
126 Err(DfxCommandError::Failed { command, stderr }) if canister_id_missing(&stderr) => {
127 let _ = command;
128 Ok(None)
129 }
130 Err(err) => Err(err),
131 }
132 }
133
134 pub fn canister_id(&self, name: &str) -> Result<String, DfxCommandError> {
136 let mut command = self.canister_command();
137 command.args(["id", name]);
138 run_output(&mut command)
139 }
140
141 pub fn canister_call_output(
143 &self,
144 canister: &str,
145 method: &str,
146 output: Option<&str>,
147 ) -> Result<String, DfxCommandError> {
148 let mut command = self.canister_command();
149 command.args(["call", canister, method]);
150 if let Some(output) = output {
151 command.args(["--output", output]);
152 }
153 run_output(&mut command)
154 }
155
156 pub fn snapshot_list(&self, canister: &str) -> Result<String, DfxCommandError> {
158 let mut command = self.canister_command();
159 command.args(["snapshot", "list", canister]);
160 run_output(&mut command)
161 }
162
163 pub fn snapshot_create(&self, canister: &str) -> Result<String, DfxCommandError> {
165 let mut command = self.canister_command();
166 command.args(["snapshot", "create", canister]);
167 run_output_with_stderr(&mut command)
168 }
169
170 pub fn snapshot_create_id(&self, canister: &str) -> Result<String, DfxCommandError> {
172 let before = self.snapshot_list_ids(canister)?;
173 let output = self.snapshot_create(canister)?;
174 if let Some(snapshot_id) = parse_snapshot_id(&output) {
175 return Ok(snapshot_id);
176 }
177
178 let before = before
179 .into_iter()
180 .collect::<std::collections::BTreeSet<_>>();
181 let mut new_ids = self
182 .snapshot_list_ids(canister)?
183 .into_iter()
184 .filter(|snapshot_id| !before.contains(snapshot_id))
185 .collect::<Vec<_>>();
186 if new_ids.len() == 1 {
187 Ok(new_ids.remove(0))
188 } else {
189 Err(DfxCommandError::SnapshotIdUnavailable { output })
190 }
191 }
192
193 pub fn snapshot_list_ids(&self, canister: &str) -> Result<Vec<String>, DfxCommandError> {
195 let output = self.snapshot_list(canister)?;
196 Ok(parse_snapshot_list_ids(&output))
197 }
198
199 pub fn stop_canister(&self, canister: &str) -> Result<(), DfxCommandError> {
201 let mut command = self.canister_command();
202 command.args(["stop", canister]);
203 run_status(&mut command)
204 }
205
206 pub fn start_canister(&self, canister: &str) -> Result<(), DfxCommandError> {
208 let mut command = self.canister_command();
209 command.args(["start", canister]);
210 run_status(&mut command)
211 }
212
213 pub fn snapshot_download(
215 &self,
216 canister: &str,
217 snapshot_id: &str,
218 artifact_path: &Path,
219 ) -> Result<(), DfxCommandError> {
220 let mut command = self.canister_command();
221 command.args(["snapshot", "download", canister, snapshot_id, "--dir"]);
222 command.arg(artifact_path);
223 run_status(&mut command)
224 }
225
226 #[must_use]
228 pub fn snapshot_create_display(&self, canister: &str) -> String {
229 let mut command = self.canister_command();
230 command.args(["snapshot", "create", canister]);
231 command_display(&command)
232 }
233
234 #[must_use]
236 pub fn snapshot_download_display(
237 &self,
238 canister: &str,
239 snapshot_id: &str,
240 artifact_path: &Path,
241 ) -> String {
242 let mut command = self.canister_command();
243 command.args(["snapshot", "download", canister, snapshot_id, "--dir"]);
244 command.arg(artifact_path);
245 command_display(&command)
246 }
247
248 #[must_use]
250 pub fn stop_canister_display(&self, canister: &str) -> String {
251 let mut command = self.canister_command();
252 command.args(["stop", canister]);
253 command_display(&command)
254 }
255
256 #[must_use]
258 pub fn start_canister_display(&self, canister: &str) -> String {
259 let mut command = self.canister_command();
260 command.args(["start", canister]);
261 command_display(&command)
262 }
263}
264
265#[must_use]
267pub fn default_command() -> Command {
268 Dfx::new("dfx", None).command()
269}
270
271#[must_use]
273pub fn default_command_in(cwd: &Path) -> Command {
274 Dfx::new("dfx", None).command_in(cwd)
275}
276
277pub fn add_network_args(command: &mut Command, network: Option<&str>) {
279 if let Some(network) = network {
280 command.args(["--network", network]);
281 }
282}
283
284pub fn run_output(command: &mut Command) -> Result<String, DfxCommandError> {
286 let display = command_display(command);
287 let output = command.output()?;
288 if output.status.success() {
289 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
290 } else {
291 Err(DfxCommandError::Failed {
292 command: display,
293 stderr: command_stderr(&output),
294 })
295 }
296}
297
298pub fn run_output_with_stderr(command: &mut Command) -> Result<String, DfxCommandError> {
300 let display = command_display(command);
301 let output = command.output()?;
302 if output.status.success() {
303 let mut text = String::from_utf8_lossy(&output.stdout).to_string();
304 text.push_str(&String::from_utf8_lossy(&output.stderr));
305 Ok(text.trim().to_string())
306 } else {
307 Err(DfxCommandError::Failed {
308 command: display,
309 stderr: command_stderr(&output),
310 })
311 }
312}
313
314pub fn run_status(command: &mut Command) -> Result<(), DfxCommandError> {
316 let display = command_display(command);
317 let output = command.output()?;
318 if output.status.success() {
319 Ok(())
320 } else {
321 Err(DfxCommandError::Failed {
322 command: display,
323 stderr: command_stderr(&output),
324 })
325 }
326}
327
328pub fn run_raw_output(program: &str, args: &[String]) -> Result<DfxRawOutput, std::io::Error> {
330 let output = Command::new(program).args(args).output()?;
331 Ok(DfxRawOutput {
332 success: output.status.success(),
333 status: exit_status_label(output.status),
334 stdout: output.stdout,
335 stderr: output.stderr,
336 })
337}
338
339#[must_use]
341pub fn command_display(command: &Command) -> String {
342 let mut parts = vec![command.get_program().to_string_lossy().to_string()];
343 parts.extend(
344 command
345 .get_args()
346 .map(|arg| arg.to_string_lossy().to_string()),
347 );
348 parts.join(" ")
349}
350
351#[must_use]
353pub fn canister_id_missing(stderr: &str) -> bool {
354 stderr.contains("Cannot find canister id")
355}
356
357#[must_use]
359pub fn parse_snapshot_id(output: &str) -> Option<String> {
360 output
361 .split(|c: char| c.is_whitespace() || matches!(c, '"' | '\'' | ':' | ','))
362 .filter(|part| !part.is_empty())
363 .rev()
364 .find(|part| {
365 part.chars()
366 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'))
367 })
368 .map(str::to_string)
369}
370
371#[must_use]
373pub fn parse_snapshot_list_ids(output: &str) -> Vec<String> {
374 output
375 .lines()
376 .filter_map(|line| {
377 line.split_once(':')
378 .map(|(snapshot_id, _)| snapshot_id.trim())
379 })
380 .filter(|snapshot_id| !snapshot_id.is_empty())
381 .map(str::to_string)
382 .collect()
383}
384
385fn command_stderr(output: &std::process::Output) -> String {
387 let stderr = String::from_utf8_lossy(&output.stderr);
388 if stderr.trim().is_empty() {
389 String::from_utf8_lossy(&output.stdout).to_string()
390 } else {
391 stderr.to_string()
392 }
393}
394
395fn exit_status_label(status: std::process::ExitStatus) -> String {
397 status
398 .code()
399 .map_or_else(|| "signal".to_string(), |code| code.to_string())
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405
406 #[test]
408 fn parses_snapshot_id_from_output() {
409 let snapshot_id = parse_snapshot_id("Created snapshot: snap_abc-123\n");
410
411 assert_eq!(snapshot_id.as_deref(), Some("snap_abc-123"));
412 }
413
414 #[test]
416 fn parses_snapshot_ids_from_list_output() {
417 let snapshot_ids = parse_snapshot_list_ids(
418 "0000000000000000ffffffffff9000050101: size 10\n\
419 0000000000000000ffffffffff9000050102: size 12\n",
420 );
421
422 assert_eq!(
423 snapshot_ids,
424 vec![
425 "0000000000000000ffffffffff9000050101",
426 "0000000000000000ffffffffff9000050102"
427 ]
428 );
429 }
430}