use super::{
ListCommandError,
options::ListOptions,
parse::{parse_canic_metadata_version_response, parse_cycle_balance_response},
render::ReadyStatus,
state_network,
};
use crate::support::registry_tree::visible_entries;
use canic_host::{
format::{byte_size, cycles_tc},
icp::IcpCli,
icp_config::resolve_current_canic_icp_root,
installed_fleet::{
InstalledFleetError, InstalledFleetRequest, InstalledFleetResolution,
read_installed_fleet_state_from_root, resolve_installed_fleet_from_root,
},
registry::RegistryEntry,
replica_query,
};
use std::{
collections::{BTreeMap, BTreeSet},
fs,
path::PathBuf,
sync::Arc,
thread,
};
use super::options::ListSource;
pub(super) fn load_registry_entries(
options: &ListOptions,
) -> Result<Vec<RegistryEntry>, ListCommandError> {
let registry = match options.source {
ListSource::RootRegistry => resolve_list_fleet(options)?.registry.entries,
ListSource::Config => {
unreachable!("config source does not use registry entries")
}
};
Ok(registry)
}
pub(super) fn resolve_tree_anchor(options: &ListOptions) -> Option<String> {
options.subtree.clone()
}
pub(super) fn list_ready_statuses(
options: &ListOptions,
registry: &[RegistryEntry],
canister: Option<&str>,
) -> Result<BTreeMap<String, ReadyStatus>, ListCommandError> {
if replica_query::should_use_local_replica_query(options.network.as_deref()) {
return local_ready_statuses(options, registry, canister);
}
let icp_root = resolve_live_icp_root(options);
let mut statuses = BTreeMap::new();
for entry in visible_entries(registry, canister)? {
statuses.insert(
entry.pid.clone(),
check_ready_status(options, icp_root.as_deref(), &entry.pid)?,
);
}
Ok(statuses)
}
pub(super) fn list_cycle_balances(
options: &ListOptions,
registry: &[RegistryEntry],
canister: Option<&str>,
) -> Result<BTreeMap<String, String>, ListCommandError> {
let icp = options.icp.clone();
let network = options.network.clone();
let icp_root = resolve_live_icp_root(options);
collect_visible_optional_values(registry, canister, move |pid| {
query_cycle_balance_endpoint(&icp, network.clone(), icp_root.as_deref(), &pid)
.map(cycles_tc)
})
}
pub(super) fn list_canic_versions(
options: &ListOptions,
registry: &[RegistryEntry],
canister: Option<&str>,
) -> Result<BTreeMap<String, String>, ListCommandError> {
let icp = options.icp.clone();
let network = options.network.clone();
let icp_root = resolve_live_icp_root(options);
collect_visible_optional_values(registry, canister, move |pid| {
query_canic_metadata_version(&icp, network.clone(), icp_root.as_deref(), &pid)
})
}
pub(super) fn list_module_hashes(
registry: &[RegistryEntry],
canister: Option<&str>,
) -> Result<BTreeMap<String, String>, ListCommandError> {
Ok(visible_entries(registry, canister)?
.into_iter()
.filter_map(|entry| {
entry
.module_hash
.as_ref()
.map(|hash| (entry.pid.clone(), hash.clone()))
})
.collect())
}
pub(super) fn resolve_wasm_sizes(
options: &ListOptions,
registry: &[RegistryEntry],
) -> BTreeMap<String, String> {
let Some(root) = resolve_icp_artifact_root(options) else {
return BTreeMap::new();
};
let network = state_network(options);
registry
.iter()
.filter_map(|entry| entry.role.as_deref())
.collect::<BTreeSet<_>>()
.into_iter()
.filter_map(|role| {
let path = root
.join(".icp")
.join(&network)
.join("canisters")
.join(role)
.join(format!("{role}.wasm.gz"));
fs::metadata(path)
.ok()
.map(|metadata| (role.to_string(), byte_size(metadata.len())))
})
.collect()
}
fn check_ready_status(
options: &ListOptions,
icp_root: Option<&std::path::Path>,
canister: &str,
) -> Result<ReadyStatus, ListCommandError> {
let mut icp = IcpCli::new(&options.icp, None, options.network.clone());
if let Some(root) = icp_root {
icp = icp.with_cwd(root);
}
let Ok(output) = icp.canister_call_output(canister, "canic_ready", Some("json")) else {
return Ok(ReadyStatus::Error);
};
let data = serde_json::from_str::<serde_json::Value>(&output)?;
Ok(if replica_query::parse_ready_json_value(&data) {
ReadyStatus::Ready
} else {
ReadyStatus::NotReady
})
}
fn local_ready_statuses(
options: &ListOptions,
registry: &[RegistryEntry],
canister: Option<&str>,
) -> Result<BTreeMap<String, ReadyStatus>, ListCommandError> {
let network = options.network.clone();
let icp_root = resolve_live_icp_root(options);
collect_visible_values(registry, canister, move |pid| {
match icp_root.as_deref().map_or_else(
|| replica_query::query_ready(network.as_deref(), &pid),
|root| replica_query::query_ready_from_root(network.as_deref(), &pid, root),
) {
Ok(true) => ReadyStatus::Ready,
Ok(false) => ReadyStatus::NotReady,
Err(_) => ReadyStatus::Error,
}
})
}
fn collect_visible_optional_values<T, F>(
registry: &[RegistryEntry],
canister: Option<&str>,
query: F,
) -> Result<BTreeMap<String, T>, ListCommandError>
where
T: Send + 'static,
F: Fn(String) -> Option<T> + Send + Sync + 'static,
{
let values = collect_visible_values(registry, canister, query)?;
Ok(values
.into_iter()
.filter_map(|(pid, value)| value.map(|value| (pid, value)))
.collect())
}
fn collect_visible_values<T, F>(
registry: &[RegistryEntry],
canister: Option<&str>,
query: F,
) -> Result<BTreeMap<String, T>, ListCommandError>
where
T: Send + 'static,
F: Fn(String) -> T + Send + Sync + 'static,
{
let query = Arc::new(query);
let mut handles = Vec::new();
for entry in visible_entries(registry, canister)? {
let pid = entry.pid.clone();
let query = Arc::clone(&query);
handles.push(thread::spawn(move || {
let value = query(pid.clone());
(pid, value)
}));
}
Ok(handles
.into_iter()
.filter_map(|handle| handle.join().ok())
.collect())
}
fn query_cycle_balance_endpoint(
icp: &str,
network: Option<String>,
icp_root: Option<&std::path::Path>,
canister: &str,
) -> Option<u128> {
let mut icp = IcpCli::new(icp, None, network);
if let Some(root) = icp_root {
icp = icp.with_cwd(root);
}
icp.canister_call_output(
canister,
canic_core::protocol::CANIC_CYCLE_BALANCE,
Some("json"),
)
.ok()
.and_then(|output| parse_cycle_balance_response(&output))
}
fn query_canic_metadata_version(
icp: &str,
network: Option<String>,
icp_root: Option<&std::path::Path>,
canister: &str,
) -> Option<String> {
let mut icp = IcpCli::new(icp, None, network);
if let Some(root) = icp_root {
icp = icp.with_cwd(root);
}
icp.canister_call_output(canister, canic_core::protocol::CANIC_METADATA, Some("json"))
.ok()
.and_then(|output| parse_canic_metadata_version_response(&output))
}
fn resolve_icp_artifact_root(options: &ListOptions) -> Option<PathBuf> {
let icp_root = resolve_live_icp_root(options)?;
if let Ok(state) =
read_installed_fleet_state_from_root(&state_network(options), &options.fleet, &icp_root)
{
return Some(PathBuf::from(state.icp_root));
}
Some(icp_root)
}
fn resolve_list_fleet(options: &ListOptions) -> Result<InstalledFleetResolution, ListCommandError> {
let icp_root = resolve_live_icp_root(options)
.ok_or_else(|| ListCommandError::InstallState("could not resolve ICP root".to_string()))?;
resolve_installed_fleet_from_root(
&InstalledFleetRequest {
fleet: options.fleet.clone(),
network: state_network(options),
icp: options.icp.clone(),
detect_lost_local_root: true,
},
&icp_root,
)
.map_err(list_installed_fleet_error)
.map_err(add_root_registry_hint)
}
fn resolve_live_icp_root(options: &ListOptions) -> Option<PathBuf> {
resolve_current_canic_icp_root(None).ok().or_else(|| {
read_installed_fleet_state_from_root(
&state_network(options),
&options.fleet,
&std::env::current_dir().ok()?,
)
.ok()
.map(|state| PathBuf::from(state.icp_root))
})
}
fn list_installed_fleet_error(error: InstalledFleetError) -> ListCommandError {
match error {
InstalledFleetError::NoInstalledFleet { network, fleet } => {
ListCommandError::NoInstalledFleet { network, fleet }
}
InstalledFleetError::InstallState(error) => ListCommandError::InstallState(error),
InstalledFleetError::ReplicaQuery(error) => ListCommandError::ReplicaQuery(error),
InstalledFleetError::IcpFailed { command, stderr } => {
ListCommandError::IcpFailed { command, stderr }
}
InstalledFleetError::LostLocalFleet {
fleet,
network,
root,
} => ListCommandError::LostLocalFleet {
fleet,
network,
root,
},
InstalledFleetError::Registry(error) => ListCommandError::Registry(error),
InstalledFleetError::Io(error) => ListCommandError::Io(error),
}
}
fn add_root_registry_hint(error: ListCommandError) -> ListCommandError {
let ListCommandError::IcpFailed { command, stderr } = error else {
return error;
};
let Some(hint) = root_registry_hint(&stderr) else {
return ListCommandError::IcpFailed { command, stderr };
};
ListCommandError::IcpFailed {
command,
stderr: format!("{stderr}\nHint: {hint}\n"),
}
}
fn root_registry_hint(stderr: &str) -> Option<&'static str> {
if stderr.contains("Cannot find canister id") {
return Some(
"no root canister id exists for this fleet. Use `canic config <name>` for the selected fleet config, or run `canic install <name>` before querying the root registry.",
);
}
if stderr.contains("contains no Wasm module") || stderr.contains("wasm-module-not-found") {
return Some(
"the root canister id exists but no Canic root code is installed. Run `canic install <name>`, then use `canic list <name>`.",
);
}
None
}
#[cfg(test)]
mod tests;