diff --git a/Cargo.lock b/Cargo.lock index cd3ec73bcc..f8d4fd3730 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -762,6 +762,23 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "console" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b1aacfaffdbff75be81c15a399b4bedf78aaefe840e8af1d299ac2ade885d2" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "regex", + "terminal_size", + "termios", + "unicode-width", + "winapi 0.3.9", + "winapi-util", +] + [[package]] name = "const-random" version = "0.1.8" @@ -985,6 +1002,7 @@ dependencies = [ "libflate", "mockall", "mockito", + "net2", "petgraph", "proptest", "rand 0.7.3", @@ -1107,9 +1125,9 @@ checksum = "134951f4028bdadb9b84baf4232681efbf277da25144b9b0ad65df75946c422b" [[package]] name = "either" -version = "1.5.3" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" +checksum = "cd56b59865bce947ac5958779cfa508f6c3b9497cc762b7e24a12d11ccde2c4f" [[package]] name = "ena" @@ -1475,9 +1493,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34f595585f103464d8d2f6e9864682d74c1601fed5e07d62b1c9058dba8246fb" +checksum = "e91b62f79061a0bc2e046024cb7ba44b08419ed238ecbd9adbd787434b9e8c25" dependencies = [ "autocfg 1.0.0", ] @@ -1691,7 +1709,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b45e59b16c76b11bf9738fd5d38879d3bd28ad292d7b313608becb17ae2df9" dependencies = [ "autocfg 1.0.0", - "hashbrown 0.8.1", + "hashbrown 0.8.2", ] [[package]] @@ -1700,7 +1718,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8572bccfb0665e70b7faf44ee28841b8e0823450cd4ad562a76b5a3c4bf48487" dependencies = [ - "console 0.11.3", + "console 0.12.0", "lazy_static", "number_prefix", "regex", @@ -1844,9 +1862,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lazycell" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b294d6fa9ee409a054354afc4352b0b9ef7ca222c69b8812cbea9e7d2bf3783f" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "leb128" @@ -2353,7 +2371,7 @@ dependencies = [ "instant", "libc", "redox_syscall", - "smallvec 1.4.1", + "smallvec 1.4.2", "winapi 0.3.9", ] @@ -3043,9 +3061,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.114" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3" +checksum = "e54c9a88f2da7238af84b5101443f0c0d0a3bbdc455e34a5c9497b1903ed55d5" dependencies = [ "serde_derive", ] @@ -3082,9 +3100,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.114" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0be94b04690fbaed37cddffc5c134bf537c8e3329d53e982fe04c374978f8e" +checksum = "609feed1d0a73cc36a0182a840a9b37b4a82f0b1150369f0536a9e3f2a31dc48" dependencies = [ "proc-macro2 1.0.19", "quote 1.0.7", @@ -3223,9 +3241,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3757cb9d89161a2f24e1cf78efa0c1fcff485d18e3f55e0aa3480824ddaa0f3f" +checksum = "fbee7696b84bbf3d89a1c2eccff0850e3047ed46bfcd2e92c29a2d074d57e252" [[package]] name = "socket2" @@ -3661,9 +3679,9 @@ checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" [[package]] name = "tracing" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0aae59226cf195d8e74d4b34beae1859257efb4e5fed3f147d2dc2c7d372178" +checksum = "6d79ca061b032d6ce30c660fded31189ca0b9922bf483cd70759f13a2d86786c" dependencies = [ "cfg-if", "log", @@ -3672,9 +3690,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d593f98af59ebc017c0648f0117525db358745a8894a8d684e185ba3f45954f9" +checksum = "db63662723c316b43ca36d833707cc93dff82a02ba3d7e354f342682cc8b3545" dependencies = [ "lazy_static", ] diff --git a/e2e/bats/bootstrap.bash b/e2e/bats/bootstrap.bash index 8a08648aac..0d1df0343c 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..15b4025887 100644 --- a/e2e/bats/build.bash +++ b/e2e/bats/build.bash @@ -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 00c289ec78..b7004bce75 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..b468eebae4 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/frontend.bash b/e2e/bats/frontend.bash index c242347596..31cda2626a 100644 --- a/e2e/bats/frontend.bash +++ b/e2e/bats/frontend.bash @@ -19,7 +19,8 @@ teardown() { dfx build --skip-frontend 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 --skip-frontend - 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..44d6cb0530 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\"" diff --git a/e2e/bats/utils/_.bash b/e2e/bats/utils/_.bash index 003e9c3cd1..833b3a5fe3 100644 --- a/e2e/bats/utils/_.bash +++ b/e2e/bats/utils/_.bash @@ -43,21 +43,31 @@ dfx_start() { cat <<<$(jq .networks.local.bind=\"127.0.0.1:${port}\" dfx.json) >dfx.json cat dfx.json - dfx bootstrap --port 8000 & + dfx bootstrap --port 0 & # Start on random port for parallel test execution + 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>&- # Start on random port for parallel test execution + 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 b177d145ac..6461fb7b7e 100644 --- a/src/dfx/Cargo.toml +++ b/src/dfx/Cargo.toml @@ -39,6 +39,7 @@ lazy_static = "1.4.0" libflate = "0.1.27" hotwatch = "0.4.3" mockall = "0.6.0" +net2 = "0.2.34" petgraph = "0.5.0" rand = "0.7.2" regex = "1.3.1" 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 628df41770..a38353ac50 100644 --- a/src/dfx/src/commands/start.rs +++ b/src/dfx/src/commands/start.rs @@ -7,6 +7,7 @@ use crate::lib::proxy::{CoordinateProxy, ProxyConfig}; use crate::lib::proxy_process::spawn_and_update_proxy; use crate::lib::replica_config::ReplicaConfig; use crate::lib::waiter::create_waiter; +use crate::util::get_reusable_socket_addr; use clap::{App, Arg, ArgMatches, SubCommand}; use crossbeam::channel::{Receiver, Sender}; @@ -20,9 +21,9 @@ use std::net::SocketAddr; use std::path::PathBuf; use std::process::Command; use std::time::Duration; + use sysinfo::{Pid, Process, ProcessExt, Signal, System, SystemExt}; use tokio::runtime::Runtime; - /// Provide necessary arguments to start the Internet Computer /// locally. See `exec` for further information. pub fn construct() -> App<'static, 'static> { @@ -71,12 +72,16 @@ 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, "")?; + 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"); @@ -113,7 +118,7 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { if args.is_present("background") { send_background()?; - return ping_and_wait(&frontend_url); + return Ok(()); } // Start the client. @@ -280,7 +285,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(|| { @@ -290,12 +295,21 @@ 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()); + println!("frontend_url {:?}", frontend_url); Ok((frontend_url, address_and_port)) } diff --git a/src/dfx/src/util/mod.rs b/src/dfx/src/util/mod.rs index c55918f2e2..ea891022ef 100644 --- a/src/dfx/src/util/mod.rs +++ b/src/dfx/src/util/mod.rs @@ -3,10 +3,30 @@ use candid::parser::typing::{check_prog, TypeEnv}; use candid::types::{Function, Type}; use candid::{parser::value::IDLValue, IDLArgs, IDLProg}; use ic_agent::Blob; +use net2::{unix::UnixTcpBuilderExt, TcpBuilder}; +use std::net::{IpAddr, SocketAddr}; 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()?) +} + /// Deserialize and print return values from canister method. pub fn print_idl_blob( blob: &Blob,