1use crate::dfx;
2use crate::release_set::{
3 configured_fleet_name, configured_install_targets, dfx_call_on_network, dfx_root,
4 emit_root_release_set_manifest_with_config, load_root_release_set_manifest,
5 resume_root_bootstrap, stage_root_release_set, workspace_root,
6};
7use canic_core::{cdk::types::Principal, protocol};
8use config_selection::resolve_install_config_path;
9use serde::Deserialize;
10use serde_json::Value;
11use std::{
12 env,
13 path::{Path, PathBuf},
14 process::Command,
15 thread,
16 time::{Duration, Instant, SystemTime, UNIX_EPOCH},
17};
18
19mod config_selection;
20mod state;
21
22pub use config_selection::discover_canic_config_choices;
23pub use state::{
24 FleetSummary, InstallState, clear_current_fleet_name_if_matches, list_current_fleets,
25 read_current_fleet_name, read_current_install_state, read_current_network_name,
26 read_current_or_fleet_install_state, select_current_fleet, select_current_fleet_name,
27 select_current_network_name,
28};
29use state::{INSTALL_STATE_SCHEMA_VERSION, validate_fleet_name, write_install_state};
30
31#[cfg(test)]
32mod tests;
33
34#[cfg(test)]
35use config_selection::config_selection_error;
36#[cfg(test)]
37use state::{
38 clear_selected_fleet_name_if_matches, current_fleet_path, current_network_path,
39 fleet_install_state_path, list_fleets, read_fleet_install_state, read_install_state,
40};
41
42#[derive(Clone, Debug)]
47pub struct InstallRootOptions {
48 pub root_canister: String,
49 pub root_build_target: String,
50 pub network: String,
51 pub ready_timeout_seconds: u64,
52 pub config_path: Option<String>,
53 pub interactive_config_selection: bool,
54}
55
56#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
61struct BootstrapStatusSnapshot {
62 ready: bool,
63 phase: String,
64 last_error: Option<String>,
65}
66
67#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
72struct InstallTimingSummary {
73 create_canisters: Duration,
74 build_all: Duration,
75 emit_manifest: Duration,
76 fabricate_cycles: Duration,
77 install_root: Duration,
78 stage_release_set: Duration,
79 resume_bootstrap: Duration,
80 wait_ready: Duration,
81}
82
83const LOCAL_ROOT_TARGET_CYCLES: u128 = 9_000_000_000_000_000;
84const LOCAL_DFX_READY_TIMEOUT_SECONDS: u64 = 30;
85
86pub fn discover_current_canic_config_choices() -> Result<Vec<PathBuf>, Box<dyn std::error::Error>> {
88 let workspace_root = workspace_root()?;
89 config_selection::discover_workspace_canic_config_choices(&workspace_root)
90}
91
92pub fn install_root(options: InstallRootOptions) -> Result<(), Box<dyn std::error::Error>> {
94 let workspace_root = workspace_root()?;
95 let dfx_root = dfx_root()?;
96 let config_path = resolve_install_config_path(
97 &workspace_root,
98 &dfx_root,
99 &options.network,
100 options.config_path.as_deref(),
101 options.interactive_config_selection,
102 )?;
103 let fleet_name = configured_fleet_name(&config_path)?;
104 validate_fleet_name(&fleet_name)?;
105 let total_started_at = Instant::now();
106 let mut timings = InstallTimingSummary::default();
107
108 println!(
109 "Installing fleet {} against DFX_NETWORK={}",
110 fleet_name, options.network
111 );
112 ensure_dfx_running(&dfx_root, &options.network)?;
113 let mut create = dfx_canister_command_in_network(&dfx_root, &options.network);
114 create.args(["create", "--all", "-qq"]);
115 let create_started_at = Instant::now();
116 run_command(&mut create)?;
117 timings.create_canisters = create_started_at.elapsed();
118
119 let build_targets = configured_install_targets(&config_path, &options.root_build_target)?;
120 let build_session_id = install_build_session_id();
121 let build_started_at = Instant::now();
122 run_dfx_build_targets(
123 &dfx_root,
124 &options.network,
125 &build_targets,
126 &build_session_id,
127 &config_path,
128 )?;
129 timings.build_all = build_started_at.elapsed();
130
131 let emit_manifest_started_at = Instant::now();
132 let manifest_path = emit_root_release_set_manifest_with_config(
133 &workspace_root,
134 &dfx_root,
135 &options.network,
136 &config_path,
137 )?;
138 timings.emit_manifest = emit_manifest_started_at.elapsed();
139
140 timings.fabricate_cycles =
141 maybe_fabricate_local_cycles(&dfx_root, &options.root_canister, &options.network)?;
142
143 let mut install = dfx_canister_command_in_network(&dfx_root, &options.network);
144 install.args([
145 "install",
146 &options.root_canister,
147 "--mode=reinstall",
148 "-y",
149 "--argument",
150 "(variant { Prime })",
151 ]);
152 let install_started_at = Instant::now();
153 run_command(&mut install)?;
154 timings.install_root = install_started_at.elapsed();
155
156 let manifest = load_root_release_set_manifest(&manifest_path)?;
157 let stage_started_at = Instant::now();
158 stage_root_release_set(
159 &dfx_root,
160 &options.network,
161 &options.root_canister,
162 &manifest,
163 )?;
164 timings.stage_release_set = stage_started_at.elapsed();
165 let resume_started_at = Instant::now();
166 resume_root_bootstrap(&options.network, &options.root_canister)?;
167 timings.resume_bootstrap = resume_started_at.elapsed();
168 let ready_started_at = Instant::now();
169 let ready_result = wait_for_root_ready(
170 &options.network,
171 &options.root_canister,
172 options.ready_timeout_seconds,
173 );
174 timings.wait_ready = ready_started_at.elapsed();
175 if let Err(err) = ready_result {
176 print_install_timing_summary(&timings, total_started_at.elapsed());
177 return Err(err);
178 }
179
180 print_install_timing_summary(&timings, total_started_at.elapsed());
181 let state = build_install_state(
182 &options,
183 &workspace_root,
184 &dfx_root,
185 &config_path,
186 &manifest_path,
187 &fleet_name,
188 )?;
189 let state_path = write_install_state(&dfx_root, &options.network, &state)?;
190 print_install_result_summary(&options.network, &state.fleet, &state_path);
191 Ok(())
192}
193
194fn build_install_state(
196 options: &InstallRootOptions,
197 workspace_root: &Path,
198 dfx_root: &Path,
199 config_path: &Path,
200 release_set_manifest_path: &Path,
201 fleet_name: &str,
202) -> Result<InstallState, Box<dyn std::error::Error>> {
203 Ok(InstallState {
204 schema_version: INSTALL_STATE_SCHEMA_VERSION,
205 fleet: fleet_name.to_string(),
206 installed_at_unix_secs: current_unix_secs()?,
207 network: options.network.clone(),
208 root_target: options.root_canister.clone(),
209 root_canister_id: resolve_root_canister_id(
210 dfx_root,
211 &options.network,
212 &options.root_canister,
213 )?,
214 root_build_target: options.root_build_target.clone(),
215 workspace_root: workspace_root.display().to_string(),
216 dfx_root: dfx_root.display().to_string(),
217 config_path: config_path.display().to_string(),
218 release_set_manifest_path: release_set_manifest_path.display().to_string(),
219 })
220}
221
222fn resolve_root_canister_id(
224 dfx_root: &Path,
225 network: &str,
226 root_canister: &str,
227) -> Result<String, Box<dyn std::error::Error>> {
228 if Principal::from_text(root_canister).is_ok() {
229 return Ok(root_canister.to_string());
230 }
231
232 let mut command = dfx_canister_command_in_network(dfx_root, network);
233 command.args(["id", root_canister]);
234 Ok(run_command_stdout(&mut command)?.trim().to_string())
235}
236
237fn current_unix_secs() -> Result<u64, Box<dyn std::error::Error>> {
239 Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())
240}
241
242fn run_dfx_build_targets(
244 dfx_root: &Path,
245 network: &str,
246 targets: &[String],
247 build_session_id: &str,
248 config_path: &Path,
249) -> Result<(), Box<dyn std::error::Error>> {
250 println!("Build artifacts:");
251 println!("{:<16} {:<18} {:>10}", "CANISTER", "PROGRESS", "ELAPSED");
252
253 for (index, target) in targets.iter().enumerate() {
254 let mut command = dfx_build_target_command(dfx_root, network, target, build_session_id);
255 command.env("CANIC_CONFIG_PATH", config_path);
256 let started_at = Instant::now();
257 let output = command.output()?;
258 let elapsed = started_at.elapsed();
259
260 if !output.status.success() {
261 return Err(format!(
262 "dfx build failed for {target}: {}\nstdout:\n{}\nstderr:\n{}",
263 output.status,
264 String::from_utf8_lossy(&output.stdout).trim(),
265 String::from_utf8_lossy(&output.stderr).trim()
266 )
267 .into());
268 }
269
270 println!(
271 "{:<16} {:<18} {:>9.2}s",
272 target,
273 progress_bar(index + 1, targets.len(), 10),
274 elapsed.as_secs_f64()
275 );
276 }
277
278 println!();
279 Ok(())
280}
281
282fn dfx_build_target_command(
285 dfx_root: &Path,
286 network: &str,
287 target: &str,
288 build_session_id: &str,
289) -> Command {
290 let mut command = dfx_command_in_network(dfx_root, network);
291 command
292 .env("CANIC_BUILD_CONTEXT_SESSION", build_session_id)
293 .args(["build", "-qq", target]);
294 command
295}
296
297fn install_build_session_id() -> String {
298 let unique = SystemTime::now()
299 .duration_since(UNIX_EPOCH)
300 .map_or(0, |duration| duration.as_nanos());
301 format!("install-root-{}-{unique}", std::process::id())
302}
303
304fn maybe_fabricate_local_cycles(
306 dfx_root: &Path,
307 root_canister: &str,
308 network: &str,
309) -> Result<Duration, Box<dyn std::error::Error>> {
310 if network != "local" {
311 return Ok(Duration::ZERO);
312 }
313
314 let current_balance = root_cycle_balance(dfx_root, network, root_canister)?;
315 let Some(fabricate_cycles) = required_local_cycle_topup(current_balance) else {
316 println!(
317 "Skipping local cycle fabrication for {root_canister}; balance {} already meets target {}",
318 format_cycles(current_balance),
319 format_cycles(LOCAL_ROOT_TARGET_CYCLES)
320 );
321 return Ok(Duration::ZERO);
322 };
323
324 let mut fabricate = dfx_command_in_network(dfx_root, network);
325 fabricate.args([
326 "ledger",
327 "fabricate-cycles",
328 "--canister",
329 root_canister,
330 "--cycles",
331 &fabricate_cycles.to_string(),
332 ]);
333 let fabricate_started_at = Instant::now();
334 let output = fabricate.output()?;
335 print_local_cycle_topup_summary(root_canister, current_balance, fabricate_cycles, &output);
336
337 Ok(fabricate_started_at.elapsed())
338}
339
340fn print_local_cycle_topup_summary(
342 root_canister: &str,
343 current_balance: u128,
344 fabricate_cycles: u128,
345 output: &std::process::Output,
346) {
347 let status = if output.status.success() {
348 "topped up"
349 } else {
350 "top-up requested"
351 };
352 println!(
353 "\n\x1b[33mcycles: {status} local root {root_canister} by {} toward target {} (was {})\x1b[0m\n",
354 format_cycles(fabricate_cycles),
355 format_cycles(LOCAL_ROOT_TARGET_CYCLES),
356 format_cycles(current_balance)
357 );
358}
359
360fn root_cycle_balance(
362 dfx_root: &Path,
363 network: &str,
364 root_canister: &str,
365) -> Result<u128, Box<dyn std::error::Error>> {
366 let mut command = dfx_canister_command_in_network(dfx_root, network);
367 command.args(["status", root_canister]);
368 let stdout = dfx::run_output(&mut command)?;
369 parse_canister_status_cycles(&stdout)
370 .ok_or_else(|| "could not parse cycle balance from `dfx canister status` output".into())
371}
372
373fn parse_canister_status_cycles(status_output: &str) -> Option<u128> {
375 status_output
376 .lines()
377 .find_map(parse_canister_status_balance_line)
378}
379
380fn parse_canister_status_balance_line(line: &str) -> Option<u128> {
381 let (label, value) = line.trim().split_once(':')?;
382 let label = label.trim().to_ascii_lowercase();
383 if label != "balance" && label != "cycle balance" {
384 return None;
385 }
386
387 let digits = value
388 .chars()
389 .filter(char::is_ascii_digit)
390 .collect::<String>();
391 if digits.is_empty() {
392 return None;
393 }
394
395 digits.parse::<u128>().ok()
396}
397
398fn required_local_cycle_topup(current_balance: u128) -> Option<u128> {
400 (current_balance < LOCAL_ROOT_TARGET_CYCLES)
401 .then_some(LOCAL_ROOT_TARGET_CYCLES.saturating_sub(current_balance))
402 .filter(|cycles| *cycles > 0)
403}
404
405fn format_cycles(value: u128) -> String {
406 let digits = value.to_string();
407 let mut out = String::with_capacity(digits.len() + (digits.len().saturating_sub(1) / 3));
408 for (index, ch) in digits.chars().enumerate() {
409 if index > 0 && (digits.len() - index).is_multiple_of(3) {
410 out.push('_');
411 }
412 out.push(ch);
413 }
414 out
415}
416
417fn progress_bar(current: usize, total: usize, width: usize) -> String {
418 if total == 0 || width == 0 {
419 return "[] 0/0".to_string();
420 }
421
422 let filled = current.saturating_mul(width).div_ceil(total);
423 let filled = filled.min(width);
424 format!(
425 "[{}{}] {current}/{total}",
426 "#".repeat(filled),
427 " ".repeat(width - filled)
428 )
429}
430
431fn ensure_dfx_running(dfx_root: &Path, network: &str) -> Result<(), Box<dyn std::error::Error>> {
433 if dfx_ping(network)? {
434 return Ok(());
435 }
436
437 if network == "local" && local_dfx_autostart_enabled() {
438 println!("Local dfx replica is not reachable; starting a clean local replica");
439 let mut stop = dfx_stop_command(dfx_root);
440 let _ = run_command_allow_failure(&mut stop)?;
441
442 let mut start = dfx_start_local_command(dfx_root);
443 run_command(&mut start)?;
444 wait_for_dfx_ping(
445 network,
446 Duration::from_secs(LOCAL_DFX_READY_TIMEOUT_SECONDS),
447 )?;
448 return Ok(());
449 }
450
451 Err(format!(
452 "dfx replica is not running for network '{network}'\nStart the target replica externally and rerun."
453 )
454 .into())
455}
456
457fn dfx_ping(network: &str) -> Result<bool, Box<dyn std::error::Error>> {
459 Ok(dfx::default_command()
460 .args(["ping", network])
461 .output()?
462 .status
463 .success())
464}
465
466fn local_dfx_autostart_enabled() -> bool {
468 parse_local_dfx_autostart(env::var("CANIC_AUTO_START_LOCAL_DFX").ok().as_deref())
469}
470
471fn parse_local_dfx_autostart(value: Option<&str>) -> bool {
472 !matches!(
473 value.map(str::trim).map(str::to_ascii_lowercase).as_deref(),
474 Some("0" | "false" | "no" | "off")
475 )
476}
477
478fn dfx_stop_command(dfx_root: &Path) -> Command {
480 let mut command = dfx_command_in_network(dfx_root, "local");
481 command.arg("stop");
482 command
483}
484
485fn dfx_start_local_command(dfx_root: &Path) -> Command {
487 let mut command = dfx_command_in_network(dfx_root, "local");
488 command.args(["start", "--background", "--clean", "--system-canisters"]);
489 command
490}
491
492fn wait_for_dfx_ping(network: &str, timeout: Duration) -> Result<(), Box<dyn std::error::Error>> {
494 let start = Instant::now();
495 while start.elapsed() < timeout {
496 if dfx_ping(network)? {
497 return Ok(());
498 }
499 thread::sleep(Duration::from_millis(500));
500 }
501
502 Err(format!(
503 "dfx replica did not become ready for network '{network}' within {}s",
504 timeout.as_secs()
505 )
506 .into())
507}
508
509fn wait_for_root_ready(
511 network: &str,
512 root_canister: &str,
513 timeout_seconds: u64,
514) -> Result<(), Box<dyn std::error::Error>> {
515 let start = std::time::Instant::now();
516 let mut next_report = 0_u64;
517
518 println!("Waiting for {root_canister} to report canic_ready (timeout {timeout_seconds}s)");
519
520 loop {
521 if root_ready(network, root_canister)? {
522 println!(
523 "{root_canister} reported canic_ready after {}s",
524 start.elapsed().as_secs()
525 );
526 return Ok(());
527 }
528
529 if let Some(status) = root_bootstrap_status(network, root_canister)?
530 && let Some(last_error) = status.last_error.as_deref()
531 {
532 eprintln!(
533 "root bootstrap reported failure during phase '{}' : {}",
534 status.phase, last_error
535 );
536 eprintln!(
537 "Diagnostic: dfx canister --network {network} call {root_canister} canic_bootstrap_status"
538 );
539 print_raw_call(network, root_canister, protocol::CANIC_BOOTSTRAP_STATUS);
540 eprintln!(
541 "Diagnostic: dfx canister --network {network} call {root_canister} canic_subnet_registry"
542 );
543 print_raw_call(network, root_canister, "canic_subnet_registry");
544 eprintln!(
545 "Diagnostic: dfx canister --network {network} call {root_canister} canic_wasm_store_bootstrap_debug"
546 );
547 print_raw_call(network, root_canister, "canic_wasm_store_bootstrap_debug");
548 eprintln!(
549 "Diagnostic: dfx canister --network {network} call {root_canister} canic_wasm_store_overview"
550 );
551 print_raw_call(network, root_canister, "canic_wasm_store_overview");
552 eprintln!(
553 "Diagnostic: dfx canister --network {network} call {root_canister} canic_log"
554 );
555 print_recent_root_logs(network, root_canister);
556 return Err(format!(
557 "root bootstrap failed during phase '{}' : {}",
558 status.phase, last_error
559 )
560 .into());
561 }
562
563 let elapsed = start.elapsed().as_secs();
564 if elapsed >= timeout_seconds {
565 eprintln!("root did not report canic_ready within {timeout_seconds}s");
566 eprintln!(
567 "Diagnostic: dfx canister --network {network} call {root_canister} canic_bootstrap_status"
568 );
569 print_raw_call(network, root_canister, protocol::CANIC_BOOTSTRAP_STATUS);
570 eprintln!(
571 "Diagnostic: dfx canister --network {network} call {root_canister} canic_subnet_registry"
572 );
573 print_raw_call(network, root_canister, "canic_subnet_registry");
574 eprintln!(
575 "Diagnostic: dfx canister --network {network} call {root_canister} canic_wasm_store_bootstrap_debug"
576 );
577 print_raw_call(network, root_canister, "canic_wasm_store_bootstrap_debug");
578 eprintln!(
579 "Diagnostic: dfx canister --network {network} call {root_canister} canic_wasm_store_overview"
580 );
581 print_raw_call(network, root_canister, "canic_wasm_store_overview");
582 eprintln!(
583 "Diagnostic: dfx canister --network {network} call {root_canister} canic_log"
584 );
585 print_recent_root_logs(network, root_canister);
586 return Err("root did not become ready".into());
587 }
588
589 if elapsed >= next_report {
590 println!("Still waiting for {root_canister} canic_ready ({elapsed}s elapsed)");
591 if let Some(status) = root_bootstrap_status(network, root_canister)? {
592 match status.last_error.as_deref() {
593 Some(last_error) => println!(
594 "Current bootstrap status: phase={} ready={} error={}",
595 status.phase, status.ready, last_error
596 ),
597 None => println!(
598 "Current bootstrap status: phase={} ready={}",
599 status.phase, status.ready
600 ),
601 }
602 }
603 if let Ok(registry_json) = dfx_call_on_network(
604 network,
605 root_canister,
606 "canic_subnet_registry",
607 None,
608 Some("json"),
609 ) {
610 println!("Current subnet registry roles:");
611 println!(" {}", registry_roles(®istry_json));
612 }
613 next_report = elapsed + 5;
614 }
615
616 thread::sleep(Duration::from_secs(1));
617 }
618}
619
620fn root_ready(network: &str, root_canister: &str) -> Result<bool, Box<dyn std::error::Error>> {
622 let output = dfx_call_on_network(network, root_canister, "canic_ready", None, Some("json"))?;
623 let data = serde_json::from_str::<Value>(&output)?;
624 Ok(parse_root_ready_value(&data))
625}
626
627fn root_bootstrap_status(
629 network: &str,
630 root_canister: &str,
631) -> Result<Option<BootstrapStatusSnapshot>, Box<dyn std::error::Error>> {
632 let output = match dfx_call_on_network(
633 network,
634 root_canister,
635 protocol::CANIC_BOOTSTRAP_STATUS,
636 None,
637 Some("json"),
638 ) {
639 Ok(output) => output,
640 Err(err) => {
641 let message = err.to_string();
642 if message.contains("has no query method")
643 || message.contains("method not found")
644 || message.contains("Canister has no query method")
645 {
646 return Ok(None);
647 }
648 return Err(err);
649 }
650 };
651 let data = serde_json::from_str::<Value>(&output)?;
652 Ok(parse_bootstrap_status_value(&data))
653}
654
655fn parse_root_ready_value(data: &Value) -> bool {
657 matches!(data, Value::Bool(true)) || matches!(data.get("Ok"), Some(Value::Bool(true)))
658}
659
660fn parse_bootstrap_status_value(data: &Value) -> Option<BootstrapStatusSnapshot> {
661 serde_json::from_value::<BootstrapStatusSnapshot>(data.clone())
662 .ok()
663 .or_else(|| {
664 data.get("Ok")
665 .cloned()
666 .and_then(|ok| serde_json::from_value::<BootstrapStatusSnapshot>(ok).ok())
667 })
668}
669
670fn print_install_timing_summary(timings: &InstallTimingSummary, total: Duration) {
671 println!("Install timing summary:");
672 println!("{:<20} {:>10}", "phase", "elapsed");
673 println!("{:<20} {:>10}", "--------------------", "----------");
674 print_timing_row("create_canisters", timings.create_canisters);
675 print_timing_row("build_all", timings.build_all);
676 print_timing_row("emit_manifest", timings.emit_manifest);
677 print_timing_row("fabricate_cycles", timings.fabricate_cycles);
678 print_timing_row("install_root", timings.install_root);
679 print_timing_row("stage_release_set", timings.stage_release_set);
680 print_timing_row("resume_bootstrap", timings.resume_bootstrap);
681 print_timing_row("wait_ready", timings.wait_ready);
682 print_timing_row("total", total);
683}
684
685fn print_timing_row(label: &str, duration: Duration) {
686 println!("{label:<20} {:>9.2}s", duration.as_secs_f64());
687}
688
689fn print_install_result_summary(network: &str, fleet: &str, state_path: &Path) {
691 println!("Install result:");
692 println!("{:<14} success", "status");
693 println!("{:<14} {}", "fleet", fleet);
694 println!("{:<14} {}", "install_state", state_path.display());
695 println!("{:<14} canic list --network {}", "smoke_check", network);
696}
697
698fn print_recent_root_logs(network: &str, root_canister: &str) {
700 let page_args = r"(null, null, null, record { limit = 8; offset = 0 })";
701 let Ok(logs_json) = dfx_call_on_network(
702 network,
703 root_canister,
704 "canic_log",
705 Some(page_args),
706 Some("json"),
707 ) else {
708 return;
709 };
710 let Ok(data) = serde_json::from_str::<Value>(&logs_json) else {
711 return;
712 };
713 let entries = data
714 .get("Ok")
715 .and_then(|ok| ok.get("entries"))
716 .and_then(Value::as_array)
717 .cloned()
718 .unwrap_or_default();
719
720 if entries.is_empty() {
721 println!(" <no runtime log entries>");
722 return;
723 }
724
725 for entry in entries.iter().rev() {
726 let level = entry.get("level").and_then(Value::as_str).unwrap_or("Info");
727 let topic = entry.get("topic").and_then(Value::as_str).unwrap_or("");
728 let message = entry
729 .get("message")
730 .and_then(Value::as_str)
731 .unwrap_or("")
732 .replace('\n', "\\n");
733 let topic_prefix = if topic.is_empty() {
734 String::new()
735 } else {
736 format!("[{topic}] ")
737 };
738 println!(" {level} {topic_prefix}{message}");
739 }
740}
741
742fn registry_roles(registry_json: &str) -> String {
744 serde_json::from_str::<Value>(registry_json)
745 .ok()
746 .and_then(|data| {
747 data.get("Ok").and_then(Value::as_array).map(|entries| {
748 entries
749 .iter()
750 .filter_map(|entry| {
751 entry
752 .get("role")
753 .and_then(Value::as_str)
754 .map(str::to_string)
755 })
756 .collect::<Vec<_>>()
757 })
758 })
759 .map_or_else(
760 || "<unavailable>".to_string(),
761 |roles| {
762 if roles.is_empty() {
763 "<empty>".to_string()
764 } else {
765 roles.join(", ")
766 }
767 },
768 )
769}
770
771fn run_command(command: &mut Command) -> Result<(), Box<dyn std::error::Error>> {
773 dfx::run_status(command).map_err(Into::into)
774}
775
776fn run_command_stdout(command: &mut Command) -> Result<String, Box<dyn std::error::Error>> {
778 dfx::run_output(command).map_err(Into::into)
779}
780
781fn run_command_allow_failure(
783 command: &mut Command,
784) -> Result<std::process::ExitStatus, Box<dyn std::error::Error>> {
785 Ok(command.status()?)
786}
787
788fn print_raw_call(network: &str, root_canister: &str, method: &str) {
790 let mut command = dfx_root().map_or_else(
791 |_| dfx_command_on_network(network),
792 |root| dfx_command_in_network(&root, network),
793 );
794 let _ = command
795 .arg("canister")
796 .args(["--network", network, "call", root_canister, method])
797 .status();
798}
799
800fn dfx_command_on_network(network: &str) -> Command {
803 let mut command = dfx::default_command();
804 command.env("DFX_NETWORK", network);
805 command
806}
807
808fn dfx_command_in_network(dfx_root: &Path, network: &str) -> Command {
810 let mut command = dfx::default_command_in(dfx_root);
811 command.env("DFX_NETWORK", network);
812 command
813}
814
815fn dfx_canister_command_in_network(dfx_root: &Path, network: &str) -> Command {
818 let mut command = dfx_command_in_network(dfx_root, network);
819 command.arg("canister");
820 dfx::add_network_args(&mut command, Some(network));
821 command
822}