From 776ec8e952297d125fbd1d0222672a53941ca48e Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Fri, 6 Mar 2026 13:28:15 -0500 Subject: [PATCH 01/17] fix: update to ic-agent/ic-utils 0.46 and add reqwest features - Add `json` and `stream` features to reqwest (required for `.json()` and `.bytes_stream()` on responses in reqwest 0.12) - Update snapshot management calls: canister_id is now embedded in the args structs, removing the separate &canister_id parameter - Add new required fields to TakeCanisterSnapshotArgs (uninstall_code, sender_canister_version) - Replace removed `with_optional_*` builder methods on UpdateSettingsBuilder with conditional `with_*` calls - Add log filter support in canister logs command using new FetchCanisterLogsArgs and CanisterLogFilter from ic-utils 0.46 Co-Authored-By: Claude Sonnet 4.6 --- Cargo.lock | 650 +++++++++++++++++- Cargo.toml | 12 +- crates/icp-cli/src/commands/canister/logs.rs | 123 +++- .../src/commands/canister/snapshot/create.rs | 4 +- .../src/commands/canister/snapshot/delete.rs | 2 +- .../src/commands/canister/snapshot/restore.rs | 2 +- crates/icp-cli/src/operations/settings.rs | 35 +- .../src/operations/snapshot_transfer.rs | 55 +- 8 files changed, 778 insertions(+), 105 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4218178ee..066da039e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,7 +16,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -434,6 +434,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "backoff" version = "0.4.0" @@ -816,12 +838,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "801927ee168e17809ab8901d9f01f700cd7d8d6a6527997fee44e4b0327a253c" dependencies = [ "ahash 0.8.12", + "cached_proc_macro", + "cached_proc_macro_types", "hashbrown 0.15.5", "once_cell", "thiserror 2.0.18", "web-time", ] +[[package]] +name = "cached_proc_macro" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9225bdcf4e4a9a4c08bf16607908eb2fbf746828d5e0b5e019726dbf6571f201" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "cached_proc_macro_types" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" + [[package]] name = "camino" version = "1.2.2" @@ -978,6 +1020,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.4" @@ -990,6 +1038,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", +] + [[package]] name = "chrono" version = "0.4.43" @@ -1062,6 +1121,15 @@ version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -1078,6 +1146,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1183,6 +1261,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -1294,7 +1381,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest 0.10.7", "fiat-crypto", @@ -1715,6 +1802,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "endi" version = "1.1.1" @@ -2005,6 +2101,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -2176,11 +2278,25 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.0", + "wasip2", + "wasip3", +] + [[package]] name = "git2" version = "0.20.4" @@ -2191,7 +2307,7 @@ dependencies = [ "libc", "libgit2-sys", "log", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "url", ] @@ -2750,9 +2866,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -2796,9 +2914,9 @@ dependencies = [ [[package]] name = "ic-agent" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20a6173286a80fc478462fc45de42faf37a79b0109a489743aeffb3e4a2fc772" +checksum = "da4fb6055538a9f0ee634f0be865cafbe79e74b8addd1eeb05ba0b51d4e8271f" dependencies = [ "arc-swap", "async-channel 2.5.0", @@ -2809,7 +2927,6 @@ dependencies = [ "bytes", "cached", "candid", - "der", "ecdsa", "ed25519-consensus", "elliptic-curve", @@ -2819,7 +2936,7 @@ dependencies = [ "http-body", "http-body-util", "ic-certification", - "ic-ed25519", + "ic-ed25519 0.6.0", "ic-transport-types", "ic-verify-bls-signature", "k256", @@ -2827,9 +2944,9 @@ dependencies = [ "p256", "pem", "pkcs8", - "rand 0.8.5", + "rand 0.10.0", "rangemap", - "reqwest", + "reqwest 0.13.2", "sec1", "serde", "serde_bytes", @@ -2846,9 +2963,9 @@ dependencies = [ [[package]] name = "ic-asset" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cc3c51dd0182931fe12cd9b4b4553d2ff8e42f279a3008f3ceae6680ad6a013" +checksum = "962a5f1992104aa7aec190ff6debbb3cce93a4cf73772c0e682b752ad7eca1ec" dependencies = [ "backoff", "brotli", @@ -2886,7 +3003,7 @@ dependencies = [ "ic-cdk-executor", "ic-cdk-macros", "ic-error-types", - "ic-management-canister-types", + "ic-management-canister-types 0.5.0", "ic0", "pin-project-lite", "serde", @@ -2948,6 +3065,23 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ic-ed25519" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ad3d86f2e35cb99ae7497b3e4fa92ad751a8e8978f0680d37b86dd51ef91714" +dependencies = [ + "curve25519-dalek", + "ed25519-dalek", + "hex-literal", + "hkdf", + "ic_principal", + "pem", + "rand 0.8.5", + "thiserror 2.0.18", + "zeroize", +] + [[package]] name = "ic-error-types" version = "0.2.0" @@ -2961,9 +3095,9 @@ dependencies = [ [[package]] name = "ic-identity-hsm" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cadfa7095085405ceaadc8aa7714e313cb778d1b98292dbfe23cd087b345b35" +checksum = "b6753cf0b4b1c8abf9c900bd2dc233635a10f800fb12f27ffe39212d8d2458c7" dependencies = [ "hex", "ic-agent", @@ -2999,6 +3133,17 @@ dependencies = [ "serde_bytes", ] +[[package]] +name = "ic-management-canister-types" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51705516ed4d23f24e8d714a70fe9d7ec17106cfd830d5434a1b29f583ef70ee" +dependencies = [ + "candid", + "serde", + "serde_bytes", +] + [[package]] name = "ic-stable-structures" version = "0.6.9" @@ -3010,9 +3155,9 @@ dependencies = [ [[package]] name = "ic-transport-types" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a775244756a5d97ff19b08071a946a4b4896904e35deb036bf215e80f2e703d" +checksum = "2957ed6ce101b3b6c4aa65bbcf8e92afdec42758b58cb03c7df76c96db13d0c0" dependencies = [ "candid", "hex", @@ -3028,22 +3173,22 @@ dependencies = [ [[package]] name = "ic-utils" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30c22aaa2924df0321705dc01d408c8b75d1e1deb40b65defd2ff04008a720be" +checksum = "99534a857fd9999b9adfde82b2623bb9925997dc1813ec315bd3b4085911104a" dependencies = [ "async-trait", "candid", "futures-util", "ic-agent", - "ic-management-canister-types", + "ic-management-canister-types 0.7.1", "once_cell", "semver", "serde", "serde_bytes", "sha2 0.10.9", - "strum 0.27.2", - "strum_macros 0.27.2", + "strum 0.28.0", + "strum_macros 0.28.0", "thiserror 2.0.18", "time", ] @@ -3123,10 +3268,10 @@ dependencies = [ "hex", "ic-agent", "ic-asset", - "ic-ed25519", + "ic-ed25519 0.5.0", "ic-identity-hsm", "ic-ledger-types", - "ic-management-canister-types", + "ic-management-canister-types 0.7.1", "ic-utils", "icp-canister-interfaces", "icrc-ledger-types", @@ -3143,7 +3288,7 @@ dependencies = [ "pem", "pkcs8", "rand 0.9.2", - "reqwest", + "reqwest 0.12.28", "schemars 1.2.0", "scrypt", "sec1", @@ -3206,9 +3351,9 @@ dependencies = [ "hex", "httptest", "ic-agent", - "ic-ed25519", + "ic-ed25519 0.5.0", "ic-ledger-types", - "ic-management-canister-types", + "ic-management-canister-types 0.7.1", "ic-utils", "icp", "icp-canister-interfaces", @@ -3229,7 +3374,7 @@ dependencies = [ "predicates", "rand 0.9.2", "regex", - "reqwest", + "reqwest 0.12.28", "sec1", "send_ctrlc", "serde", @@ -3248,7 +3393,7 @@ dependencies = [ "tracing-subscriber", "url", "uuid", - "wasmparser", + "wasmparser 0.240.0", "wslpath2", ] @@ -3369,6 +3514,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -3601,6 +3752,28 @@ dependencies = [ "jiff-tzdb", ] +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" version = "0.1.34" @@ -3653,7 +3826,7 @@ dependencies = [ "referencing", "regex", "regex-syntax 0.8.8", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "uuid-simd", @@ -3679,7 +3852,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -3771,6 +3944,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.180" @@ -4385,6 +4564,12 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "openssl-src" version = "300.5.5+3.5.5" @@ -4862,6 +5047,16 @@ dependencies = [ "unicode-width 0.2.2", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.114", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -4954,6 +5149,7 @@ version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ + "aws-lc-rs", "bytes", "getrandom 0.3.4", "lru-slab", @@ -4998,6 +5194,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -5025,6 +5227,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.0", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -5063,6 +5276,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + [[package]] name = "rangemap" version = "1.7.1" @@ -5247,11 +5466,55 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.4.2", "web-sys", "webpki-roots", ] +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.5.0", + "web-sys", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -5398,6 +5661,7 @@ version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ + "aws-lc-rs", "once_cell", "ring", "rustls-pki-types", @@ -5406,6 +5670,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe 0.2.1", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", +] + [[package]] name = "rustls-pki-types" version = "1.14.0" @@ -5416,12 +5692,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework 3.5.1", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -5475,6 +5779,15 @@ dependencies = [ "sdd", ] +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schema-gen" version = "0.2.0" @@ -5862,7 +6175,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -5884,7 +6197,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.9.0", "opaque-debug", ] @@ -5896,7 +6209,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -6139,6 +6452,12 @@ dependencies = [ "strum_macros 0.27.2", ] +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" + [[package]] name = "strum_macros" version = "0.26.4" @@ -6164,6 +6483,18 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "subtle" version = "2.6.1" @@ -6232,6 +6563,27 @@ dependencies = [ "windows", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" @@ -6458,9 +6810,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -6891,6 +7243,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.108" @@ -6950,6 +7311,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser 0.244.0", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -6963,6 +7346,19 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmparser" version = "0.240.0" @@ -6976,6 +7372,18 @@ dependencies = [ "serde", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.10.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + [[package]] name = "web-sys" version = "0.3.85" @@ -6996,6 +7404,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "1.0.5" @@ -7139,6 +7556,17 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -7175,6 +7603,15 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -7211,6 +7648,21 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -7253,6 +7705,12 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -7265,6 +7723,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -7277,6 +7741,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -7301,6 +7771,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -7313,6 +7789,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -7325,6 +7807,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -7337,6 +7825,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -7373,6 +7867,88 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.114", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.114", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.10.0", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser 0.244.0", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.244.0", +] [[package]] name = "writeable" diff --git a/Cargo.toml b/Cargo.toml index d57d8fddc..3231706ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,15 +43,15 @@ glob = "0.3.2" handlebars = "6.3.2" hex = { version = "0.4.3", features = ["serde"] } httptest = "0.16.3" -ic-agent = { version = "0.45.0" } -ic-asset = "0.27.0" +ic-agent = { version = "0.46.0" } +ic-asset = "0.28.0" ic-ed25519 = "0.5.0" ic-ledger-types = "0.16.0" -ic-management-canister-types = { version = "0.5.0" } -ic-utils = { version = "0.45.0" } +ic-management-canister-types = { version = "0.7.1" } +ic-utils = { version = "0.46.0" } icp = { path = "crates/icp" } icp-canister-interfaces = { path = "crates/icp-canister-interfaces" } -ic-identity-hsm = "0.45.0" +ic-identity-hsm = "0.46.0" icrc-ledger-types = "0.1.10" indicatif = "0.18.0" indoc = "2.0.6" @@ -100,7 +100,7 @@ zeroize = "1.8.1" [workspace.dependencies.reqwest] version = "0.12.15" default-features = false -features = ["rustls-tls"] +features = ["rustls-tls", "json", "stream"] [workspace.dependencies.schemars] version = "1.0.4" diff --git a/crates/icp-cli/src/commands/canister/logs.rs b/crates/icp-cli/src/commands/canister/logs.rs index b991b2dda..4c964cb42 100644 --- a/crates/icp-cli/src/commands/canister/logs.rs +++ b/crates/icp-cli/src/commands/canister/logs.rs @@ -1,7 +1,9 @@ use anyhow::{Context as _, anyhow}; use clap::Args; -use ic_management_canister_types::{CanisterLogRecord, FetchCanisterLogsResult}; use ic_utils::interfaces::ManagementCanister; +use ic_utils::interfaces::management_canister::{ + CanisterLogFilter, CanisterLogRecord, FetchCanisterLogsArgs, FetchCanisterLogsResult, +}; use icp::context::Context; use icp::signal::stop_signal; use time::{OffsetDateTime, format_description::well_known::Rfc3339}; @@ -22,6 +24,38 @@ pub(crate) struct LogsArgs { /// Polling interval in seconds when following logs (requires --follow) #[arg(long, requires = "follow", default_value = "2")] pub(crate) interval: u64, + + /// Show logs at or after this timestamp. Accepts nanoseconds since Unix epoch or RFC3339 + /// (e.g. '2024-01-01T00:00:00Z') + #[arg(long, value_name = "TIMESTAMP", conflicts_with_all = ["since_index", "until_index"], value_parser = parse_timestamp)] + pub(crate) since: Option, + + /// Show logs at or before this timestamp. Accepts nanoseconds since Unix epoch or RFC3339 + /// (e.g. '2024-01-01T00:00:00Z'). Requires --since + #[arg(long, value_name = "TIMESTAMP", requires = "since", conflicts_with_all = ["since_index", "until_index"], value_parser = parse_timestamp)] + pub(crate) until: Option, + + /// Show logs at or after this log index (inclusive) + #[arg(long, value_name = "INDEX", conflicts_with_all = ["since", "until"])] + pub(crate) since_index: Option, + + /// Show logs at or before this log index (inclusive). Requires --since-index + #[arg(long, value_name = "INDEX", requires = "since_index", conflicts_with_all = ["since", "until"])] + pub(crate) until_index: Option, +} + +fn parse_timestamp(s: &str) -> Result { + // Try raw nanoseconds first + if let Ok(nanos) = s.parse::() { + return Ok(nanos); + } + // Fall back to RFC3339 + OffsetDateTime::parse(s, &Rfc3339) + .map(|dt| { + let nanos_per_sec = 1_000_000_000u64; + (dt.unix_timestamp() as u64) * nanos_per_sec + dt.nanosecond() as u64 + }) + .map_err(|_| format!("'{s}' is not a valid nanosecond timestamp or RFC3339 datetime")) } pub(crate) async fn exec(ctx: &Context, args: &LogsArgs) -> Result<(), anyhow::Error> { @@ -50,12 +84,30 @@ pub(crate) async fn exec(ctx: &Context, args: &LogsArgs) -> Result<(), anyhow::E let mgmt = ManagementCanister::create(&agent); + let initial_filter = build_filter(args); + if args.follow { // Follow mode: continuously fetch and display new logs - follow_logs(ctx, &mgmt, &canister_id, args.interval).await + follow_logs(ctx, &mgmt, &canister_id, args.interval, initial_filter).await } else { // Single fetch mode: fetch all logs once - fetch_and_display_logs(ctx, &mgmt, &canister_id).await + fetch_and_display_logs(ctx, &mgmt, &canister_id, initial_filter).await + } +} + +fn build_filter(args: &LogsArgs) -> Option { + if let Some(start) = args.since_index { + Some(CanisterLogFilter::ByIdx { + start, + end: args.until_index.unwrap_or(u64::MAX), + }) + } else if let Some(start) = args.since { + Some(CanisterLogFilter::ByTimestampNanos { + start, + end: args.until.unwrap_or(u64::MAX), + }) + } else { + None } } @@ -63,9 +115,14 @@ async fn fetch_and_display_logs( ctx: &Context, mgmt: &ManagementCanister<'_>, canister_id: &candid::Principal, + filter: Option, ) -> Result<(), anyhow::Error> { + let fetch_args = FetchCanisterLogsArgs { + canister_id: *canister_id, + filter, + }; let (result,): (FetchCanisterLogsResult,) = mgmt - .fetch_canister_logs(canister_id) + .fetch_canister_logs(&fetch_args) .await .context("Failed to fetch canister logs")?; @@ -82,26 +139,31 @@ async fn follow_logs( mgmt: &ManagementCanister<'_>, canister_id: &candid::Principal, interval_seconds: u64, + initial_filter: Option, ) -> Result<(), anyhow::Error> { let mut last_idx: Option = None; let interval = std::time::Duration::from_secs(interval_seconds); loop { - // Fetch all logs + // On first iteration use the user-supplied filter; on subsequent iterations use + // server-side idx filtering to fetch only new logs. + let filter = match last_idx { + Some(idx) => Some(CanisterLogFilter::ByIdx { + start: idx + 1, + end: u64::MAX, + }), + None => initial_filter.clone(), + }; + let fetch_args = FetchCanisterLogsArgs { + canister_id: *canister_id, + filter, + }; let (result,): (FetchCanisterLogsResult,) = mgmt - .fetch_canister_logs(canister_id) + .fetch_canister_logs(&fetch_args) .await .context("Failed to fetch canister logs")?; - // Filter to only new logs based on last_idx - let new_logs: Vec<_> = result - .canister_log_records - .into_iter() - .filter(|log| match last_idx { - None => true, // First iteration, show all logs - Some(idx) => log.idx > idx, - }) - .collect(); + let new_logs = result.canister_log_records; if !new_logs.is_empty() { for log in &new_logs { @@ -217,4 +279,35 @@ mod tests { "[42. 2024-01-01T10:00:00.123456789Z]: Test message" ); } + + #[test] + fn test_parse_timestamp_raw_nanos() { + assert_eq!( + parse_timestamp("1704103200123456789"), + Ok(1704103200123456789) + ); + assert_eq!(parse_timestamp("0"), Ok(0)); + } + + #[test] + fn test_parse_timestamp_rfc3339() { + // 2024-01-01T10:00:00Z = 1704103200000000000 nanos + assert_eq!( + parse_timestamp("2024-01-01T10:00:00Z"), + Ok(1704103200_000_000_000) + ); + } + + #[test] + fn test_parse_timestamp_rfc3339_with_nanos() { + assert_eq!( + parse_timestamp("2024-01-01T10:00:00.123456789Z"), + Ok(1704103200123456789) + ); + } + + #[test] + fn test_parse_timestamp_invalid() { + assert!(parse_timestamp("not-a-timestamp").is_err()); + } } diff --git a/crates/icp-cli/src/commands/canister/snapshot/create.rs b/crates/icp-cli/src/commands/canister/snapshot/create.rs index 10f2c3a9f..056c1c54c 100644 --- a/crates/icp-cli/src/commands/canister/snapshot/create.rs +++ b/crates/icp-cli/src/commands/canister/snapshot/create.rs @@ -58,9 +58,11 @@ pub(crate) async fn exec(ctx: &Context, args: &CreateArgs) -> Result<(), anyhow: let take_args = TakeCanisterSnapshotArgs { canister_id: cid, replace_snapshot: args.replace.as_ref().map(|s| s.0.clone()), + uninstall_code: None, + sender_canister_version: None, }; - let (snapshot,) = mgmt.take_canister_snapshot(&cid, &take_args).await?; + let (snapshot,) = mgmt.take_canister_snapshot(&take_args).await?; ctx.term.write_line(&format!( "Created snapshot {id} for canister {name} ({cid})", diff --git a/crates/icp-cli/src/commands/canister/snapshot/delete.rs b/crates/icp-cli/src/commands/canister/snapshot/delete.rs index 5c8bef526..dc33efec4 100644 --- a/crates/icp-cli/src/commands/canister/snapshot/delete.rs +++ b/crates/icp-cli/src/commands/canister/snapshot/delete.rs @@ -41,7 +41,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DeleteArgs) -> Result<(), anyhow: snapshot_id: args.snapshot_id.0.clone(), }; - mgmt.delete_canister_snapshot(&cid, &delete_args).await?; + mgmt.delete_canister_snapshot(&delete_args).await?; let name = &args.cmd_args.canister; ctx.term.write_line(&format!( diff --git a/crates/icp-cli/src/commands/canister/snapshot/restore.rs b/crates/icp-cli/src/commands/canister/snapshot/restore.rs index dd54c327c..3f2d79c17 100644 --- a/crates/icp-cli/src/commands/canister/snapshot/restore.rs +++ b/crates/icp-cli/src/commands/canister/snapshot/restore.rs @@ -58,7 +58,7 @@ pub(crate) async fn exec(ctx: &Context, args: &RestoreArgs) -> Result<(), anyhow sender_canister_version: None, }; - mgmt.load_canister_snapshot(&cid, &load_args).await?; + mgmt.load_canister_snapshot(&load_args).await?; ctx.term.write_line(&format!( "Restored canister {name} ({cid}) from snapshot {id}", diff --git a/crates/icp-cli/src/operations/settings.rs b/crates/icp-cli/src/operations/settings.rs index 651e6b8c4..9797f513f 100644 --- a/crates/icp-cli/src/operations/settings.rs +++ b/crates/icp-cli/src/operations/settings.rs @@ -141,15 +141,32 @@ pub(crate) async fn sync_settings( // No changes needed return Ok(()); } - mgmt.update_settings(cid) - .with_optional_log_visibility(log_visibility_setting) - .with_optional_compute_allocation(compute_allocation) - .with_optional_memory_allocation(memory_allocation.as_ref().map(|m| m.get())) - .with_optional_freezing_threshold(freezing_threshold.as_ref().map(|d| d.get())) - .with_optional_reserved_cycles_limit(reserved_cycles_limit.as_ref().map(|r| r.get())) - .with_optional_wasm_memory_limit(wasm_memory_limit.as_ref().map(|m| m.get())) - .with_optional_wasm_memory_threshold(wasm_memory_threshold.as_ref().map(|m| m.get())) - .with_optional_environment_variables(environment_variable_setting) + let mut builder = mgmt.update_settings(cid); + if let Some(v) = log_visibility_setting { + builder = builder.with_log_visibility(v); + } + if let Some(v) = compute_allocation { + builder = builder.with_compute_allocation(v); + } + if let Some(v) = memory_allocation.as_ref().map(|m| m.get()) { + builder = builder.with_memory_allocation(v); + } + if let Some(v) = freezing_threshold.as_ref().map(|d| d.get()) { + builder = builder.with_freezing_threshold(v); + } + if let Some(v) = reserved_cycles_limit.as_ref().map(|r| r.get()) { + builder = builder.with_reserved_cycles_limit(v); + } + if let Some(v) = wasm_memory_limit.as_ref().map(|m| m.get()) { + builder = builder.with_wasm_memory_limit(v); + } + if let Some(v) = wasm_memory_threshold.as_ref().map(|m| m.get()) { + builder = builder.with_wasm_memory_threshold(v); + } + if let Some(v) = environment_variable_setting { + builder = builder.with_environment_variables(v); + } + builder .build() .context(ValidateSettingsSnafu { name: &canister.name, diff --git a/crates/icp-cli/src/operations/snapshot_transfer.rs b/crates/icp-cli/src/operations/snapshot_transfer.rs index 7413b7054..ddaf9c4ac 100644 --- a/crates/icp-cli/src/operations/snapshot_transfer.rs +++ b/crates/icp-cli/src/operations/snapshot_transfer.rs @@ -439,12 +439,9 @@ pub async fn read_snapshot_metadata( snapshot_id: snapshot_id.to_vec(), }; - let (metadata,) = with_retry(|| async { - mgmt.read_canister_snapshot_metadata(&canister_id, &args) - .await - }) - .await - .context(ReadMetadataSnafu { canister_id })?; + let (metadata,) = with_retry(|| async { mgmt.read_canister_snapshot_metadata(&args).await }) + .await + .context(ReadMetadataSnafu { canister_id })?; Ok(metadata) } @@ -480,12 +477,9 @@ pub async fn upload_snapshot_metadata( on_low_wasm_memory_hook_status: metadata.on_low_wasm_memory_hook_status.clone(), }; - let (result,) = with_retry(|| async { - mgmt.upload_canister_snapshot_metadata(&canister_id, &args) - .await - }) - .await - .context(UploadMetadataSnafu { canister_id })?; + let (result,) = with_retry(|| async { mgmt.upload_canister_snapshot_metadata(&args).await }) + .await + .context(UploadMetadataSnafu { canister_id })?; Ok(result) } @@ -559,13 +553,11 @@ pub async fn download_blob_to_file( let mgmt = mgmt.clone(); in_progress.push(async move { - let result = with_retry(|| async { - mgmt.read_canister_snapshot_data(&canister_id, &args).await - }) - .await - .context(ReadDataChunkSnafu { - offset: chunk_offset, - })?; + let result = with_retry(|| async { mgmt.read_canister_snapshot_data(&args).await }) + .await + .context(ReadDataChunkSnafu { + offset: chunk_offset, + })?; Ok::<_, SnapshotTransferError>((chunk_offset, result.0.chunk)) }); } @@ -629,10 +621,9 @@ pub async fn download_wasm_chunk( let hash_hex = hex::encode(&chunk_hash.hash); let output_path = paths.wasm_chunk_path(&chunk_hash.hash); - let (result,) = - with_retry(|| async { mgmt.read_canister_snapshot_data(&canister_id, &args).await }) - .await - .context(ReadWasmChunkSnafu { hash: &hash_hex })?; + let (result,) = with_retry(|| async { mgmt.read_canister_snapshot_data(&args).await }) + .await + .context(ReadWasmChunkSnafu { hash: &hash_hex })?; icp::fs::write(&output_path, &result.chunk)?; @@ -706,12 +697,9 @@ pub async fn upload_blob_from_file( let mgmt = mgmt.clone(); in_progress.push(async move { - with_retry(|| async { - mgmt.upload_canister_snapshot_data(&canister_id, &args) - .await - }) - .await - .context(UploadDataChunkSnafu { offset })?; + with_retry(|| async { mgmt.upload_canister_snapshot_data(&args).await }) + .await + .context(UploadDataChunkSnafu { offset })?; Ok::<_, SnapshotTransferError>((offset, args.chunk.len() as u64)) }); } @@ -782,12 +770,9 @@ pub async fn upload_wasm_chunk( let hash_hex = hex::encode(chunk_hash); - with_retry(|| async { - mgmt.upload_canister_snapshot_data(&canister_id, &args) - .await - }) - .await - .context(UploadWasmChunkSnafu { hash: hash_hex })?; + with_retry(|| async { mgmt.upload_canister_snapshot_data(&args).await }) + .await + .context(UploadWasmChunkSnafu { hash: hash_hex })?; Ok(()) } From b73752dd04f9e135311273bfac031448982ecbf6 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Fri, 6 Mar 2026 13:52:09 -0500 Subject: [PATCH 02/17] fix: allow --until and --until-index flags without requiring --since/--since-index When only --until or --until-index is provided, default the start to 0 so users can filter logs up to a given point without specifying a start. Also add missing conflicts_with for --follow on all filter flags. Co-Authored-By: Claude Opus 4.6 --- crates/icp-cli/src/commands/canister/logs.rs | 47 +++++++++++--------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/crates/icp-cli/src/commands/canister/logs.rs b/crates/icp-cli/src/commands/canister/logs.rs index 4c964cb42..3c6a7bded 100644 --- a/crates/icp-cli/src/commands/canister/logs.rs +++ b/crates/icp-cli/src/commands/canister/logs.rs @@ -26,21 +26,21 @@ pub(crate) struct LogsArgs { pub(crate) interval: u64, /// Show logs at or after this timestamp. Accepts nanoseconds since Unix epoch or RFC3339 - /// (e.g. '2024-01-01T00:00:00Z') - #[arg(long, value_name = "TIMESTAMP", conflicts_with_all = ["since_index", "until_index"], value_parser = parse_timestamp)] + /// (e.g. '2024-01-01T00:00:00Z'). Cannot be used with --follow + #[arg(long, value_name = "TIMESTAMP", conflicts_with_all = ["follow", "since_index", "until_index"], value_parser = parse_timestamp)] pub(crate) since: Option, /// Show logs at or before this timestamp. Accepts nanoseconds since Unix epoch or RFC3339 - /// (e.g. '2024-01-01T00:00:00Z'). Requires --since - #[arg(long, value_name = "TIMESTAMP", requires = "since", conflicts_with_all = ["since_index", "until_index"], value_parser = parse_timestamp)] + /// (e.g. '2024-01-01T00:00:00Z'). Cannot be used with --follow + #[arg(long, value_name = "TIMESTAMP", conflicts_with_all = ["follow", "since_index", "until_index"], value_parser = parse_timestamp)] pub(crate) until: Option, - /// Show logs at or after this log index (inclusive) - #[arg(long, value_name = "INDEX", conflicts_with_all = ["since", "until"])] + /// Show logs at or after this log index (inclusive). Cannot be used with --follow + #[arg(long, value_name = "INDEX", conflicts_with_all = ["follow", "since", "until"])] pub(crate) since_index: Option, - /// Show logs at or before this log index (inclusive). Requires --since-index - #[arg(long, value_name = "INDEX", requires = "since_index", conflicts_with_all = ["since", "until"])] + /// Show logs at or before this log index (inclusive). Cannot be used with --follow + #[arg(long, value_name = "INDEX", conflicts_with_all = ["follow", "since", "until"])] pub(crate) until_index: Option, } @@ -84,26 +84,24 @@ pub(crate) async fn exec(ctx: &Context, args: &LogsArgs) -> Result<(), anyhow::E let mgmt = ManagementCanister::create(&agent); - let initial_filter = build_filter(args); - if args.follow { // Follow mode: continuously fetch and display new logs - follow_logs(ctx, &mgmt, &canister_id, args.interval, initial_filter).await + follow_logs(ctx, &mgmt, &canister_id, args.interval).await } else { // Single fetch mode: fetch all logs once - fetch_and_display_logs(ctx, &mgmt, &canister_id, initial_filter).await + fetch_and_display_logs(ctx, &mgmt, &canister_id, build_filter(args)).await } } fn build_filter(args: &LogsArgs) -> Option { - if let Some(start) = args.since_index { + if args.since_index.is_some() || args.until_index.is_some() { Some(CanisterLogFilter::ByIdx { - start, + start: args.since_index.unwrap_or(0), end: args.until_index.unwrap_or(u64::MAX), }) - } else if let Some(start) = args.since { + } else if args.since.is_some() || args.until.is_some() { Some(CanisterLogFilter::ByTimestampNanos { - start, + start: args.since.unwrap_or(0), end: args.until.unwrap_or(u64::MAX), }) } else { @@ -134,25 +132,34 @@ async fn fetch_and_display_logs( Ok(()) } +const FOLLOW_LOOKBACK_NANOS: u64 = 60 * 60 * 1_000_000_000; // 1 hour + async fn follow_logs( ctx: &Context, mgmt: &ManagementCanister<'_>, canister_id: &candid::Principal, interval_seconds: u64, - initial_filter: Option, ) -> Result<(), anyhow::Error> { let mut last_idx: Option = None; let interval = std::time::Duration::from_secs(interval_seconds); loop { - // On first iteration use the user-supplied filter; on subsequent iterations use - // server-side idx filtering to fetch only new logs. let filter = match last_idx { Some(idx) => Some(CanisterLogFilter::ByIdx { start: idx + 1, end: u64::MAX, }), - None => initial_filter.clone(), + None => { + // First fetch: look back 1 hour from now + let now_nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_nanos() as u64) + .unwrap_or(0); + Some(CanisterLogFilter::ByTimestampNanos { + start: now_nanos.saturating_sub(FOLLOW_LOOKBACK_NANOS), + end: u64::MAX, + }) + } }; let fetch_args = FetchCanisterLogsArgs { canister_id: *canister_id, From 5bf0e1a316fcbf6ccdc89694e8a0a41004a5ce04 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Fri, 6 Mar 2026 13:56:56 -0500 Subject: [PATCH 03/17] docs: regenerate CLI docs for canister logs filter flags Co-Authored-By: Claude Opus 4.6 --- docs/reference/cli.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 49c7bf788..f1fedd0b9 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -333,6 +333,10 @@ Fetch and display canister logs * `--interval ` — Polling interval in seconds when following logs (requires --follow) Default value: `2` +* `--since ` — Show logs at or after this timestamp. Accepts nanoseconds since Unix epoch or RFC3339 (e.g. '2024-01-01T00:00:00Z'). Cannot be used with --follow +* `--until ` — Show logs at or before this timestamp. Accepts nanoseconds since Unix epoch or RFC3339 (e.g. '2024-01-01T00:00:00Z'). Cannot be used with --follow +* `--since-index ` — Show logs at or after this log index (inclusive). Cannot be used with --follow +* `--until-index ` — Show logs at or before this log index (inclusive). Cannot be used with --follow From ad89c6dffab8941c557539b10ba6297475d37bad Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Fri, 6 Mar 2026 14:13:09 -0500 Subject: [PATCH 04/17] feat: support log_memory_limit canister setting Add the log_memory_limit field to the Settings model, canister settings update command, settings sync operation, and canister status display. Co-Authored-By: Claude Opus 4.6 --- .../src/commands/canister/settings/update.rs | 13 +++++++++++++ crates/icp-cli/src/commands/canister/status.rs | 7 +++++++ crates/icp-cli/src/operations/settings.rs | 8 ++++++++ crates/icp/src/canister/mod.rs | 4 ++++ docs/reference/cli.md | 1 + 5 files changed, 33 insertions(+) diff --git a/crates/icp-cli/src/commands/canister/settings/update.rs b/crates/icp-cli/src/commands/canister/settings/update.rs index 2bd7d8cea..57417f256 100644 --- a/crates/icp-cli/src/commands/canister/settings/update.rs +++ b/crates/icp-cli/src/commands/canister/settings/update.rs @@ -120,6 +120,11 @@ pub(crate) struct UpdateArgs { #[arg(long)] wasm_memory_threshold: Option, + /// Log memory limit in bytes (max 2 MiB). Oldest logs are purged when usage exceeds this value. + /// Supports suffixes: kb, kib, mb, mib (e.g. "2mib" or "256kib"). Canister default is 4096 bytes. + #[arg(long)] + log_memory_limit: Option, + #[command(flatten)] log_visibility: Option, @@ -266,6 +271,14 @@ pub(crate) async fn exec(ctx: &Context, args: &UpdateArgs) -> Result<(), anyhow: } update = update.with_wasm_memory_threshold(wasm_memory_threshold.get()); } + if let Some(log_memory_limit) = &args.log_memory_limit { + if configured_settings.log_memory_limit.is_some() { + ctx.term.write_line( + "Warning: Log memory limit is already set in icp.yaml; this new value will be overridden on next settings sync" + )? + } + update = update.with_log_memory_limit(log_memory_limit.get()); + } if let Some(log_visibility) = log_visibility { if configured_settings.log_visibility.is_some() { ctx.term.write_line( diff --git a/crates/icp-cli/src/commands/canister/status.rs b/crates/icp-cli/src/commands/canister/status.rs index ebc71cda0..6d603352f 100644 --- a/crates/icp-cli/src/commands/canister/status.rs +++ b/crates/icp-cli/src/commands/canister/status.rs @@ -325,6 +325,7 @@ struct SerializableCanisterSettings { reserved_cycles_limit: String, wasm_memory_limit: String, wasm_memory_threshold: String, + log_memory_limit: String, log_visibility: SerializableLogVisibility, environment_variables: Vec, } @@ -377,6 +378,7 @@ impl SerializableCanisterSettings { reserved_cycles_limit: settings.reserved_cycles_limit.to_string(), wasm_memory_limit: settings.wasm_memory_limit.to_string(), wasm_memory_threshold: settings.wasm_memory_threshold.to_string(), + log_memory_limit: settings.log_memory_limit.to_string(), log_visibility: SerializableLogVisibility::from(&settings.log_visibility), environment_variables: settings.environment_variables.clone(), } @@ -471,6 +473,11 @@ fn build_output(result: &SerializableCanisterStatusResult) -> Result "Controllers".to_string(), diff --git a/crates/icp-cli/src/operations/settings.rs b/crates/icp-cli/src/operations/settings.rs index 9797f513f..27bd300d1 100644 --- a/crates/icp-cli/src/operations/settings.rs +++ b/crates/icp-cli/src/operations/settings.rs @@ -85,6 +85,7 @@ pub(crate) async fn sync_settings( ref reserved_cycles_limit, ref wasm_memory_limit, ref wasm_memory_threshold, + ref log_memory_limit, ref environment_variables, } = &canister.settings; let current_settings = status.settings; @@ -134,6 +135,10 @@ pub(crate) async fn sync_settings( .as_ref() .map(|m| m.get()) .is_none_or(|s| current_settings.wasm_memory_threshold.0.to_u64() == Some(s)) + && log_memory_limit + .as_ref() + .map(|m| m.get()) + .is_none_or(|s| current_settings.log_memory_limit.0.to_u64() == Some(s)) && environment_variable_setting .as_ref() .is_none_or(|s| environment_variables_eq(s, ¤t_settings.environment_variables)) @@ -163,6 +168,9 @@ pub(crate) async fn sync_settings( if let Some(v) = wasm_memory_threshold.as_ref().map(|m| m.get()) { builder = builder.with_wasm_memory_threshold(v); } + if let Some(v) = log_memory_limit.as_ref().map(|m| m.get()) { + builder = builder.with_log_memory_limit(v); + } if let Some(v) = environment_variable_setting { builder = builder.with_environment_variables(v); } diff --git a/crates/icp/src/canister/mod.rs b/crates/icp/src/canister/mod.rs index 2c568a45d..a851b952c 100644 --- a/crates/icp/src/canister/mod.rs +++ b/crates/icp/src/canister/mod.rs @@ -231,6 +231,10 @@ pub struct Settings { /// Supports suffixes in YAML: kb, kib, mb, mib, gb, gib (e.g. "4gib" or "2.5kb"). pub wasm_memory_threshold: Option, + /// Log memory limit in bytes (max 2 MiB). Oldest logs are purged when usage exceeds this value. + /// Supports suffixes in YAML: kb, kib, mb, mib (e.g. "2mib" or "256kib"). Canister default is 4096 bytes. + pub log_memory_limit: Option, + /// Environment variables for the canister as key-value pairs. /// These variables are accessible within the canister and can be used to configure /// behavior without hardcoding values in the WASM module. diff --git a/docs/reference/cli.md b/docs/reference/cli.md index f1fedd0b9..b3d8bd91f 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -451,6 +451,7 @@ Change a canister's settings to specified values * `--reserved-cycles-limit ` — Upper limit on cycles reserved for future resource payments. Memory allocations that would push the reserved balance above this limit will fail. Supports suffixes: k (thousand), m (million), b (billion), t (trillion) * `--wasm-memory-limit ` — Wasm memory limit in bytes. Supports suffixes: kb, kib, mb, mib, gb, gib (e.g. "4gib" or "2.5kb") * `--wasm-memory-threshold ` — Wasm memory threshold in bytes. Supports suffixes: kb, kib, mb, mib, gb, gib (e.g. "4gib" or "2.5kb") +* `--log-memory-limit ` — Log memory limit in bytes (max 2 MiB). Oldest logs are purged when usage exceeds this value. Supports suffixes: kb, kib, mb, mib (e.g. "2mib" or "256kib"). Canister default is 4096 bytes * `--log-visibility ` * `--add-log-viewer ` * `--remove-log-viewer ` From fff1dd6cb2af0fb84d10c10d06f0ae81f65af435 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Fri, 6 Mar 2026 14:15:59 -0500 Subject: [PATCH 05/17] docs: update changelog for log filter flags and log_memory_limit setting Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4f47e146..69d9b75cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +* feat: `icp canister logs` supports filtering by timestamp (`--since`, `--until`) and log index (`--since-index`, `--until-index`) +* feat: Support `log_memory_limit` canister setting in `icp canister settings update` and `icp canister settings sync` * fix: Correct templating of special HTML characters in recipes # v0.2.0 From 6f44691f90584b1b01033dbb2fdad1b32f22f60a Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Fri, 6 Mar 2026 14:22:43 -0500 Subject: [PATCH 06/17] refactor: extract test timestamp constants in canister logs tests Co-Authored-By: Claude Opus 4.6 --- crates/icp-cli/src/commands/canister/logs.rs | 24 +++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/crates/icp-cli/src/commands/canister/logs.rs b/crates/icp-cli/src/commands/canister/logs.rs index 3c6a7bded..8d08ae164 100644 --- a/crates/icp-cli/src/commands/canister/logs.rs +++ b/crates/icp-cli/src/commands/canister/logs.rs @@ -243,6 +243,10 @@ fn format_content(content: &[u8]) -> String { mod tests { use super::*; + /// 2024-01-01T10:00:00.123456789Z as nanoseconds since Unix epoch. + const TEST_TIMESTAMP_NANOS: u64 = 1_704_103_200_123_456_789; + const TEST_TIMESTAMP_RFC3339: &str = "2024-01-01T10:00:00.123456789Z"; + #[test] fn test_format_content_valid_utf8() { let content = b"Hello, World!"; @@ -267,31 +271,29 @@ mod tests { #[test] fn test_format_timestamp() { - // Test timestamp: 2024-01-01T10:00:00.123456789Z - let nanos = 1704103200123456789u64; - let formatted = format_timestamp(nanos); - assert_eq!(formatted, "2024-01-01T10:00:00.123456789Z"); + let formatted = format_timestamp(TEST_TIMESTAMP_NANOS); + assert_eq!(formatted, TEST_TIMESTAMP_RFC3339); } #[test] fn test_format_log() { let log = CanisterLogRecord { idx: 42, - timestamp_nanos: 1704103200123456789, + timestamp_nanos: TEST_TIMESTAMP_NANOS, content: b"Test message".to_vec(), }; let formatted = format_log(&log); assert_eq!( formatted, - "[42. 2024-01-01T10:00:00.123456789Z]: Test message" + format!("[42. {TEST_TIMESTAMP_RFC3339}]: Test message") ); } #[test] fn test_parse_timestamp_raw_nanos() { assert_eq!( - parse_timestamp("1704103200123456789"), - Ok(1704103200123456789) + parse_timestamp(&TEST_TIMESTAMP_NANOS.to_string()), + Ok(TEST_TIMESTAMP_NANOS) ); assert_eq!(parse_timestamp("0"), Ok(0)); } @@ -301,15 +303,15 @@ mod tests { // 2024-01-01T10:00:00Z = 1704103200000000000 nanos assert_eq!( parse_timestamp("2024-01-01T10:00:00Z"), - Ok(1704103200_000_000_000) + Ok(1_704_103_200_000_000_000) ); } #[test] fn test_parse_timestamp_rfc3339_with_nanos() { assert_eq!( - parse_timestamp("2024-01-01T10:00:00.123456789Z"), - Ok(1704103200123456789) + parse_timestamp(TEST_TIMESTAMP_RFC3339), + Ok(TEST_TIMESTAMP_NANOS) ); } From e887ddcc06c33eaf7bdb9d856a61153033f60a12 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Fri, 6 Mar 2026 14:29:32 -0500 Subject: [PATCH 07/17] chore: allow OpenSSL license in cargo-deny aws-lc-sys (OpenSSL licensed) was pulled in via: aws-lc-sys -> aws-lc-rs -> rustls -> hyper-rustls -> reqwest -> ic-agent Co-Authored-By: Claude Opus 4.6 --- deny.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/deny.toml b/deny.toml index 0529bb78a..e734e2e5f 100644 --- a/deny.toml +++ b/deny.toml @@ -17,6 +17,7 @@ allow = [ "Zlib", "Unicode-DFS-2016", "Unicode-3.0", + "OpenSSL", ] unused-allowed-license = "allow" From af6612f0f4a12033927dbc1e17d5faf469c3cf1b Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Fri, 6 Mar 2026 14:36:18 -0500 Subject: [PATCH 08/17] docs: regenerate JSON schemas for log_memory_limit setting Co-Authored-By: Claude Opus 4.6 --- docs/schemas/canister-yaml-schema.json | 12 ++++++++++++ docs/schemas/environment-yaml-schema.json | 11 +++++++++++ docs/schemas/icp-yaml-schema.json | 12 ++++++++++++ 3 files changed, 35 insertions(+) diff --git a/docs/schemas/canister-yaml-schema.json b/docs/schemas/canister-yaml-schema.json index b0e9035d0..5e4982b38 100644 --- a/docs/schemas/canister-yaml-schema.json +++ b/docs/schemas/canister-yaml-schema.json @@ -352,6 +352,17 @@ ], "description": "Freezing threshold in seconds. Controls how long a canister can be inactive before being frozen.\nSupports duration suffixes in YAML: s, m, h, d, w (e.g. \"30d\" or \"4w\")." }, + "log_memory_limit": { + "anyOf": [ + { + "$ref": "#/$defs/MemoryAmount" + }, + { + "type": "null" + } + ], + "description": "Log memory limit in bytes (max 2 MiB). Oldest logs are purged when usage exceeds this value.\nSupports suffixes in YAML: kb, kib, mb, mib (e.g. \"2mib\" or \"256kib\"). Canister default is 4096 bytes." + }, "log_visibility": { "anyOf": [ { @@ -528,6 +539,7 @@ "compute_allocation": null, "environment_variables": null, "freezing_threshold": null, + "log_memory_limit": null, "log_visibility": null, "memory_allocation": null, "reserved_cycles_limit": null, diff --git a/docs/schemas/environment-yaml-schema.json b/docs/schemas/environment-yaml-schema.json index 840b4d865..5dfceca4e 100644 --- a/docs/schemas/environment-yaml-schema.json +++ b/docs/schemas/environment-yaml-schema.json @@ -164,6 +164,17 @@ ], "description": "Freezing threshold in seconds. Controls how long a canister can be inactive before being frozen.\nSupports duration suffixes in YAML: s, m, h, d, w (e.g. \"30d\" or \"4w\")." }, + "log_memory_limit": { + "anyOf": [ + { + "$ref": "#/$defs/MemoryAmount" + }, + { + "type": "null" + } + ], + "description": "Log memory limit in bytes (max 2 MiB). Oldest logs are purged when usage exceeds this value.\nSupports suffixes in YAML: kb, kib, mb, mib (e.g. \"2mib\" or \"256kib\"). Canister default is 4096 bytes." + }, "log_visibility": { "anyOf": [ { diff --git a/docs/schemas/icp-yaml-schema.json b/docs/schemas/icp-yaml-schema.json index 52b5ef2cf..6a058f015 100644 --- a/docs/schemas/icp-yaml-schema.json +++ b/docs/schemas/icp-yaml-schema.json @@ -204,6 +204,7 @@ "compute_allocation": null, "environment_variables": null, "freezing_threshold": null, + "log_memory_limit": null, "log_visibility": null, "memory_allocation": null, "reserved_cycles_limit": null, @@ -835,6 +836,17 @@ ], "description": "Freezing threshold in seconds. Controls how long a canister can be inactive before being frozen.\nSupports duration suffixes in YAML: s, m, h, d, w (e.g. \"30d\" or \"4w\")." }, + "log_memory_limit": { + "anyOf": [ + { + "$ref": "#/$defs/MemoryAmount" + }, + { + "type": "null" + } + ], + "description": "Log memory limit in bytes (max 2 MiB). Oldest logs are purged when usage exceeds this value.\nSupports suffixes in YAML: kb, kib, mb, mib (e.g. \"2mib\" or \"256kib\"). Canister default is 4096 bytes." + }, "log_visibility": { "anyOf": [ { From 0fe420e3c2be95e86b51f40edba21fa1febdbbee Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Fri, 6 Mar 2026 15:45:37 -0500 Subject: [PATCH 09/17] fix: handle timestamp overflow and inverted filter ranges in canister logs - Use unix_timestamp_nanos() instead of manual arithmetic in parse_timestamp to prevent silent overflow/wrapping for pre-epoch or far-future timestamps - Validate that --since/--until and --since-index/--until-index ranges are not inverted, returning a clear error instead of empty or confusing results Co-Authored-By: Claude Opus 4.6 --- crates/icp-cli/src/commands/canister/logs.rs | 49 +++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/crates/icp-cli/src/commands/canister/logs.rs b/crates/icp-cli/src/commands/canister/logs.rs index 8d08ae164..c29b6de23 100644 --- a/crates/icp-cli/src/commands/canister/logs.rs +++ b/crates/icp-cli/src/commands/canister/logs.rs @@ -50,12 +50,19 @@ fn parse_timestamp(s: &str) -> Result { return Ok(nanos); } // Fall back to RFC3339 - OffsetDateTime::parse(s, &Rfc3339) - .map(|dt| { - let nanos_per_sec = 1_000_000_000u64; - (dt.unix_timestamp() as u64) * nanos_per_sec + dt.nanosecond() as u64 - }) - .map_err(|_| format!("'{s}' is not a valid nanosecond timestamp or RFC3339 datetime")) + let dt = OffsetDateTime::parse(s, &Rfc3339) + .map_err(|_| format!("'{s}' is not a valid nanosecond timestamp or RFC3339 datetime"))?; + let nanos = dt.unix_timestamp_nanos(); + u64::try_from(nanos).map_err(|_| { + if nanos < 0 { + format!( + "'{s}' is before the Unix epoch; timestamp must be a non-negative number \ + or an RFC3339 datetime at or after 1970-01-01T00:00:00Z" + ) + } else { + format!("'{s}' overflows the nanosecond timestamp range (u64)") + } + }) } pub(crate) async fn exec(ctx: &Context, args: &LogsArgs) -> Result<(), anyhow::Error> { @@ -89,23 +96,31 @@ pub(crate) async fn exec(ctx: &Context, args: &LogsArgs) -> Result<(), anyhow::E follow_logs(ctx, &mgmt, &canister_id, args.interval).await } else { // Single fetch mode: fetch all logs once - fetch_and_display_logs(ctx, &mgmt, &canister_id, build_filter(args)).await + fetch_and_display_logs(ctx, &mgmt, &canister_id, build_filter(args)?).await } } -fn build_filter(args: &LogsArgs) -> Option { +fn build_filter(args: &LogsArgs) -> Result, anyhow::Error> { if args.since_index.is_some() || args.until_index.is_some() { - Some(CanisterLogFilter::ByIdx { - start: args.since_index.unwrap_or(0), - end: args.until_index.unwrap_or(u64::MAX), - }) + let start = args.since_index.unwrap_or(0); + let end = args.until_index.unwrap_or(u64::MAX); + if start > end { + return Err(anyhow!( + "--since-index ({start}) must not be greater than --until-index ({end})" + )); + } + Ok(Some(CanisterLogFilter::ByIdx { start, end })) } else if args.since.is_some() || args.until.is_some() { - Some(CanisterLogFilter::ByTimestampNanos { - start: args.since.unwrap_or(0), - end: args.until.unwrap_or(u64::MAX), - }) + let start = args.since.unwrap_or(0); + let end = args.until.unwrap_or(u64::MAX); + if start > end { + return Err(anyhow!( + "--since timestamp must not be after --until timestamp" + )); + } + Ok(Some(CanisterLogFilter::ByTimestampNanos { start, end })) } else { - None + Ok(None) } } From 9eeaeef211cc36a2bf2a22b5aa7a369edc1b361c Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Fri, 6 Mar 2026 19:56:50 -0500 Subject: [PATCH 10/17] test: add tests for log filter flags and log_memory_limit parsing Add unit tests for build_filter and parse_timestamp edge cases, integration tests for --since-index/--until-index and --since/--until filtering, unit tests for log_memory_limit suffix parsing, and a comment explaining why log_memory_limit is not covered in canister_settings_tests (PocketIC does not yet support it). Co-Authored-By: Claude Opus 4.6 --- crates/icp-cli/src/commands/canister/logs.rs | 121 +++++++++ crates/icp-cli/tests/canister_logs_tests.rs | 256 ++++++++++++++++++ .../icp-cli/tests/canister_settings_tests.rs | 5 +- crates/icp/src/canister/mod.rs | 20 ++ 4 files changed, 401 insertions(+), 1 deletion(-) diff --git a/crates/icp-cli/src/commands/canister/logs.rs b/crates/icp-cli/src/commands/canister/logs.rs index c29b6de23..25e5e2559 100644 --- a/crates/icp-cli/src/commands/canister/logs.rs +++ b/crates/icp-cli/src/commands/canister/logs.rs @@ -334,4 +334,125 @@ mod tests { fn test_parse_timestamp_invalid() { assert!(parse_timestamp("not-a-timestamp").is_err()); } + + #[test] + fn test_parse_timestamp_before_epoch() { + let result = parse_timestamp("1969-12-31T23:59:59Z"); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("before the Unix epoch")); + } + + fn make_logs_args( + since: Option, + until: Option, + since_index: Option, + until_index: Option, + ) -> LogsArgs { + LogsArgs { + cmd_args: args::CanisterCommandArgs { + canister: args::Canister::Name("test".to_string()), + network: Default::default(), + environment: Default::default(), + identity: Default::default(), + }, + follow: false, + interval: 2, + since, + until, + since_index, + until_index, + } + } + + #[test] + fn build_filter_no_flags() { + let args = make_logs_args(None, None, None, None); + assert!(build_filter(&args).unwrap().is_none()); + } + + #[test] + fn build_filter_since_index_only() { + let args = make_logs_args(None, None, Some(5), None); + let filter = build_filter(&args).unwrap().unwrap(); + assert!(matches!( + filter, + CanisterLogFilter::ByIdx { + start: 5, + end: u64::MAX + } + )); + } + + #[test] + fn build_filter_until_index_only() { + let args = make_logs_args(None, None, None, Some(10)); + let filter = build_filter(&args).unwrap().unwrap(); + assert!(matches!( + filter, + CanisterLogFilter::ByIdx { start: 0, end: 10 } + )); + } + + #[test] + fn build_filter_both_indices() { + let args = make_logs_args(None, None, Some(3), Some(7)); + let filter = build_filter(&args).unwrap().unwrap(); + assert!(matches!( + filter, + CanisterLogFilter::ByIdx { start: 3, end: 7 } + )); + } + + #[test] + fn build_filter_inverted_indices_error() { + let args = make_logs_args(None, None, Some(10), Some(5)); + let err = build_filter(&args).unwrap_err().to_string(); + assert!(err.contains("--since-index (10) must not be greater than --until-index (5)")); + } + + #[test] + fn build_filter_since_timestamp_only() { + let args = make_logs_args(Some(1000), None, None, None); + let filter = build_filter(&args).unwrap().unwrap(); + assert!(matches!( + filter, + CanisterLogFilter::ByTimestampNanos { + start: 1000, + end: u64::MAX + } + )); + } + + #[test] + fn build_filter_until_timestamp_only() { + let args = make_logs_args(None, Some(2000), None, None); + let filter = build_filter(&args).unwrap().unwrap(); + assert!(matches!( + filter, + CanisterLogFilter::ByTimestampNanos { + start: 0, + end: 2000 + } + )); + } + + #[test] + fn build_filter_both_timestamps() { + let args = make_logs_args(Some(1000), Some(2000), None, None); + let filter = build_filter(&args).unwrap().unwrap(); + assert!(matches!( + filter, + CanisterLogFilter::ByTimestampNanos { + start: 1000, + end: 2000 + } + )); + } + + #[test] + fn build_filter_inverted_timestamps_error() { + let args = make_logs_args(Some(2000), Some(1000), None, None); + let err = build_filter(&args).unwrap_err().to_string(); + assert!(err.contains("--since timestamp must not be after --until timestamp")); + } } diff --git a/crates/icp-cli/tests/canister_logs_tests.rs b/crates/icp-cli/tests/canister_logs_tests.rs index 12db5553f..89982875a 100644 --- a/crates/icp-cli/tests/canister_logs_tests.rs +++ b/crates/icp-cli/tests/canister_logs_tests.rs @@ -3,6 +3,7 @@ use { crate::common::{ENVIRONMENT_RANDOM_PORT, NETWORK_RANDOM_PORT, TestContext}, icp::fs::write_string, indoc::formatdoc, + predicates::prelude::PredicateBooleanExt, predicates::str::contains, std::time::Duration, }; @@ -163,3 +164,258 @@ async fn canister_logs_follow_mode() { .stdout(contains("4 Repeated")) .stdout(contains("5 Repeated")); } + +#[cfg(unix)] // moc +#[tokio::test] +async fn canister_logs_filter_by_index() { + let ctx = TestContext::new(); + let project_dir = ctx.create_project_dir("canister_logs"); + + ctx.copy_asset_dir("canister_logs", &project_dir); + + let pm = formatdoc! {r#" + canisters: + - name: logger + recipe: + type: "@dfinity/motoko@v4.0.0" + configuration: + main: main.mo + args: "" + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + ctx.icp() + .current_dir(&project_dir) + .args(["deploy", "logger", "--environment", "random-environment"]) + .assert() + .success(); + + // Create several log entries + for i in 1..=3 { + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "--environment", + "random-environment", + "logger", + "log", + &format!("(\"Message {i}\")"), + ]) + .assert() + .success(); + } + + // Fetch all logs to verify baseline + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "logs", + "logger", + "--environment", + "random-environment", + ]) + .assert() + .success() + .stdout( + contains("Message 1") + .and(contains("Message 2")) + .and(contains("Message 3")), + ); + + // Filter by --since-index: only the last log entry + // Log indices are 0-based, so index 2 is the third entry + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "logs", + "logger", + "--since-index", + "2", + "--environment", + "random-environment", + ]) + .assert() + .success() + .stdout(contains("Message 3")) + .stdout(contains("Message 1").not()); + + // Filter by --until-index: only the first log entry + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "logs", + "logger", + "--until-index", + "0", + "--environment", + "random-environment", + ]) + .assert() + .success() + .stdout(contains("Message 1")) + .stdout(contains("Message 2").not()); + + // Inverted range should error + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "logs", + "logger", + "--since-index", + "5", + "--until-index", + "0", + "--environment", + "random-environment", + ]) + .assert() + .failure() + .stderr(contains( + "--since-index (5) must not be greater than --until-index (0)", + )); +} + +#[cfg(unix)] // moc +#[tokio::test] +async fn canister_logs_filter_by_timestamp() { + let ctx = TestContext::new(); + let project_dir = ctx.create_project_dir("canister_logs"); + + ctx.copy_asset_dir("canister_logs", &project_dir); + + let pm = formatdoc! {r#" + canisters: + - name: logger + recipe: + type: "@dfinity/motoko@v4.0.0" + configuration: + main: main.mo + args: "" + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + ctx.icp() + .current_dir(&project_dir) + .args(["deploy", "logger", "--environment", "random-environment"]) + .assert() + .success(); + + // Create a log entry + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "--environment", + "random-environment", + "logger", + "log", + "(\"Timestamped message\")", + ]) + .assert() + .success(); + + // Filter with --since far in the future should return no logs + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "logs", + "logger", + "--since", + "99999999999999999999", + "--environment", + "random-environment", + ]) + .assert() + .success() + .stdout(contains("Timestamped message").not()); + + // Filter with --until 0 (epoch) should return no logs + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "logs", + "logger", + "--until", + "0", + "--environment", + "random-environment", + ]) + .assert() + .success() + .stdout(contains("Timestamped message").not()); + + // Filter with --since 0 should return all logs + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "logs", + "logger", + "--since", + "0", + "--environment", + "random-environment", + ]) + .assert() + .success() + .stdout(contains("Timestamped message")); + + // RFC3339 timestamp: --since with a past date should include the log + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "logs", + "logger", + "--since", + "2020-01-01T00:00:00Z", + "--environment", + "random-environment", + ]) + .assert() + .success() + .stdout(contains("Timestamped message")); + + // Inverted timestamp range should error + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "logs", + "logger", + "--since", + "99999999999999999999", + "--until", + "0", + "--environment", + "random-environment", + ]) + .assert() + .failure() + .stderr(contains( + "--since timestamp must not be after --until timestamp", + )); +} diff --git a/crates/icp-cli/tests/canister_settings_tests.rs b/crates/icp-cli/tests/canister_settings_tests.rs index 1e4c80439..748d34c8f 100644 --- a/crates/icp-cli/tests/canister_settings_tests.rs +++ b/crates/icp-cli/tests/canister_settings_tests.rs @@ -713,7 +713,10 @@ async fn canister_settings_update_miscellaneous() { .and(contains("Wasm memory threshold: 0")), ); - // Update compute allocation + // Update miscellaneous settings. + // NOTE: `log_memory_limit` is intentionally not tested here because PocketIC + // does not yet support it — setting it via `update_settings` has no effect, + // and querying the canister settings always returns 0 regardless of the value set. ctx.icp() .current_dir(&project_dir) .args([ diff --git a/crates/icp/src/canister/mod.rs b/crates/icp/src/canister/mod.rs index a851b952c..9b9a5a262 100644 --- a/crates/icp/src/canister/mod.rs +++ b/crates/icp/src/canister/mod.rs @@ -396,6 +396,26 @@ allowed_viewers: ); } + #[test] + fn settings_log_memory_limit_parses_suffix() { + let yaml = "log_memory_limit: 256kib"; + let settings: Settings = serde_yaml::from_str(yaml).unwrap(); + assert_eq!( + settings.log_memory_limit.as_ref().map(|m| m.get()), + Some(256 * 1024) + ); + } + + #[test] + fn settings_log_memory_limit_parses_mib() { + let yaml = "log_memory_limit: 2mib"; + let settings: Settings = serde_yaml::from_str(yaml).unwrap(); + assert_eq!( + settings.log_memory_limit.as_ref().map(|m| m.get()), + Some(2 * 1024 * 1024) + ); + } + #[test] fn log_visibility_conversion_to_ic_type() { let controllers = LogVisibility::Controllers; From 90c67dc1f2947d07c3bdfc90306e6696ed8d20b9 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Fri, 6 Mar 2026 21:31:13 -0500 Subject: [PATCH 11/17] fix: correct integration tests for log filter flags - Use 0-based message names to match 0-based log indices - Fix --since timestamp to use valid u64 value (was overflowing) - Document expected --until-index inclusive behavior vs current PocketIC exclusive implementation Co-Authored-By: Claude Opus 4.6 --- crates/icp-cli/tests/canister_logs_tests.rs | 31 ++++++++++++--------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/crates/icp-cli/tests/canister_logs_tests.rs b/crates/icp-cli/tests/canister_logs_tests.rs index 89982875a..e859712ab 100644 --- a/crates/icp-cli/tests/canister_logs_tests.rs +++ b/crates/icp-cli/tests/canister_logs_tests.rs @@ -198,7 +198,7 @@ async fn canister_logs_filter_by_index() { .success(); // Create several log entries - for i in 1..=3 { + for i in 0..=2 { ctx.icp() .current_dir(&project_dir) .args([ @@ -227,13 +227,12 @@ async fn canister_logs_filter_by_index() { .assert() .success() .stdout( - contains("Message 1") - .and(contains("Message 2")) - .and(contains("Message 3")), + contains("Message 0") + .and(contains("Message 1")) + .and(contains("Message 2")), ); - // Filter by --since-index: only the last log entry - // Log indices are 0-based, so index 2 is the third entry + // Log indices are 0-based ctx.icp() .current_dir(&project_dir) .args([ @@ -241,16 +240,20 @@ async fn canister_logs_filter_by_index() { "logs", "logger", "--since-index", - "2", + "1", "--environment", "random-environment", ]) .assert() .success() - .stdout(contains("Message 3")) - .stdout(contains("Message 1").not()); + .stdout(contains("Message 0").not()) + .stdout(contains("Message 1")) + .stdout(contains("Message 2")); - // Filter by --until-index: only the first log entry + // Per the IC specification, --until-index should be inclusive: indices <= 1 + // means Message 0 (index 0) and Message 1 (index 1) should both be returned. + // However, the current PocketIC implementation treats the end bound as exclusive, + // so --until-index 1 only returns Message 0 (index 0). ctx.icp() .current_dir(&project_dir) .args([ @@ -258,12 +261,13 @@ async fn canister_logs_filter_by_index() { "logs", "logger", "--until-index", - "0", + "1", "--environment", "random-environment", ]) .assert() .success() + .stdout(contains("Message 0")) .stdout(contains("Message 1")) .stdout(contains("Message 2").not()); @@ -336,6 +340,7 @@ async fn canister_logs_filter_by_timestamp() { .success(); // Filter with --since far in the future should return no logs + // Use a large but valid u64 nanosecond value (~year 2286) ctx.icp() .current_dir(&project_dir) .args([ @@ -343,7 +348,7 @@ async fn canister_logs_filter_by_timestamp() { "logs", "logger", "--since", - "99999999999999999999", + "9999999999999999999", "--environment", "random-environment", ]) @@ -407,7 +412,7 @@ async fn canister_logs_filter_by_timestamp() { "logs", "logger", "--since", - "99999999999999999999", + "9999999999999999999", "--until", "0", "--environment", From c40f94c827c16c8b45eb77da17111ae50259ed2e Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 9 Mar 2026 09:48:44 -0400 Subject: [PATCH 12/17] fix: clarify log filter range ends as exclusive The `--until` and `--until-index` flags use exclusive upper bounds, not inclusive. Update help text, error messages, validation (reject equal bounds), and tests to reflect this. Co-Authored-By: Claude Opus 4.6 --- crates/icp-cli/src/commands/canister/logs.rs | 20 +++--- crates/icp-cli/tests/canister_logs_tests.rs | 65 +------------------- docs/reference/cli.md | 6 +- 3 files changed, 16 insertions(+), 75 deletions(-) diff --git a/crates/icp-cli/src/commands/canister/logs.rs b/crates/icp-cli/src/commands/canister/logs.rs index 25e5e2559..6bc91af25 100644 --- a/crates/icp-cli/src/commands/canister/logs.rs +++ b/crates/icp-cli/src/commands/canister/logs.rs @@ -25,12 +25,12 @@ pub(crate) struct LogsArgs { #[arg(long, requires = "follow", default_value = "2")] pub(crate) interval: u64, - /// Show logs at or after this timestamp. Accepts nanoseconds since Unix epoch or RFC3339 + /// Show logs at or after this timestamp (inclusive). Accepts nanoseconds since Unix epoch or RFC3339 /// (e.g. '2024-01-01T00:00:00Z'). Cannot be used with --follow #[arg(long, value_name = "TIMESTAMP", conflicts_with_all = ["follow", "since_index", "until_index"], value_parser = parse_timestamp)] pub(crate) since: Option, - /// Show logs at or before this timestamp. Accepts nanoseconds since Unix epoch or RFC3339 + /// Show logs before this timestamp (exclusive). Accepts nanoseconds since Unix epoch or RFC3339 /// (e.g. '2024-01-01T00:00:00Z'). Cannot be used with --follow #[arg(long, value_name = "TIMESTAMP", conflicts_with_all = ["follow", "since_index", "until_index"], value_parser = parse_timestamp)] pub(crate) until: Option, @@ -39,7 +39,7 @@ pub(crate) struct LogsArgs { #[arg(long, value_name = "INDEX", conflicts_with_all = ["follow", "since", "until"])] pub(crate) since_index: Option, - /// Show logs at or before this log index (inclusive). Cannot be used with --follow + /// Show logs before this log index (exclusive). Cannot be used with --follow #[arg(long, value_name = "INDEX", conflicts_with_all = ["follow", "since", "until"])] pub(crate) until_index: Option, } @@ -104,18 +104,18 @@ fn build_filter(args: &LogsArgs) -> Result, anyhow::Er if args.since_index.is_some() || args.until_index.is_some() { let start = args.since_index.unwrap_or(0); let end = args.until_index.unwrap_or(u64::MAX); - if start > end { + if start >= end { return Err(anyhow!( - "--since-index ({start}) must not be greater than --until-index ({end})" + "--since-index ({start}) must be less than --until-index ({end})" )); } Ok(Some(CanisterLogFilter::ByIdx { start, end })) } else if args.since.is_some() || args.until.is_some() { let start = args.since.unwrap_or(0); let end = args.until.unwrap_or(u64::MAX); - if start > end { + if start >= end { return Err(anyhow!( - "--since timestamp must not be after --until timestamp" + "--since timestamp must be less than --until timestamp" )); } Ok(Some(CanisterLogFilter::ByTimestampNanos { start, end })) @@ -161,7 +161,7 @@ async fn follow_logs( loop { let filter = match last_idx { Some(idx) => Some(CanisterLogFilter::ByIdx { - start: idx + 1, + start: idx + 1, // Start from the next log index after the last one we displayed end: u64::MAX, }), None => { @@ -407,7 +407,7 @@ mod tests { fn build_filter_inverted_indices_error() { let args = make_logs_args(None, None, Some(10), Some(5)); let err = build_filter(&args).unwrap_err().to_string(); - assert!(err.contains("--since-index (10) must not be greater than --until-index (5)")); + assert!(err.contains("--since-index (10) must be less than --until-index (5)")); } #[test] @@ -453,6 +453,6 @@ mod tests { fn build_filter_inverted_timestamps_error() { let args = make_logs_args(Some(2000), Some(1000), None, None); let err = build_filter(&args).unwrap_err().to_string(); - assert!(err.contains("--since timestamp must not be after --until timestamp")); + assert!(err.contains("--since timestamp must be less than --until timestamp")); } } diff --git a/crates/icp-cli/tests/canister_logs_tests.rs b/crates/icp-cli/tests/canister_logs_tests.rs index e859712ab..b84484b77 100644 --- a/crates/icp-cli/tests/canister_logs_tests.rs +++ b/crates/icp-cli/tests/canister_logs_tests.rs @@ -232,7 +232,7 @@ async fn canister_logs_filter_by_index() { .and(contains("Message 2")), ); - // Log indices are 0-based + // --since-index is inclusive, so --since-index 1 should include Message 1 and Message 2 but not Message 0 ctx.icp() .current_dir(&project_dir) .args([ @@ -250,10 +250,7 @@ async fn canister_logs_filter_by_index() { .stdout(contains("Message 1")) .stdout(contains("Message 2")); - // Per the IC specification, --until-index should be inclusive: indices <= 1 - // means Message 0 (index 0) and Message 1 (index 1) should both be returned. - // However, the current PocketIC implementation treats the end bound as exclusive, - // so --until-index 1 only returns Message 0 (index 0). + // --until-index is exclusive, so --until-index 1 should only include Message 0 ctx.icp() .current_dir(&project_dir) .args([ @@ -268,28 +265,8 @@ async fn canister_logs_filter_by_index() { .assert() .success() .stdout(contains("Message 0")) - .stdout(contains("Message 1")) + .stdout(contains("Message 1").not()) .stdout(contains("Message 2").not()); - - // Inverted range should error - ctx.icp() - .current_dir(&project_dir) - .args([ - "canister", - "logs", - "logger", - "--since-index", - "5", - "--until-index", - "0", - "--environment", - "random-environment", - ]) - .assert() - .failure() - .stderr(contains( - "--since-index (5) must not be greater than --until-index (0)", - )); } #[cfg(unix)] // moc @@ -356,22 +333,6 @@ async fn canister_logs_filter_by_timestamp() { .success() .stdout(contains("Timestamped message").not()); - // Filter with --until 0 (epoch) should return no logs - ctx.icp() - .current_dir(&project_dir) - .args([ - "canister", - "logs", - "logger", - "--until", - "0", - "--environment", - "random-environment", - ]) - .assert() - .success() - .stdout(contains("Timestamped message").not()); - // Filter with --since 0 should return all logs ctx.icp() .current_dir(&project_dir) @@ -403,24 +364,4 @@ async fn canister_logs_filter_by_timestamp() { .assert() .success() .stdout(contains("Timestamped message")); - - // Inverted timestamp range should error - ctx.icp() - .current_dir(&project_dir) - .args([ - "canister", - "logs", - "logger", - "--since", - "9999999999999999999", - "--until", - "0", - "--environment", - "random-environment", - ]) - .assert() - .failure() - .stderr(contains( - "--since timestamp must not be after --until timestamp", - )); } diff --git a/docs/reference/cli.md b/docs/reference/cli.md index d91c1408b..a98e5129c 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -333,10 +333,10 @@ Fetch and display canister logs * `--interval ` — Polling interval in seconds when following logs (requires --follow) Default value: `2` -* `--since ` — Show logs at or after this timestamp. Accepts nanoseconds since Unix epoch or RFC3339 (e.g. '2024-01-01T00:00:00Z'). Cannot be used with --follow -* `--until ` — Show logs at or before this timestamp. Accepts nanoseconds since Unix epoch or RFC3339 (e.g. '2024-01-01T00:00:00Z'). Cannot be used with --follow +* `--since ` — Show logs at or after this timestamp (inclusive). Accepts nanoseconds since Unix epoch or RFC3339 (e.g. '2024-01-01T00:00:00Z'). Cannot be used with --follow +* `--until ` — Show logs before this timestamp (exclusive). Accepts nanoseconds since Unix epoch or RFC3339 (e.g. '2024-01-01T00:00:00Z'). Cannot be used with --follow * `--since-index ` — Show logs at or after this log index (inclusive). Cannot be used with --follow -* `--until-index ` — Show logs at or before this log index (inclusive). Cannot be used with --follow +* `--until-index ` — Show logs before this log index (exclusive). Cannot be used with --follow From 2b291c99223f6b7933f1f5bc10b8300f3215c9d1 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 9 Mar 2026 09:54:11 -0400 Subject: [PATCH 13/17] fix: detect numeric overflow in parse_timestamp before RFC3339 fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A purely numeric timestamp exceeding u64 range now gets a clear "overflows the nanosecond timestamp range" error instead of the misleading "not a valid … or RFC3339 datetime" message. Co-Authored-By: Claude Opus 4.6 --- crates/icp-cli/src/commands/canister/logs.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/icp-cli/src/commands/canister/logs.rs b/crates/icp-cli/src/commands/canister/logs.rs index 6bc91af25..b1fa986d0 100644 --- a/crates/icp-cli/src/commands/canister/logs.rs +++ b/crates/icp-cli/src/commands/canister/logs.rs @@ -49,6 +49,10 @@ fn parse_timestamp(s: &str) -> Result { if let Ok(nanos) = s.parse::() { return Ok(nanos); } + // Detect numeric overflow before falling back to RFC3339 + if s.parse::().is_ok() { + return Err(format!("'{s}' overflows the nanosecond timestamp range (u64)")); + } // Fall back to RFC3339 let dt = OffsetDateTime::parse(s, &Rfc3339) .map_err(|_| format!("'{s}' is not a valid nanosecond timestamp or RFC3339 datetime"))?; @@ -335,6 +339,13 @@ mod tests { assert!(parse_timestamp("not-a-timestamp").is_err()); } + #[test] + fn test_parse_timestamp_numeric_overflow() { + let result = parse_timestamp("99999999999999999999999"); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("overflows the nanosecond timestamp range")); + } + #[test] fn test_parse_timestamp_before_epoch() { let result = parse_timestamp("1969-12-31T23:59:59Z"); From 125b1a8f8bc22857424867fcc8c77a5121fbca82 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 9 Mar 2026 09:55:37 -0400 Subject: [PATCH 14/17] fix: use checked conversion for system clock nanos to avoid silent truncation Replace `as u64` cast with `u64::try_from().ok()` when computing now_nanos in follow mode, falling back to 0 on overflow. Also applies rustfmt formatting fixes. Co-Authored-By: Claude Opus 4.6 --- crates/icp-cli/src/commands/canister/logs.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/icp-cli/src/commands/canister/logs.rs b/crates/icp-cli/src/commands/canister/logs.rs index b1fa986d0..f4cf7ee88 100644 --- a/crates/icp-cli/src/commands/canister/logs.rs +++ b/crates/icp-cli/src/commands/canister/logs.rs @@ -51,7 +51,9 @@ fn parse_timestamp(s: &str) -> Result { } // Detect numeric overflow before falling back to RFC3339 if s.parse::().is_ok() { - return Err(format!("'{s}' overflows the nanosecond timestamp range (u64)")); + return Err(format!( + "'{s}' overflows the nanosecond timestamp range (u64)" + )); } // Fall back to RFC3339 let dt = OffsetDateTime::parse(s, &Rfc3339) @@ -172,7 +174,8 @@ async fn follow_logs( // First fetch: look back 1 hour from now let now_nanos = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_nanos() as u64) + .ok() + .and_then(|d| u64::try_from(d.as_nanos()).ok()) .unwrap_or(0); Some(CanisterLogFilter::ByTimestampNanos { start: now_nanos.saturating_sub(FOLLOW_LOOKBACK_NANOS), @@ -343,7 +346,11 @@ mod tests { fn test_parse_timestamp_numeric_overflow() { let result = parse_timestamp("99999999999999999999999"); assert!(result.is_err()); - assert!(result.unwrap_err().contains("overflows the nanosecond timestamp range")); + assert!( + result + .unwrap_err() + .contains("overflows the nanosecond timestamp range") + ); } #[test] From 0dbb03cb3a71ef8938db50c795c33585b2db5465 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 9 Mar 2026 09:56:50 -0400 Subject: [PATCH 15/17] chore: upgrade reqwest from 0.12 to 0.13 Update reqwest to 0.13.2 and rename the `rustls-tls` feature to `rustls` to match the new API. This also removes unused transitive dependencies (webpki-roots, wasm-streams 0.4, ring). Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 38 +++----------------------------------- Cargo.toml | 4 ++-- 2 files changed, 5 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 066da039e..c4e0895f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2844,7 +2844,6 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", ] [[package]] @@ -3288,7 +3287,7 @@ dependencies = [ "pem", "pkcs8", "rand 0.9.2", - "reqwest 0.12.28", + "reqwest 0.13.2", "schemars 1.2.0", "scrypt", "sec1", @@ -3374,7 +3373,7 @@ dependencies = [ "predicates", "rand 0.9.2", "regex", - "reqwest 0.12.28", + "reqwest 0.13.2", "sec1", "send_ctrlc", "serde", @@ -5444,31 +5443,23 @@ dependencies = [ "http-body", "http-body-util", "hyper", - "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls", - "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams 0.4.2", "web-sys", - "webpki-roots", ] [[package]] @@ -5511,7 +5502,7 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams 0.5.0", + "wasm-streams", "web-sys", ] @@ -5663,7 +5654,6 @@ checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "aws-lc-rs", "once_cell", - "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -7333,19 +7323,6 @@ dependencies = [ "wasmparser 0.244.0", ] -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "wasm-streams" version = "0.5.0" @@ -7413,15 +7390,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "webpki-roots" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 3231706ba..2b4ae78ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,9 +98,9 @@ wslpath2 = "0.1" zeroize = "1.8.1" [workspace.dependencies.reqwest] -version = "0.12.15" +version = "0.13.2" default-features = false -features = ["rustls-tls", "json", "stream"] +features = ["rustls", "json", "stream"] [workspace.dependencies.schemars] version = "1.0.4" From 26a155cddb3284bd37c3908c51a2a75d46c300f6 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 9 Mar 2026 10:05:03 -0400 Subject: [PATCH 16/17] fix: improve error message when --until or --until-index is 0 When only --until-index 0 or --until 0 is passed, the previous error misleadingly showed both start and end as 0. Now these cases get a dedicated message explaining the exclusive end bound. Co-Authored-By: Claude Opus 4.6 --- crates/icp-cli/src/commands/canister/logs.rs | 24 ++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/crates/icp-cli/src/commands/canister/logs.rs b/crates/icp-cli/src/commands/canister/logs.rs index f4cf7ee88..283b7d6e6 100644 --- a/crates/icp-cli/src/commands/canister/logs.rs +++ b/crates/icp-cli/src/commands/canister/logs.rs @@ -110,6 +110,11 @@ fn build_filter(args: &LogsArgs) -> Result, anyhow::Er if args.since_index.is_some() || args.until_index.is_some() { let start = args.since_index.unwrap_or(0); let end = args.until_index.unwrap_or(u64::MAX); + if end == 0 { + return Err(anyhow!( + "--until-index must be greater than 0 (the end bound is exclusive)" + )); + } if start >= end { return Err(anyhow!( "--since-index ({start}) must be less than --until-index ({end})" @@ -119,6 +124,11 @@ fn build_filter(args: &LogsArgs) -> Result, anyhow::Er } else if args.since.is_some() || args.until.is_some() { let start = args.since.unwrap_or(0); let end = args.until.unwrap_or(u64::MAX); + if end == 0 { + return Err(anyhow!( + "--until must be greater than 0 (the end bound is exclusive)" + )); + } if start >= end { return Err(anyhow!( "--since timestamp must be less than --until timestamp" @@ -473,4 +483,18 @@ mod tests { let err = build_filter(&args).unwrap_err().to_string(); assert!(err.contains("--since timestamp must be less than --until timestamp")); } + + #[test] + fn build_filter_until_index_zero_error() { + let args = make_logs_args(None, None, None, Some(0)); + let err = build_filter(&args).unwrap_err().to_string(); + assert!(err.contains("--until-index must be greater than 0")); + } + + #[test] + fn build_filter_until_timestamp_zero_error() { + let args = make_logs_args(None, Some(0), None, None); + let err = build_filter(&args).unwrap_err().to_string(); + assert!(err.contains("--until must be greater than 0")); + } } From 5371c537f015f4f0e51098f51edc6346f62dfef2 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 9 Mar 2026 11:36:57 -0400 Subject: [PATCH 17/17] test: assert log_memory_limit behavior in canister settings update test Include log_memory_limit in the miscellaneous settings update test so that when PocketIC gains support for the field, the now-expected 0 will fail and remind us to update the assertion to the correct value. Co-Authored-By: Claude Opus 4.6 --- crates/icp-cli/tests/canister_settings_tests.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/icp-cli/tests/canister_settings_tests.rs b/crates/icp-cli/tests/canister_settings_tests.rs index 748d34c8f..2faf86771 100644 --- a/crates/icp-cli/tests/canister_settings_tests.rs +++ b/crates/icp-cli/tests/canister_settings_tests.rs @@ -714,9 +714,10 @@ async fn canister_settings_update_miscellaneous() { ); // Update miscellaneous settings. - // NOTE: `log_memory_limit` is intentionally not tested here because PocketIC - // does not yet support it — setting it via `update_settings` has no effect, - // and querying the canister settings always returns 0 regardless of the value set. + // NOTE: `log_memory_limit` is included below, but PocketIC does not yet + // support it — setting it via `update_settings` has no effect, and querying + // the canister settings always returns 0 regardless of the value set. + // When PocketIC gains support, the assertion should change from 0 to 1_048_576. ctx.icp() .current_dir(&project_dir) .args([ @@ -738,6 +739,8 @@ async fn canister_settings_update_miscellaneous() { "4GiB", "--wasm-memory-threshold", "4GiB", + "--log-memory-limit", + "1MiB", ]) .assert() .success(); @@ -762,7 +765,10 @@ async fn canister_settings_update_miscellaneous() { .and(contains("Freezing threshold: 8_640_000")) .and(contains("Reserved cycles limit: 6_000_000_000_000")) .and(contains("Wasm memory limit: 4_294_967_296")) - .and(contains("Wasm memory threshold: 4_294_967_296")), + .and(contains("Wasm memory threshold: 4_294_967_296")) + // PocketIC does not support log_memory_limit yet — always returns 0. + // Update to 1_048_576 once PocketIC support lands. + .and(contains("Log memory limit: 0")), ); }