1use candid::{CandidType, Principal, decode_one, encode_args, encode_one, utils::ArgumentEncoder};
2use canic::{
3 Error,
4 cdk::types::TC,
5 dto::{
6 abi::v1::CanisterInitPayload,
7 env::EnvBootstrapArgs,
8 subnet::SubnetIdentity,
9 topology::{AppDirectoryArgs, SubnetDirectoryArgs, SubnetRegistryResponse},
10 },
11 ids::CanisterRole,
12 protocol,
13};
14use pocket_ic::{PocketIc, PocketIcBuilder};
15use serde::de::DeserializeOwned;
16use std::{
17 any::Any,
18 collections::HashMap,
19 env, fs, io,
20 ops::{Deref, DerefMut},
21 panic::{AssertUnwindSafe, catch_unwind},
22 path::{Path, PathBuf},
23 process,
24 sync::{Mutex, MutexGuard},
25 thread,
26 time::{Duration, Instant},
27};
28
29mod standalone;
30
31const INSTALL_CYCLES: u128 = 500 * TC;
32const PIC_PROCESS_LOCK_DIR_NAME: &str = "canic-pocket-ic.lock";
33const PIC_PROCESS_LOCK_RETRY_DELAY: Duration = Duration::from_millis(100);
34const PIC_PROCESS_LOCK_LOG_AFTER: Duration = Duration::from_secs(1);
35static PIC_PROCESS_LOCK_STATE: Mutex<ProcessLockState> = Mutex::new(ProcessLockState {
36 ref_count: 0,
37 process_lock: None,
38});
39
40struct ControllerSnapshot {
41 snapshot_id: Vec<u8>,
42 sender: Option<Principal>,
43}
44
45struct ProcessLockGuard {
46 path: PathBuf,
47}
48
49struct ProcessLockOwner {
50 pid: u32,
51 start_ticks: Option<u64>,
52}
53
54struct ProcessLockState {
55 ref_count: usize,
56 process_lock: Option<ProcessLockGuard>,
57}
58
59#[derive(Debug, Eq, PartialEq)]
64pub enum PicStartError {
65 BinaryUnavailable { message: String },
66 BinaryInvalid { message: String },
67 DownloadFailed { message: String },
68 ServerStartFailed { message: String },
69 StartupTimedOut { message: String },
70 Panic { message: String },
71}
72
73#[derive(Debug)]
78pub enum PicSerialGuardError {
79 LockParentUnavailable { path: PathBuf, source: io::Error },
80 LockUnavailable { path: PathBuf, source: io::Error },
81 LockOwnerRecordFailed { path: PathBuf, source: io::Error },
82}
83
84#[derive(Debug, Eq, PartialEq)]
89pub struct PicInstallError {
90 canister_id: Principal,
91 message: String,
92}
93
94#[derive(Debug)]
99pub enum StandaloneCanisterFixtureError {
100 SerialGuard(PicSerialGuardError),
101 Start(PicStartError),
102 Install(PicInstallError),
103}
104
105pub struct ControllerSnapshots(HashMap<Principal, ControllerSnapshot>);
110
111pub struct CachedPicBaseline<T> {
116 pub pic: Pic,
117 pub snapshots: ControllerSnapshots,
118 pub metadata: T,
119 _serial_guard: PicSerialGuard,
120}
121
122pub struct CachedPicBaselineGuard<'a, T> {
127 guard: MutexGuard<'a, Option<CachedPicBaseline<T>>>,
128}
129
130pub struct PicSerialGuard {
135 _private: (),
136}
137
138pub use standalone::{
139 StandaloneCanisterFixture, install_prebuilt_canister, install_prebuilt_canister_with_cycles,
140 install_standalone_canister, try_install_prebuilt_canister,
141 try_install_prebuilt_canister_with_cycles,
142};
143
144#[must_use]
153pub fn pic() -> Pic {
154 try_pic().unwrap_or_else(|err| panic!("failed to start PocketIC: {err}"))
155}
156
157pub fn try_pic() -> Result<Pic, PicStartError> {
159 PicBuilder::new().with_application_subnet().try_build()
160}
161
162#[must_use]
164pub fn acquire_pic_serial_guard() -> PicSerialGuard {
165 try_acquire_pic_serial_guard()
166 .unwrap_or_else(|err| panic!("failed to acquire PocketIC serial guard: {err}"))
167}
168
169pub fn try_acquire_pic_serial_guard() -> Result<PicSerialGuard, PicSerialGuardError> {
171 let mut state = PIC_PROCESS_LOCK_STATE
172 .lock()
173 .unwrap_or_else(std::sync::PoisonError::into_inner);
174
175 if state.ref_count == 0 {
176 state.process_lock = Some(acquire_process_lock()?);
177 }
178 state.ref_count += 1;
179
180 Ok(PicSerialGuard { _private: () })
181}
182
183pub fn acquire_cached_pic_baseline<T, F>(
185 slot: &'static Mutex<Option<CachedPicBaseline<T>>>,
186 build: F,
187) -> (CachedPicBaselineGuard<'static, T>, bool)
188where
189 F: FnOnce() -> CachedPicBaseline<T>,
190{
191 let mut guard = slot
192 .lock()
193 .unwrap_or_else(std::sync::PoisonError::into_inner);
194 let cache_hit = guard.is_some();
195
196 if !cache_hit {
197 *guard = Some(build());
198 }
199
200 (CachedPicBaselineGuard { guard }, cache_hit)
201}
202
203pub fn wait_until_ready(pic: &PocketIc, canister_id: Principal, tick_limit: usize) {
205 let payload = encode_args(()).expect("encode empty args");
206
207 for _ in 0..tick_limit {
208 if let Ok(bytes) = pic.query_call(
209 canister_id,
210 Principal::anonymous(),
211 protocol::CANIC_READY,
212 payload.clone(),
213 ) && let Ok(ready) = decode_one::<bool>(&bytes)
214 && ready
215 {
216 return;
217 }
218 pic.tick();
219 }
220
221 panic!("canister did not report ready in time: {canister_id}");
222}
223
224#[must_use]
226pub fn role_pid(
227 pic: &PocketIc,
228 root_id: Principal,
229 role: &'static str,
230 tick_limit: usize,
231) -> Principal {
232 for _ in 0..tick_limit {
233 let registry: Result<Result<SubnetRegistryResponse, Error>, Error> = {
234 let payload = encode_args(()).expect("encode empty args");
235 pic.query_call(
236 root_id,
237 Principal::anonymous(),
238 protocol::CANIC_SUBNET_REGISTRY,
239 payload,
240 )
241 .map_err(|err| {
242 Error::internal(format!(
243 "pocket_ic query_call failed (canister={root_id}, method={}): {err}",
244 protocol::CANIC_SUBNET_REGISTRY
245 ))
246 })
247 .and_then(|bytes| {
248 decode_one(&bytes).map_err(|err| {
249 Error::internal(format!("decode_one failed for subnet registry: {err}"))
250 })
251 })
252 };
253
254 if let Ok(Ok(registry)) = registry
255 && let Some(pid) = registry
256 .0
257 .into_iter()
258 .find(|entry| entry.role == CanisterRole::new(role))
259 .map(|entry| entry.pid)
260 {
261 return pid;
262 }
263
264 pic.tick();
265 }
266
267 panic!("{role} canister must be registered");
268}
269
270pub struct PicBuilder(PocketIcBuilder);
281
282#[expect(clippy::new_without_default)]
283impl PicBuilder {
284 #[must_use]
286 pub fn new() -> Self {
287 Self(PocketIcBuilder::new())
288 }
289
290 #[must_use]
292 pub fn with_application_subnet(mut self) -> Self {
293 self.0 = self.0.with_application_subnet();
294 self
295 }
296
297 #[must_use]
299 pub fn with_ii_subnet(mut self) -> Self {
300 self.0 = self.0.with_ii_subnet();
301 self
302 }
303
304 #[must_use]
306 pub fn with_nns_subnet(mut self) -> Self {
307 self.0 = self.0.with_nns_subnet();
308 self
309 }
310
311 #[must_use]
313 pub fn build(self) -> Pic {
314 self.try_build()
315 .unwrap_or_else(|err| panic!("failed to start PocketIC: {err}"))
316 }
317
318 pub fn try_build(self) -> Result<Pic, PicStartError> {
320 let build = catch_unwind(AssertUnwindSafe(|| self.0.build()));
321
322 match build {
323 Ok(inner) => Ok(Pic { inner }),
324 Err(payload) => Err(classify_pic_start_panic(payload)),
325 }
326 }
327}
328
329impl std::fmt::Display for PicStartError {
330 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
331 match self {
332 Self::BinaryUnavailable { message }
333 | Self::BinaryInvalid { message }
334 | Self::DownloadFailed { message }
335 | Self::ServerStartFailed { message }
336 | Self::StartupTimedOut { message }
337 | Self::Panic { message } => f.write_str(message),
338 }
339 }
340}
341
342impl std::error::Error for PicStartError {}
343
344impl std::fmt::Display for PicSerialGuardError {
345 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346 match self {
347 Self::LockParentUnavailable { path, source } => write!(
348 f,
349 "failed to create PocketIC lock parent at {}: {source}",
350 path.display()
351 ),
352 Self::LockUnavailable { path, source } => write!(
353 f,
354 "failed to create PocketIC process lock dir at {}: {source}",
355 path.display()
356 ),
357 Self::LockOwnerRecordFailed { path, source } => write!(
358 f,
359 "failed to record PocketIC process lock owner at {}: {source}",
360 path.display()
361 ),
362 }
363 }
364}
365
366impl std::error::Error for PicSerialGuardError {
367 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
368 match self {
369 Self::LockParentUnavailable { source, .. }
370 | Self::LockUnavailable { source, .. }
371 | Self::LockOwnerRecordFailed { source, .. } => Some(source),
372 }
373 }
374}
375
376impl PicInstallError {
377 #[must_use]
379 pub const fn new(canister_id: Principal, message: String) -> Self {
380 Self {
381 canister_id,
382 message,
383 }
384 }
385
386 #[must_use]
388 pub const fn canister_id(&self) -> Principal {
389 self.canister_id
390 }
391
392 #[must_use]
394 pub fn message(&self) -> &str {
395 &self.message
396 }
397}
398
399impl std::fmt::Display for PicInstallError {
400 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
401 write!(
402 f,
403 "failed to install canister {}: {}",
404 self.canister_id, self.message
405 )
406 }
407}
408
409impl std::error::Error for PicInstallError {}
410
411impl std::fmt::Display for StandaloneCanisterFixtureError {
412 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
413 match self {
414 Self::SerialGuard(err) => write!(f, "{err}"),
415 Self::Start(err) => write!(f, "{err}"),
416 Self::Install(err) => write!(f, "{err}"),
417 }
418 }
419}
420
421impl std::error::Error for StandaloneCanisterFixtureError {
422 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
423 match self {
424 Self::SerialGuard(err) => Some(err),
425 Self::Start(err) => Some(err),
426 Self::Install(err) => Some(err),
427 }
428 }
429}
430
431pub struct Pic {
441 inner: PocketIc,
442}
443
444impl<T> Deref for CachedPicBaselineGuard<'_, T> {
445 type Target = CachedPicBaseline<T>;
446
447 fn deref(&self) -> &Self::Target {
448 self.guard
449 .as_ref()
450 .expect("cached PocketIC baseline must exist")
451 }
452}
453
454impl<T> DerefMut for CachedPicBaselineGuard<'_, T> {
455 fn deref_mut(&mut self) -> &mut Self::Target {
456 self.guard
457 .as_mut()
458 .expect("cached PocketIC baseline must exist")
459 }
460}
461
462impl<T> CachedPicBaseline<T> {
463 pub fn capture<I>(
465 pic: Pic,
466 controller_id: Principal,
467 canister_ids: I,
468 metadata: T,
469 ) -> Option<Self>
470 where
471 I: IntoIterator<Item = Principal>,
472 {
473 let snapshots = pic.capture_controller_snapshots(controller_id, canister_ids)?;
474
475 Some(Self {
476 pic,
477 snapshots,
478 metadata,
479 _serial_guard: acquire_pic_serial_guard(),
480 })
481 }
482
483 pub fn restore(&self, controller_id: Principal) {
485 self.pic
486 .restore_controller_snapshots(controller_id, &self.snapshots);
487 }
488}
489
490impl Pic {
491 #[must_use]
493 pub fn current_time_nanos(&self) -> u64 {
494 self.inner.get_time().as_nanos_since_unix_epoch()
495 }
496
497 pub fn restore_time_nanos(&self, nanos_since_epoch: u64) {
499 let restored = pocket_ic::Time::from_nanos_since_unix_epoch(nanos_since_epoch);
500 self.inner.set_time(restored);
501 self.inner.set_certified_time(restored);
502 }
503
504 pub fn create_and_install_root_canister(&self, wasm: Vec<u8>) -> Result<Principal, Error> {
506 let init_bytes = install_root_args()?;
507
508 Ok(self.create_and_install_with_args(wasm, init_bytes, INSTALL_CYCLES))
509 }
510
511 pub fn create_and_install_canister(
515 &self,
516 role: CanisterRole,
517 wasm: Vec<u8>,
518 ) -> Result<Principal, Error> {
519 let init_bytes = install_args(role)?;
520
521 Ok(self.create_and_install_with_args(wasm, init_bytes, INSTALL_CYCLES))
522 }
523
524 #[must_use]
529 pub fn create_and_install_with_args(
530 &self,
531 wasm: Vec<u8>,
532 init_bytes: Vec<u8>,
533 install_cycles: u128,
534 ) -> Principal {
535 self.try_create_and_install_with_args(wasm, init_bytes, install_cycles)
536 .unwrap_or_else(|err| panic!("{err}"))
537 }
538
539 pub fn try_create_and_install_with_args(
541 &self,
542 wasm: Vec<u8>,
543 init_bytes: Vec<u8>,
544 install_cycles: u128,
545 ) -> Result<Principal, PicInstallError> {
546 self.try_create_funded_and_install(wasm, init_bytes, install_cycles)
547 }
548
549 pub fn wait_for_ready(&self, canister_id: Principal, tick_limit: usize, context: &str) {
551 for _ in 0..tick_limit {
552 self.tick();
553 if self.fetch_ready(canister_id) {
554 return;
555 }
556 }
557
558 self.dump_canister_debug(canister_id, context);
559 panic!("{context}: canister {canister_id} did not become ready after {tick_limit} ticks");
560 }
561
562 pub fn wait_for_all_ready<I>(&self, canister_ids: I, tick_limit: usize, context: &str)
564 where
565 I: IntoIterator<Item = Principal>,
566 {
567 let canister_ids = canister_ids.into_iter().collect::<Vec<_>>();
568
569 for _ in 0..tick_limit {
570 self.tick();
571 if canister_ids
572 .iter()
573 .copied()
574 .all(|canister_id| self.fetch_ready(canister_id))
575 {
576 return;
577 }
578 }
579
580 for canister_id in &canister_ids {
581 self.dump_canister_debug(*canister_id, context);
582 }
583 panic!("{context}: canisters did not become ready after {tick_limit} ticks");
584 }
585
586 pub fn wait_out_install_code_rate_limit(&self, cooldown: Duration) {
588 self.advance_time(cooldown);
589 self.tick_n(2);
590 }
591
592 pub fn retry_install_code_ok<T, F>(
594 &self,
595 retry_limit: usize,
596 cooldown: Duration,
597 mut op: F,
598 ) -> Result<T, String>
599 where
600 F: FnMut() -> Result<T, String>,
601 {
602 let mut last_err = None;
603
604 for _ in 0..retry_limit {
605 match op() {
606 Ok(value) => return Ok(value),
607 Err(err) if is_install_code_rate_limited(&err) => {
608 last_err = Some(err);
609 self.wait_out_install_code_rate_limit(cooldown);
610 }
611 Err(err) => return Err(err),
612 }
613 }
614
615 Err(last_err.unwrap_or_else(|| "install_code retry loop exhausted".to_string()))
616 }
617
618 pub fn retry_install_code_err<F>(
620 &self,
621 retry_limit: usize,
622 cooldown: Duration,
623 first: Result<(), String>,
624 mut op: F,
625 ) -> Result<(), String>
626 where
627 F: FnMut() -> Result<(), String>,
628 {
629 match first {
630 Ok(()) => return Ok(()),
631 Err(err) if !is_install_code_rate_limited(&err) => return Err(err),
632 Err(_) => {}
633 }
634
635 self.wait_out_install_code_rate_limit(cooldown);
636
637 for _ in 1..retry_limit {
638 match op() {
639 Ok(()) => return Ok(()),
640 Err(err) if is_install_code_rate_limited(&err) => {
641 self.wait_out_install_code_rate_limit(cooldown);
642 }
643 Err(err) => return Err(err),
644 }
645 }
646
647 op()
648 }
649
650 pub fn dump_canister_debug(&self, canister_id: Principal, context: &str) {
652 eprintln!("{context}: debug for canister {canister_id}");
653
654 match self.canister_status(canister_id, None) {
655 Ok(status) => eprintln!("canister_status: {status:?}"),
656 Err(err) => eprintln!("canister_status failed: {err:?}"),
657 }
658
659 match self.fetch_canister_logs(canister_id, Principal::anonymous()) {
660 Ok(records) => {
661 if records.is_empty() {
662 eprintln!("canister logs: <empty>");
663 } else {
664 for record in records {
665 eprintln!("canister log: {record:?}");
666 }
667 }
668 }
669 Err(err) => eprintln!("fetch_canister_logs failed: {err:?}"),
670 }
671 }
672
673 pub fn capture_controller_snapshots<I>(
675 &self,
676 controller_id: Principal,
677 canister_ids: I,
678 ) -> Option<ControllerSnapshots>
679 where
680 I: IntoIterator<Item = Principal>,
681 {
682 let mut snapshots = HashMap::new();
683
684 for canister_id in canister_ids {
685 let Some(snapshot) = self.try_take_controller_snapshot(controller_id, canister_id)
686 else {
687 eprintln!(
688 "capture_controller_snapshots: snapshot capture unavailable for {canister_id}"
689 );
690 return None;
691 };
692 snapshots.insert(canister_id, snapshot);
693 }
694
695 Some(ControllerSnapshots(snapshots))
696 }
697
698 pub fn restore_controller_snapshots(
700 &self,
701 controller_id: Principal,
702 snapshots: &ControllerSnapshots,
703 ) {
704 for (canister_id, snapshot) in &snapshots.0 {
705 self.restore_controller_snapshot(controller_id, *canister_id, snapshot);
706 }
707 }
708
709 pub fn update_call<T, A>(
711 &self,
712 canister_id: Principal,
713 method: &str,
714 args: A,
715 ) -> Result<T, Error>
716 where
717 T: CandidType + DeserializeOwned,
718 A: ArgumentEncoder,
719 {
720 let bytes: Vec<u8> = encode_args(args)
721 .map_err(|err| Error::internal(format!("encode_args failed: {err}")))?;
722 let result = self
723 .inner
724 .update_call(canister_id, Principal::anonymous(), method, bytes)
725 .map_err(|err| {
726 Error::internal(format!(
727 "pocket_ic update_call failed (canister={canister_id}, method={method}): {err}"
728 ))
729 })?;
730
731 decode_one(&result).map_err(|err| Error::internal(format!("decode_one failed: {err}")))
732 }
733
734 pub fn update_call_as<T, A>(
736 &self,
737 canister_id: Principal,
738 caller: Principal,
739 method: &str,
740 args: A,
741 ) -> Result<T, Error>
742 where
743 T: CandidType + DeserializeOwned,
744 A: ArgumentEncoder,
745 {
746 let bytes: Vec<u8> = encode_args(args)
747 .map_err(|err| Error::internal(format!("encode_args failed: {err}")))?;
748 let result = self
749 .inner
750 .update_call(canister_id, caller, method, bytes)
751 .map_err(|err| {
752 Error::internal(format!(
753 "pocket_ic update_call failed (canister={canister_id}, method={method}): {err}"
754 ))
755 })?;
756
757 decode_one(&result).map_err(|err| Error::internal(format!("decode_one failed: {err}")))
758 }
759
760 pub fn query_call<T, A>(
762 &self,
763 canister_id: Principal,
764 method: &str,
765 args: A,
766 ) -> Result<T, Error>
767 where
768 T: CandidType + DeserializeOwned,
769 A: ArgumentEncoder,
770 {
771 let bytes: Vec<u8> = encode_args(args)
772 .map_err(|err| Error::internal(format!("encode_args failed: {err}")))?;
773 let result = self
774 .inner
775 .query_call(canister_id, Principal::anonymous(), method, bytes)
776 .map_err(|err| {
777 Error::internal(format!(
778 "pocket_ic query_call failed (canister={canister_id}, method={method}): {err}"
779 ))
780 })?;
781
782 decode_one(&result).map_err(|err| Error::internal(format!("decode_one failed: {err}")))
783 }
784
785 pub fn query_call_as<T, A>(
787 &self,
788 canister_id: Principal,
789 caller: Principal,
790 method: &str,
791 args: A,
792 ) -> Result<T, Error>
793 where
794 T: CandidType + DeserializeOwned,
795 A: ArgumentEncoder,
796 {
797 let bytes: Vec<u8> = encode_args(args)
798 .map_err(|err| Error::internal(format!("encode_args failed: {err}")))?;
799 let result = self
800 .inner
801 .query_call(canister_id, caller, method, bytes)
802 .map_err(|err| {
803 Error::internal(format!(
804 "pocket_ic query_call failed (canister={canister_id}, method={method}): {err}"
805 ))
806 })?;
807
808 decode_one(&result).map_err(|err| Error::internal(format!("decode_one failed: {err}")))
809 }
810
811 pub fn tick_n(&self, times: usize) {
813 for _ in 0..times {
814 self.tick();
815 }
816 }
817
818 fn try_create_funded_and_install(
820 &self,
821 wasm: Vec<u8>,
822 init_bytes: Vec<u8>,
823 install_cycles: u128,
824 ) -> Result<Principal, PicInstallError> {
825 let canister_id = self.create_canister();
826 self.add_cycles(canister_id, install_cycles);
827
828 let install = catch_unwind(AssertUnwindSafe(|| {
829 self.inner
830 .install_canister(canister_id, wasm, init_bytes, None);
831 }));
832 if let Err(payload) = install {
833 eprintln!("install_canister trapped for {canister_id}");
834 if let Ok(status) = self.inner.canister_status(canister_id, None) {
835 eprintln!("canister_status for {canister_id}: {status:?}");
836 }
837 if let Ok(logs) = self
838 .inner
839 .fetch_canister_logs(canister_id, Principal::anonymous())
840 {
841 for record in logs {
842 eprintln!("canister_log {canister_id}: {record:?}");
843 }
844 }
845 return Err(PicInstallError::new(
846 canister_id,
847 panic_payload_to_string(payload.as_ref()),
848 ));
849 }
850
851 Ok(canister_id)
852 }
853
854 fn fetch_ready(&self, canister_id: Principal) -> bool {
856 match self.query_call(canister_id, protocol::CANIC_READY, ()) {
857 Ok(ready) => ready,
858 Err(err) => {
859 self.dump_canister_debug(canister_id, "query canic_ready failed");
860 panic!("query canic_ready failed: {err:?}");
861 }
862 }
863 }
864
865 fn try_take_controller_snapshot(
867 &self,
868 controller_id: Principal,
869 canister_id: Principal,
870 ) -> Option<ControllerSnapshot> {
871 let candidates = controller_sender_candidates(controller_id, canister_id);
872 let mut last_err = None;
873
874 for sender in candidates {
875 match self.take_canister_snapshot(canister_id, sender, None) {
876 Ok(snapshot) => {
877 return Some(ControllerSnapshot {
878 snapshot_id: snapshot.id,
879 sender,
880 });
881 }
882 Err(err) => last_err = Some((sender, err)),
883 }
884 }
885
886 if let Some((sender, err)) = last_err {
887 eprintln!(
888 "failed to capture canister snapshot for {canister_id} using sender {sender:?}: {err}"
889 );
890 }
891 None
892 }
893
894 fn restore_controller_snapshot(
896 &self,
897 controller_id: Principal,
898 canister_id: Principal,
899 snapshot: &ControllerSnapshot,
900 ) {
901 let fallback_sender = if snapshot.sender.is_some() {
902 None
903 } else {
904 Some(controller_id)
905 };
906 let candidates = [snapshot.sender, fallback_sender];
907 let mut last_err = None;
908
909 for sender in candidates {
910 match self.load_canister_snapshot(canister_id, sender, snapshot.snapshot_id.clone()) {
911 Ok(()) => return,
912 Err(err) => last_err = Some((sender, err)),
913 }
914 }
915
916 let (sender, err) =
917 last_err.expect("snapshot restore must have at least one sender attempt");
918 panic!(
919 "failed to restore canister snapshot for {canister_id} using sender {sender:?}: {err}"
920 );
921 }
922}
923
924fn is_install_code_rate_limited(message: &str) -> bool {
925 message.contains("CanisterInstallCodeRateLimited")
926}
927
928impl Drop for ProcessLockGuard {
929 fn drop(&mut self) {
930 let _ = fs::remove_dir_all(&self.path);
931 }
932}
933
934impl Drop for PicSerialGuard {
935 fn drop(&mut self) {
936 let mut state = PIC_PROCESS_LOCK_STATE
937 .lock()
938 .unwrap_or_else(std::sync::PoisonError::into_inner);
939
940 state.ref_count = state
941 .ref_count
942 .checked_sub(1)
943 .expect("PocketIC serial guard refcount underflow");
944 if state.ref_count == 0 {
945 state.process_lock.take();
946 }
947 }
948}
949
950impl Deref for Pic {
951 type Target = PocketIc;
952
953 fn deref(&self) -> &Self::Target {
954 &self.inner
955 }
956}
957
958impl DerefMut for Pic {
959 fn deref_mut(&mut self) -> &mut Self::Target {
960 &mut self.inner
961 }
962}
963
964fn install_args(role: CanisterRole) -> Result<Vec<u8>, Error> {
979 if role.is_root() {
980 install_root_args()
981 } else {
982 let env = EnvBootstrapArgs {
985 prime_root_pid: None,
986 subnet_role: None,
987 subnet_pid: None,
988 root_pid: None,
989 canister_role: Some(role),
990 parent_pid: None,
991 };
992
993 let payload = CanisterInitPayload {
996 env,
997 app_directory: AppDirectoryArgs(Vec::new()),
998 subnet_directory: SubnetDirectoryArgs(Vec::new()),
999 };
1000
1001 encode_args::<(CanisterInitPayload, Option<Vec<u8>>)>((payload, None))
1002 .map_err(|err| Error::internal(format!("encode_args failed: {err}")))
1003 }
1004}
1005
1006fn install_root_args() -> Result<Vec<u8>, Error> {
1007 encode_one(SubnetIdentity::Manual)
1008 .map_err(|err| Error::internal(format!("encode_one failed: {err}")))
1009}
1010
1011fn controller_sender_candidates(
1013 controller_id: Principal,
1014 canister_id: Principal,
1015) -> [Option<Principal>; 2] {
1016 if canister_id == controller_id {
1017 [None, Some(controller_id)]
1018 } else {
1019 [Some(controller_id), None]
1020 }
1021}
1022
1023fn acquire_process_lock() -> Result<ProcessLockGuard, PicSerialGuardError> {
1025 let lock_dir = process_lock_dir();
1026 ensure_process_lock_parent(&lock_dir)?;
1027 let started_waiting = Instant::now();
1028 let mut logged_wait = false;
1029
1030 loop {
1031 match fs::create_dir(&lock_dir) {
1032 Ok(()) => {
1033 if let Err(source) = fs::write(
1034 process_lock_owner_path(&lock_dir),
1035 render_process_lock_owner(),
1036 ) {
1037 let _ = fs::remove_dir(&lock_dir);
1038 return Err(PicSerialGuardError::LockOwnerRecordFailed {
1039 path: lock_dir,
1040 source,
1041 });
1042 }
1043
1044 if logged_wait {
1045 eprintln!(
1046 "[canic_testkit::pic] acquired cross-process PocketIC lock at {}",
1047 lock_dir.display()
1048 );
1049 }
1050
1051 return Ok(ProcessLockGuard { path: lock_dir });
1052 }
1053 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {
1054 if process_lock_is_stale(&lock_dir) && clear_stale_process_lock(&lock_dir).is_ok() {
1055 continue;
1056 }
1057
1058 if !logged_wait && started_waiting.elapsed() >= PIC_PROCESS_LOCK_LOG_AFTER {
1059 eprintln!(
1060 "[canic_testkit::pic] waiting for cross-process PocketIC lock at {}",
1061 lock_dir.display()
1062 );
1063 logged_wait = true;
1064 }
1065
1066 thread::sleep(PIC_PROCESS_LOCK_RETRY_DELAY);
1067 }
1068 Err(source) => {
1069 return Err(PicSerialGuardError::LockUnavailable {
1070 path: lock_dir,
1071 source,
1072 });
1073 }
1074 }
1075 }
1076}
1077
1078fn process_lock_dir() -> PathBuf {
1080 process_lock_dir_from_temp_root(&env::temp_dir())
1081}
1082
1083fn process_lock_dir_from_temp_root(temp_root: &Path) -> PathBuf {
1085 temp_root.join(PIC_PROCESS_LOCK_DIR_NAME)
1086}
1087
1088fn ensure_process_lock_parent(lock_dir: &Path) -> Result<(), PicSerialGuardError> {
1090 let parent = lock_dir.parent().unwrap_or_else(|| Path::new("."));
1091 fs::create_dir_all(parent).map_err(|source| PicSerialGuardError::LockParentUnavailable {
1092 path: parent.to_path_buf(),
1093 source,
1094 })
1095}
1096
1097fn panic_payload_to_string(payload: &(dyn Any + Send)) -> String {
1099 if let Some(message) = payload.downcast_ref::<String>() {
1100 return message.clone();
1101 }
1102 if let Some(message) = payload.downcast_ref::<&'static str>() {
1103 return (*message).to_string();
1104 }
1105
1106 "non-string panic payload".to_string()
1107}
1108
1109fn classify_pic_start_panic(payload: Box<dyn Any + Send>) -> PicStartError {
1111 let message = panic_payload_to_string(payload.as_ref());
1112
1113 if message.starts_with("Failed to validate PocketIC server binary") {
1114 if message.contains("No such file or directory") || message.contains("os error 2") {
1115 return PicStartError::BinaryUnavailable { message };
1116 }
1117
1118 return PicStartError::BinaryInvalid { message };
1119 }
1120
1121 if message.starts_with("Failed to download PocketIC server")
1122 || message.starts_with("Failed to write PocketIC server binary")
1123 {
1124 return PicStartError::DownloadFailed { message };
1125 }
1126
1127 if message.starts_with("Failed to start PocketIC binary")
1128 || message.starts_with("Failed to create PocketIC server directory")
1129 {
1130 return PicStartError::ServerStartFailed { message };
1131 }
1132
1133 if message.starts_with("Timed out waiting for PocketIC server being available") {
1134 return PicStartError::StartupTimedOut { message };
1135 }
1136
1137 PicStartError::Panic { message }
1138}
1139
1140fn process_lock_owner_path(lock_dir: &Path) -> PathBuf {
1141 lock_dir.join("owner")
1142}
1143
1144fn clear_stale_process_lock(lock_dir: &Path) -> io::Result<()> {
1145 match fs::remove_dir_all(lock_dir) {
1146 Ok(()) => Ok(()),
1147 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()),
1148 Err(err) => Err(err),
1149 }
1150}
1151
1152fn process_lock_is_stale(lock_dir: &Path) -> bool {
1153 process_lock_is_stale_with_proc_root(lock_dir, Path::new("/proc"))
1154}
1155
1156fn process_lock_is_stale_with_proc_root(lock_dir: &Path, proc_root: &Path) -> bool {
1157 let Some(owner) = read_process_lock_owner(&process_lock_owner_path(lock_dir)) else {
1158 return true;
1159 };
1160
1161 let proc_dir = proc_root.join(owner.pid.to_string());
1162 if !proc_dir.exists() {
1163 return true;
1164 }
1165
1166 match owner.start_ticks {
1167 Some(expected_ticks) => {
1168 read_process_start_ticks(proc_root, owner.pid) != Some(expected_ticks)
1169 }
1170 None => false,
1171 }
1172}
1173
1174fn render_process_lock_owner() -> String {
1175 let owner = current_process_lock_owner();
1176 match owner.start_ticks {
1177 Some(start_ticks) => format!("pid={}\nstart_ticks={start_ticks}\n", owner.pid),
1178 None => format!("pid={}\n", owner.pid),
1179 }
1180}
1181
1182fn current_process_lock_owner() -> ProcessLockOwner {
1183 ProcessLockOwner {
1184 pid: process::id(),
1185 start_ticks: read_process_start_ticks(Path::new("/proc"), process::id()),
1186 }
1187}
1188
1189fn read_process_lock_owner(path: &Path) -> Option<ProcessLockOwner> {
1190 let text = fs::read_to_string(path).ok()?;
1191 parse_process_lock_owner(&text)
1192}
1193
1194fn parse_process_lock_owner(text: &str) -> Option<ProcessLockOwner> {
1195 let trimmed = text.trim();
1196 if trimmed.is_empty() {
1197 return None;
1198 }
1199
1200 if let Ok(pid) = trimmed.parse::<u32>() {
1201 return Some(ProcessLockOwner {
1202 pid,
1203 start_ticks: None,
1204 });
1205 }
1206
1207 let mut pid = None;
1208 let mut start_ticks = None;
1209 for line in trimmed.lines() {
1210 if let Some(value) = line.strip_prefix("pid=") {
1211 pid = value.trim().parse::<u32>().ok();
1212 } else if let Some(value) = line.strip_prefix("start_ticks=") {
1213 start_ticks = value.trim().parse::<u64>().ok();
1214 }
1215 }
1216
1217 Some(ProcessLockOwner {
1218 pid: pid?,
1219 start_ticks,
1220 })
1221}
1222
1223fn read_process_start_ticks(proc_root: &Path, pid: u32) -> Option<u64> {
1224 let stat_path = proc_root.join(pid.to_string()).join("stat");
1225 let stat = fs::read_to_string(stat_path).ok()?;
1226 let close_paren = stat.rfind(')')?;
1227 let rest = stat.get(close_paren + 2..)?;
1228 let fields = rest.split_whitespace().collect::<Vec<_>>();
1229 fields.get(19)?.parse::<u64>().ok()
1230}
1231
1232#[cfg(test)]
1233mod process_lock_tests {
1234 use super::{
1235 PicStartError, classify_pic_start_panic, clear_stale_process_lock,
1236 ensure_process_lock_parent, parse_process_lock_owner, process_lock_dir_from_temp_root,
1237 process_lock_is_stale_with_proc_root, process_lock_owner_path,
1238 };
1239 use std::{
1240 fs,
1241 path::PathBuf,
1242 time::{SystemTime, UNIX_EPOCH},
1243 };
1244
1245 fn unique_lock_dir() -> PathBuf {
1246 let nanos = SystemTime::now()
1247 .duration_since(UNIX_EPOCH)
1248 .expect("clock must be after unix epoch")
1249 .as_nanos();
1250 std::env::temp_dir().join(format!("canic-pocket-ic-test-lock-{nanos}"))
1251 }
1252
1253 #[test]
1254 fn stale_process_lock_is_detected_and_removed() {
1255 let lock_dir = unique_lock_dir();
1256 fs::create_dir(&lock_dir).expect("create lock dir");
1257 fs::write(process_lock_owner_path(&lock_dir), "999999").expect("write stale owner");
1258
1259 assert!(process_lock_is_stale_with_proc_root(
1260 &lock_dir,
1261 std::path::Path::new("/proc")
1262 ));
1263 clear_stale_process_lock(&lock_dir).expect("remove stale lock dir");
1264 assert!(!lock_dir.exists());
1265 }
1266
1267 #[test]
1268 fn owner_parser_accepts_legacy_pid_only_format() {
1269 let owner = parse_process_lock_owner("12345\n").expect("parse pid-only owner");
1270 assert_eq!(owner.pid, 12345);
1271 assert_eq!(owner.start_ticks, None);
1272 }
1273
1274 #[test]
1275 fn stale_process_lock_detects_pid_reuse_via_start_ticks() {
1276 let root = unique_lock_dir();
1277 let lock_dir = root.join("lock");
1278 let proc_root = root.join("proc");
1279 let proc_pid = proc_root.join("77");
1280 fs::create_dir_all(&lock_dir).expect("create lock dir");
1281 fs::create_dir_all(&proc_pid).expect("create proc pid dir");
1282 fs::write(
1283 process_lock_owner_path(&lock_dir),
1284 "pid=77\nstart_ticks=41\n",
1285 )
1286 .expect("write owner");
1287 fs::write(
1288 proc_pid.join("stat"),
1289 "77 (cargo) S 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 99 0 0\n",
1290 )
1291 .expect("write proc stat");
1292
1293 assert!(process_lock_is_stale_with_proc_root(&lock_dir, &proc_root));
1294 }
1295
1296 #[test]
1297 fn ensure_process_lock_parent_creates_missing_temp_root_chain() {
1298 let root = unique_lock_dir();
1299 let temp_root = root.join("repo-local").join("tmp");
1300 let lock_dir = process_lock_dir_from_temp_root(&temp_root);
1301
1302 ensure_process_lock_parent(&lock_dir).expect("create temp-root parent chain");
1303
1304 assert!(temp_root.exists());
1305 }
1306
1307 #[test]
1308 fn pic_start_error_classifies_missing_binary() {
1309 let error = classify_pic_start_panic(Box::new(
1310 "Failed to validate PocketIC server binary `/tmp/pocket-ic`: `No such file or directory (os error 2)`.".to_string(),
1311 ));
1312
1313 assert!(matches!(error, PicStartError::BinaryUnavailable { .. }));
1314 }
1315
1316 #[test]
1317 fn pic_start_error_classifies_failed_spawn() {
1318 let error = classify_pic_start_panic(Box::new(
1319 "Failed to start PocketIC binary (/tmp/pocket-ic)".to_string(),
1320 ));
1321
1322 assert!(matches!(error, PicStartError::ServerStartFailed { .. }));
1323 }
1324}