use std::{error::Error, fmt, path::Path, process::Command};
#[derive(Debug)]
pub enum DfxCommandError {
Io(std::io::Error),
Failed { command: String, stderr: String },
}
impl fmt::Display for DfxCommandError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(err) => write!(formatter, "{err}"),
Self::Failed { command, stderr } => {
write!(formatter, "dfx command failed: {command}\n{stderr}")
}
}
}
}
impl Error for DfxCommandError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Io(err) => Some(err),
Self::Failed { .. } => None,
}
}
}
impl From<std::io::Error> for DfxCommandError {
fn from(err: std::io::Error) -> Self {
Self::Io(err)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Dfx {
executable: String,
network: Option<String>,
}
impl Dfx {
#[must_use]
pub fn new(executable: impl Into<String>, network: Option<String>) -> Self {
Self {
executable: executable.into(),
network,
}
}
#[must_use]
pub fn network(&self) -> Option<&str> {
self.network.as_deref()
}
#[must_use]
pub fn canister_command(&self) -> Command {
let mut command = Command::new(&self.executable);
command.arg("canister");
add_network_args(&mut command, self.network());
command
}
pub fn canister_id_optional(&self, name: &str) -> Result<Option<String>, DfxCommandError> {
let mut command = self.canister_command();
command.args(["id", name]);
match run_output(&mut command) {
Ok(output) => Ok(Some(output)),
Err(DfxCommandError::Failed { command, stderr }) if canister_id_missing(&stderr) => {
let _ = command;
Ok(None)
}
Err(err) => Err(err),
}
}
pub fn canister_id(&self, name: &str) -> Result<String, DfxCommandError> {
let mut command = self.canister_command();
command.args(["id", name]);
run_output(&mut command)
}
pub fn canister_call_output(
&self,
canister: &str,
method: &str,
output: Option<&str>,
) -> Result<String, DfxCommandError> {
let mut command = self.canister_command();
command.args(["call", canister, method]);
if let Some(output) = output {
command.args(["--output", output]);
}
run_output(&mut command)
}
pub fn snapshot_list(&self, canister: &str) -> Result<String, DfxCommandError> {
let mut command = self.canister_command();
command.args(["snapshot", "list", canister]);
run_output(&mut command)
}
pub fn snapshot_create(&self, canister: &str) -> Result<String, DfxCommandError> {
let mut command = self.canister_command();
command.args(["snapshot", "create", canister]);
run_output_with_stderr(&mut command)
}
pub fn stop_canister(&self, canister: &str) -> Result<(), DfxCommandError> {
let mut command = self.canister_command();
command.args(["stop", canister]);
run_status(&mut command)
}
pub fn start_canister(&self, canister: &str) -> Result<(), DfxCommandError> {
let mut command = self.canister_command();
command.args(["start", canister]);
run_status(&mut command)
}
pub fn snapshot_download(
&self,
canister: &str,
snapshot_id: &str,
artifact_path: &Path,
) -> Result<(), DfxCommandError> {
let mut command = self.canister_command();
command.args(["snapshot", "download", canister, snapshot_id, "--dir"]);
command.arg(artifact_path);
run_status(&mut command)
}
#[must_use]
pub fn snapshot_create_display(&self, canister: &str) -> String {
let mut command = self.canister_command();
command.args(["snapshot", "create", canister]);
command_display(&command)
}
#[must_use]
pub fn snapshot_download_display(
&self,
canister: &str,
snapshot_id: &str,
artifact_path: &Path,
) -> String {
let mut command = self.canister_command();
command.args(["snapshot", "download", canister, snapshot_id, "--dir"]);
command.arg(artifact_path);
command_display(&command)
}
#[must_use]
pub fn stop_canister_display(&self, canister: &str) -> String {
let mut command = self.canister_command();
command.args(["stop", canister]);
command_display(&command)
}
#[must_use]
pub fn start_canister_display(&self, canister: &str) -> String {
let mut command = self.canister_command();
command.args(["start", canister]);
command_display(&command)
}
}
pub fn add_network_args(command: &mut Command, network: Option<&str>) {
if let Some(network) = network {
command.args(["--network", network]);
}
}
pub fn run_output(command: &mut Command) -> Result<String, DfxCommandError> {
let display = command_display(command);
let output = command.output()?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
} else {
Err(DfxCommandError::Failed {
command: display,
stderr: command_stderr(&output),
})
}
}
pub fn run_output_with_stderr(command: &mut Command) -> Result<String, DfxCommandError> {
let display = command_display(command);
let output = command.output()?;
if output.status.success() {
let mut text = String::from_utf8_lossy(&output.stdout).to_string();
text.push_str(&String::from_utf8_lossy(&output.stderr));
Ok(text.trim().to_string())
} else {
Err(DfxCommandError::Failed {
command: display,
stderr: command_stderr(&output),
})
}
}
pub fn run_status(command: &mut Command) -> Result<(), DfxCommandError> {
let display = command_display(command);
let output = command.output()?;
if output.status.success() {
Ok(())
} else {
Err(DfxCommandError::Failed {
command: display,
stderr: command_stderr(&output),
})
}
}
#[must_use]
pub fn command_display(command: &Command) -> String {
let mut parts = vec![command.get_program().to_string_lossy().to_string()];
parts.extend(
command
.get_args()
.map(|arg| arg.to_string_lossy().to_string()),
);
parts.join(" ")
}
#[must_use]
pub fn canister_id_missing(stderr: &str) -> bool {
stderr.contains("Cannot find canister id")
}
#[must_use]
pub fn parse_snapshot_id(output: &str) -> Option<String> {
output
.split(|c: char| c.is_whitespace() || matches!(c, '"' | '\'' | ':' | ','))
.filter(|part| !part.is_empty())
.rev()
.find(|part| {
part.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'))
})
.map(str::to_string)
}
#[must_use]
pub fn parse_snapshot_list_ids(output: &str) -> Vec<String> {
output
.lines()
.filter_map(|line| {
line.split_once(':')
.map(|(snapshot_id, _)| snapshot_id.trim())
})
.filter(|snapshot_id| !snapshot_id.is_empty())
.map(str::to_string)
.collect()
}
fn command_stderr(output: &std::process::Output) -> String {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.trim().is_empty() {
String::from_utf8_lossy(&output.stdout).to_string()
} else {
stderr.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_snapshot_id_from_output() {
let snapshot_id = parse_snapshot_id("Created snapshot: snap_abc-123\n");
assert_eq!(snapshot_id.as_deref(), Some("snap_abc-123"));
}
#[test]
fn parses_snapshot_ids_from_list_output() {
let snapshot_ids = parse_snapshot_list_ids(
"0000000000000000ffffffffff9000050101: size 10\n\
0000000000000000ffffffffff9000050102: size 12\n",
);
assert_eq!(
snapshot_ids,
vec![
"0000000000000000ffffffffff9000050101",
"0000000000000000ffffffffff9000050102"
]
);
}
}