Skip to content

Commit 8c44b17

Browse files
author
Hans Larsen
committed
feat: dfx start now starts an http server frontend
It will respect address and port from the config (which it did not previously), and will proxy calls to /api/ to the port 8080 locally which is always reserved by the client. Also added tests for (some parts of) the config. Also now waits and blocks the process. When the user Ctrl-C or kill dfx it will kill the nodemanager and the http server.
1 parent d2d103a commit 8c44b17

File tree

7 files changed

+350
-25
lines changed

7 files changed

+350
-25
lines changed

Cargo.lock

Lines changed: 164 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dfx/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ tar = "0.4.26"
1414
flate2 = "1.0.11"
1515

1616
[dependencies]
17+
actix = "0.8.3"
1718
actix-web = "1.0.8"
19+
actix-files = "0.1.4"
1820
clap = "2.33.0"
1921
console = "0.7.7"
2022
flate2 = "1.0.11"

dfx/assets/new_project_files/dfinity.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
"output": "build/"
1313
},
1414
"start": {
15-
"port": 8080,
16-
"address": "127.0.0.1"
15+
"port": 8000,
16+
"address": "127.0.0.1",
17+
"serve_root": "app/"
1718
}
1819
}
1920
}

dfx/src/commands/start.rs

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::lib::api_client::{ping, Client, ClientConfig};
2-
use crate::lib::env::BinaryResolverEnv;
2+
use crate::lib::env::{BinaryResolverEnv, ProjectConfigEnv};
33
use crate::lib::error::{DfxError, DfxResult};
44
use crate::lib::webserver::webserver;
55
use clap::{App, Arg, ArgMatches, SubCommand};
@@ -9,21 +9,22 @@ use tokio::prelude::FutureExt;
99
use tokio::runtime::Runtime;
1010

1111
const TIMEOUT_IN_SECS: u64 = 5;
12+
const IC_CLIENT_BIND_ADDR: &str = "http://localhost:8080/api";
1213

1314
pub fn construct() -> App<'static, 'static> {
1415
SubCommand::with_name("start")
1516
.about("Start a local network in the background.")
1617
.arg(
1718
Arg::with_name("host")
18-
.help("The host (with port) to send the query to.")
19+
.help("The host (with port) to bind the frontend to.")
1920
.long("host")
2021
.takes_value(true),
2122
)
2223
}
2324

2425
pub fn exec<T>(env: &T, args: &ArgMatches<'_>) -> DfxResult
2526
where
26-
T: BinaryResolverEnv,
27+
T: ProjectConfigEnv + BinaryResolverEnv,
2728
{
2829
let b = ProgressBar::new_spinner();
2930
b.set_draw_target(ProgressDrawTarget::stderr());
@@ -33,6 +34,22 @@ where
3334

3435
let client_pathbuf = env.get_binary_command_path("client").unwrap();
3536
let nodemanager_pathbuf = env.get_binary_command_path("nodemanager").unwrap();
37+
38+
let config = env.get_config().unwrap();
39+
let address_and_port = args
40+
.value_of("host")
41+
.and_then(|host| Option::from(host.parse()))
42+
.unwrap_or_else(|| {
43+
Ok(config
44+
.get_config()
45+
.get_defaults()
46+
.get_start()
47+
.get_binding_socket_addr("localhost:8000")
48+
.unwrap())
49+
})?;
50+
let frontend_url = format!("{}:{}", address_and_port.ip(), address_and_port.port());
51+
let project_root = config.get_path().parent().unwrap();
52+
3653
let client_watchdog = std::thread::spawn(move || {
3754
let client = client_pathbuf.as_path();
3855
let nodemanager = nodemanager_pathbuf.as_path();
@@ -44,26 +61,39 @@ where
4461

4562
// If the nodemanager itself fails, we are probably deeper into troubles than
4663
// we can solve at this point and the user is better rerunning the server.
47-
cmd.output().unwrap();
64+
let mut child = cmd.spawn().unwrap();
65+
if child.wait().is_err() {
66+
break;
67+
}
4868
}
4969
});
5070
let frontend_watchdog = webserver(
51-
"localhost:8000",
52-
url::Url::parse("http://localhost:8080/api"),
71+
address_and_port,
72+
url::Url::parse(IC_CLIENT_BIND_ADDR).unwrap(),
73+
project_root
74+
.join(
75+
config
76+
.get_config()
77+
.get_defaults()
78+
.get_start()
79+
.get_serve_root(".")
80+
.as_path(),
81+
)
82+
.as_path(),
5383
);
5484

5585
b.set_message("Pinging the DFINITY client...");
5686

5787
std::thread::sleep(Duration::from_millis(500));
5888

59-
let url = String::from(args.value_of("host").unwrap_or("http://localhost:8080"));
60-
6189
let mut runtime = Runtime::new().expect("Unable to create a runtime");
6290

6391
// Try to ping for 1 second, then timeout after 5 seconds if ping hasn't succeeded.
6492
let start = Instant::now();
6593
while {
66-
let client = Client::new(ClientConfig { url: url.clone() });
94+
let client = Client::new(ClientConfig {
95+
url: frontend_url.clone(),
96+
});
6797

6898
runtime
6999
.block_on(ping(client).timeout(Duration::from_millis(TIMEOUT_IN_SECS * 1000 / 4)))

dfx/src/config/dfinity.rs

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use crate::lib::error::DfxResult;
44
use serde::{Deserialize, Serialize};
55
use serde_json::{Map, Value};
6+
use std::net::{SocketAddr, ToSocketAddrs};
67
use std::path::{Path, PathBuf};
78

89
pub const CONFIG_FILE_NAME: &str = "dfinity.json";
@@ -15,6 +16,7 @@ const EMPTY_CONFIG_DEFAULTS_START: ConfigDefaultsStart = ConfigDefaultsStart {
1516
address: None,
1617
port: None,
1718
nodes: None,
19+
serve_root: None,
1820
};
1921
const EMPTY_CONFIG_DEFAULTS_BUILD: ConfigDefaultsBuild = ConfigDefaultsBuild { output: None };
2022

@@ -28,6 +30,7 @@ pub struct ConfigDefaultsStart {
2830
pub address: Option<String>,
2931
pub nodes: Option<u64>,
3032
pub port: Option<u16>,
33+
pub serve_root: Option<String>,
3134
}
3235

3336
#[derive(Debug, Serialize, Deserialize)]
@@ -61,6 +64,28 @@ impl ConfigDefaultsStart {
6164
.to_owned()
6265
.unwrap_or_else(|| default.to_string())
6366
}
67+
pub fn get_binding_socket_addr(&self, default: &str) -> Option<SocketAddr> {
68+
default
69+
.to_socket_addrs()
70+
.ok()
71+
.and_then(|mut x| x.next())
72+
.and_then(|default_addr| {
73+
let addr = self.get_address(default_addr.ip().to_string().as_str());
74+
let port = self.get_port(default_addr.port());
75+
76+
format!("{}:{}", addr, port)
77+
.to_socket_addrs()
78+
.ok()
79+
.and_then(|mut x| x.next())
80+
})
81+
}
82+
pub fn get_serve_root(&self, default: &str) -> PathBuf {
83+
PathBuf::from(
84+
self.serve_root
85+
.to_owned()
86+
.unwrap_or_else(|| default.to_string()),
87+
)
88+
}
6489
pub fn get_nodes(&self, default: u64) -> u64 {
6590
self.nodes.unwrap_or(default)
6691
}
@@ -136,16 +161,25 @@ impl Config {
136161
))
137162
}
138163

139-
pub fn load_from(working_dir: &PathBuf) -> std::io::Result<Config> {
164+
pub fn from_file(working_dir: &PathBuf) -> std::io::Result<Config> {
140165
let path = Config::resolve_config_path(working_dir)?;
141166
let content = std::fs::read(&path)?;
167+
Config::from_slice(path, &content)
168+
}
169+
170+
pub fn from_current_dir() -> std::io::Result<Config> {
171+
Config::from_file(&std::env::current_dir()?)
172+
}
173+
174+
fn from_slice(path: PathBuf, content: &[u8]) -> std::io::Result<Config> {
142175
let config = serde_json::from_slice(&content)?;
143176
let json = serde_json::from_slice(&content)?;
144177
Ok(Config { path, json, config })
145178
}
146179

147-
pub fn from_current_dir() -> std::io::Result<Config> {
148-
Config::load_from(&std::env::current_dir()?)
180+
/// Create a configuration from a string.
181+
pub fn from_str(content: &str) -> std::io::Result<Config> {
182+
Config::from_slice(PathBuf::from("-"), content.as_bytes())
149183
}
150184

151185
pub fn get_path(&self) -> &PathBuf {
@@ -218,4 +252,77 @@ mod tests {
218252
Config::resolve_config_path(subdir_path.as_path()).unwrap(),
219253
);
220254
}
255+
256+
#[test]
257+
fn config_defaults_start_addr() {
258+
let config = Config::from_str(
259+
r#"{
260+
"defaults": {
261+
"start": {
262+
"address": "localhost",
263+
"port": 8000
264+
}
265+
}
266+
}"#,
267+
)
268+
.unwrap();
269+
270+
assert_eq!(
271+
config
272+
.get_config()
273+
.get_defaults()
274+
.get_start()
275+
.get_binding_socket_addr("1.2.3.4:123")
276+
.unwrap(),
277+
"localhost:8000".to_socket_addrs().unwrap().next().unwrap()
278+
);
279+
}
280+
281+
#[test]
282+
fn config_defaults_start_addr_no_address() {
283+
let config = Config::from_str(
284+
r#"{
285+
"defaults": {
286+
"start": {
287+
"port": 8000
288+
}
289+
}
290+
}"#,
291+
)
292+
.unwrap();
293+
294+
assert_eq!(
295+
config
296+
.get_config()
297+
.get_defaults()
298+
.get_start()
299+
.get_binding_socket_addr("1.2.3.4:123")
300+
.unwrap(),
301+
"1.2.3.4:8000".to_socket_addrs().unwrap().next().unwrap()
302+
);
303+
}
304+
305+
#[test]
306+
fn config_defaults_start_addr_no_port() {
307+
let config = Config::from_str(
308+
r#"{
309+
"defaults": {
310+
"start": {
311+
"address": "localhost"
312+
}
313+
}
314+
}"#,
315+
)
316+
.unwrap();
317+
318+
assert_eq!(
319+
config
320+
.get_config()
321+
.get_defaults()
322+
.get_start()
323+
.get_binding_socket_addr("1.2.3.4:123")
324+
.unwrap(),
325+
"localhost:123".to_socket_addrs().unwrap().next().unwrap()
326+
);
327+
}
221328
}

dfx/src/lib/error.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub enum DfxError {
1717
SerdeJson(serde_json::error::Error),
1818
Url(reqwest::UrlError),
1919
WabtError(wabt::Error),
20+
AddrParseError(std::net::AddrParseError),
2021

2122
/// An unknown command was used. The argument is the command itself.
2223
UnknownCommand(String),
@@ -78,3 +79,9 @@ impl From<wabt::Error> for DfxError {
7879
DfxError::WabtError(err)
7980
}
8081
}
82+
83+
impl From<std::net::AddrParseError> for DfxError {
84+
fn from(err: std::net::AddrParseError) -> DfxError {
85+
DfxError::AddrParseError(err)
86+
}
87+
}

dfx/src/lib/webserver.rs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use actix_web::client::Client;
22
use actix_web::{middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer};
3-
use clap::{value_t, Arg};
43
use futures::Future;
5-
use std::net::ToSocketAddrs;
4+
use std::net::SocketAddr;
5+
use std::path::{Path, PathBuf};
66
use url::Url;
77

88
fn forward(
@@ -40,21 +40,35 @@ fn forward(
4040
})
4141
}
4242

43-
pub fn webserver(bind: &str, client_api_uri: url::Url, serve_dir: Path) -> std::io::Result<()> {
44-
let listen_addr = matches.value_of("listen_addr").unwrap();
45-
let listen_port = value_t!(matches, "listen_port", u16).unwrap_or_else(|e| e.exit());
46-
47-
let forwarded_addr = matches.value_of("forward_addr").unwrap();
48-
let forwarded_port = value_t!(matches, "forward_port", u16).unwrap_or_else(|e| e.exit());
43+
/// Run the webserver in the current thread.
44+
fn run_webserver(
45+
bind: SocketAddr,
46+
client_api_uri: url::Url,
47+
serve_dir: PathBuf,
48+
) -> Result<(), std::io::Error> {
49+
eprintln!("binding to: {:?}", bind);
50+
eprintln!("client: {:?}", client_api_uri);
4951

5052
HttpServer::new(move || {
5153
App::new()
5254
.data(Client::new())
5355
.data(client_api_uri.clone())
5456
.wrap(middleware::Logger::default())
55-
.default_service(web::route().to_async(forward))
57+
.service(web::scope(client_api_uri.path()).default_service(web::to_async(forward)))
58+
.default_service(actix_files::Files::new("/", &serve_dir).index_file("index.html"))
5659
})
57-
.bind((listen_addr, listen_port))?
60+
.bind(bind)?
5861
.system_exit()
59-
.run()
62+
.start();
63+
64+
Ok(())
65+
}
66+
67+
pub fn webserver(
68+
bind: SocketAddr,
69+
client_api_uri: url::Url,
70+
serve_dir: &Path,
71+
) -> std::thread::JoinHandle<()> {
72+
let serve_dir = PathBuf::from(serve_dir);
73+
std::thread::spawn(move || run_webserver(bind, client_api_uri, serve_dir).unwrap())
6074
}

0 commit comments

Comments
 (0)