diff --git a/.github/CONTRIBUTING.adoc b/.github/CONTRIBUTING.adoc index d65b06ba3f..f9d54e979a 100644 --- a/.github/CONTRIBUTING.adoc +++ b/.github/CONTRIBUTING.adoc @@ -30,7 +30,7 @@ dfx 0.5.7-1-gad81116 [source,bash] ---- -sdk $ nix-shell -A e2e-tests default.nix +sdk $ nix-shell -A ${name_of_any_e2e_test} e2e/bats/default.nix [nix-shell:~/d/sdk]$ cd e2e/bats [nix-shell:~/d/sdk]$ bats *.bash ---- @@ -42,9 +42,9 @@ https://github.com/dfinity-lab/ic-ref[reference implementation of the Internet C [source,bash] ---- -sdk $ nix-shell -A e2e-tests-ic-ref default.nix +sdk $ nix-shell -A ${name_of_any_e2e_test} --arg use_ic_ref true e2e/bats/default.nix [nix-shell:~/d/sdk]$ cd e2e/bats -[nix-shell:~/d/sdk]$ USE_IC_REF=1 bats *.bash +[nix-shell:~/d/sdk]$ bats *.bash ---- ==== Running `dfx` in a Debugger diff --git a/Cargo.lock b/Cargo.lock index 5bf4a3f691..2a27a8d962 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -697,9 +697,9 @@ checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "chrono" -version = "0.4.17" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b05acab8a90ff05c15f407779397ff10ed4049bbe086d47cd7c95817207ad81" +checksum = "d021fddb7bd3e734370acfa4a83f34095571d8570c039f1420d77540f68d5772" dependencies = [ "libc", "num-integer", @@ -1020,6 +1020,7 @@ dependencies = [ "libflate", "mockall", "mockito", + "net2", "pem 0.7.0", "petgraph", "proptest", @@ -1514,9 +1515,9 @@ checksum = "d36fab90f82edc3c747f9d438e06cf0a491055896f2a279638bb5beed6c40177" [[package]] name = "hashbrown" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00d63df3d41950fb462ed38308eea019113ad1508da725bbedcd0fa5a85ef5f7" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" [[package]] name = "heck" @@ -2409,18 +2410,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "0.4.23" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca4433fff2ae79342e497d9f8ee990d174071408f28f726d6d83af93e58e48aa" +checksum = "f48fad7cfbff853437be7cf54d7b993af21f53be7f0988cbfe4a51535aa77205" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "0.4.23" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c0e815c3ee9a031fdf5af21c10aa17c573c9c6a566328d99e3936c34e36461f" +checksum = "24c6d293bdd3ca5a1697997854c6cf7855e43fb6a0ba1c47af57a5bcafd158ae" dependencies = [ "proc-macro2", "quote", @@ -2429,9 +2430,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282adbf10f2698a7a77f8e983a74b2d18176c19a7fd32a45446139ae7b02b715" +checksum = "71f349a4f0e70676ffb2dbafe16d0c992382d02f0a952e3ddf584fc289dac6b3" [[package]] name = "pin-utils" @@ -2543,9 +2544,9 @@ checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" [[package]] name = "proc-macro2" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4b93dba1818d32e781f9d008edd577bab215e83ef50e8a1ddf1ad301b19a09f" +checksum = "51ef7cd2518ead700af67bf9d1a658d90b6037d77110fd9c0445429d0ba1c6c9" dependencies = [ "unicode-xid", ] @@ -3307,9 +3308,9 @@ checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" [[package]] name = "syn" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6690e3e9f692504b941dc6c3b188fd28df054f7fb8469ab40680df52fdcc842b" +checksum = "9c51d92969d209b54a98397e1b91c8ae82d8c87a7bb87df0b29aa2ad81454228" dependencies = [ "proc-macro2", "quote", @@ -3598,20 +3599,21 @@ checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" [[package]] name = "tracing" -version = "0.1.19" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d79ca061b032d6ce30c660fded31189ca0b9922bf483cd70759f13a2d86786c" +checksum = "b0987850db3733619253fe60e17cb59b82d37c7e6c0236bb81e4d6b87c879f27" dependencies = [ "cfg-if", "log", + "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bcf46c1f1f06aeea2d6b81f3c863d0930a596c86ad1920d4e5bad6dd1d7119a" +checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" dependencies = [ "lazy_static", ] diff --git a/e2e/bats/bootstrap.bash b/e2e/bats/bootstrap.bash index 9138f433c0..96aa2470ec 100644 --- a/e2e/bats/bootstrap.bash +++ b/e2e/bats/bootstrap.bash @@ -18,10 +18,10 @@ teardown() { dfx build dfx canister install hello ID=$(dfx canister id hello) - - assert_command curl http://localhost:8000/_/candid?canisterId="$ID" -o ./web.txt + PORT=$(cat .dfx/webserver-port) + assert_command curl http://localhost:"$PORT"/_/candid?canisterId="$ID" -o ./web.txt assert_command diff .dfx/local/canisters/hello/hello.did ./web.txt - assert_command curl http://localhost:8000/_/candid?canisterId="$ID"\&format=js -o ./web.txt + assert_command curl http://localhost:"$PORT"/_/candid?canisterId="$ID"\&format=js -o ./web.txt # Relax diff as it's produced by two different compilers. assert_command diff --ignore-all-space --ignore-blank-lines .dfx/local/canisters/hello/hello.did.js ./web.txt } diff --git a/e2e/bats/build.bash b/e2e/bats/build.bash index c6a8676897..89718f97a7 100644 --- a/e2e/bats/build.bash +++ b/e2e/bats/build.bash @@ -74,8 +74,8 @@ teardown() { } @test "can build a custom canister type" { - dfx_start install_asset custom_canister + dfx_start dfx canister create --all assert_command dfx build assert_match "CUSTOM_CANISTER_BUILD_DONE" @@ -93,7 +93,8 @@ teardown() { @test "build succeeds when requested network is configured" { dfx_start - assert_command dfx config networks.tungsten.providers '[ "http://127.0.0.1:8000" ]' + webserver_port=$(cat .dfx/webserver-port) + assert_command dfx config networks.tungsten.providers '[ "http://127.0.0.1:'$webserver_port'" ]' assert_command dfx canister --network tungsten create --all assert_command dfx build --network tungsten } @@ -108,7 +109,8 @@ teardown() { @test "build output for non-local network is in expected directory" { dfx_start - assert_command dfx config networks.tungsten.providers '[ "http://127.0.0.1:8000" ]' + webserver_port=$(cat .dfx/webserver-port) + assert_command dfx config networks.tungsten.providers '[ "http://127.0.0.1:'$webserver_port'" ]' dfx canister --network tungsten create --all assert_command dfx build --network tungsten assert_command ls .dfx/tungsten/canisters/e2e_project/ diff --git a/e2e/bats/create.bash b/e2e/bats/create.bash index 780dfe7a23..242f4ccf13 100644 --- a/e2e/bats/create.bash +++ b/e2e/bats/create.bash @@ -52,7 +52,8 @@ teardown() { @test "create succeeds when requested network is configured" { dfx_start - assert_command dfx config networks.tungsten.providers '[ "http://127.0.0.1:8000" ]' + webserver_port=$(cat .dfx/webserver-port) + assert_command dfx config networks.tungsten.providers '[ "http://127.0.0.1:'$webserver_port'" ]' assert_command dfx canister --network tungsten create --all } diff --git a/e2e/bats/default.nix b/e2e/bats/default.nix index f4f0e92b73..8f5ff7487d 100644 --- a/e2e/bats/default.nix +++ b/e2e/bats/default.nix @@ -4,47 +4,60 @@ , use_ic_ref ? false }: let - e2e = lib.noNixFiles (lib.gitOnlySource ./.); - lib = pkgs.lib; - sources = pkgs.sources; + inherit (pkgs) lib; - inputs = with pkgs; [ - bats - bash - coreutils - diffutils - curl - findutils - gnugrep - gnutar - gzip - jq - netcat - ps - python3 - procps - which - dfx.standalone - ] ++ lib.optional use_ic_ref ic-ref; -in + isBatsTest = fileName: type: lib.hasSuffix ".bash" fileName && type == "regular"; -builtins.derivation { - name = "e2e-tests"; - system = pkgs.stdenv.system; - PATH = pkgs.lib.makeSearchPath "bin" inputs; - BATSLIB = sources.bats-support; - builder = - pkgs.writeScript "builder.sh" '' - #!${pkgs.stdenv.shell} - set -eo pipefail + here = ./.; - # We want $HOME/.cache to be in a new temporary directory. - export HOME=$(mktemp -d -t dfx-e2e-home-XXXX) + mkBatsTest = fileName: + let + name = lib.removeSuffix ".bash" fileName; + in + lib.nameValuePair name ( + pkgs.runCommandNoCC "e2e-test-${name}${lib.optionalString use_ic_ref "-use_ic_ref"}" { + nativeBuildInputs = with pkgs; [ + bats + diffutils + curl + findutils + gnugrep + gnutar + gzip + jq + netcat + ps + python3 + procps + which + dfx.standalone + ] ++ lib.optional use_ic_ref ic-ref; + BATSLIB = pkgs.sources.bats-support; + USE_IC_REF = use_ic_ref; + utils = lib.gitOnlySource ./utils; + assets = lib.gitOnlySource ./assets; + test = here + "/${fileName}"; + } '' + export HOME=$(pwd) - export USE_IC_REF=${if use_ic_ref then "1" else ""} + ln -s $utils utils + ln -s $assets assets + ln -s $test test - # Timeout of 10 minutes is enough for now. Reminder; CI might be running with - # less resources than a dev's computer, so e2e might take longer. - timeout --preserve-status 3600 bats --recursive ${e2e}/* | tee $out - ''; -} // { meta = {}; } + # Timeout of 10 minutes is enough for now. Reminder; CI might be running with + # less resources than a dev's computer, so e2e might take longer. + timeout --preserve-status 3600 bats test | tee $out + '' + ); +in +builtins.listToAttrs + ( + builtins.map mkBatsTest + ( + lib.attrNames + ( + lib.filterAttrs isBatsTest + (builtins.readDir here) + ) + ) + ) diff --git a/e2e/bats/deploy.bash b/e2e/bats/deploy.bash index 865a2d0273..3ae6263c67 100644 --- a/e2e/bats/deploy.bash +++ b/e2e/bats/deploy.bash @@ -6,9 +6,6 @@ setup() { # We want to work from a temporary directory, different for every test. cd $(mktemp -d -t dfx-e2e-XXXXXXXX) export RUST_BACKTRACE=1 - - dfx_new hello - dfx_start } teardown() { @@ -16,6 +13,8 @@ teardown() { } @test "deploy from a fresh project" { + dfx_new hello + dfx_start install_asset greet assert_command dfx deploy @@ -24,6 +23,8 @@ teardown() { } @test "deploy a canister without dependencies" { + dfx_new hello + dfx_start install_asset greet assert_command dfx deploy hello assert_match 'Deploying: hello' @@ -31,6 +32,8 @@ teardown() { } @test "deploy a canister with dependencies" { + dfx_new hello + dfx_start install_asset greet assert_command dfx deploy hello_assets assert_match 'Deploying: hello hello_assets' @@ -38,18 +41,22 @@ teardown() { @test "deploy a canister with non-circular shared dependencies" { install_asset transitive_deps_canisters + dfx_start assert_command dfx deploy canister_f assert_match 'Deploying: canister_a canister_f canister_g canister_h' } @test "report an error on attempt to deploy a canister with circular dependencies" { install_asset transitive_deps_canisters + dfx_start assert_command_fail dfx deploy canister_d assert_match 'canister_d -> canister_e -> canister_d' } @test "if already registered, try to upgrade then install" { + dfx_new hello install_asset greet + dfx_start assert_command dfx canister create --all assert_command dfx deploy diff --git a/e2e/bats/frontend.bash b/e2e/bats/frontend.bash index 9a37e9b5ab..8d0a8e34c5 100644 --- a/e2e/bats/frontend.bash +++ b/e2e/bats/frontend.bash @@ -19,7 +19,8 @@ teardown() { dfx build e2e_project sleep 1 - assert_command curl http://localhost:8000 # 8000 = default port. + PORT=$(cat .dfx/webserver-port) + assert_command curl http://localhost:"$PORT" assert_match "" } @@ -33,7 +34,7 @@ teardown() { dfx canister create --all dfx build e2e_project - assert_command curl http://localhost:12345 # 8000 = default port. + assert_command curl http://localhost:12345 assert_match "" assert_command_fail curl http://localhost:8000 diff --git a/e2e/bats/network.bash b/e2e/bats/network.bash index 9a3abe0f6e..eec57d13cc 100644 --- a/e2e/bats/network.bash +++ b/e2e/bats/network.bash @@ -17,7 +17,8 @@ teardown() { @test "create stores canister ids for default-persistent networks in canister_ids.json" { dfx_start - assert_command dfx config networks.tungsten.providers '[ "http://127.0.0.1:8000" ]' + webserver_port=$(cat .dfx/webserver-port) + assert_command dfx config networks.tungsten.providers '[ "http://127.0.0.1:'$webserver_port'" ]' assert_command dfx canister --network tungsten create --all @@ -29,7 +30,8 @@ teardown() { @test "create stores canister ids for configured-ephemeral networks in canister_ids.json" { dfx_start - assert_command dfx config networks.tungsten.providers '[ "http://127.0.0.1:8000" ]' + webserver_port=$(cat .dfx/webserver-port) + assert_command dfx config networks.tungsten.providers '[ "http://127.0.0.1:'$webserver_port'" ]' cat <<<$(jq .networks.tungsten.type=\"ephemeral\" dfx.json) >dfx.json assert_command dfx canister --network tungsten create --all @@ -71,7 +73,8 @@ teardown() { @test "failure message does include network if for non-local network" { dfx_start - assert_command dfx config networks.tungsten.providers '[ "http://127.0.0.1:8000" ]' + webserver_port=$(cat .dfx/webserver-port) + assert_command dfx config networks.tungsten.providers '[ "http://127.0.0.1:'$webserver_port'" ]' assert_command_fail dfx build --network tungsten assert_match "Cannot find canister id. Please issue 'dfx canister --network tungsten create e2e_project" diff --git a/e2e/bats/ping.bash b/e2e/bats/ping.bash index 62af13b3f5..2e25b8a52a 100644 --- a/e2e/bats/ping.bash +++ b/e2e/bats/ping.bash @@ -17,9 +17,6 @@ teardown() { assert_command_fail dfx ping } -@test "dfx start succeeds" { - dfx_start -} @test "dfx ping succeeds if replica is running" { dfx_start @@ -30,7 +27,8 @@ teardown() { @test "dfx ping succeeds by specific host:post" { dfx_start - assert_command dfx ping http://127.0.0.1:8000 + webserver_port=$(cat .dfx/webserver-port) + assert_command dfx ping http://127.0.0.1:"$webserver_port" assert_match "\"ic_api_version\"" } @@ -44,7 +42,8 @@ teardown() { @test "dfx ping succeeds by network name if network bind address is host:port format" { dfx_start - assert_command dfx config networks.local.bind '"127.0.0.1:8000"' + webserver_port=$(cat .dfx/webserver-port) + assert_command dfx config networks.local.bind '"127.0.0.1:'$webserver_port'"' assert_command dfx ping local assert_match "\"ic_api_version\"" @@ -54,6 +53,8 @@ teardown() { [ "$USE_IC_REF" ] && skip "skipped for ic-ref" dfx_start --host 127.0.0.1:12345 + # dfx_start overwrites local bind with provided port arg, set it back to default + assert_command dfx config networks.local.bind '"127.0.0.1:8000"' cat <<<$(jq .networks.arbitrary.providers=[\"http://127.0.0.1:12345\"] dfx.json) >dfx.json assert_command dfx ping arbitrary diff --git a/e2e/bats/utils/_.bash b/e2e/bats/utils/_.bash index 003e9c3cd1..97fca81011 100644 --- a/e2e/bats/utils/_.bash +++ b/e2e/bats/utils/_.bash @@ -5,6 +5,8 @@ load utils/assertions install_asset() { ASSET_ROOT=${BATS_TEST_DIRNAME}/assets/$1/ cp -R $ASSET_ROOT/* . + # set write perms to overwrite local bind in assets which have a dfx.json + chmod -R a+w . [ -f ./patch.bash ] && source ./patch.bash } @@ -43,21 +45,35 @@ dfx_start() { cat <<<$(jq .networks.local.bind=\"127.0.0.1:${port}\" dfx.json) >dfx.json cat dfx.json - dfx bootstrap --port 8000 & + if [[ "$@" == "" ]]; then + dfx bootstrap --port 0 & # Start on random port for parallel test execution + else + dfx bootstrap --port 8000 & + fi + local webserver_port=$(cat .dfx/webserver-port) echo $! > dfx-bootstrap.pid else # Bats creates a FD 3 for test output, but child processes inherit it and Bats will # wait for it to close. Because `dfx start` leaves child processes running, we need # to close this pipe, otherwise Bats will wait indefinitely. - dfx start --background "$@" 3>&- + if [[ "$@" == "" ]]; then + dfx start --background --host "127.0.0.1:0" 3>&- # Start on random port for parallel test execution + else + dfx start --background "$@" 3>&- + fi local project_dir=${pwd} local dfx_config_root=.dfx/client-configuration printf "Configuration Root for DFX: %s\n" "${dfx_config_root}" test -f ${dfx_config_root}/client-1.port local port=$(cat ${dfx_config_root}/client-1.port) + + # Overwrite the default networks.local.bind 127.0.0.1:8000 with allocated port + local webserver_port=$(cat .dfx/webserver-port) + cat <<<$(jq .networks.local.bind=\"127.0.0.1:${webserver_port}\" dfx.json) >dfx.json fi printf "Replica Configured Port: %s\n" "${port}" + printf "Webserver Configured Port: %s\n" "${webserver_port}" timeout 5 sh -c \ "until nc -z localhost ${port}; do echo waiting for replica; sleep 1; done" \ diff --git a/src/dfx/Cargo.toml b/src/dfx/Cargo.toml index 9881c5a0e9..d10e9f9c03 100644 --- a/src/dfx/Cargo.toml +++ b/src/dfx/Cargo.toml @@ -39,6 +39,7 @@ libflate = "0.1.27" hotwatch = "0.4.3" humanize-rs = "0.1.5" mockall = "0.6.0" +net2 = "0.2.34" pem = "0.7.0" petgraph = "0.5.0" rand = "0.7.2" @@ -61,7 +62,7 @@ tar = "0.4.26" tempfile = "3.1.0" thiserror = "1.0.20" toml = "0.5.5" -tokio = "0.2.10" +tokio = { version = "0.2.10", features = [ "fs" ] } url = "2.1.0" walkdir = "2.2.9" wasmparser = "0.45.0" diff --git a/src/dfx/src/commands/bootstrap.rs b/src/dfx/src/commands/bootstrap.rs index e5edff1c23..3c969a4735 100644 --- a/src/dfx/src/commands/bootstrap.rs +++ b/src/dfx/src/commands/bootstrap.rs @@ -5,12 +5,13 @@ use crate::lib::message::UserMessage; use crate::lib::network::network_descriptor::NetworkDescriptor; use crate::lib::provider::get_network_descriptor; use crate::lib::webserver::webserver; +use crate::util::get_reusable_socket_addr; use clap::{App, Arg, ArgMatches, SubCommand}; use slog::info; use std::default::Default; use std::fs; use std::io::{Error, ErrorKind}; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::net::{IpAddr, Ipv4Addr}; use std::path::PathBuf; use std::str::FromStr; use std::time::Duration; @@ -70,11 +71,21 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { let (sender, receiver) = crossbeam::unbounded(); + // Since the user may have provided port "0", we need to grab a dynamically + // allocated port and construct a resuable SocketAddr which the actix + // HttpServer will bind to + let socket_addr = + get_reusable_socket_addr(config_bootstrap.ip.unwrap(), config_bootstrap.port.unwrap())?; + + let webserver_port_path = env.get_temp_dir().join("webserver-port"); + std::fs::write(&webserver_port_path, "")?; + std::fs::write(&webserver_port_path, socket_addr.port().to_string())?; + webserver( logger.clone(), build_output_root, network_descriptor, - SocketAddr::new(config_bootstrap.ip.unwrap(), config_bootstrap.port.unwrap()), + socket_addr, providers .iter() .map(|uri| Url::from_str(uri).unwrap()) diff --git a/src/dfx/src/commands/start.rs b/src/dfx/src/commands/start.rs index 2f53462fea..d9579b0da7 100644 --- a/src/dfx/src/commands/start.rs +++ b/src/dfx/src/commands/start.rs @@ -6,6 +6,7 @@ use crate::lib::provider::get_network_descriptor; use crate::lib::proxy::{CoordinateProxy, ProxyConfig}; use crate::lib::proxy_process::spawn_and_update_proxy; use crate::lib::replica_config::ReplicaConfig; +use crate::util::get_reusable_socket_addr; use clap::{App, Arg, ArgMatches, SubCommand}; use crossbeam::channel::{Receiver, Sender}; @@ -15,7 +16,7 @@ use futures::executor::block_on; use ic_agent::Agent; use indicatif::{ProgressBar, ProgressDrawTarget}; use std::fs; -use std::io::{Error, ErrorKind}; +use std::io::{Error, ErrorKind, Read}; use std::net::SocketAddr; use std::path::PathBuf; use std::process::Command; @@ -74,6 +75,40 @@ fn ping_and_wait(frontend_url: &str) -> DfxResult { }) } +// The frontend webserver is brought up by the bg process; thus, the fg process +// needs to wait and verify it's up before exiting. +// Because the user may have specified to start on port 0, here we wait for +// webserver_port_path to get written to and modify the frontend_url so we +// ping the correct address. +fn fg_ping_and_wait(webserver_port_path: PathBuf, frontend_url: String) -> DfxResult { + let mut waiter = Delay::builder() + .timeout(std::time::Duration::from_secs(30)) + .throttle(std::time::Duration::from_secs(1)) + .build(); + let mut runtime = Runtime::new().expect("Unable to create a runtime"); + let port = runtime.block_on(async { + waiter.start(); + let mut contents = String::new(); + loop { + let tokio_file = tokio::fs::File::open(&webserver_port_path).await?; + let mut std_file = tokio_file.into_std().await; + std_file.read_to_string(&mut contents)?; + if !contents.is_empty() { + break; + } + waiter.wait()?; + } + Ok::(contents.clone()) + })?; + let mut frontend_url_mod = frontend_url.clone(); + let port_offset = frontend_url_mod + .as_str() + .rfind(':') + .ok_or_else(|| DfxError::MalformedFrontendUrl(frontend_url))?; + frontend_url_mod.replace_range((port_offset + 1).., port.as_str()); + ping_and_wait(&frontend_url_mod) +} + // TODO(eftychis)/In progress: Rename to replica. /// Start the Internet Computer locally. Spawns a proxy to forward and /// manage browser requests. Responsible for running the network (one @@ -83,12 +118,20 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { .get_config() .ok_or(DfxError::CommandMustBeRunInAProject)?; + let temp_dir = env.get_temp_dir(); + let (frontend_url, address_and_port) = frontend_address(args, &config)?; + let webserver_port_path = temp_dir.join("webserver-port"); + std::fs::write(&webserver_port_path, "")?; + + // don't write to file since this arg means we send_background() + if !args.is_present("background") { + std::fs::write(&webserver_port_path, address_and_port.port().to_string())?; + } let client_pathbuf = env.get_cache().get_binary_command_path("replica")?; let ic_starter_pathbuf = env.get_cache().get_binary_command_path("ic-starter")?; - let temp_dir = env.get_temp_dir(); let state_root = env.get_state_dir(); let pid_file_path = temp_dir.join("pid"); @@ -125,7 +168,7 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { if args.is_present("background") { send_background()?; - return ping_and_wait(&frontend_url); + return fg_ping_and_wait(webserver_port_path, frontend_url); } // Start the client. @@ -286,7 +329,7 @@ fn send_background() -> DfxResult<()> { } fn frontend_address(args: &ArgMatches<'_>, config: &Config) -> DfxResult<(String, SocketAddr)> { - let address_and_port = args + let mut address_and_port = args .value_of("host") .and_then(|host| Option::from(host.parse())) .unwrap_or_else(|| { @@ -296,12 +339,20 @@ fn frontend_address(args: &ArgMatches<'_>, config: &Config) -> DfxResult<(String .expect("could not get socket_addr")) }) .map_err(|e| DfxError::InvalidArgument(format!("Invalid host: {}", e)))?; - let frontend_url = format!( - "http://{}:{}", - address_and_port.ip(), - address_and_port.port() - ); + if !args.is_present("background") { + // Since the user may have provided port "0", we need to grab a dynamically + // allocated port and construct a resuable SocketAddr which the actix + // HttpServer will bind to + address_and_port = + get_reusable_socket_addr(address_and_port.ip(), address_and_port.port())?; + } + let ip = if address_and_port.is_ipv6() { + format!("[{}]", address_and_port.ip()) + } else { + address_and_port.ip().to_string() + }; + let frontend_url = format!("http://{}:{}", ip, address_and_port.port()); Ok((frontend_url, address_and_port)) } diff --git a/src/dfx/src/lib/error/mod.rs b/src/dfx/src/lib/error/mod.rs index 562dfa9b2a..145e26053f 100644 --- a/src/dfx/src/lib/error/mod.rs +++ b/src/dfx/src/lib/error/mod.rs @@ -144,7 +144,14 @@ pub enum DfxError { /// Could not save the contents of the file CouldNotSaveCanisterIds(String, std::io::Error), + /// Could not parse a string using Humanize HumanizeParseError(humanize_rs::ParseError), + + /// An error occured when waiting using the Delay crate + WaiterError(delay::WaiterError), + + /// The url for the frontend webserver is malformed + MalformedFrontendUrl(String), } /// The result of running a DFX command. @@ -239,6 +246,9 @@ impl Display for DfxError { DfxError::CouldNotSaveCanisterIds(path, error) => { f.write_fmt(format_args!("Failed to save {} due to: {}", path, error))?; } + DfxError::MalformedFrontendUrl(err) => { + f.write_fmt(format_args!("Malformed frontend url: {}", err))?; + } err => { f.write_fmt(format_args!("An error occured:\n{:#?}", err))?; } @@ -314,3 +324,9 @@ impl From for DfxError { DfxError::HumanizeParseError(err) } } + +impl From for DfxError { + fn from(err: delay::WaiterError) -> DfxError { + DfxError::WaiterError(err) + } +} diff --git a/src/dfx/src/util/mod.rs b/src/dfx/src/util/mod.rs index 4de673146f..c21d3189e2 100644 --- a/src/dfx/src/util/mod.rs +++ b/src/dfx/src/util/mod.rs @@ -2,11 +2,31 @@ use crate::lib::error::{DfxError, DfxResult}; use candid::parser::typing::{check_prog, TypeEnv}; use candid::types::{Function, Type}; use candid::{parser::value::IDLValue, IDLArgs, IDLProg}; +use net2::{unix::UnixTcpBuilderExt, TcpBuilder}; +use std::net::{IpAddr, SocketAddr}; use std::time::Duration; pub mod assets; pub mod clap; +// The user can pass in port "0" to dfx start or dfx bootstrap i.e. "127.0.0.1:0" or "[::1]:0", +// thus, we need to recreate SocketAddr with the kernel provided dynmically allocated port here. +// TcpBuilder is used with reuse_address and reuse_port set to "true" because +// the Actix HttpServer in webserver.rs will bind to this SocketAddr. +pub fn get_reusable_socket_addr(ip: IpAddr, port: u16) -> DfxResult { + let tcp_builder = if ip.is_ipv4() { + TcpBuilder::new_v4()? + } else { + TcpBuilder::new_v6()? + }; + Ok(tcp_builder + .bind(SocketAddr::new(ip, port))? + .reuse_address(true)? + .reuse_port(true)? + .to_tcp_listener()? + .local_addr()?) +} + pub fn expiry_duration() -> Duration { // 5 minutes is max ingress timeout Duration::from_secs(60 * 5)