Skip to main content

canic_host/
dfx.rs

1use std::{error::Error, fmt, path::Path, process::Command};
2
3///
4/// DfxCommandError
5///
6
7#[derive(Debug)]
8pub enum DfxCommandError {
9    Io(std::io::Error),
10    Failed { command: String, stderr: String },
11}
12
13impl fmt::Display for DfxCommandError {
14    // Render dfx command failures with the command line and captured diagnostics.
15    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
16        match self {
17            Self::Io(err) => write!(formatter, "{err}"),
18            Self::Failed { command, stderr } => {
19                write!(formatter, "dfx command failed: {command}\n{stderr}")
20            }
21        }
22    }
23}
24
25impl Error for DfxCommandError {
26    // Preserve the underlying I/O error as the source when command execution fails locally.
27    fn source(&self) -> Option<&(dyn Error + 'static)> {
28        match self {
29            Self::Io(err) => Some(err),
30            Self::Failed { .. } => None,
31        }
32    }
33}
34
35impl From<std::io::Error> for DfxCommandError {
36    // Convert process-spawn failures into the shared dfx command error type.
37    fn from(err: std::io::Error) -> Self {
38        Self::Io(err)
39    }
40}
41
42///
43/// Dfx
44///
45
46#[derive(Clone, Debug, Eq, PartialEq)]
47pub struct Dfx {
48    executable: String,
49    network: Option<String>,
50}
51
52impl Dfx {
53    /// Build a dfx command context from an executable path and optional network.
54    #[must_use]
55    pub fn new(executable: impl Into<String>, network: Option<String>) -> Self {
56        Self {
57            executable: executable.into(),
58            network,
59        }
60    }
61
62    /// Return the optional network name carried by this command context.
63    #[must_use]
64    pub fn network(&self) -> Option<&str> {
65        self.network.as_deref()
66    }
67
68    /// Build a `dfx canister ...` command with optional network args applied.
69    #[must_use]
70    pub fn canister_command(&self) -> Command {
71        let mut command = Command::new(&self.executable);
72        command.arg("canister");
73        add_network_args(&mut command, self.network());
74        command
75    }
76
77    /// Resolve one project canister id, returning `None` when the id is absent.
78    pub fn canister_id_optional(&self, name: &str) -> Result<Option<String>, DfxCommandError> {
79        let mut command = self.canister_command();
80        command.args(["id", name]);
81        match run_output(&mut command) {
82            Ok(output) => Ok(Some(output)),
83            Err(DfxCommandError::Failed { command, stderr }) if canister_id_missing(&stderr) => {
84                let _ = command;
85                Ok(None)
86            }
87            Err(err) => Err(err),
88        }
89    }
90
91    /// Resolve one project canister id.
92    pub fn canister_id(&self, name: &str) -> Result<String, DfxCommandError> {
93        let mut command = self.canister_command();
94        command.args(["id", name]);
95        run_output(&mut command)
96    }
97
98    /// Call one canister method with optional dfx JSON output.
99    pub fn canister_call_output(
100        &self,
101        canister: &str,
102        method: &str,
103        output: Option<&str>,
104    ) -> Result<String, DfxCommandError> {
105        let mut command = self.canister_command();
106        command.args(["call", canister, method]);
107        if let Some(output) = output {
108            command.args(["--output", output]);
109        }
110        run_output(&mut command)
111    }
112
113    /// List snapshot ids for one canister.
114    pub fn snapshot_list(&self, canister: &str) -> Result<String, DfxCommandError> {
115        let mut command = self.canister_command();
116        command.args(["snapshot", "list", canister]);
117        run_output(&mut command)
118    }
119
120    /// Create one canister snapshot and return combined stdout/stderr text.
121    pub fn snapshot_create(&self, canister: &str) -> Result<String, DfxCommandError> {
122        let mut command = self.canister_command();
123        command.args(["snapshot", "create", canister]);
124        run_output_with_stderr(&mut command)
125    }
126
127    /// Stop one canister.
128    pub fn stop_canister(&self, canister: &str) -> Result<(), DfxCommandError> {
129        let mut command = self.canister_command();
130        command.args(["stop", canister]);
131        run_status(&mut command)
132    }
133
134    /// Start one canister.
135    pub fn start_canister(&self, canister: &str) -> Result<(), DfxCommandError> {
136        let mut command = self.canister_command();
137        command.args(["start", canister]);
138        run_status(&mut command)
139    }
140
141    /// Download one canister snapshot into an artifact directory.
142    pub fn snapshot_download(
143        &self,
144        canister: &str,
145        snapshot_id: &str,
146        artifact_path: &Path,
147    ) -> Result<(), DfxCommandError> {
148        let mut command = self.canister_command();
149        command.args(["snapshot", "download", canister, snapshot_id, "--dir"]);
150        command.arg(artifact_path);
151        run_status(&mut command)
152    }
153
154    /// Render a dry-run snapshot-create command.
155    #[must_use]
156    pub fn snapshot_create_display(&self, canister: &str) -> String {
157        let mut command = self.canister_command();
158        command.args(["snapshot", "create", canister]);
159        command_display(&command)
160    }
161
162    /// Render a dry-run snapshot-download command.
163    #[must_use]
164    pub fn snapshot_download_display(
165        &self,
166        canister: &str,
167        snapshot_id: &str,
168        artifact_path: &Path,
169    ) -> String {
170        let mut command = self.canister_command();
171        command.args(["snapshot", "download", canister, snapshot_id, "--dir"]);
172        command.arg(artifact_path);
173        command_display(&command)
174    }
175
176    /// Render a dry-run stop command.
177    #[must_use]
178    pub fn stop_canister_display(&self, canister: &str) -> String {
179        let mut command = self.canister_command();
180        command.args(["stop", canister]);
181        command_display(&command)
182    }
183
184    /// Render a dry-run start command.
185    #[must_use]
186    pub fn start_canister_display(&self, canister: &str) -> String {
187        let mut command = self.canister_command();
188        command.args(["start", canister]);
189        command_display(&command)
190    }
191}
192
193/// Add optional `--network` arguments after `dfx canister`.
194pub fn add_network_args(command: &mut Command, network: Option<&str>) {
195    if let Some(network) = network {
196        command.args(["--network", network]);
197    }
198}
199
200/// Execute a command and capture trimmed stdout.
201pub fn run_output(command: &mut Command) -> Result<String, DfxCommandError> {
202    let display = command_display(command);
203    let output = command.output()?;
204    if output.status.success() {
205        Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
206    } else {
207        Err(DfxCommandError::Failed {
208            command: display,
209            stderr: command_stderr(&output),
210        })
211    }
212}
213
214/// Execute a command and capture stdout plus stderr on success.
215pub fn run_output_with_stderr(command: &mut Command) -> Result<String, DfxCommandError> {
216    let display = command_display(command);
217    let output = command.output()?;
218    if output.status.success() {
219        let mut text = String::from_utf8_lossy(&output.stdout).to_string();
220        text.push_str(&String::from_utf8_lossy(&output.stderr));
221        Ok(text.trim().to_string())
222    } else {
223        Err(DfxCommandError::Failed {
224            command: display,
225            stderr: command_stderr(&output),
226        })
227    }
228}
229
230/// Execute a command and require a successful status.
231pub fn run_status(command: &mut Command) -> Result<(), DfxCommandError> {
232    let display = command_display(command);
233    let output = command.output()?;
234    if output.status.success() {
235        Ok(())
236    } else {
237        Err(DfxCommandError::Failed {
238            command: display,
239            stderr: command_stderr(&output),
240        })
241    }
242}
243
244/// Render a command for diagnostics and dry-run previews.
245#[must_use]
246pub fn command_display(command: &Command) -> String {
247    let mut parts = vec![command.get_program().to_string_lossy().to_string()];
248    parts.extend(
249        command
250            .get_args()
251            .map(|arg| arg.to_string_lossy().to_string()),
252    );
253    parts.join(" ")
254}
255
256/// Detect dfx's missing-canister-id diagnostic.
257#[must_use]
258pub fn canister_id_missing(stderr: &str) -> bool {
259    stderr.contains("Cannot find canister id")
260}
261
262// Prefer stderr, but keep stdout diagnostics for dfx commands that report there.
263fn command_stderr(output: &std::process::Output) -> String {
264    let stderr = String::from_utf8_lossy(&output.stderr);
265    if stderr.trim().is_empty() {
266        String::from_utf8_lossy(&output.stdout).to_string()
267    } else {
268        stderr.to_string()
269    }
270}