Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Unreleased

* fix: `icp canister call` with both `--json` and `-o hex` no longer prints both kinds of output at once.
* fix: `icp` no longer picks up a stale inherited `$PWD` when launched as a subprocess via `chdir(2)` + `execve` (e.g. from a test harness). The logical `$PWD` path is now validated against `getcwd()` by inode before use, preserving symlink-aware project root discovery while ignoring stale values.

# v0.2.6
Expand Down
128 changes: 70 additions & 58 deletions crates/icp-cli/src/commands/canister/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,85 +228,97 @@ pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), anyhow::E
};

let mut term = Term::buffered_stdout();
let res_hex = || format!("response (hex): {}", hex::encode(&res));
let mut json_response = JsonCallResponse {
response_bytes: hex::encode(&res),
response_text: None,
response_candid: None,
};
let decoded = decode_response(&res, args.output, declared_method.as_ref());

// catch errors, because the json result should be printed regardless of errors
let res: Result<(), anyhow::Error> = (|| {
match args.output {
CallOutputMode::Auto => {
if let Ok(ret) = try_decode_candid(&res, declared_method.as_ref()) {
if args.json {
json_response.response_candid = Some(format!("{ret}"));
} else {
print_candid_for_term(&mut term, &ret)
.context("failed to print candid return value")?;
}
} else if let Ok(s) = std::str::from_utf8(&res) {
if args.json {
json_response.response_text = Some(s.to_string());
} else {
writeln!(term, "{s}")?;
}
} else if !args.json {
writeln!(term, "{}", hex::encode(&res))?;
}
}
CallOutputMode::Candid => {
let ret =
try_decode_candid(&res, declared_method.as_ref()).with_context(res_hex)?;
if args.json {
json_response.response_candid = Some(format!("{ret}"));
} else {
print_candid_for_term(&mut term, &ret)
.context("failed to print candid return value")?;
}
}
CallOutputMode::Text => {
let s = std::str::from_utf8(&res)
.with_context(res_hex)
.context("response is not valid UTF-8")?;
if args.json {
json_response.response_text = Some(s.to_string());
} else {
writeln!(term, "{s}")?;
}
}
CallOutputMode::Hex => {
writeln!(term, "{}", hex::encode(&res))?;
}
};
anyhow::Ok(())
})();
if args.json {
let write_result = serde_json::to_writer(&term, &json_response);
if let Err(write_err) = write_result {
if let Err(decode_err) = res {
let envelope = JsonCallResponse::build(&res, decoded.as_ref().ok());
let write_result = serde_json::to_writer(&term, &envelope);
match (write_result, decoded) {
(Ok(()), decode_result) => {
decode_result?;
}
(Err(write_err), Err(decode_err)) => {
// Prefer the decode error; the write failure is incidental.
error!("failed to write JSON response: {write_err}");
return Err(decode_err);
} else {
}
(Err(write_err), Ok(_)) => {
return Err(write_err).context("failed to write JSON response");
}
}
} else {
match decoded? {
Decoded::Candid(ret) => print_candid_for_term(&mut term, &ret)
.context("failed to print candid return value")?,
Decoded::Text(s) => writeln!(term, "{s}")?,
Decoded::Bytes => writeln!(term, "{}", hex::encode(&res))?,
}
}
res?;

// term is buffered; this single flush covers all output paths (json and non-json).
term.flush()?;

Ok(())
}

/// A response decoded according to the requested `CallOutputMode`.
enum Decoded {
Candid(IDLArgs),
Text(String),
/// No decoding was attempted or all attempts failed; emit raw bytes as hex.
Bytes,
}

fn decode_response(
res: &[u8],
mode: CallOutputMode,
method: Option<&(TypeEnv, Function)>,
) -> Result<Decoded, anyhow::Error> {
let res_hex = || format!("response (hex): {}", hex::encode(res));
match mode {
CallOutputMode::Auto => {
if let Ok(args) = try_decode_candid(res, method) {
Ok(Decoded::Candid(args))
} else if let Ok(s) = std::str::from_utf8(res) {
Ok(Decoded::Text(s.to_string()))
} else {
Ok(Decoded::Bytes)
}
}
CallOutputMode::Candid => try_decode_candid(res, method)
.map(Decoded::Candid)
.with_context(res_hex),
CallOutputMode::Text => std::str::from_utf8(res)
.map(|s| Decoded::Text(s.to_string()))
.with_context(res_hex)
.context("response is not valid UTF-8"),
CallOutputMode::Hex => Ok(Decoded::Bytes),
}
}

#[derive(Serialize)]
struct JsonCallResponse {
response_bytes: String,
response_text: Option<String>,
response_candid: Option<String>,
}

impl JsonCallResponse {
fn build(res: &[u8], decoded: Option<&Decoded>) -> Self {
Self {
response_bytes: hex::encode(res),
response_text: match decoded {
Some(Decoded::Text(s)) => Some(s.clone()),
_ => None,
},
response_candid: match decoded {
Some(Decoded::Candid(args)) => Some(format!("{args}")),
_ => None,
},
}
}
}

/// Tries to decode the response as Candid. Returns `None` if decoding fails.
fn try_decode_candid(
res: &[u8],
Expand Down
Loading