1use std::{error::Error, fmt, path::Path, process::Command};
2
3#[derive(Debug)]
8pub enum DfxCommandError {
9 Io(std::io::Error),
10 Failed { command: String, stderr: String },
11}
12
13impl fmt::Display for DfxCommandError {
14 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 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 fn from(err: std::io::Error) -> Self {
38 Self::Io(err)
39 }
40}
41
42#[derive(Clone, Debug, Eq, PartialEq)]
47pub struct Dfx {
48 executable: String,
49 network: Option<String>,
50}
51
52impl Dfx {
53 #[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 #[must_use]
64 pub fn network(&self) -> Option<&str> {
65 self.network.as_deref()
66 }
67
68 #[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 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 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 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 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 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 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 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 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 #[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 #[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 #[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 #[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
193pub fn add_network_args(command: &mut Command, network: Option<&str>) {
195 if let Some(network) = network {
196 command.args(["--network", network]);
197 }
198}
199
200pub 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
214pub 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
230pub 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#[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#[must_use]
258pub fn canister_id_missing(stderr: &str) -> bool {
259 stderr.contains("Cannot find canister id")
260}
261
262fn 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}