diff --git a/Cargo.lock b/Cargo.lock index 904154645a..d61ffbbe06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -835,6 +835,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "ci_info" +version = "0.14.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eb8c8d139abf8673b1805f52f6ad398825e24c397dc9ac8283fe588dd6c80d9" +dependencies = [ + "envmnt", +] + [[package]] name = "cipher" version = "0.3.0" @@ -1331,6 +1340,7 @@ dependencies = [ "byte-unit", "bytes", "candid 0.9.1", + "ci_info", "clap", "console", "crc32fast", @@ -1553,6 +1563,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + [[package]] name = "dyn-clone" version = "1.0.11" @@ -1699,6 +1715,16 @@ dependencies = [ "termcolor", ] +[[package]] +name = "envmnt" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d73999a2b8871e74c8b8bc23759ee9f3d85011b24fafc91a4b3b5c8cc8185501" +dependencies = [ + "fsio", + "indexmap 1.9.3", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -1874,6 +1900,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsio" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad0ce30be0cc441b325c5d705c8b613a0ca0d92b6a8953d41bd236dc09a36d0" +dependencies = [ + "dunce", +] + [[package]] name = "funty" version = "2.0.0" diff --git a/e2e/assets/playground_backend/service/pool/Logs.mo b/e2e/assets/playground_backend/service/pool/Logs.mo index 10e467328c..7e28c0af99 100644 --- a/e2e/assets/playground_backend/service/pool/Logs.mo +++ b/e2e/assets/playground_backend/service/pool/Logs.mo @@ -1,4 +1,88 @@ +import Map "mo:base/RBTree"; +import {compare} "mo:base/Text"; +import {toArray} "mo:base/Iter"; +import {now = timeNow} "mo:base/Time"; +import {toText} "mo:base/Int"; +import {get} "mo:base/Option"; + module { + public type Origin = { origin: Text; tags: [Text] }; + public type SharedStatsByOrigin = (Map.Tree, Map.Tree); + public class StatsByOrigin() { + var canisters = Map.RBTree(compare); + var installs = Map.RBTree(compare); + public func share() : SharedStatsByOrigin = (canisters.share(), installs.share()); + public func unshare(x : SharedStatsByOrigin) { + canisters.unshare(x.0); + installs.unshare(x.1); + }; + func addTags(map: Map.RBTree, list: [Text]) { + for (tag in list.vals()) { + switch (map.get(tag)) { + case null { map.put(tag, 1) }; + case (?n) { map.put(tag, n + 1) }; + }; + }; + }; + // if to is null, delete the from tag + func merge_tag_(map: Map.RBTree, from: Text, opt_to: ?Text) { + ignore do ? { + let n1 = map.remove(from)!; + let to = opt_to!; + switch (map.get(to)) { + case null { map.put(to, n1) }; + case (?n2) { map.put(to, n1 + n2) }; + }; + }; + }; + public func merge_tag(from: Text, to: ?Text) { + merge_tag_(canisters, from, to); + merge_tag_(installs, from, to); + }; + public func addCanister(origin: Origin) { + addTags(canisters, ["origin:" # origin.origin]); + addTags(canisters, origin.tags); + }; + public func addInstall(origin: Origin) { + addTags(installs, ["origin:" # origin.origin]); + addTags(installs, origin.tags); + }; + public func dump() : ([(Text, Nat)], [(Text, Nat)]) { + (toArray<(Text, Nat)>(canisters.entries()), + toArray<(Text, Nat)>(installs.entries()), + ) + }; + public func metrics() : Text { + var result = ""; + let now = timeNow() / 1_000_000; + let canister_playground = get(canisters.get("origin:playground"), 0); + let canister_dfx = get(canisters.get("origin:dfx"), 0); + let install_playground = get(installs.get("origin:playground"), 0); + let install_dfx = get(installs.get("origin:dfx"), 0); + let profiling = get(installs.get("wasm:profiling"), 0); + let asset = get(installs.get("wasm:asset"), 0); + let install = get(installs.get("mode:install"), 0); + let reinstall = get(installs.get("mode:reinstall"), 0); + let upgrade = get(installs.get("mode:upgrade"), 0); + result := result + # encode_single_value("counter", "create_from_playground", canister_playground, "Number of canisters created from playground", now) + # encode_single_value("counter", "install_from_playground", install_playground, "Number of Wasms installed from playground", now) + # encode_single_value("counter", "create_from_dfx", canister_dfx, "Number of canisters created from dfx", now) + # encode_single_value("counter", "install_from_dfx", install_dfx, "Number of Wasms installed from dfx", now) + # encode_single_value("counter", "profiling", profiling, "Number of Wasms profiled", now) + # encode_single_value("counter", "asset", asset, "Number of asset Wasms canister installed", now) + # encode_single_value("counter", "install", install, "Number of Wasms with install mode", now) + # encode_single_value("counter", "reinstall", reinstall, "Number of Wasms with reinstall mode", now) + # encode_single_value("counter", "upgrade", upgrade, "Number of Wasms with upgrad mode", now); + result; + }; + }; + public func encode_single_value(kind: Text, name: Text, number: Int, desc: Text, time: Int) : Text { + "# HELP " # name # " " # desc # "\n" # + "# TYPE " # name # " " # kind # "\n" # + name # " " # toText(number) # " " # toText(time) # "\n" + }; + public type Stats = { num_of_canisters: Nat; num_of_installs: Nat; diff --git a/e2e/assets/playground_backend/service/pool/Main.mo b/e2e/assets/playground_backend/service/pool/Main.mo index 5d42e6b8b3..ef519739b0 100644 --- a/e2e/assets/playground_backend/service/pool/Main.mo +++ b/e2e/assets/playground_backend/service/pool/Main.mo @@ -6,6 +6,7 @@ import Option "mo:base/Option"; import Nat "mo:base/Nat"; import Text "mo:base/Text"; import Array "mo:base/Array"; +import Buffer "mo:base/Buffer"; import List "mo:base/List"; import Deque "mo:base/Deque"; import Result "mo:base/Result"; @@ -17,29 +18,31 @@ import PoW "./PoW"; import Logs "./Logs"; import Metrics "./Metrics"; import Wasm "canister:wasm-utils"; -import Blob "mo:base/Blob"; -import Buffer "mo:base/Buffer"; -import Nat32 "mo:base/Nat32"; shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { let IC : ICType.Self = actor "aaaaa-aa"; let params = Option.get(opt_params, Types.defaultParams); var pool = Types.CanisterPool(params.max_num_canisters, params.canister_time_to_live, params.max_family_tree_size); let nonceCache = PoW.NonceCache(params.nonce_time_to_live); + var statsByOrigin = Logs.StatsByOrigin(); stable let controller = creator.caller; stable var stats = Logs.defaultStats; stable var stablePool : [Types.CanisterInfo] = []; stable var stableMetadata : [(Principal, (Int, Bool))] = []; stable var stableChildren : [(Principal, [Principal])] = []; + stable var stableTimers : [Types.CanisterInfo] = []; stable var previousParam : ?Types.InitParams = null; + stable var stableStatsByOrigin : Logs.SharedStatsByOrigin = (#leaf, #leaf); system func preupgrade() { - let (tree, metadata, children) = pool.share(); + let (tree, metadata, children, timers) = pool.share(); stablePool := tree; stableMetadata := metadata; stableChildren := children; + stableTimers := timers; previousParam := ?params; + stableStatsByOrigin := statsByOrigin.share(); }; system func postupgrade() { @@ -49,14 +52,19 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { }; }; pool.unshare(stablePool, stableMetadata, stableChildren); + for (info in stableTimers.vals()) { + updateTimer(info); + }; + statsByOrigin.unshare(stableStatsByOrigin); }; public query func getInitParams() : async Types.InitParams { params; }; - public query func getStats() : async Logs.Stats { - stats; + public query func getStats() : async (Logs.Stats, [(Text, Nat)], [(Text, Nat)]) { + let (canister, install) = statsByOrigin.dump(); + (stats, canister, install); }; public query func balance() : async Nat { @@ -68,7 +76,7 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { ignore Cycles.accept amount; }; - private func getExpiredCanisterInfo() : async Types.CanisterInfo { + private func getExpiredCanisterInfo(origin : Logs.Origin) : async Types.CanisterInfo { switch (pool.getExpiredCanisterId()) { case (#newId) { Cycles.add(params.cycles_per_canister); @@ -77,6 +85,7 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { let info = { id = cid.canister_id; timestamp = now }; pool.add info; stats := Logs.updateStats(stats, #getId(params.cycles_per_canister)); + statsByOrigin.addCanister(origin); info; }; case (#reuse info) { @@ -89,8 +98,9 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { Cycles.add topUpCycles; await IC.deposit_cycles cid; }; - // Lazily cleanup the reused canister - await IC.uninstall_code cid; + if (Option.isSome(status.module_hash)) { + await IC.uninstall_code cid; + }; switch (status.status) { case (#stopped or #stopping) { await IC.start_canister cid; @@ -98,6 +108,7 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { case _ {}; }; stats := Logs.updateStats(stats, #getId topUpCycles); + statsByOrigin.addCanister(origin); info; }; case (#outOfCapacity time) { @@ -107,8 +118,23 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { }; }; }; + func validateOrigin(origin: Logs.Origin) : Bool { + if (origin.origin == "") { + return false; + }; + for (tag in origin.tags.vals()) { + // reject server side tags + if (tag == "mode:install" or tag == "mode:reinstall" or tag == "mode:upgrade" or tag == "wasm:profiling" or tag == "wasm:asset") { + return false; + } + }; + return true; + }; - public shared ({ caller }) func getCanisterId(nonce : PoW.Nonce) : async Types.CanisterInfo { + public shared ({ caller }) func getCanisterId(nonce : PoW.Nonce, origin : Logs.Origin) : async Types.CanisterInfo { + if (not validateOrigin(origin)) { + throw Error.reject "Please specify a valid origin"; + }; if (caller != controller and not nonceCache.checkProofOfWork(nonce)) { stats := Logs.updateStats(stats, #mismatch); throw Error.reject "Proof of work check failed"; @@ -119,10 +145,14 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { throw Error.reject "Nonce already used"; }; nonceCache.add nonce; - await getExpiredCanisterInfo(); + await getExpiredCanisterInfo(origin); }; - public shared ({ caller }) func installCode(info : Types.CanisterInfo, args : Types.InstallArgs, profiling : Bool, is_whitelisted : Bool) : async Types.CanisterInfo { + type InstallConfig = { profiling: Bool; is_whitelisted: Bool; origin: Logs.Origin }; + public shared ({ caller }) func installCode(info : Types.CanisterInfo, args : Types.InstallArgs, install_config : InstallConfig) : async Types.CanisterInfo { + if (not validateOrigin(install_config.origin)) { + throw Error.reject "Please specify a valid origin"; + }; if (info.timestamp == 0) { stats := Logs.updateStats(stats, #mismatch); throw Error.reject "Cannot install removed canister"; @@ -132,14 +162,14 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { throw Error.reject "Cannot find canister"; } else { let config = { - profiling; + profiling = install_config.profiling; remove_cycles_add = true; limit_stable_memory_page = ?(16384 : Nat32); // Limit to 1G of stable memory backend_canister_id = ?Principal.fromActor(this); }; - let wasm = if (caller == controller) { + let wasm = if (caller == controller and install_config.is_whitelisted) { args.wasm_module; - } else if (is_whitelisted) { + } else if (install_config.is_whitelisted) { await Wasm.is_whitelisted(args.wasm_module); } else { await Wasm.transform(args.wasm_module, config); @@ -152,13 +182,42 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { }; await IC.install_code newArgs; stats := Logs.updateStats(stats, #install); - switch (pool.refresh(info, profiling)) { - case (?newInfo) newInfo; + + // Build tags from install arguments + let tags = Buffer.fromArray(install_config.origin.tags); + if (install_config.profiling) { + tags.add("wasm:profiling"); + }; + if (install_config.is_whitelisted) { + tags.add("wasm:asset"); + }; + switch (args.mode) { + case (#install) { tags.add("mode:install") }; + case (#upgrade) { tags.add("mode:upgrade") }; + case (#reinstall) { tags.add("mode:reinstall") }; + }; + let origin = { origin = install_config.origin.origin; tags = Buffer.toArray(tags) }; + statsByOrigin.addInstall(origin); + switch (pool.refresh(info, install_config.profiling)) { + case (?newInfo) { + updateTimer(newInfo); + newInfo; + }; case null { throw Error.reject "Cannot find canister" }; }; }; }; + func updateTimer(info: Types.CanisterInfo) { + func job() : async () { + pool.removeTimer(info.id); + // It is important that the timer job checks for the timestamp first. + // This prevents late-runner jobs from deleting newly installed code. + await removeCode(info); + }; + pool.updateTimer(info, job); + }; + public func callForward(info : Types.CanisterInfo, function : Text, args : Blob) : async Blob { if (pool.find info) { await InternetComputer.call(info.id, function, args); @@ -225,12 +284,19 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { throw Error.reject "Only called by controller"; }; stats := Logs.defaultStats; + statsByOrigin := Logs.StatsByOrigin(); + }; + public shared ({ caller }) func mergeTags(from: Text, to: ?Text) : async () { + if (caller != controller) { + throw Error.reject "Only called by controller"; + }; + statsByOrigin.merge_tag(from, to); }; // Metrics public query func http_request(req : Metrics.HttpRequest) : async Metrics.HttpResponse { if (req.url == "/metrics") { - let body = Metrics.metrics stats; + let body = Metrics.metrics(stats); { status_code = 200; headers = [("Content-Type", "text/plain; version=0.0.4"), ("Content-Length", Nat.toText(body.size()))]; @@ -278,7 +344,7 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { if (not pool.findId caller) { throw Error.reject "Only a canister managed by the Motoko Playground can call create_canister"; }; - let info = await getExpiredCanisterInfo(); + let info = await getExpiredCanisterInfo({origin="spawned"; tags=[]}); let result = pool.setChild(caller, info.id); if (not result) { throw Error.reject("In the Motoko Playground, each top level canister can only spawn " # Nat.toText(params.max_family_tree_size) # " descendants including itself"); @@ -299,12 +365,12 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { wasm_module : ICType.wasm_module; mode : { #reinstall; #upgrade; #install }; canister_id : ICType.canister_id; - is_whitelisted : Bool; }) : async () { switch (sanitizeInputs(caller, canister_id)) { case (#ok info) { let args = { arg; wasm_module; mode; canister_id }; - ignore await installCode(info, args, pool.profiling caller, is_whitelisted); // inherit the profiling of the parent + let config = { profiling = pool.profiling caller; is_whitelisted = false; origin = {origin = "spawned"; tags = [] } }; + ignore await installCode(info, args, config); // inherit the profiling of the parent }; case (#err makeMsg) throw Error.reject(makeMsg "install_code"); }; @@ -379,6 +445,7 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { #installCode : Any; #removeCode : Any; #resetStats : Any; + #mergeTags : Any; #wallet_receive : Any; #create_canister : Any; diff --git a/e2e/assets/playground_backend/service/pool/Metrics.mo b/e2e/assets/playground_backend/service/pool/Metrics.mo index 4165615f7c..6f29605d9f 100644 --- a/e2e/assets/playground_backend/service/pool/Metrics.mo +++ b/e2e/assets/playground_backend/service/pool/Metrics.mo @@ -1,6 +1,5 @@ import Text "mo:base/Text"; import Time "mo:base/Time"; -import Int "mo:base/Int"; import Logs "./Logs"; module { @@ -15,11 +14,7 @@ module { headers: [(Text, Text)]; body: Blob; }; - func encode_single_value(kind: Text, name: Text, number: Int, desc: Text, time: Int) : Text { - "# HELP " # name # " " # desc # "\n" # - "# TYPE " # name # " " # kind # "\n" # - name # " " # Int.toText(number) # " " # Int.toText(time) # "\n" - }; + let encode_single_value = Logs.encode_single_value; public func metrics(stats: Logs.Stats) : Blob { let now = Time.now() / 1_000_000; var result = ""; diff --git a/e2e/assets/playground_backend/service/pool/PoW.mo b/e2e/assets/playground_backend/service/pool/PoW.mo index 7792b74a17..213ef29a3f 100644 --- a/e2e/assets/playground_backend/service/pool/PoW.mo +++ b/e2e/assets/playground_backend/service/pool/PoW.mo @@ -2,54 +2,41 @@ import Splay "mo:splay"; import Time "mo:base/Time"; import Text "mo:base/Text"; import Int "mo:base/Int"; -import Debug "mo:base/Debug" module { public type Nonce = { - timestamp : Int; - nonce : Nat; + timestamp: Int; + nonce: Nat; }; - func nonceCompare(x : Nonce, y : Nonce) : { #less; #equal; #greater } { - if (x.timestamp < y.timestamp) { #less } else if (x.timestamp == y.timestamp and x.nonce < y.nonce) { - #less; - } else if (x.timestamp == y.timestamp and x.nonce == y.nonce) { #equal } else { - #greater; - }; + func nonceCompare(x: Nonce, y: Nonce): {#less;#equal;#greater} { + if (x.timestamp < y.timestamp) { #less } + else if (x.timestamp == y.timestamp and x.nonce < y.nonce) { #less } + else if (x.timestamp == y.timestamp and x.nonce == y.nonce) { #equal } + else { #greater } }; - public class NonceCache(TTL : Nat) { + public class NonceCache(TTL: Nat) { let known_nonces = Splay.Splay(nonceCompare); - public func add(nonce : Nonce) { + public func add(nonce: Nonce) { known_nonces.insert(nonce); }; public func pruneExpired() { let now = Time.now(); for (info in known_nonces.entries()) { - if (info.timestamp > now - TTL) { return }; + if (info.timestamp > now - TTL) { return; }; known_nonces.remove(info); }; }; - public func contains(nonce : Nonce) : Bool { - known_nonces.find(nonce); + public func contains(nonce: Nonce) : Bool { + known_nonces.find(nonce) }; - public func checkProofOfWork(nonce : Nonce) : Bool { + public func checkProofOfWork(nonce: Nonce) : Bool { let now = Time.now(); - if (nonce.timestamp < now - TTL) { - Debug.trap("too late"); - return false; - }; - if (nonce.timestamp > now + TTL) { - Debug.trap("too early"); - return false; - }; + if (nonce.timestamp < now - TTL) return false; + if (nonce.timestamp > now + TTL) return false; let raw = "motoko-playground" # (Int.toText(nonce.timestamp)) # (Int.toText(nonce.nonce)); - Debug.print(raw); let hash = Text.hash(raw); - Debug.print("The Motoko-calculated hash is " # debug_show (hash)); - if (hash & 0xc0000000 != 0) { - Debug.trap("other stuff failed"); - return false; - }; - true; + if (hash & 0xc0000000 != 0) return false; + true }; }; -}; +} diff --git a/e2e/assets/playground_backend/service/pool/Types.mo b/e2e/assets/playground_backend/service/pool/Types.mo index 54fbf070f7..440087095e 100644 --- a/e2e/assets/playground_backend/service/pool/Types.mo +++ b/e2e/assets/playground_backend/service/pool/Types.mo @@ -9,6 +9,7 @@ import Array "mo:base/Array"; import List "mo:base/List"; import Option "mo:base/Option"; import Int "mo:base/Int"; +import Timer "mo:base/Timer"; module { public type InitParams = { @@ -51,9 +52,11 @@ module { public class CanisterPool(size: Nat, ttl: Nat, max_family_tree_size: Nat) { var len = 0; var tree = Splay.Splay(canisterInfoCompare); + // Metadata is a replicate of splay tree, which allows lookup without timestamp. Internal use only. var metadata = TrieMap.TrieMap(Principal.equal, Principal.hash); var childrens = TrieMap.TrieMap>(Principal.equal, Principal.hash); var parents = TrieMap.TrieMap(Principal.equal, Principal.hash); + let timers = TrieMap.TrieMap(Principal.equal, Principal.hash); public type NewId = { #newId; #reuse:CanisterInfo; #outOfCapacity:Nat }; @@ -123,6 +126,24 @@ module { return true; }; + public func updateTimer(info: CanisterInfo, job : () -> async ()) { + let elapsed = Time.now() - info.timestamp; + let duration = if (elapsed > ttl) { 0 } else { Int.abs(ttl - elapsed) }; + let tid = Timer.setTimer(#nanoseconds duration, job); + switch (timers.replace(info.id, tid)) { + case null {}; + case (?old_id) { + // The old job can still run when it has expired, but the future + // just started to run. To be safe, the job needs to check for timestamp. + Timer.cancelTimer(old_id); + }; + }; + }; + + public func removeTimer(cid: Principal) { + timers.delete cid; + }; + private func notExpired(info: CanisterInfo, now: Int) : Bool = (info.timestamp > now - ttl); // Return a list of canister IDs from which to uninstall code @@ -140,17 +161,22 @@ module { result }; - public func share() : ([CanisterInfo], [(Principal, (Int, Bool))], [(Principal, [Principal])]) { + public func share() : ([CanisterInfo], [(Principal, (Int, Bool))], [(Principal, [Principal])], [CanisterInfo]) { let stableInfos = Iter.toArray(tree.entries()); let stableMetadata = Iter.toArray(metadata.entries()); - let stableChildrens = + let stableChildren = Iter.toArray( Iter.map<(Principal, List.List), (Principal, [Principal])>( childrens.entries(), func((parent, children)) = (parent, List.toArray(children)) ) ); - (stableInfos, stableMetadata, stableChildrens) + let stableTimers = Iter.toArray( + Iter.filter( + tree.entries(), + func (info) = Option.isSome(timers.get(info.id)) + )); + (stableInfos, stableMetadata, stableChildren, stableTimers) }; public func unshare(stableInfos: [CanisterInfo], stableMetadata: [(Principal, (Int, Bool))], stableChildrens : [(Principal, [Principal])]) { diff --git a/e2e/assets/playground_backend/wasm-utils.wasm b/e2e/assets/playground_backend/wasm-utils.wasm index b5f605eaca..30de263cf5 100644 Binary files a/e2e/assets/playground_backend/wasm-utils.wasm and b/e2e/assets/playground_backend/wasm-utils.wasm differ diff --git a/src/dfx-core/src/config/model/network_descriptor.rs b/src/dfx-core/src/config/model/network_descriptor.rs index f5922b7bb7..73c2e217a8 100644 --- a/src/dfx-core/src/config/model/network_descriptor.rs +++ b/src/dfx-core/src/config/model/network_descriptor.rs @@ -10,7 +10,7 @@ use slog::Logger; use std::path::{Path, PathBuf}; use url::Url; -const MAINNET_MOTOKO_PLAYGROUND_CANISTER_ID: Principal = +pub const MAINNET_MOTOKO_PLAYGROUND_CANISTER_ID: Principal = Principal::from_slice(&[0, 0, 0, 0, 0, 48, 0, 97, 1, 1]); pub const PLAYGROUND_NETWORK_NAME: &str = "playground"; pub const MOTOKO_PLAYGROUND_CANISTER_TIMEOUT_SECONDS: u64 = 1200; diff --git a/src/dfx/Cargo.toml b/src/dfx/Cargo.toml index 8ac73541a9..4f6f1ae8a2 100644 --- a/src/dfx/Cargo.toml +++ b/src/dfx/Cargo.toml @@ -118,6 +118,7 @@ url.workspace = true walkdir.workspace = true walrus = "0.20.1" which = "4.2.5" +ci_info = "0.14" [target.'cfg(windows)'.dependencies] junction = "1.0.0" diff --git a/src/dfx/src/lib/operations/canister/motoko_playground.rs b/src/dfx/src/lib/operations/canister/motoko_playground.rs index 6dc644f302..b53293582c 100644 --- a/src/dfx/src/lib/operations/canister/motoko_playground.rs +++ b/src/dfx/src/lib/operations/canister/motoko_playground.rs @@ -1,4 +1,6 @@ -use dfx_core::config::model::network_descriptor::NetworkTypeDescriptor; +use dfx_core::config::model::network_descriptor::{ + NetworkTypeDescriptor, MAINNET_MOTOKO_PLAYGROUND_CANISTER_ID, +}; use num_traits::ToPrimitive; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -49,6 +51,25 @@ pub struct InstallArgs<'a> { pub mode: InstallMode, pub canister_id: Principal, } +#[derive(CandidType)] +struct InstallConfig<'a> { + profiling: bool, + is_whitelisted: bool, + origin: Origin<'a>, +} +#[derive(CandidType)] +struct Origin<'a> { + origin: &'a str, + tags: &'a [&'a str], +} +impl<'a> Origin<'a> { + fn new() -> Self { + Self { + origin: "dfx", + tags: &[], + } + } +} #[context("Failed to reserve canister '{}'.", canister_name)] pub async fn reserve_canister_with_playground( @@ -67,9 +88,13 @@ pub async fn reserve_canister_with_playground( } else { bail!("Trying to reserve canister with playground on non-playground network.") }; + if ci_info::is_ci() && playground_canister == MAINNET_MOTOKO_PLAYGROUND_CANISTER_ID { + bail!("Cannot reserve playground canister in CI, please run `dfx start` to use the local replica.") + } + let mut canister_id_store = env.get_canister_id_store()?; let (timestamp, nonce) = create_nonce(); - let get_can_arg = Encode!(&GetCanisterIdArgs { timestamp, nonce })?; + let get_can_arg = Encode!(&GetCanisterIdArgs { timestamp, nonce }, &Origin::new())?; let result = agent .update(&playground_canister, "getCanisterId") .with_arg(get_can_arg) @@ -148,7 +173,12 @@ pub async fn playground_install_code( mode, canister_id: canister_info.id, }; - let encoded_arg = encode_args((canister_info, install_arg, false, is_asset_canister))?; + let install_config = InstallConfig { + profiling: false, + is_whitelisted: is_asset_canister, + origin: Origin::new(), + }; + let encoded_arg = encode_args((canister_info, install_arg, install_config))?; let result = agent .update(&playground_canister, "installCode") .with_arg(encoded_arg.as_slice())