Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 96 additions & 65 deletions src/dfx/src/commands/replica.rs
Original file line number Diff line number Diff line change
@@ -1,110 +1,142 @@
use crate::commands::canister::create_waiter;
use crate::config::dfinity::ConfigDefaultsReplica;
use crate::lib::environment::Environment;
use crate::lib::error::{DfxError, DfxResult};
use crate::lib::message::UserMessage;
use crate::lib::replica_config::{ReplicaConfig, SchedulerConfig};
use crate::lib::replica_config::{
HttpHandlerConfig, ReplicaConfig, SchedulerConfig, StateManagerConfig,
};

use clap::{App, Arg, ArgMatches, SubCommand};
use crossbeam::channel::{Receiver, Sender};
use crossbeam::unbounded;
use ic_http_agent::{Agent, AgentConfig};
use indicatif::{ProgressBar, ProgressDrawTarget};
use std::default::Default;
use std::io::{Error, ErrorKind};
use std::path::PathBuf;
use std::time::Duration;
use sysinfo::{Pid, Process, ProcessExt, Signal};
use tokio::runtime::Runtime;

/// Provide necessary arguments to start the Internet Computer
/// locally. See `exec` for further information.
/// Constructs a sub-command to run the Internet Computer replica.
pub fn construct() -> App<'static, 'static> {
SubCommand::with_name("replica")
.about(UserMessage::Replica.to_str())
.arg(
Arg::with_name("message-gas-limit")
.help(UserMessage::ReplicaMessageGasLimit.to_str())
.long("message-gas-limit")
.takes_value(true),
)
.arg(
Arg::with_name("port")
.help(UserMessage::ReplicaPort.to_str())
.long("port")
.takes_value(true)
.default_value("8080")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are those removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want command-line argument defaults when considering the dfx configuration file. The strategy is to first check if the configuration option was provided on the command line, and if it wasn't, then check if the configuration option was specified in the dfx configuration file, otherwise resort to the default value. If you use command-line argument defaults, then you short circuit the dfx configuration file.

.validator(|v| {
v.parse::<u16>()
.map_err(|_| "Must pass a valid port number.".to_owned())
.map(|_| ())
}),
)
.arg(
Arg::with_name("message-gas-max")
.help(UserMessage::ReplicaMessageGasMax.to_str())
.long("message-gas-max")
.takes_value(true)
.default_value("5368709120"),
.takes_value(true),
)
.arg(
Arg::with_name("round-gas-max")
.help(UserMessage::ReplicaRoundGasMax.to_str())
.long("round-gas-max")
.takes_value(true)
.default_value("26843545600"),
Arg::with_name("round-gas-limit")
.help(UserMessage::ReplicaRoundGasLimit.to_str())
.long("round-gas-limit")
.takes_value(true),
)
}

/// Gets the configuration options for the Internet Computer replica.
fn get_config(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult<ReplicaConfig> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to avoid that an move to using Claps' proc macros instead. This code makes everything more obscure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let config = get_config_from_file(env);
let port = get_port(&config, args)?;
let mut http_handler: HttpHandlerConfig = Default::default();
if port == 0 {
let file = env.get_temp_dir().join("config").join("port.txt");
http_handler.write_port_to = Some(file);
} else {
http_handler.use_port = Some(port);
};
let message_gas_limit = get_message_gas_limit(&config, args)?;
let round_gas_limit = get_round_gas_limit(&config, args)?;
let scheduler = SchedulerConfig {
exec_gas: Some(message_gas_limit),
round_gas_max: Some(round_gas_limit),
}
.validate()?;
let state_manager = StateManagerConfig {
state_root: env.get_state_dir(),
};
Ok(ReplicaConfig {
http_handler,
scheduler,
state_manager,
})
}

/// Gets the configuration options for the Internet Computer replica as they were specified in the
/// dfx configuration file.
fn get_config_from_file(env: &dyn Environment) -> ConfigDefaultsReplica {
env.get_config().map_or(Default::default(), |config| {
config.get_config().get_defaults().get_replica().to_owned()
})
}

/// Gets the port number that the Internet Computer replica listens on. First checks if the port
/// number was specified on the command-line using --port, otherwise checks if the port number was
/// specified in the dfx configuration file, otherise defaults to 8080.
fn get_port(config: &ConfigDefaultsReplica, args: &ArgMatches<'_>) -> DfxResult<u16> {
args.value_of("port")
.map(|port| port.parse())
.unwrap_or_else(|| {
let default = 8080;
Ok(config.port.unwrap_or(default))
})
.map_err(|err| DfxError::InvalidArgument(format!("Invalid port number: {}", err)))
}

/// Gets the maximum amount of gas a single message can consume. First checks if the gas limit was
/// specified on the command-line using --message-gas-limit, otherwise checks if the gas limit was
/// specified in the dfx configuration file, otherise defaults to 5368709120.
fn get_message_gas_limit(config: &ConfigDefaultsReplica, args: &ArgMatches<'_>) -> DfxResult<u64> {
args.value_of("message-gas-limit")
.map(|limit| limit.parse())
.unwrap_or_else(|| {
let default = 5_368_709_120;
Ok(config.message_gas_limit.unwrap_or(default))
})
.map_err(|err| DfxError::InvalidArgument(format!("Invalid message gas limit: {}", err)))
}

/// Gets the maximum amount of gas a single round can consume. First checks if the gas limit was
/// specified on the command-line using --round-gas-limit, otherwise checks if the gas limit was
/// specified in the dfx configuration file, otherise defaults to 26843545600.
fn get_round_gas_limit(config: &ConfigDefaultsReplica, args: &ArgMatches<'_>) -> DfxResult<u64> {
args.value_of("round-gas-limit")
.map(|limit| limit.parse())
.unwrap_or_else(|| {
let default = 26_843_545_600;
Ok(config.round_gas_limit.unwrap_or(default))
})
.map_err(|err| DfxError::InvalidArgument(format!("Invalid round gas limit: {}", err)))
}

fn ping_and_wait(frontend_url: &str) -> DfxResult {
let mut runtime = Runtime::new().expect("Unable to create a runtime");

let agent = Agent::new(AgentConfig {
url: frontend_url,
..AgentConfig::default()
})?;

runtime
.block_on(agent.ping(create_waiter()))
.map_err(DfxError::from)
}

fn get_scheduler(args: &ArgMatches<'_>) -> DfxResult<SchedulerConfig> {
// Get mssage gas limit.
let message_gas_max = args
.value_of("message-gas-max")
.expect("default value")
.parse()
.map_err(|err| DfxError::InvalidArgument(format!("Invalid message gas limit: {}", err)))?;
// Get round gas limit.
let round_gas_max = args
.value_of("round-gas-max")
.expect("default value")
.parse()
.map_err(|err| DfxError::InvalidArgument(format!("Invalid round gas limit: {}", err)))?;
// Check message and round gas limits.
if message_gas_max >= round_gas_max {
let err = "Round gas limit must exceed message gas limit.".to_string();
Err(DfxError::InvalidArgument(err))
} else {
// Return scheduler configuration.
Ok(SchedulerConfig {
exec_gas: Some(message_gas_max),
round_gas_max: Some(round_gas_max),
})
}
}

// 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
/// replica at the moment) and the proxy.
pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult {
let replica_binary_path = env.get_cache().get_binary_command_path("replica")?;
let temp_dir = env.get_temp_dir();
let state_root = env.get_state_dir();
let pid_file_path = temp_dir.join("pid");

let port = args
.value_of("port")
.unwrap_or("8080")
.parse::<u16>()
.expect("Unreachable. Port should have been validated by clap.");

let scheduler = get_scheduler(args)?;

// We are doing this here to make sure we can write to the temp
// pid file.
std::fs::write(&pid_file_path, "")?;
Expand All @@ -121,10 +153,9 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult {
let (_broadcast_stop, is_killed_replica) = unbounded();

b.set_message("Generating IC local replica configuration.");
let replica_config = ReplicaConfig::new(&state_root)
.with_port(port)
.with_scheduler(scheduler)
.to_toml()?;
let config = get_config(env, args)?;
let port = config.http_handler.use_port.expect("non-random port");
let toml = config.to_toml()?;

// TODO(eftychis): we need a proper manager type when we start
// spawning multiple replica processes and registry.
Expand All @@ -137,7 +168,7 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult {
&pid_file_path,
is_killed_replica,
request_stop,
replica_config,
toml,
b,
)
}
Expand Down
39 changes: 30 additions & 9 deletions src/dfx/src/config/dfinity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub const CONFIG_FILE_NAME: &str = "dfx.json";
const EMPTY_CONFIG_DEFAULTS: ConfigDefaults = ConfigDefaults {
bootstrap: None,
build: None,
replica: None,
start: None,
};

Expand All @@ -24,16 +25,22 @@ const EMPTY_CONFIG_DEFAULTS_BOOTSTRAP: ConfigDefaultsBootstrap = ConfigDefaultsB
timeout: None,
};

const EMPTY_CONFIG_DEFAULTS_BUILD: ConfigDefaultsBuild = ConfigDefaultsBuild { output: None };

const EMPTY_CONFIG_DEFAULTS_REPLICA: ConfigDefaultsReplica = ConfigDefaultsReplica {
message_gas_limit: None,
port: None,
round_gas_limit: None,
};

const EMPTY_CONFIG_DEFAULTS_START: ConfigDefaultsStart = ConfigDefaultsStart {
address: None,
port: None,
nodes: None,
serve_root: None,
};

const EMPTY_CONFIG_DEFAULTS_BUILD: ConfigDefaultsBuild = ConfigDefaultsBuild { output: None };

#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ConfigCanistersCanister {
pub main: Option<String>,
pub frontend: Option<Value>,
Expand All @@ -48,19 +55,26 @@ pub struct ConfigDefaultsBootstrap {
pub timeout: Option<u64>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ConfigDefaultsBuild {
pub output: Option<String>,
}

#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ConfigDefaultsReplica {
pub message_gas_limit: Option<u64>,
pub port: Option<u16>,
pub round_gas_limit: Option<u64>,
}

#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ConfigDefaultsStart {
pub address: Option<String>,
pub nodes: Option<u64>,
pub port: Option<u16>,
pub serve_root: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ConfigDefaultsBuild {
pub output: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Profile {
// debug is for development only
Expand All @@ -73,6 +87,7 @@ pub enum Profile {
pub struct ConfigDefaults {
pub bootstrap: Option<ConfigDefaultsBootstrap>,
pub build: Option<ConfigDefaultsBuild>,
pub replica: Option<ConfigDefaultsReplica>,
pub start: Option<ConfigDefaultsStart>,
}

Expand Down Expand Up @@ -154,6 +169,12 @@ impl ConfigDefaults {
None => &EMPTY_CONFIG_DEFAULTS_BUILD,
}
}
pub fn get_replica(&self) -> &ConfigDefaultsReplica {
match &self.replica {
Some(x) => &x,
None => &EMPTY_CONFIG_DEFAULTS_REPLICA,
}
}
pub fn get_start(&self) -> &ConfigDefaultsStart {
match &self.start {
Some(x) => &x,
Expand Down
4 changes: 2 additions & 2 deletions src/dfx/src/lib/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ user_message!(

// dfx replica
Replica => "Start a local replica.",
ReplicaMessageGasMax => "Maximum amount of gas a single message can consume.",
ReplicaMessageGasLimit => "Maximum amount of gas a single message can consume.",
ReplicaPort => "The port the local replica should listen to.",
ReplicaRoundGasMax => "Maximum amount of gas a single round can consume.",
ReplicaRoundGasLimit => "Maximum amount of gas a single round can consume.",

// dfx start
CleanState => "Cleans state of current project.",
Expand Down
28 changes: 18 additions & 10 deletions src/dfx/src/lib/replica_config.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use crate::lib::error::{DfxError, DfxResult};

use serde::Serialize;
use serde::{Deserialize, Serialize};
use std::default::Default;
use std::path::{Path, PathBuf};

#[derive(Debug, Serialize)]
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct HttpHandlerConfig {
/// Instructs the HTTP handler to use the specified port
pub use_port: Option<u16>,
Expand All @@ -15,18 +16,29 @@ pub struct HttpHandlerConfig {
pub write_port_to: Option<PathBuf>,
}

#[derive(Debug, Serialize)]
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct SchedulerConfig {
pub exec_gas: Option<u64>,
pub round_gas_max: Option<u64>,
}

#[derive(Debug, Serialize)]
impl SchedulerConfig {
pub fn validate(self) -> DfxResult<Self> {
if self.exec_gas >= self.round_gas_max {
let message = "Round gas limit must exceed message gas limit.";
Err(DfxError::InvalidData(message.to_string()))
} else {
Ok(self)
}
}
}

#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct StateManagerConfig {
pub state_root: PathBuf,
}

#[derive(Debug, Serialize)]
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ReplicaConfig {
pub http_handler: HttpHandlerConfig,
pub scheduler: SchedulerConfig,
Expand All @@ -50,6 +62,7 @@ impl ReplicaConfig {
}
}

#[allow(dead_code)]
pub fn with_port(&mut self, port: u16) -> &mut Self {
self.http_handler.use_port = Some(port);
self.http_handler.write_port_to = None;
Expand All @@ -62,11 +75,6 @@ impl ReplicaConfig {
self
}

pub fn with_scheduler(&mut self, scheduler: SchedulerConfig) -> &mut Self {
self.scheduler = scheduler;
self
}

pub fn to_toml(&self) -> DfxResult<String> {
toml::to_string(&self).map_err(DfxError::CouldNotSerializeClientConfiguration)
}
Expand Down