Node Health warnings (#5951)

* Health endpoint.

* Asynchronous health endpoint.

* Configure time api URL via CLI.

* Tests for TimeChecker.

* Health indication on Status page.

* Adding status indication to tab titles.

* Add status to ParityBar.

* Fixing lints.

* Add health status on SyncWarning.

* Fix health URL for embed.

* Nicer messages.

* Fix tests.

* Fixing JS tests.

* NTP time sync (#5956)

* use NTP to check time drift

* update time module documentation

* replace time_api flag with ntp_server

* fix TimeChecker tests

* fix ntp-server flag usage

* hide status tooltip if there's no message to show

* remove TimeProvider trait

* use Cell in FakeNtp test trait

* share fetch client and ntp client cpu pool

* Add documentation to public method.

* Removing peer count from status.

* Remove unknown upgrade status.

* Send two time requests at the time.

* Revert "Send two time requests at the time."

This reverts commit f7b754b1155076a5a5d8fdafa022801fae324452.

* Defer reporting time synchronization issues.

* Fix tests.

* Fix linting.
This commit is contained in:
Tomasz Drwięga 2017-07-11 12:23:46 +02:00 committed by Gav Wood
parent 7fb46bff06
commit 4936e99f30
48 changed files with 1296 additions and 125 deletions

88
Cargo.lock generated
View File

@ -67,6 +67,29 @@ dependencies = [
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "backtrace"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"backtrace-sys 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
"cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"dbghelp-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc-demangle 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "backtrace-sys"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"gcc 0.3.51 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "base-x"
version = "0.2.2"
@ -159,6 +182,11 @@ dependencies = [
"rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "byteorder"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "byteorder"
version = "1.0.0"
@ -225,6 +253,14 @@ dependencies = [
"unicode-normalization 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "conv"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"custom_derive 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "cookie"
version = "0.3.1"
@ -275,6 +311,11 @@ dependencies = [
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "custom_derive"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "daemonize"
version = "0.2.2"
@ -283,6 +324,15 @@ dependencies = [
"libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "dbghelp-sys"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "deque"
version = "0.3.1"
@ -335,6 +385,14 @@ dependencies = [
"regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "error-chain"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"backtrace 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "eth-secp256k1"
version = "0.5.6"
@ -1467,6 +1525,19 @@ name = "nom"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "ntp"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"byteorder 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
"conv 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"custom_derive 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
"error-chain 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "num"
version = "0.1.32"
@ -1627,6 +1698,7 @@ dependencies = [
"ethsync 1.7.0",
"fdlimit 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
"futures-cpupool 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"isatty 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonrpc-core 7.0.0 (git+https://github.com/paritytech/jsonrpc.git?branch=parity-1.7)",
"log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1669,12 +1741,14 @@ dependencies = [
"ethcore-util 1.7.0",
"fetch 0.1.0",
"futures 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
"futures-cpupool 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonrpc-core 7.0.0 (git+https://github.com/paritytech/jsonrpc.git?branch=parity-1.7)",
"jsonrpc-http-server 7.0.0 (git+https://github.com/paritytech/jsonrpc.git?branch=parity-1.7)",
"linked-hash-map 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
"mime 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"mime_guess 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
"ntp 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"parity-dapps-glue 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"parity-hash-fetch 1.7.0",
"parity-reactor 0.1.0",
@ -2239,6 +2313,11 @@ dependencies = [
"time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rustc-demangle"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "rustc-hex"
version = "1.0.0"
@ -2961,6 +3040,8 @@ dependencies = [
"checksum arrayvec 0.3.20 (registry+https://github.com/rust-lang/crates.io-index)" = "d89f1b0e242270b5b797778af0c8d182a1a2ccac5d8d6fadf414223cc0fab096"
"checksum aster 0.41.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4ccfdf7355d9db158df68f976ed030ab0f6578af811f5a7bb6dcf221ec24e0e0"
"checksum atty 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d912da0db7fa85514874458ca3651fe2cddace8d0b0505571dbdcd41ab490159"
"checksum backtrace 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "346d7644f0b5f9bc73082d3b2236b69a05fd35cce0cfa3724e184e6a5c9e2a2f"
"checksum backtrace-sys 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "3a0d842ea781ce92be2bf78a9b38883948542749640b8378b3b2f03d1fd9f1ff"
"checksum base-x 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2f59103b47307f76e03bef1633aec7fa9e29bfb5aa6daf5a334f94233c71f6c1"
"checksum base32 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1b9605ba46d61df0410d8ac686b0007add8172eba90e8e909c347856fe794d8c"
"checksum bigint 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d0673c930652d3d4d6dcd5c45b5db4fa5f8f33994d7323618c43c083b223e8c"
@ -2975,6 +3056,7 @@ dependencies = [
"checksum blastfig 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "09640e0509d97d5cdff03a9f5daf087a8e04c735c3b113a75139634a19cfc7b2"
"checksum bloomchain 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3f421095d2a76fc24cd3fb3f912b90df06be7689912b1bdb423caefae59c258d"
"checksum bn 0.4.4 (git+https://github.com/paritytech/bn)" = "<none>"
"checksum byteorder 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0fc10e8cc6b2580fda3f36eb6dc5316657f812a3df879a44a66fc9f0fdbc4855"
"checksum byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c40977b0ee6b9885c9013cd41d9feffdd22deb3bb4dc3a71d901cc7a77de18c8"
"checksum bytes 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8b24f16593f445422331a5eed46b72f7f171f910fead4f2ea8f17e727e9c5c14"
"checksum cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de1e760d7b6535af4241fca8bd8adf68e2e7edacc6b29f5d399050c5e48cf88c"
@ -2982,13 +3064,16 @@ dependencies = [
"checksum clap 2.24.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6b8f69e518f967224e628896b54e41ff6acfb4dcfefc5076325c36525dac900f"
"checksum clippy 0.0.103 (registry+https://github.com/rust-lang/crates.io-index)" = "5b4fabf979ddf6419a313c1c0ada4a5b95cfd2049c56e8418d622d27b4b6ff32"
"checksum clippy_lints 0.0.103 (registry+https://github.com/rust-lang/crates.io-index)" = "ce96ec05bfe018a0d5d43da115e54850ea2217981ff0f2e462780ab9d594651a"
"checksum conv 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "78ff10625fd0ac447827aa30ea8b861fead473bb60aeb73af6c1c58caf0d1299"
"checksum cookie 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d53b80dde876f47f03cda35303e368a79b91c70b0d65ecba5fd5280944a08591"
"checksum core-foundation 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "20a6d0448d3a99d977ae4a2aa5a98d886a923e863e81ad9ff814645b6feb3bbd"
"checksum core-foundation-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "05eed248dc504a5391c63794fe4fb64f46f071280afaa1b73308f3c0ce4574c5"
"checksum crossbeam 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "0c5ea215664ca264da8a9d9c3be80d2eaf30923c259d03e870388eb927508f97"
"checksum crypt32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e34988f7e069e0b2f3bfc064295161e489b2d4e04a2e4248fb94360cdf00b4ec"
"checksum ctrlc 1.1.1 (git+https://github.com/paritytech/rust-ctrlc.git)" = "<none>"
"checksum custom_derive 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "ef8ae57c4978a2acd8b869ce6b9ca1dfe817bff704c220209fdef2c0b75a01b9"
"checksum daemonize 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "271ec51b7e0bee92f0d04601422c73eb76ececf197026711c97ad25038a010cf"
"checksum dbghelp-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "97590ba53bcb8ac28279161ca943a924d1fd4a8fb3fa63302591647c4fc5b850"
"checksum deque 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1614659040e711785ed8ea24219140654da1729f3ec8a47a9719d041112fe7bf"
"checksum difference 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3304d19798a8e067e48d8e69b2c37f0b5e9b4e462504ad9e27e9f3fce02bba8"
"checksum docopt 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3b5b93718f8b3e5544fcc914c43de828ca6c6ace23e0332c6080a2977b49787a"
@ -2996,6 +3081,7 @@ dependencies = [
"checksum either 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3d2b503c86dad62aaf414ecf2b8c527439abedb3f8d812537f0b12bfd6f32a91"
"checksum elastic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "258ff6a9a94f648d0379dbd79110e057edbb53eb85cc237e33eadf8e5a30df85"
"checksum env_logger 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e3856f1697098606fc6cb97a93de88ca3f3bc35bb878c725920e6e82ecf05e83"
"checksum error-chain 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bd5c82c815138e278b8dcdeffc49f27ea6ffb528403e9dea4194f2e3dd40b143"
"checksum eth-secp256k1 0.5.6 (git+https://github.com/paritytech/rust-secp256k1)" = "<none>"
"checksum ethabi 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0c3d62319ee0f35abf20afe8859dd2668195912614346447bb2dee9fb8da7c62"
"checksum fdlimit 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b1ee15a7050e5580b3712877157068ea713b245b080ff302ae2ca973cfcd9baa"
@ -3066,6 +3152,7 @@ dependencies = [
"checksum net2 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)" = "bc01404e7568680f1259aa5729539f221cb1e6d047a0d9053cab4be8a73b5d67"
"checksum nodrop 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "52cd74cd09beba596430cc6e3091b74007169a56246e1262f0ba451ea95117b2"
"checksum nom 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6caab12c5f97aa316cb249725aa32115118e1522b445e26c257dd77cad5ffd4e"
"checksum ntp 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d23f30ae7da76e2c6c2f5de53f298aa9a3911d3955ab2c349eb944caedceb088"
"checksum num 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "c04bd954dbf96f76bab6e5bd6cef6f1ce1262d15268ce4f926d2b5b778fa7af2"
"checksum num-bigint 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "41655c8d667be847a0b72fe0888857a7b3f052f691cf40852be5fcf87b274a65"
"checksum num-complex 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "ccac67baf893ac97474f8d70eff7761dabb1f6c66e71f8f1c67a6859218db810"
@ -3121,6 +3208,7 @@ dependencies = [
"checksum rpassword 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5d3a99497c5c544e629cc8b359ae5ede321eba5fa8e5a8078f3ced727a976c3f"
"checksum rpassword 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ab6e42be826e215f30ff830904f8f4a0933c6e2ae890e1af8b408f5bae60081e"
"checksum rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)" = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a"
"checksum rustc-demangle 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3058a43ada2c2d0b92b3ae38007a2d0fa5e9db971be260e0171408a4ff471c95"
"checksum rustc-hex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0ceb8ce7a5e520de349e1fa172baeba4a9e8d5ef06c47471863530bc4972ee1e"
"checksum rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)" = "6159e4e6e559c81bd706afe9c8fd68f547d3e851ce12e76b1de7914bab61691b"
"checksum rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c5f5376ea5e30ce23c03eb77cbe4962b988deead10910c372b226388b594c084"

View File

@ -25,6 +25,7 @@ serde_json = "1.0"
serde_derive = "1.0"
app_dirs = "1.1.1"
futures = "0.1"
futures-cpupool = "0.1"
fdlimit = "0.1"
ws2_32-sys = "0.2"
ctrlc = { git = "https://github.com/paritytech/rust-ctrlc.git" }

View File

@ -11,11 +11,13 @@ authors = ["Parity Technologies <admin@parity.io>"]
base32 = "0.3"
env_logger = "0.4"
futures = "0.1"
futures-cpupool = "0.1"
linked-hash-map = "0.3"
log = "0.3"
parity-dapps-glue = "1.7"
mime = "0.2"
mime_guess = "1.6.1"
ntp = "0.2.0"
rand = "0.3"
rustc-hex = "1.0"
serde = "1.0"

View File

@ -18,23 +18,36 @@ use std::sync::Arc;
use hyper::{server, net, Decoder, Encoder, Next, Control};
use hyper::method::Method;
use hyper::status::StatusCode;
use api::types::ApiError;
use api::response;
use api::{response, types};
use api::time::TimeChecker;
use apps::fetcher::Fetcher;
use handlers::extract_url;
use handlers::{self, extract_url};
use endpoint::{Endpoint, Handler, EndpointPath};
use parity_reactor::Remote;
use {SyncStatus};
#[derive(Clone)]
pub struct RestApi {
fetcher: Arc<Fetcher>,
sync_status: Arc<SyncStatus>,
time: TimeChecker,
remote: Remote,
}
impl RestApi {
pub fn new(fetcher: Arc<Fetcher>) -> Box<Endpoint> {
pub fn new(
fetcher: Arc<Fetcher>,
sync_status: Arc<SyncStatus>,
time: TimeChecker,
remote: Remote,
) -> Box<Endpoint> {
Box::new(RestApi {
fetcher: fetcher,
fetcher,
sync_status,
time,
remote,
})
}
}
@ -58,11 +71,11 @@ impl RestApiRouter {
path: Some(path),
control: Some(control),
api: api,
handler: response::as_json_error(&ApiError {
handler: Box::new(response::as_json_error(StatusCode::NotFound, &types::ApiError {
code: "404".into(),
title: "Not Found".into(),
detail: "Resource you requested has not been found.".into(),
}),
})),
}
}
@ -75,6 +88,78 @@ impl RestApiRouter {
_ => None
}
}
fn health(&self, control: Control) -> Box<Handler> {
use self::types::{HealthInfo, HealthStatus, Health};
trace!(target: "dapps", "Checking node health.");
// Check timediff
let sync_status = self.api.sync_status.clone();
let map = move |time| {
// Check peers
let peers = {
let (connected, max) = sync_status.peers();
let (status, message) = match connected {
0 => {
(HealthStatus::Bad, "You are not connected to any peers. There is most likely some network issue. Fix connectivity.".into())
},
1 => (HealthStatus::NeedsAttention, "You are connected to only one peer. Your node might not be reliable. Check your network connection.".into()),
_ => (HealthStatus::Ok, "".into()),
};
HealthInfo { status, message, details: (connected, max) }
};
// Check sync
let sync = {
let is_syncing = sync_status.is_major_importing();
let (status, message) = if is_syncing {
(HealthStatus::NeedsAttention, "Your node is still syncing, the values you see might be outdated. Wait until it's fully synced.".into())
} else {
(HealthStatus::Ok, "".into())
};
HealthInfo { status, message, details: is_syncing }
};
// Check time
let time = {
const MAX_DRIFT: i64 = 500;
let (status, message, details) = match time {
Ok(Ok(diff)) if diff < MAX_DRIFT && diff > -MAX_DRIFT => {
(HealthStatus::Ok, "".into(), diff)
},
Ok(Ok(diff)) => {
(HealthStatus::Bad, format!(
"Your clock is not in sync. Detected difference is too big for the protocol to work: {}ms. Synchronize your clock.",
diff,
), diff)
},
Ok(Err(err)) => {
(HealthStatus::NeedsAttention, format!(
"Unable to reach time API: {}. Make sure that your clock is synchronized.",
err,
), 0)
},
Err(_) => {
(HealthStatus::NeedsAttention, "Time API request timed out. Make sure that the clock is synchronized.".into(), 0)
},
};
HealthInfo { status, message, details, }
};
let status = if [&peers.status, &sync.status, &time.status].iter().any(|x| *x != &HealthStatus::Ok) {
StatusCode::PreconditionFailed // HTTP 412
} else {
StatusCode::Ok // HTTP 200
};
response::as_json(status, &Health { peers, sync, time })
};
let time = self.api.time.time_drift();
let remote = self.api.remote.clone();
Box::new(handlers::AsyncHandler::new(time, map, remote, control))
}
}
impl server::Handler<net::HttpStream> for RestApiRouter {
@ -103,6 +188,7 @@ impl server::Handler<net::HttpStream> for RestApiRouter {
let handler = endpoint.and_then(|v| match v {
"ping" => Some(response::ping()),
"health" => Some(self.health(control)),
"content" => self.resolve_content(hash, path, control),
_ => None
});

View File

@ -18,6 +18,8 @@
mod api;
mod response;
mod time;
mod types;
pub use self::api::RestApi;
pub use self::time::TimeChecker;

View File

@ -16,6 +16,8 @@
use serde::Serialize;
use serde_json;
use hyper::status::StatusCode;
use endpoint::Handler;
use handlers::{ContentHandler, EchoHandler};
@ -23,10 +25,16 @@ pub fn empty() -> Box<Handler> {
Box::new(ContentHandler::ok("".into(), mime!(Text/Plain)))
}
pub fn as_json_error<T: Serialize>(val: &T) -> Box<Handler> {
pub fn as_json<T: Serialize>(status: StatusCode, val: &T) -> ContentHandler {
let json = serde_json::to_string(val)
.expect("serialization to string is infallible; qed");
Box::new(ContentHandler::not_found(json, mime!(Application/Json)))
ContentHandler::new(status, json, mime!(Application/Json))
}
pub fn as_json_error<T: Serialize>(status: StatusCode, val: &T) -> ContentHandler {
let json = serde_json::to_string(val)
.expect("serialization to string is infallible; qed");
ContentHandler::new(status, json, mime!(Application/Json))
}
pub fn ping() -> Box<Handler> {

264
dapps/src/api/time.rs Normal file
View File

@ -0,0 +1,264 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
//! Periodically checks node's time drift using [SNTP](https://tools.ietf.org/html/rfc1769).
//!
//! An NTP packet is sent to the server with a local timestamp, the server then completes the packet, yielding the
//! following timestamps:
//!
//! Timestamp Name ID When Generated
//! ------------------------------------------------------------
//! Originate Timestamp T1 time request sent by client
//! Receive Timestamp T2 time request received at server
//! Transmit Timestamp T3 time reply sent by server
//! Destination Timestamp T4 time reply received at client
//!
//! The drift is defined as:
//!
//! drift = ((T2 - T1) + (T3 - T4)) / 2.
//!
use std::io;
use std::{fmt, mem, time};
use std::collections::VecDeque;
use futures::{self, Future, BoxFuture};
use futures_cpupool::CpuPool;
use ntp;
use time::{Duration, Timespec};
use util::{Arc, RwLock};
/// Time checker error.
#[derive(Debug, Clone, PartialEq)]
pub enum Error {
/// There was an error when trying to reach the NTP server.
Ntp(String),
/// IO error when reading NTP response.
Io(String),
}
impl fmt::Display for Error {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
use self::Error::*;
match *self {
Ntp(ref err) => write!(fmt, "NTP error: {}", err),
Io(ref err) => write!(fmt, "Connection Error: {}", err),
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Self { Error::Io(format!("{}", err)) }
}
impl From<ntp::errors::Error> for Error {
fn from(err: ntp::errors::Error) -> Self { Error::Ntp(format!("{}", err)) }
}
/// NTP time drift checker.
pub trait Ntp {
/// Returns the current time drift.
fn drift(&self) -> BoxFuture<Duration, Error>;
}
/// NTP client using the SNTP algorithm for calculating drift.
#[derive(Clone)]
pub struct SimpleNtp {
address: Arc<String>,
pool: CpuPool,
}
impl fmt::Debug for SimpleNtp {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Ntp {{ address: {} }}", self.address)
}
}
impl SimpleNtp {
fn new(address: &str, pool: CpuPool) -> SimpleNtp {
SimpleNtp {
address: Arc::new(address.to_owned()),
pool: pool,
}
}
}
impl Ntp for SimpleNtp {
fn drift(&self) -> BoxFuture<Duration, Error> {
let address = self.address.clone();
self.pool.spawn_fn(move || {
let packet = ntp::request(&*address)?;
let dest_time = ::time::now_utc().to_timespec();
let orig_time = Timespec::from(packet.orig_time);
let recv_time = Timespec::from(packet.recv_time);
let transmit_time = Timespec::from(packet.transmit_time);
let drift = ((recv_time - orig_time) + (transmit_time - dest_time)) / 2;
Ok(drift)
}).boxed()
}
}
const MAX_RESULTS: usize = 4;
const UPDATE_TIMEOUT_OK_SECS: u64 = 30;
const UPDATE_TIMEOUT_ERR_SECS: u64 = 2;
#[derive(Debug, Clone)]
/// A time checker.
pub struct TimeChecker<N: Ntp = SimpleNtp> {
ntp: N,
last_result: Arc<RwLock<(time::Instant, VecDeque<Result<i64, Error>>)>>,
}
impl TimeChecker<SimpleNtp> {
/// Creates new time checker given the NTP server address.
pub fn new(ntp_address: String, pool: CpuPool) -> Self {
let last_result = Arc::new(RwLock::new(
// Assume everything is ok at the very beginning.
(time::Instant::now(), vec![Ok(0)].into())
));
let ntp = SimpleNtp::new(&ntp_address, pool);
TimeChecker {
ntp,
last_result,
}
}
}
impl<N: Ntp> TimeChecker<N> {
/// Updates the time
pub fn update(&self) -> BoxFuture<i64, Error> {
let last_result = self.last_result.clone();
self.ntp.drift().then(move |res| {
let mut results = mem::replace(&mut last_result.write().1, VecDeque::new());
let valid_till = time::Instant::now() + time::Duration::from_secs(
if res.is_ok() && results.len() == MAX_RESULTS {
UPDATE_TIMEOUT_OK_SECS
} else {
UPDATE_TIMEOUT_ERR_SECS
}
);
// Push the result.
results.push_back(res.map(|d| d.num_milliseconds()));
while results.len() > MAX_RESULTS {
results.pop_front();
}
// Select a response and update last result.
let res = select_result(results.iter());
*last_result.write() = (valid_till, results);
res
}).boxed()
}
/// Returns a current time drift or error if last request to NTP server failed.
pub fn time_drift(&self) -> BoxFuture<i64, Error> {
// return cached result
{
let res = self.last_result.read();
if res.0 > time::Instant::now() {
return futures::done(select_result(res.1.iter())).boxed();
}
}
// or update and return result
self.update()
}
}
fn select_result<'a, T: Iterator<Item=&'a Result<i64, Error>>>(results: T) -> Result<i64, Error> {
let mut min = None;
for res in results {
min = Some(match (min.take(), res) {
(Some(Ok(min)), &Ok(ref new)) => Ok(::std::cmp::min(min, *new)),
(Some(Ok(old)), &Err(_)) => Ok(old),
(_, ref new) => (*new).clone(),
})
}
min.unwrap_or_else(|| Err(Error::Ntp("NTP server unavailable.".into())))
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::cell::{Cell, RefCell};
use std::time::Instant;
use time::Duration;
use futures::{self, BoxFuture, Future};
use super::{Ntp, TimeChecker, Error};
use util::RwLock;
#[derive(Clone)]
struct FakeNtp(RefCell<Vec<Duration>>, Cell<u64>);
impl FakeNtp {
fn new() -> FakeNtp {
FakeNtp(
RefCell::new(vec![Duration::milliseconds(150)]),
Cell::new(0))
}
}
impl Ntp for FakeNtp {
fn drift(&self) -> BoxFuture<Duration, Error> {
self.1.set(self.1.get() + 1);
futures::future::ok(self.0.borrow_mut().pop().expect("Unexpected call to drift().")).boxed()
}
}
fn time_checker() -> TimeChecker<FakeNtp> {
let last_result = Arc::new(RwLock::new(
(Instant::now(), vec![Err(Error::Ntp("NTP server unavailable.".into()))].into())
));
TimeChecker {
ntp: FakeNtp::new(),
last_result: last_result,
}
}
#[test]
fn should_fetch_time_on_start() {
// given
let time = time_checker();
// when
let diff = time.time_drift().wait().unwrap();
// then
assert_eq!(diff, 150);
assert_eq!(time.ntp.1.get(), 1);
}
#[test]
fn should_not_fetch_twice_if_timeout_has_not_passed() {
// given
let time = time_checker();
// when
let diff1 = time.time_drift().wait().unwrap();
let diff2 = time.time_drift().wait().unwrap();
// then
assert_eq!(diff1, 150);
assert_eq!(diff2, 150);
assert_eq!(time.ntp.1.get(), 1);
}
}

View File

@ -14,11 +14,54 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
/// A structure representing any error in REST API.
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ApiError {
/// Error code.
pub code: String,
/// Human-readable error summary.
pub title: String,
/// More technical error details.
pub detail: String,
}
/// Health API endpoint status.
#[derive(Debug, PartialEq, Serialize)]
pub enum HealthStatus {
/// Everything's OK.
#[serde(rename = "ok")]
Ok,
/// Node health need attention
/// (the issue is not critical, but may need investigation)
#[serde(rename = "needsAttention")]
NeedsAttention,
/// There is something bad detected with the node.
#[serde(rename = "bad")]
Bad
}
/// Represents a single check in node health.
/// Cointains the status of that check and apropriate message and details.
#[derive(Debug, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct HealthInfo<T> {
/// Check status.
pub status: HealthStatus,
/// Human-readable message.
pub message: String,
/// Technical details of the check.
pub details: T,
}
/// Node Health status.
#[derive(Debug, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Health {
/// Status of peers.
pub peers: HealthInfo<(usize, usize)>,
/// Sync status.
pub sync: HealthInfo<bool>,
/// Time diff info.
pub time: HealthInfo<i64>,
}

View File

@ -271,6 +271,7 @@ mod tests {
use endpoint::EndpointInfo;
use page::LocalPageEndpoint;
use super::{ContentFetcher, Fetcher};
use {SyncStatus};
#[derive(Clone)]
struct FakeResolver;
@ -280,11 +281,17 @@ mod tests {
}
}
struct FakeSync(bool);
impl SyncStatus for FakeSync {
fn is_major_importing(&self) -> bool { self.0 }
fn peers(&self) -> (usize, usize) { (0, 5) }
}
#[test]
fn should_true_if_contains_the_app() {
// given
let path = env::temp_dir();
let fetcher = ContentFetcher::new(FakeResolver, Arc::new(|| false), Remote::new_sync(), Client::new().unwrap())
let fetcher = ContentFetcher::new(FakeResolver, Arc::new(FakeSync(false)), Remote::new_sync(), Client::new().unwrap())
.allow_dapps(true);
let handler = LocalPageEndpoint::new(path, EndpointInfo {
name: "fake".into(),

112
dapps/src/handlers/async.rs Normal file
View File

@ -0,0 +1,112 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
//! Async Content Handler
//! Temporary solution until we switch to future-based server.
//! Wraps a future and converts it to hyper::server::Handler;
use std::{mem, time};
use std::sync::mpsc;
use futures::Future;
use hyper::{server, Decoder, Encoder, Next, Control};
use hyper::net::HttpStream;
use handlers::ContentHandler;
use parity_reactor::Remote;
const TIMEOUT_SECS: u64 = 15;
enum State<F, T, M> {
Initial(F, M, Remote, Control),
Waiting(mpsc::Receiver<Result<T, ()>>, M),
Done(ContentHandler),
Invalid,
}
pub struct AsyncHandler<F, T, M> {
state: State<F, T, M>,
}
impl<F, T, M> AsyncHandler<F, T, M> {
pub fn new(future: F, map: M, remote: Remote, control: Control) -> Self {
AsyncHandler {
state: State::Initial(future, map, remote, control),
}
}
}
impl<F, T, E, M> server::Handler<HttpStream> for AsyncHandler<F, Result<T, E>, M> where
F: Future<Item=T, Error=E> + Send + 'static,
M: FnOnce(Result<Result<T, E>, ()>) -> ContentHandler,
T: Send + 'static,
E: Send + 'static,
{
fn on_request(&mut self, _request: server::Request<HttpStream>) -> Next {
if let State::Initial(future, map, remote, control) = mem::replace(&mut self.state, State::Invalid) {
let (tx, rx) = mpsc::sync_channel(1);
let control2 = control.clone();
let tx2 = tx.clone();
remote.spawn_with_timeout(move || future.then(move |result| {
// Send a result (ignore errors if the connection was dropped)
let _ = tx.send(Ok(result));
// Resume handler
let _ = control.ready(Next::read());
Ok(())
}), time::Duration::from_secs(TIMEOUT_SECS), move || {
// Notify about error
let _ = tx2.send(Err(()));
// Resume handler
let _ = control2.ready(Next::read());
});
self.state = State::Waiting(rx, map);
}
Next::wait()
}
fn on_request_readable(&mut self, _decoder: &mut Decoder<HttpStream>) -> Next {
if let State::Waiting(rx, map) = mem::replace(&mut self.state, State::Invalid) {
match rx.try_recv() {
Ok(result) => {
self.state = State::Done(map(result));
},
Err(err) => {
warn!("Resuming handler in incorrect state: {:?}", err);
}
}
}
Next::write()
}
fn on_response(&mut self, res: &mut server::Response) -> Next {
if let State::Done(ref mut handler) = self.state {
handler.on_response(res)
} else {
Next::end()
}
}
fn on_response_writable(&mut self, encoder: &mut Encoder<HttpStream>) -> Next {
if let State::Done(ref mut handler) = self.state {
handler.on_response_writable(encoder)
} else {
Next::end()
}
}
}

View File

@ -39,10 +39,6 @@ impl ContentHandler {
Self::new(StatusCode::Ok, content, mimetype)
}
pub fn not_found(content: String, mimetype: Mime) -> Self {
Self::new(StatusCode::NotFound, content, mimetype)
}
pub fn html(code: StatusCode, content: String, embeddable_on: Option<(String, u16)>) -> Self {
Self::new_embeddable(code, content, mime!(Text/Html), embeddable_on)
}

View File

@ -16,12 +16,14 @@
//! Hyper handlers implementations.
mod async;
mod content;
mod echo;
mod fetch;
mod redirect;
mod streaming;
pub use self::async::AsyncHandler;
pub use self::content::ContentHandler;
pub use self::echo::EchoHandler;
pub use self::fetch::{ContentFetcherHandler, ContentValidator, FetchControl, ValidatorResponse};

View File

@ -21,8 +21,10 @@
extern crate base32;
extern crate futures;
extern crate futures_cpupool;
extern crate linked_hash_map;
extern crate mime_guess;
extern crate ntp;
extern crate rand;
extern crate rustc_hex;
extern crate serde;
@ -74,6 +76,7 @@ use std::collections::HashMap;
use jsonrpc_http_server::{self as http, hyper, Origin};
use fetch::Fetch;
use futures_cpupool::CpuPool;
use parity_reactor::Remote;
pub use hash_fetch::urlhint::ContractClient;
@ -82,10 +85,9 @@ pub use hash_fetch::urlhint::ContractClient;
pub trait SyncStatus: Send + Sync {
/// Returns true if there is a major sync happening.
fn is_major_importing(&self) -> bool;
}
impl<F> SyncStatus for F where F: Fn() -> bool + Send + Sync {
fn is_major_importing(&self) -> bool { self() }
/// Returns number of connected and ideal peers.
fn peers(&self) -> (usize, usize);
}
/// Validates Web Proxy tokens
@ -127,21 +129,29 @@ impl Middleware {
}
/// Creates new middleware for UI server.
pub fn ui<F: Fetch + Clone>(
pub fn ui<F: Fetch>(
ntp_server: &str,
pool: CpuPool,
remote: Remote,
dapps_domain: &str,
registrar: Arc<ContractClient>,
sync_status: Arc<SyncStatus>,
fetch: F,
dapps_domain: String,
) -> Self {
let content_fetcher = Arc::new(apps::fetcher::ContentFetcher::new(
hash_fetch::urlhint::URLHintContract::new(registrar),
sync_status,
sync_status.clone(),
remote.clone(),
fetch.clone(),
).embeddable_on(None).allow_dapps(false));
let special = {
let mut special = special_endpoints(content_fetcher.clone());
let mut special = special_endpoints(
ntp_server,
pool,
content_fetcher.clone(),
remote.clone(),
sync_status.clone(),
);
special.insert(router::SpecialEndpoint::Home, Some(apps::ui()));
special
};
@ -150,7 +160,7 @@ impl Middleware {
None,
special,
None,
dapps_domain,
dapps_domain.to_owned(),
);
Middleware {
@ -160,12 +170,14 @@ impl Middleware {
}
/// Creates new Dapps server middleware.
pub fn dapps<F: Fetch + Clone>(
pub fn dapps<F: Fetch>(
ntp_server: &str,
pool: CpuPool,
remote: Remote,
ui_address: Option<(String, u16)>,
dapps_path: PathBuf,
extra_dapps: Vec<PathBuf>,
dapps_domain: String,
dapps_domain: &str,
registrar: Arc<ContractClient>,
sync_status: Arc<SyncStatus>,
web_proxy_tokens: Arc<WebProxyTokens>,
@ -173,14 +185,14 @@ impl Middleware {
) -> Self {
let content_fetcher = Arc::new(apps::fetcher::ContentFetcher::new(
hash_fetch::urlhint::URLHintContract::new(registrar),
sync_status,
sync_status.clone(),
remote.clone(),
fetch.clone(),
).embeddable_on(ui_address.clone()).allow_dapps(true));
let endpoints = apps::all_endpoints(
dapps_path,
extra_dapps,
dapps_domain.clone(),
dapps_domain.to_owned(),
ui_address.clone(),
web_proxy_tokens,
remote.clone(),
@ -188,7 +200,13 @@ impl Middleware {
);
let special = {
let mut special = special_endpoints(content_fetcher.clone());
let mut special = special_endpoints(
ntp_server,
pool,
content_fetcher.clone(),
remote.clone(),
sync_status,
);
special.insert(router::SpecialEndpoint::Home, Some(apps::ui_redirection(ui_address.clone())));
special
};
@ -198,7 +216,7 @@ impl Middleware {
Some(endpoints.clone()),
special,
ui_address,
dapps_domain,
dapps_domain.to_owned(),
);
Middleware {
@ -214,11 +232,22 @@ impl http::RequestMiddleware for Middleware {
}
}
fn special_endpoints(content_fetcher: Arc<apps::fetcher::Fetcher>) -> HashMap<router::SpecialEndpoint, Option<Box<endpoint::Endpoint>>> {
fn special_endpoints(
ntp_server: &str,
pool: CpuPool,
content_fetcher: Arc<apps::fetcher::Fetcher>,
remote: Remote,
sync_status: Arc<SyncStatus>,
) -> HashMap<router::SpecialEndpoint, Option<Box<endpoint::Endpoint>>> {
let mut special = HashMap::new();
special.insert(router::SpecialEndpoint::Rpc, None);
special.insert(router::SpecialEndpoint::Utils, Some(apps::utils()));
special.insert(router::SpecialEndpoint::Api, Some(api::RestApi::new(content_fetcher)));
special.insert(router::SpecialEndpoint::Api, Some(api::RestApi::new(
content_fetcher,
sync_status,
api::TimeChecker::new(ntp_server.into(), pool),
remote,
)));
special
}

View File

@ -26,6 +26,7 @@ use jsonrpc_http_server::{self as http, Host, DomainsValidation};
use devtools::http_client;
use hash_fetch::urlhint::ContractClient;
use fetch::{Fetch, Client as FetchClient};
use futures_cpupool::CpuPool;
use parity_reactor::Remote;
use {Middleware, SyncStatus, WebProxyTokens};
@ -38,6 +39,12 @@ use self::fetch::FakeFetch;
const SIGNER_PORT: u16 = 18180;
struct FakeSync(bool);
impl SyncStatus for FakeSync {
fn is_major_importing(&self) -> bool { self.0 }
fn peers(&self) -> (usize, usize) { (0, 5) }
}
fn init_logger() {
// Initialize logger
if let Ok(log) = env::var("RUST_LOG") {
@ -82,7 +89,7 @@ pub fn serve_with_registrar() -> (Server, Arc<FakeRegistrar>) {
pub fn serve_with_registrar_and_sync() -> (Server, Arc<FakeRegistrar>) {
init_server(|builder| {
builder.sync_status(Arc::new(|| true))
builder.sync_status(Arc::new(FakeSync(true)))
}, Default::default(), Remote::new_sync())
}
@ -148,7 +155,7 @@ impl ServerBuilder {
ServerBuilder {
dapps_path: dapps_path.as_ref().to_owned(),
registrar: registrar,
sync_status: Arc::new(|| false),
sync_status: Arc::new(FakeSync(false)),
web_proxy_tokens: Arc::new(|_| None),
signer_address: None,
allowed_hosts: DomainsValidation::Disabled,
@ -248,6 +255,8 @@ impl Server {
fetch: F,
) -> Result<Server, http::Error> {
let middleware = Middleware::dapps(
"pool.ntp.org:123",
CpuPool::new(4),
remote,
signer_address,
dapps_path,
@ -290,4 +299,3 @@ impl Drop for Server {
self.server.take().unwrap().close()
}
}

View File

@ -94,6 +94,7 @@ class FrameSecureApi extends SecureApi {
const transport = window.secureTransport || new FakeTransport();
const uiUrl = transport.uiUrl || 'http://127.0.0.1:8180';
transport.uiUrlWithProtocol = uiUrl;
transport.uiUrl = uiUrl.replace('http://', '').replace('https://', '');
const api = new FrameSecureApi(transport);

View File

@ -25,6 +25,10 @@ import { statusBlockNumber, statusCollection } from './statusActions';
const log = getLogger(LOG_KEYS.Signer);
let instance = null;
const STATUS_OK = 'ok';
const STATUS_WARN = 'needsAttention';
const STATUS_BAD = 'bad';
export default class Status {
_apiStatus = {};
_status = {};
@ -195,13 +199,16 @@ export default class Status {
const statusPromises = [
this._api.eth.syncing(),
this._api.parity.netPeers()
this._api.parity.netPeers(),
this._fetchHealth()
];
return Promise
.all(statusPromises)
.then(([ syncing, netPeers ]) => {
const status = { netPeers, syncing };
.then(([ syncing, netPeers, health ]) => {
const status = { netPeers, syncing, health };
health.overall = this._overallStatus(health);
if (!isEqual(status, this._status)) {
this._store.dispatch(statusCollection(status));
@ -216,6 +223,33 @@ export default class Status {
});
}
_overallStatus = (health) => {
const all = [health.peers, health.sync, health.time].filter(x => x);
const statuses = all.map(x => x.status);
const bad = statuses.find(x => x === STATUS_BAD);
const needsAttention = statuses.find(x => x === STATUS_WARN);
const message = all.map(x => x.message).filter(x => x);
if (all.length) {
return {
status: bad || needsAttention || STATUS_OK,
message
};
}
return {
status: STATUS_BAD,
message: ['Unable to fetch node health.']
};
}
_fetchHealth = () => {
// Support Parity-Extension.
const uiUrl = this._api.transport.uiUrlWithProtocol || '';
return fetch(`${uiUrl}/api/health`).then(res => res.json());
}
/**
* The data fetched here should not change
* unless Parity is restarted. They are thus

View File

@ -18,11 +18,28 @@ import BigNumber from 'bignumber.js';
import { handleActions } from 'redux-actions';
const DEFAULT_NETCHAIN = '(unknown)';
const DEFAULT_STATUS = 'needsAttention';
const initialState = {
blockNumber: new BigNumber(0),
blockTimestamp: new Date(),
clientVersion: '',
gasLimit: new BigNumber(0),
health: {
peers: {
status: DEFAULT_STATUS
},
sync: {
status: DEFAULT_STATUS
},
time: {
status: DEFAULT_STATUS
},
overall: {
isReady: false,
status: DEFAULT_STATUS,
message: []
}
},
netChain: DEFAULT_NETCHAIN,
netPeers: {
active: new BigNumber(0),

View File

@ -31,7 +31,8 @@ export default class Actionbar extends Component {
title: nodeOrStringProptype(),
buttons: PropTypes.array,
children: PropTypes.node,
className: PropTypes.string
className: PropTypes.string,
health: PropTypes.node
};
static defaultProps = {

View File

@ -0,0 +1,17 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './statusIndicator';

View File

@ -0,0 +1,88 @@
/* Copyright 2015-2017 Parity Technologies (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.status {
display: inline-block;
}
.radial,.signal {
display: inline-block;
margin: .2em;
width: 1em;
height: 1em;
}
.radial {
border-radius: 100%;
border-top: 1px solid rgba(255, 255, 255, 0.5);
background-image: radial-gradient(ellipse at top, rgba(255, 255, 255, 0.38) 0%, rgba(255, 255, 255, 0) 100%);
&.ok {
background-color: #070;
}
&.bad {
background-color: #c00;
}
&.needsAttention {
background-color: #dc0;
}
}
.signal {
width: 2em;
width: calc(.9em + 5px);
text-transform: initial;
vertical-align: bottom;
margin-top: -1em;
> .bar {
display: inline-block;
border: 1px solid #444;
box-shadow: 0 0 1px rgba(0, 0, 0, 0.8);
width: .3em;
height: 1em;
opacity: 0.7;
background-color: rgba(0, 0, 0, 0.6);
vertical-align: bottom;
&.active {
opacity: 1.0;
background-image: linear-gradient(0, rgba(255, 255, 255, 0.38) 0%, rgba(255, 255, 255, 0) 100%);
}
&.bad {
height: .4em;
border-right: 0;
}
&.needsAttention {
height: .6em;
border-right: 0;
}
&.ok {
height: 1em;
}
}
&.bad > .bar.active {
background-color: #c00;
}
&.ok > .bar.active {
background-color: #080;
}
&.needsAttention > .bar.active {
background-color: #dc0;
}
}

View File

@ -0,0 +1,70 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import ReactTooltip from 'react-tooltip';
import styles from './statusIndicator.css';
const statuses = ['bad', 'needsAttention', 'ok'];
export default class StatusIndicator extends Component {
static propTypes = {
type: PropTypes.oneOf(['radial', 'signal']),
id: PropTypes.string.isRequired,
status: PropTypes.oneOf(statuses).isRequired,
title: PropTypes.arrayOf(PropTypes.node),
tooltipPlacement: PropTypes.oneOf(['left', 'top', 'bottom', 'right'])
};
static defaultProps = {
type: 'signal',
title: []
};
render () {
const { id, status, title, type, tooltipPlacement } = this.props;
const tooltip = title.find(x => !x.isEmpty) ? (
<ReactTooltip id={ `status-${id}` }>
{ title.map(x => (<div key={ x }>{ x }</div>)) }
</ReactTooltip>
) : null;
return (
<span className={ styles.status }>
<span className={ `${styles[type]} ${styles[status]}` }
data-tip={ title.length }
data-for={ `status-${id}` }
data-place={ tooltipPlacement }
data-effect='solid'
>
{ type === 'signal' && statuses.map(this.renderBar) }
</span>
{tooltip}
</span>
);
}
renderBar = (signal) => {
const idx = statuses.indexOf(this.props.status);
const isActive = statuses.indexOf(signal) <= idx;
const activeClass = isActive ? styles.active : '';
return (
<span key={ signal } className={ `${styles.bar} ${styles[signal]} ${activeClass}` } />
);
}
}

View File

@ -52,6 +52,7 @@ export SectionList from './SectionList';
export SelectionList from './SelectionList';
export ShortenedHash from './ShortenedHash';
export SignerIcon from './SignerIcon';
export StatusIndicator from './StatusIndicator';
export Tags from './Tags';
export Title from './Title';
export Tooltips, { Tooltip } from './Tooltips';

View File

@ -43,6 +43,7 @@ class Accounts extends Component {
accountsInfo: PropTypes.object.isRequired,
availability: PropTypes.string.isRequired,
hasAccounts: PropTypes.bool.isRequired,
health: PropTypes.object.isRequired,
setVisibleAccounts: PropTypes.func.isRequired
}
@ -496,12 +497,14 @@ class Accounts extends Component {
function mapStateToProps (state) {
const { accounts, accountsInfo, hasAccounts } = state.personal;
const { availability = 'unknown' } = state.nodeStatus.nodeKind || {};
const { health } = state.nodeStatus;
return {
accounts,
accountsInfo,
availability,
hasAccounts
hasAccounts,
health
};
}

View File

@ -18,7 +18,7 @@ import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { BlockStatus } from '~/ui';
import { BlockStatus, StatusIndicator } from '~/ui';
import styles from './status.css';
@ -28,11 +28,12 @@ class Status extends Component {
isTest: PropTypes.bool,
netChain: PropTypes.string,
netPeers: PropTypes.object,
health: PropTypes.object,
upgradeStore: PropTypes.object.isRequired
}
render () {
const { clientVersion, isTest, netChain, netPeers } = this.props;
const { clientVersion, isTest, netChain, netPeers, health } = this.props;
return (
<div className={ styles.status }>
@ -44,13 +45,20 @@ class Status extends Component {
{ this.renderUpgradeButton() }
</div>
<div className={ styles.netinfo }>
<BlockStatus />
<div>
<StatusIndicator
type='signal'
id='application.status.health'
status={ health.overall.status }
title={ health.overall.message }
/>
</div>
<span title={ `${netPeers.connected.toFormat()}/${netPeers.max.toFormat()} peers` }>
<BlockStatus />
</span>
<div className={ `${styles.network} ${styles[isTest ? 'test' : 'live']}` }>
{ netChain }
</div>
<div className={ styles.peers }>
{ netPeers.connected.toFormat() }/{ netPeers.max.toFormat() } peers
</div>
</div>
</div>
);
@ -102,14 +110,7 @@ class Status extends Component {
);
}
return (
<div>
<FormattedMessage
id='application.status.consensus.unknown'
defaultMessage='Upgrade status is unknown.'
/>
</div>
);
return;
}
renderUpgradeButton () {
@ -136,10 +137,11 @@ class Status extends Component {
}
function mapStateToProps (state) {
const { clientVersion, netPeers, netChain, isTest } = state.nodeStatus;
const { clientVersion, health, netPeers, netChain, isTest } = state.nodeStatus;
return {
clientVersion,
health,
netPeers,
netChain,
isTest

View File

@ -81,6 +81,16 @@
white-space: nowrap;
}
.indicatorTab {
font-size: 1.5rem;
flex: 0;
}
.indicator {
padding: 20px 12px 0;
opacity: 0.8;
}
.first {
margin-left: -24px;
}

View File

@ -21,7 +21,7 @@ import { Link } from 'react-router';
import { Toolbar, ToolbarGroup } from 'material-ui/Toolbar';
import { isEqual } from 'lodash';
import { Tooltip } from '~/ui';
import { Tooltip, StatusIndicator } from '~/ui';
import Tab from './Tab';
import styles from './tabBar.css';
@ -33,6 +33,7 @@ class TabBar extends Component {
static propTypes = {
pending: PropTypes.array,
health: PropTypes.object.isRequired,
views: PropTypes.array.isRequired
};
@ -41,12 +42,29 @@ class TabBar extends Component {
};
render () {
const { health } = this.props;
return (
<Toolbar className={ styles.toolbar }>
<ToolbarGroup className={ styles.first }>
<div />
</ToolbarGroup>
<div className={ styles.tabs }>
<Link
activeClassName={ styles.tabactive }
className={ `${styles.tabLink} ${styles.indicatorTab}` }
key='status'
to='/status'
>
<div className={ styles.indicator }>
<StatusIndicator
type='signal'
id='topbar.health'
status={ health.overall.status }
title={ health.overall.message }
/>
</div>
</Link>
{ this.renderTabItems() }
<Tooltip
className={ styles.tabbarTooltip }
@ -101,6 +119,7 @@ function mapStateToProps (initState) {
return (state) => {
const { availability = 'unknown' } = state.nodeStatus.nodeKind || {};
const { views } = state.settings;
const { health } = state.nodeStatus;
const viewIds = Object
.keys(views)
@ -114,7 +133,7 @@ function mapStateToProps (initState) {
});
if (isEqual(viewIds, filteredViewIds)) {
return { views: filteredViews };
return { views: filteredViews, health };
}
filteredViewIds = viewIds;
@ -123,7 +142,7 @@ function mapStateToProps (initState) {
id
}));
return { views: filteredViews };
return { views: filteredViews, health };
};
}

View File

@ -37,6 +37,12 @@ function createStore () {
nodeStatus: {
nodeKind: {
'availability': 'personal'
},
health: {
overall: {
status: 'ok',
message: []
}
}
}
};

View File

@ -24,7 +24,7 @@ import { connect } from 'react-redux';
import store from 'store';
import imagesEthcoreBlock from '~/../assets/images/parity-logo-white-no-text.svg';
import { AccountCard, Badge, Button, ContainerTitle, IdentityIcon, ParityBackground, SelectionList } from '~/ui';
import { AccountCard, Badge, Button, ContainerTitle, IdentityIcon, ParityBackground, SelectionList, StatusIndicator } from '~/ui';
import { CancelIcon, FingerprintIcon } from '~/ui/Icons';
import DappsStore from '~/views/Dapps/dappsStore';
import { Embedded as Signer } from '~/views/Signer';
@ -50,7 +50,8 @@ class ParityBar extends Component {
static propTypes = {
dapp: PropTypes.bool,
externalLink: PropTypes.string,
pending: PropTypes.array
pending: PropTypes.array,
health: PropTypes.object
};
state = {
@ -210,7 +211,7 @@ class ParityBar extends Component {
}
renderBar () {
const { dapp } = this.props;
const { dapp, health } = this.props;
if (!dapp) {
return null;
@ -218,6 +219,13 @@ class ParityBar extends Component {
return (
<div className={ styles.cornercolor }>
<StatusIndicator
type='signal'
id='paritybar.health'
status={ health.overall.status }
title={ health.overall.message }
tooltipPlacement='right'
/>
<Button
className={ styles.iconButton }
icon={
@ -699,9 +707,11 @@ class ParityBar extends Component {
function mapStateToProps (state) {
const { pending } = state.signer;
const { health } = state.nodeStatus;
return {
pending
pending,
health
};
}

View File

@ -37,6 +37,14 @@ function createRedux (state = {}) {
},
signer: {
pending: []
},
nodeStatus: {
health: {
overall: {
status: 'ok',
message: []
}
}
}
}, state)
};

View File

@ -17,7 +17,7 @@
import React from 'react';
import imagesEthcoreBlock from '~/../assets/images/parity-logo-white-no-text.svg';
import { AccountsIcon, AddressesIcon, AppsIcon, ContactsIcon, FingerprintIcon, SettingsIcon, StatusIcon } from '~/ui/Icons';
import { AccountsIcon, AddressesIcon, AppsIcon, ContactsIcon, FingerprintIcon, SettingsIcon } from '~/ui/Icons';
import styles from './views.css';
@ -65,14 +65,6 @@ const defaultViews = {
value: 'contract'
},
status: {
active: false,
onlyPersonal: true,
icon: <StatusIcon />,
route: '/status',
value: 'status'
},
signer: {
active: true,
fixed: true,

View File

@ -113,17 +113,6 @@ class Views extends Component {
/>
)
}
{
this.renderView('status',
<FormattedMessage
id='settings.views.status.label'
/>,
<FormattedMessage
id='settings.views.status.description'
defaultMessage='See how the Parity node is performing in terms of connections to the network, logs from the actual running instance and details of mining (if enabled and configured).'
/>
)
}
{
this.renderView('signer',
<FormattedMessage

View File

@ -0,0 +1,152 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { Container, ContainerTitle, StatusIndicator } from '~/ui';
import grid from '../NodeStatus/nodeStatus.css';
const HealthItem = (props) => {
const status = props.item.status || 'needsAttention';
return (
<div>
<h3>
<StatusIndicator
id={ props.id }
title={ [
(<div>{ props.item.message }</div>)
] }
status={ status }
/>
{ props.title }
<small>&nbsp;({ props.details })</small>
</h3>
<p>
{ status !== 'ok' ? props.item.message : '' }
</p>
</div>
);
};
HealthItem.propTypes = {
id: PropTypes.string.isRequired,
title: PropTypes.node.isRequired,
details: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node
]).isRequired,
item: PropTypes.object.isRequired
};
class Health extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
peers: PropTypes.object.isRequired,
sync: PropTypes.object.isRequired,
time: PropTypes.object.isRequired
};
state = {};
render () {
const { peers, sync, time } = this.props;
const [yes, no] = [(
<FormattedMessage
id='status.health.yes'
defaultMessage='yes'
/>
), (
<FormattedMessage
id='status.health.no'
defaultMessage='no'
/>
)];
return (
<Container>
<ContainerTitle
title={
<div>
<FormattedMessage
id='status.health.title'
defaultMessage='Node Health'
/>
</div>
}
/>
<div className={ grid.container }>
<div className={ grid.row }>
<div className={ grid.col4 }>
<HealthItem
id='status.health.sync'
title={
<FormattedMessage
id='status.health.sync'
defaultMessage='Chain Synchronized'
/>
}
details={ !sync.details ? yes : no }
item={ sync }
/>
</div>
<div className={ grid.col4 }>
<HealthItem
id='status.health.peers'
title={
<FormattedMessage
id='status.health.peers'
defaultMessage='Connected Peers'
/>
}
details={ (peers.details || []).join('/') }
item={ peers }
/>
</div>
<div className={ grid.col4 }>
<HealthItem
id='status.health.time'
title={
<FormattedMessage
id='status.health.time'
defaultMessage='Time Synchronized'
/>
}
details={ `${time.details || 0} ms` }
item={ time }
/>
</div>
</div>
</div>
</Container>
);
}
}
function mapStateToProps (state) {
return state.nodeStatus.health;
}
export default connect(
mapStateToProps,
null
)(Health);

View File

@ -0,0 +1,17 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './health';

View File

@ -44,7 +44,7 @@
}
.col,
.col3, .col4_5, .col6, .col12 {
.col3, .col4, .col4_5, .col6, .col12 {
float: left;
padding: 0 1em;
box-sizing: border-box;
@ -57,6 +57,13 @@
width: calc(100% / 12 * 3);
}
.col4 {
width: 33.3%;
width: -webkit-calc(100% / 12 * 4);
width: -moz-calc(100% / 12 * 4);
width: calc(100% / 12 * 4);
}
.col4_5 {
width: 37.5%;
width: -webkit-calc(100% / 12 * 4.5);

View File

@ -20,6 +20,7 @@ import { FormattedMessage } from 'react-intl';
import { Page } from '~/ui';
import Debug from './Debug';
import Health from './Health';
import Peers from './Peers';
import NodeStatus from './NodeStatus';
@ -35,6 +36,7 @@ export default () => (
}
>
<div className={ styles.body }>
<Health />
<NodeStatus />
<Peers />
<Debug />

View File

@ -58,3 +58,7 @@
margin: 0.5em 0;
}
}
.status {
font-size: 4rem;
}

View File

@ -20,7 +20,7 @@ import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import store from 'store';
import { Button } from '~/ui';
import { Button, StatusIndicator } from '~/ui';
import styles from './syncWarning.css';
@ -38,7 +38,8 @@ export const showSyncWarning = () => {
class SyncWarning extends Component {
static propTypes = {
isSyncing: PropTypes.bool
isOk: PropTypes.bool.isRequired,
health: PropTypes.object.isRequired
};
state = {
@ -47,10 +48,10 @@ class SyncWarning extends Component {
};
render () {
const { isSyncing } = this.props;
const { isOk, health } = this.props;
const { dontShowAgain, show } = this.state;
if (!isSyncing || isSyncing === null || !show) {
if (isOk || !show) {
return null;
}
@ -59,18 +60,19 @@ class SyncWarning extends Component {
<div className={ styles.overlay } />
<div className={ styles.modal }>
<div className={ styles.body }>
<FormattedMessage
id='syncWarning.message.line1'
defaultMessage={ `
Your Parity node is still syncing to the chain.
` }
/>
<FormattedMessage
id='syncWarning.message.line2'
defaultMessage={ `
Some of the shown information might be out-of-date.
` }
/>
<div className={ styles.status }>
<StatusIndicator
type='signal'
id='healthWarning.indicator'
status={ health.overall.status }
/>
</div>
{
health.overall.message.map(message => (
<p key={ message }>{ message }</p>
))
}
<div className={ styles.button }>
<Checkbox
@ -113,14 +115,13 @@ class SyncWarning extends Component {
}
function mapStateToProps (state) {
const { syncing } = state.nodeStatus;
// syncing could be an Object, false, or null
const isSyncing = syncing
? true
: syncing;
const { health } = state.nodeStatus;
const isNotAvailableYet = health.overall.isReady;
const isOk = isNotAvailableYet || health.overall.status === 'ok';
return {
isSyncing
isOk,
health
};
}

View File

@ -26,7 +26,12 @@ function createRedux (syncing = null) {
getState: () => {
return {
nodeStatus: {
syncing
health: {
overall: {
status: syncing ? 'needsAttention' : 'ok',
message: []
}
}
}
};
}

View File

@ -67,10 +67,11 @@ pub fn setup_log(config: &Config) -> Result<Arc<RotatingLogger>, String> {
let mut levels = String::new();
let mut builder = LogBuilder::new();
// Disable ws info logging by default.
// Disable info logging by default for some modules:
builder.filter(Some("ws"), LogLevelFilter::Warn);
// Disable rustls info logging by default.
builder.filter(Some("reqwest"), LogLevelFilter::Warn);
builder.filter(Some("rustls"), LogLevelFilter::Warn);
// Enable info for others.
builder.filter(None, LogLevelFilter::Info);
if let Ok(lvl) = env::var("RUST_LOG") {

View File

@ -78,6 +78,7 @@ disable_periodic = true
jit = false
[misc]
ntp_server = "pool.ntp.org:123"
logging = "own_tx=trace"
log_file = "/var/log/parity.log"
color = true

View File

@ -354,6 +354,8 @@ usage! {
or |c: &Config| otry!(c.vm).jit.clone(),
// -- Miscellaneous Options
flag_ntp_server: String = "pool.ntp.org:123",
or |c: &Config| otry!(c.misc).ntp_server.clone(),
flag_logging: Option<String> = None,
or |c: &Config| otry!(c.misc).logging.clone().map(Some),
flag_log_file: Option<String> = None,
@ -590,6 +592,7 @@ struct VM {
#[derive(Default, Debug, PartialEq, Deserialize)]
struct Misc {
ntp_server: Option<String>,
logging: Option<String>,
log_file: Option<String>,
color: Option<bool>,
@ -890,6 +893,7 @@ mod tests {
flag_dapps_apis_all: None,
// -- Miscellaneous Options
flag_ntp_server: "pool.ntp.org:123".into(),
flag_version: false,
flag_logging: Some("own_tx=trace".into()),
flag_log_file: Some("/var/log/parity.log".into()),
@ -1066,6 +1070,7 @@ mod tests {
jit: Some(false),
}),
misc: Some(Misc {
ntp_server: Some("pool.ntp.org:123".into()),
logging: Some("own_tx=trace".into()),
log_file: Some("/var/log/parity.log".into()),
color: Some(true),

View File

@ -467,6 +467,8 @@ Internal Options:
--can-restart Executable will auto-restart if exiting with 69.
Miscellaneous Options:
--ntp-server HOST NTP server to provide current time (host:port). Used to verify node health.
(default: {flag_ntp_server})
-l --logging LOGGING Specify the logging level. Must conform to the same
format as RUST_LOG. (default: {flag_logging:?})
--log-file FILENAME Specify a filename into which logging should be

View File

@ -556,6 +556,7 @@ impl Configuration {
fn ui_config(&self) -> UiConfiguration {
UiConfiguration {
enabled: self.ui_enabled(),
ntp_server: self.args.flag_ntp_server.clone(),
interface: self.ui_interface(),
port: self.args.flag_ports_shift + self.args.flag_ui_port,
hosts: self.ui_hosts(),
@ -565,6 +566,7 @@ impl Configuration {
fn dapps_config(&self) -> DappsConfiguration {
DappsConfiguration {
enabled: self.dapps_enabled(),
ntp_server: self.args.flag_ntp_server.clone(),
dapps_path: PathBuf::from(self.directories().dapps),
extra_dapps: if self.args.cmd_dapp {
self.args.arg_path.iter().map(|path| PathBuf::from(path)).collect()
@ -1265,6 +1267,7 @@ mod tests {
support_token_api: true
}, UiConfiguration {
enabled: true,
ntp_server: "pool.ntp.org:123".into(),
interface: "127.0.0.1".into(),
port: 8180,
hosts: Some(vec![]),
@ -1505,6 +1508,7 @@ mod tests {
assert_eq!(conf0.directories().signer, "signer".to_owned());
assert_eq!(conf0.ui_config(), UiConfiguration {
enabled: true,
ntp_server: "pool.ntp.org:123".into(),
interface: "127.0.0.1".into(),
port: 8180,
hosts: Some(vec![]),
@ -1513,6 +1517,7 @@ mod tests {
assert_eq!(conf1.directories().signer, "signer".to_owned());
assert_eq!(conf1.ui_config(), UiConfiguration {
enabled: true,
ntp_server: "pool.ntp.org:123".into(),
interface: "127.0.0.1".into(),
port: 8180,
hosts: Some(vec![]),
@ -1521,6 +1526,7 @@ mod tests {
assert_eq!(conf2.directories().signer, "signer".to_owned());
assert_eq!(conf2.ui_config(), UiConfiguration {
enabled: true,
ntp_server: "pool.ntp.org:123".into(),
interface: "127.0.0.1".into(),
port: 3123,
hosts: Some(vec![]),
@ -1529,6 +1535,7 @@ mod tests {
assert_eq!(conf3.directories().signer, "signer".to_owned());
assert_eq!(conf3.ui_config(), UiConfiguration {
enabled: true,
ntp_server: "pool.ntp.org:123".into(),
interface: "test".into(),
port: 8180,
hosts: Some(vec![]),

View File

@ -22,6 +22,7 @@ use ethcore::client::{Client, BlockChainClient, BlockId};
use ethcore::transaction::{Transaction, Action};
use ethsync::LightSync;
use futures::{future, IntoFuture, Future, BoxFuture};
use futures_cpupool::CpuPool;
use hash_fetch::fetch::Client as FetchClient;
use hash_fetch::urlhint::ContractClient;
use helpers::replace_home;
@ -35,6 +36,7 @@ use util::{Bytes, Address};
#[derive(Debug, PartialEq, Clone)]
pub struct Configuration {
pub enabled: bool,
pub ntp_server: String,
pub dapps_path: PathBuf,
pub extra_dapps: Vec<PathBuf>,
}
@ -44,6 +46,7 @@ impl Default for Configuration {
let data_dir = default_data_path();
Configuration {
enabled: true,
ntp_server: "pool.ntp.org:123".into(),
dapps_path: replace_home(&data_dir, "$BASE/dapps").into(),
extra_dapps: vec![],
}
@ -140,6 +143,7 @@ pub struct Dependencies {
pub sync_status: Arc<SyncStatus>,
pub contract_client: Arc<ContractClient>,
pub remote: parity_reactor::TokioRemote,
pub pool: CpuPool,
pub fetch: FetchClient,
pub signer: Arc<SignerService>,
pub ui_address: Option<(String, u16)>,
@ -152,20 +156,22 @@ pub fn new(configuration: Configuration, deps: Dependencies) -> Result<Option<Mi
server::dapps_middleware(
deps,
&configuration.ntp_server,
configuration.dapps_path,
configuration.extra_dapps,
rpc::DAPPS_DOMAIN.into(),
rpc::DAPPS_DOMAIN,
).map(Some)
}
pub fn new_ui(enabled: bool, deps: Dependencies) -> Result<Option<Middleware>, String> {
pub fn new_ui(enabled: bool, ntp_server: &str, deps: Dependencies) -> Result<Option<Middleware>, String> {
if !enabled {
return Ok(None);
}
server::ui_middleware(
deps,
rpc::DAPPS_DOMAIN.into(),
ntp_server,
rpc::DAPPS_DOMAIN,
).map(Some)
}
@ -192,16 +198,18 @@ mod server {
pub fn dapps_middleware(
_deps: Dependencies,
_ntp_server: &str,
_dapps_path: PathBuf,
_extra_dapps: Vec<PathBuf>,
_dapps_domain: String,
_dapps_domain: &str,
) -> Result<Middleware, String> {
Err("Your Parity version has been compiled without WebApps support.".into())
}
pub fn ui_middleware(
_deps: Dependencies,
_dapps_domain: String,
_ntp_server: &str,
_dapps_domain: &str,
) -> Result<Middleware, String> {
Err("Your Parity version has been compiled without UI support.".into())
}
@ -226,15 +234,18 @@ mod server {
pub fn dapps_middleware(
deps: Dependencies,
ntp_server: &str,
dapps_path: PathBuf,
extra_dapps: Vec<PathBuf>,
dapps_domain: String,
dapps_domain: &str,
) -> Result<Middleware, String> {
let signer = deps.signer;
let parity_remote = parity_reactor::Remote::new(deps.remote.clone());
let web_proxy_tokens = Arc::new(move |token| signer.web_proxy_access_token_domain(&token));
Ok(parity_dapps::Middleware::dapps(
ntp_server,
deps.pool,
parity_remote,
deps.ui_address,
dapps_path,
@ -249,15 +260,18 @@ mod server {
pub fn ui_middleware(
deps: Dependencies,
dapps_domain: String,
ntp_server: &str,
dapps_domain: &str,
) -> Result<Middleware, String> {
let parity_remote = parity_reactor::Remote::new(deps.remote.clone());
Ok(parity_dapps::Middleware::ui(
ntp_server,
deps.pool,
parity_remote,
dapps_domain,
deps.contract_client,
deps.sync_status,
deps.fetch,
dapps_domain,
))
}

View File

@ -29,6 +29,7 @@ extern crate docopt;
extern crate env_logger;
extern crate fdlimit;
extern crate futures;
extern crate futures_cpupool;
extern crate isatty;
extern crate jsonrpc_core;
extern crate num_cpus;

View File

@ -74,6 +74,7 @@ impl Default for HttpConfiguration {
#[derive(Debug, PartialEq, Clone)]
pub struct UiConfiguration {
pub enabled: bool,
pub ntp_server: String,
pub interface: String,
pub port: u16,
pub hosts: Option<Vec<String>>,
@ -107,6 +108,7 @@ impl Default for UiConfiguration {
fn default() -> Self {
UiConfiguration {
enabled: true && cfg!(feature = "ui-enabled"),
ntp_server: "pool.ntp.org:123".into(),
port: 8180,
interface: "127.0.0.1".into(),
hosts: Some(vec![]),

View File

@ -27,8 +27,7 @@ use ethcore::miner::{StratumOptions, Stratum};
use ethcore::service::ClientService;
use ethcore::snapshot;
use ethcore::verification::queue::VerifierSettings;
use ethsync::NetworkConfiguration;
use ethsync::SyncConfig;
use ethsync::{self, SyncConfig};
use fdlimit::raise_fd_limit;
use hash_fetch::fetch::{Fetch, Client as FetchClient};
use informant::{Informant, LightNodeInformantData, FullNodeInformantData};
@ -84,7 +83,7 @@ pub struct RunCmd {
pub ws_conf: rpc::WsConfiguration,
pub http_conf: rpc::HttpConfiguration,
pub ipc_conf: rpc::IpcConfiguration,
pub net_conf: NetworkConfiguration,
pub net_conf: ethsync::NetworkConfiguration,
pub network_id: Option<u64>,
pub warp_sync: bool,
pub public_node: bool,
@ -237,7 +236,7 @@ fn execute_light(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) ->
network_config: net_conf.into_basic().map_err(|e| format!("Failed to produce network config: {}", e))?,
client: Arc::new(provider),
network_id: cmd.network_id.unwrap_or(spec.network_id()),
subprotocol_name: ::ethsync::LIGHT_PROTOCOL,
subprotocol_name: ethsync::LIGHT_PROTOCOL,
handlers: vec![on_demand.clone()],
};
let light_sync = LightSync::new(sync_params).map_err(|e| format!("Error starting network: {}", e))?;
@ -277,9 +276,18 @@ fn execute_light(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) ->
on_demand: on_demand.clone(),
});
let sync = light_sync.clone();
struct LightSyncStatus(Arc<LightSync>);
impl dapps::SyncStatus for LightSyncStatus {
fn is_major_importing(&self) -> bool { self.0.is_major_importing() }
fn peers(&self) -> (usize, usize) {
let peers = ethsync::LightSyncProvider::peer_numbers(&*self.0);
(peers.connected, peers.max)
}
}
dapps::Dependencies {
sync_status: Arc::new(move || sync.is_major_importing()),
sync_status: Arc::new(LightSyncStatus(light_sync.clone())),
pool: fetch.pool(),
contract_client: contract_client,
remote: event_loop.raw_remote(),
fetch: fetch.clone(),
@ -289,7 +297,7 @@ fn execute_light(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) ->
};
let dapps_middleware = dapps::new(cmd.dapps_conf.clone(), dapps_deps.clone())?;
let ui_middleware = dapps::new_ui(cmd.ui_conf.enabled, dapps_deps)?;
let ui_middleware = dapps::new_ui(cmd.ui_conf.enabled, &cmd.ui_conf.ntp_server, dapps_deps)?;
// start RPCs
let dapps_service = dapps::service(&dapps_middleware);
@ -586,7 +594,7 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) -> R
let (sync_provider, manage_network, chain_notify) = modules::sync(
&mut hypervisor,
sync_config,
net_conf.into(),
net_conf.clone().into(),
client.clone(),
snapshot_service.clone(),
client.clone(),
@ -630,8 +638,20 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) -> R
let (sync, client) = (sync_provider.clone(), client.clone());
let contract_client = Arc::new(::dapps::FullRegistrar { client: client.clone() });
struct SyncStatus(Arc<ethsync::SyncProvider>, Arc<Client>, ethsync::NetworkConfiguration);
impl dapps::SyncStatus for SyncStatus {
fn is_major_importing(&self) -> bool {
is_major_importing(Some(self.0.status().state), self.1.queue_info())
}
fn peers(&self) -> (usize, usize) {
let status = self.0.status();
(status.num_peers, status.current_max_peers(self.2.min_peers, self.2.max_peers) as usize)
}
}
dapps::Dependencies {
sync_status: Arc::new(move || is_major_importing(Some(sync.status().state), client.queue_info())),
sync_status: Arc::new(SyncStatus(sync, client, net_conf)),
pool: fetch.pool(),
contract_client: contract_client,
remote: event_loop.raw_remote(),
fetch: fetch.clone(),
@ -640,7 +660,7 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) -> R
}
};
let dapps_middleware = dapps::new(cmd.dapps_conf.clone(), dapps_deps.clone())?;
let ui_middleware = dapps::new_ui(cmd.ui_conf.enabled, dapps_deps)?;
let ui_middleware = dapps::new_ui(cmd.ui_conf.enabled, &cmd.ui_conf.ntp_server, dapps_deps)?;
let dapps_service = dapps::service(&dapps_middleware);
let deps_for_rpc_apis = Arc::new(rpc_apis::FullDependencies {

View File

@ -126,6 +126,11 @@ impl Client {
*self.client.write() = (time::Instant::now(), client.clone());
Ok(client)
}
/// Returns a handle to underlying CpuPool of this client.
pub fn pool(&self) -> CpuPool {
self.pool.clone()
}
}
impl Fetch for Client {
@ -204,6 +209,15 @@ pub enum Error {
Aborted,
}
impl fmt::Display for Error {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match *self {
Error::Aborted => write!(fmt, "The request has been aborted."),
Error::Fetch(ref err) => write!(fmt, "{}", err),
}
}
}
impl From<reqwest::Error> for Error {
fn from(error: reqwest::Error) -> Self {
Error::Fetch(error)