From 4936e99f3005b42bc131768efbeb7a4d7c42ac4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Tue, 11 Jul 2017 12:23:46 +0200 Subject: [PATCH] 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. --- Cargo.lock | 88 ++++++ Cargo.toml | 1 + dapps/Cargo.toml | 2 + dapps/src/api/api.rs | 102 ++++++- dapps/src/api/mod.rs | 2 + dapps/src/api/response.rs | 12 +- dapps/src/api/time.rs | 264 ++++++++++++++++++ dapps/src/api/types.rs | 43 +++ dapps/src/apps/fetcher/mod.rs | 9 +- dapps/src/handlers/async.rs | 112 ++++++++ dapps/src/handlers/content.rs | 4 - dapps/src/handlers/mod.rs | 2 + dapps/src/lib.rs | 61 ++-- dapps/src/tests/helpers/mod.rs | 14 +- js/src/embed.js | 1 + js/src/redux/providers/status.js | 40 ++- js/src/redux/providers/statusReducer.js | 17 ++ js/src/ui/Actionbar/actionbar.js | 3 +- js/src/ui/StatusIndicator/index.js | 17 ++ js/src/ui/StatusIndicator/statusIndicator.css | 88 ++++++ js/src/ui/StatusIndicator/statusIndicator.js | 70 +++++ js/src/ui/index.js | 1 + js/src/views/Accounts/accounts.js | 5 +- js/src/views/Application/Status/status.js | 32 ++- js/src/views/Application/TabBar/tabBar.css | 10 + js/src/views/Application/TabBar/tabBar.js | 25 +- .../views/Application/TabBar/tabBar.spec.js | 6 + js/src/views/ParityBar/parityBar.js | 18 +- js/src/views/ParityBar/parityBar.spec.js | 8 + js/src/views/Settings/Views/defaults.js | 10 +- js/src/views/Settings/Views/views.js | 11 - js/src/views/Status/Health/health.js | 152 ++++++++++ js/src/views/Status/Health/index.js | 17 ++ js/src/views/Status/NodeStatus/nodeStatus.css | 9 +- js/src/views/Status/status.js | 2 + js/src/views/SyncWarning/syncWarning.css | 4 + js/src/views/SyncWarning/syncWarning.js | 45 +-- js/src/views/SyncWarning/syncWarning.spec.js | 7 +- logger/src/lib.rs | 5 +- parity/cli/config.toml | 1 + parity/cli/mod.rs | 5 + parity/cli/usage.txt | 2 + parity/configuration.rs | 7 + parity/dapps.rs | 30 +- parity/main.rs | 1 + parity/rpc.rs | 2 + parity/run.rs | 40 ++- util/fetch/src/client.rs | 14 + 48 files changed, 1296 insertions(+), 125 deletions(-) create mode 100644 dapps/src/api/time.rs create mode 100644 dapps/src/handlers/async.rs create mode 100644 js/src/ui/StatusIndicator/index.js create mode 100644 js/src/ui/StatusIndicator/statusIndicator.css create mode 100644 js/src/ui/StatusIndicator/statusIndicator.js create mode 100644 js/src/views/Status/Health/health.js create mode 100644 js/src/views/Status/Health/index.js diff --git a/Cargo.lock b/Cargo.lock index b4cdb7f72..fe59b17b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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)" = "" +"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)" = "" +"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)" = "" "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" diff --git a/Cargo.toml b/Cargo.toml index 429ea0dd4..93341b564 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/dapps/Cargo.toml b/dapps/Cargo.toml index f34335d2f..a0d63b6e3 100644 --- a/dapps/Cargo.toml +++ b/dapps/Cargo.toml @@ -11,11 +11,13 @@ authors = ["Parity Technologies "] 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" diff --git a/dapps/src/api/api.rs b/dapps/src/api/api.rs index d377ebe57..7d38e288f 100644 --- a/dapps/src/api/api.rs +++ b/dapps/src/api/api.rs @@ -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, + sync_status: Arc, + time: TimeChecker, + remote: Remote, } impl RestApi { - pub fn new(fetcher: Arc) -> Box { + pub fn new( + fetcher: Arc, + sync_status: Arc, + time: TimeChecker, + remote: Remote, + ) -> Box { 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 { + 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 for RestApiRouter { @@ -103,6 +188,7 @@ impl server::Handler 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 }); diff --git a/dapps/src/api/mod.rs b/dapps/src/api/mod.rs index 4ffb9f791..59c634399 100644 --- a/dapps/src/api/mod.rs +++ b/dapps/src/api/mod.rs @@ -18,6 +18,8 @@ mod api; mod response; +mod time; mod types; pub use self::api::RestApi; +pub use self::time::TimeChecker; diff --git a/dapps/src/api/response.rs b/dapps/src/api/response.rs index 2da2d0c14..6ecc2df60 100644 --- a/dapps/src/api/response.rs +++ b/dapps/src/api/response.rs @@ -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 { Box::new(ContentHandler::ok("".into(), mime!(Text/Plain))) } -pub fn as_json_error(val: &T) -> Box { +pub fn as_json(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(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 { diff --git a/dapps/src/api/time.rs b/dapps/src/api/time.rs new file mode 100644 index 000000000..084890dc9 --- /dev/null +++ b/dapps/src/api/time.rs @@ -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 . + +//! 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 for Error { + fn from(err: io::Error) -> Self { Error::Io(format!("{}", err)) } +} + +impl From 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; +} + +/// NTP client using the SNTP algorithm for calculating drift. +#[derive(Clone)] +pub struct SimpleNtp { + address: Arc, + 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 { + 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 { + ntp: N, + last_result: Arc>)>>, +} + +impl TimeChecker { + /// 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 TimeChecker { + /// Updates the time + pub fn update(&self) -> BoxFuture { + 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 { + // 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>>(results: T) -> Result { + 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>, Cell); + impl FakeNtp { + fn new() -> FakeNtp { + FakeNtp( + RefCell::new(vec![Duration::milliseconds(150)]), + Cell::new(0)) + } + } + + impl Ntp for FakeNtp { + fn drift(&self) -> BoxFuture { + 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 { + 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); + } +} diff --git a/dapps/src/api/types.rs b/dapps/src/api/types.rs index 549186955..a61964143 100644 --- a/dapps/src/api/types.rs +++ b/dapps/src/api/types.rs @@ -14,11 +14,54 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +/// 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 { + /// 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, + /// Time diff info. + pub time: HealthInfo, +} diff --git a/dapps/src/apps/fetcher/mod.rs b/dapps/src/apps/fetcher/mod.rs index cbc2ab212..347799ccf 100644 --- a/dapps/src/apps/fetcher/mod.rs +++ b/dapps/src/apps/fetcher/mod.rs @@ -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(), diff --git a/dapps/src/handlers/async.rs b/dapps/src/handlers/async.rs new file mode 100644 index 000000000..d68c55cce --- /dev/null +++ b/dapps/src/handlers/async.rs @@ -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 . + +//! 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 { + Initial(F, M, Remote, Control), + Waiting(mpsc::Receiver>, M), + Done(ContentHandler), + Invalid, +} + +pub struct AsyncHandler { + state: State, +} + +impl AsyncHandler { + pub fn new(future: F, map: M, remote: Remote, control: Control) -> Self { + AsyncHandler { + state: State::Initial(future, map, remote, control), + } + } +} + +impl server::Handler for AsyncHandler, M> where + F: Future + Send + 'static, + M: FnOnce(Result, ()>) -> ContentHandler, + T: Send + 'static, + E: Send + 'static, +{ + fn on_request(&mut self, _request: server::Request) -> 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) -> 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) -> Next { + if let State::Done(ref mut handler) = self.state { + handler.on_response_writable(encoder) + } else { + Next::end() + } + } +} diff --git a/dapps/src/handlers/content.rs b/dapps/src/handlers/content.rs index 5e0658c23..700e9819e 100644 --- a/dapps/src/handlers/content.rs +++ b/dapps/src/handlers/content.rs @@ -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) } diff --git a/dapps/src/handlers/mod.rs b/dapps/src/handlers/mod.rs index 56b013800..41e787336 100644 --- a/dapps/src/handlers/mod.rs +++ b/dapps/src/handlers/mod.rs @@ -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}; diff --git a/dapps/src/lib.rs b/dapps/src/lib.rs index 0b93f4e39..7f7dbe0ac 100644 --- a/dapps/src/lib.rs +++ b/dapps/src/lib.rs @@ -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 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( + pub fn ui( + ntp_server: &str, + pool: CpuPool, remote: Remote, + dapps_domain: &str, registrar: Arc, sync_status: Arc, 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( + pub fn dapps( + ntp_server: &str, + pool: CpuPool, remote: Remote, ui_address: Option<(String, u16)>, dapps_path: PathBuf, extra_dapps: Vec, - dapps_domain: String, + dapps_domain: &str, registrar: Arc, sync_status: Arc, web_proxy_tokens: Arc, @@ -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) -> HashMap>> { +fn special_endpoints( + ntp_server: &str, + pool: CpuPool, + content_fetcher: Arc, + remote: Remote, + sync_status: Arc, +) -> HashMap>> { 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 } diff --git a/dapps/src/tests/helpers/mod.rs b/dapps/src/tests/helpers/mod.rs index 060420792..955c3c890 100644 --- a/dapps/src/tests/helpers/mod.rs +++ b/dapps/src/tests/helpers/mod.rs @@ -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) { pub fn serve_with_registrar_and_sync() -> (Server, Arc) { 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 { 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() } } - diff --git a/js/src/embed.js b/js/src/embed.js index b59f55cd1..5e8bf7ffe 100644 --- a/js/src/embed.js +++ b/js/src/embed.js @@ -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); diff --git a/js/src/redux/providers/status.js b/js/src/redux/providers/status.js index ef329f662..fa636498c 100644 --- a/js/src/redux/providers/status.js +++ b/js/src/redux/providers/status.js @@ -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 diff --git a/js/src/redux/providers/statusReducer.js b/js/src/redux/providers/statusReducer.js index 3a6968548..23da8616f 100644 --- a/js/src/redux/providers/statusReducer.js +++ b/js/src/redux/providers/statusReducer.js @@ -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), diff --git a/js/src/ui/Actionbar/actionbar.js b/js/src/ui/Actionbar/actionbar.js index d7274b999..7e5c40fbb 100644 --- a/js/src/ui/Actionbar/actionbar.js +++ b/js/src/ui/Actionbar/actionbar.js @@ -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 = { diff --git a/js/src/ui/StatusIndicator/index.js b/js/src/ui/StatusIndicator/index.js new file mode 100644 index 000000000..7d2978dc4 --- /dev/null +++ b/js/src/ui/StatusIndicator/index.js @@ -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 . + +export default from './statusIndicator'; diff --git a/js/src/ui/StatusIndicator/statusIndicator.css b/js/src/ui/StatusIndicator/statusIndicator.css new file mode 100644 index 000000000..fa8c0bfcf --- /dev/null +++ b/js/src/ui/StatusIndicator/statusIndicator.css @@ -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 . +*/ +.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; + } +} diff --git a/js/src/ui/StatusIndicator/statusIndicator.js b/js/src/ui/StatusIndicator/statusIndicator.js new file mode 100644 index 000000000..cacce2a36 --- /dev/null +++ b/js/src/ui/StatusIndicator/statusIndicator.js @@ -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 . + +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) ? ( + + { title.map(x => (
{ x }
)) } +
+ ) : null; + + return ( + + + { type === 'signal' && statuses.map(this.renderBar) } + + {tooltip} + + ); + } + + renderBar = (signal) => { + const idx = statuses.indexOf(this.props.status); + const isActive = statuses.indexOf(signal) <= idx; + const activeClass = isActive ? styles.active : ''; + + return ( + + ); + } +} diff --git a/js/src/ui/index.js b/js/src/ui/index.js index 1cb0e468b..7e07bad7b 100644 --- a/js/src/ui/index.js +++ b/js/src/ui/index.js @@ -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'; diff --git a/js/src/views/Accounts/accounts.js b/js/src/views/Accounts/accounts.js index a4f0e1126..2d37b7a4b 100644 --- a/js/src/views/Accounts/accounts.js +++ b/js/src/views/Accounts/accounts.js @@ -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 }; } diff --git a/js/src/views/Application/Status/status.js b/js/src/views/Application/Status/status.js index 303449363..f44f48eb3 100644 --- a/js/src/views/Application/Status/status.js +++ b/js/src/views/Application/Status/status.js @@ -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 (
@@ -44,13 +45,20 @@ class Status extends Component { { this.renderUpgradeButton() }
- +
+ +
+ + +
{ netChain }
-
- { netPeers.connected.toFormat() }/{ netPeers.max.toFormat() } peers -
); @@ -102,14 +110,7 @@ class Status extends Component { ); } - return ( -
- -
- ); + 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 diff --git a/js/src/views/Application/TabBar/tabBar.css b/js/src/views/Application/TabBar/tabBar.css index 2903f3ed3..fa8478f8b 100644 --- a/js/src/views/Application/TabBar/tabBar.css +++ b/js/src/views/Application/TabBar/tabBar.css @@ -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; } diff --git a/js/src/views/Application/TabBar/tabBar.js b/js/src/views/Application/TabBar/tabBar.js index 6388427e5..7ac0d8eac 100644 --- a/js/src/views/Application/TabBar/tabBar.js +++ b/js/src/views/Application/TabBar/tabBar.js @@ -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 (
+ +
+ +
+ { this.renderTabItems() } { 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 }; }; } diff --git a/js/src/views/Application/TabBar/tabBar.spec.js b/js/src/views/Application/TabBar/tabBar.spec.js index ee6845768..f629bca1f 100644 --- a/js/src/views/Application/TabBar/tabBar.spec.js +++ b/js/src/views/Application/TabBar/tabBar.spec.js @@ -37,6 +37,12 @@ function createStore () { nodeStatus: { nodeKind: { 'availability': 'personal' + }, + health: { + overall: { + status: 'ok', + message: [] + } } } }; diff --git a/js/src/views/ParityBar/parityBar.js b/js/src/views/ParityBar/parityBar.js index 618a1d05a..f6ae8d48e 100644 --- a/js/src/views/ParityBar/parityBar.js +++ b/js/src/views/ParityBar/parityBar.js @@ -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 (
+
+ ); +}; + +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] = [( + + ), ( + + )]; + + return ( + + + +
+ } + /> +
+
+
+ + } + details={ !sync.details ? yes : no } + item={ sync } + /> +
+
+ + } + details={ (peers.details || []).join('/') } + item={ peers } + /> +
+
+ + } + details={ `${time.details || 0} ms` } + item={ time } + /> +
+
+
+ + ); + } +} + +function mapStateToProps (state) { + return state.nodeStatus.health; +} + +export default connect( + mapStateToProps, + null +)(Health); diff --git a/js/src/views/Status/Health/index.js b/js/src/views/Status/Health/index.js new file mode 100644 index 000000000..3fefcb039 --- /dev/null +++ b/js/src/views/Status/Health/index.js @@ -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 . + +export default from './health'; diff --git a/js/src/views/Status/NodeStatus/nodeStatus.css b/js/src/views/Status/NodeStatus/nodeStatus.css index e96e3bc04..e96dad03d 100644 --- a/js/src/views/Status/NodeStatus/nodeStatus.css +++ b/js/src/views/Status/NodeStatus/nodeStatus.css @@ -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); diff --git a/js/src/views/Status/status.js b/js/src/views/Status/status.js index b1892631e..bac520a46 100644 --- a/js/src/views/Status/status.js +++ b/js/src/views/Status/status.js @@ -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 () => ( } >
+ diff --git a/js/src/views/SyncWarning/syncWarning.css b/js/src/views/SyncWarning/syncWarning.css index 828036499..324f7d3fa 100644 --- a/js/src/views/SyncWarning/syncWarning.css +++ b/js/src/views/SyncWarning/syncWarning.css @@ -58,3 +58,7 @@ margin: 0.5em 0; } } + +.status { + font-size: 4rem; +} diff --git a/js/src/views/SyncWarning/syncWarning.js b/js/src/views/SyncWarning/syncWarning.js index 67deff075..cf448a9bb 100644 --- a/js/src/views/SyncWarning/syncWarning.js +++ b/js/src/views/SyncWarning/syncWarning.js @@ -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 {
- - +
+ +
+ + { + health.overall.message.map(message => ( +

{ message }

+ )) + }
{ return { nodeStatus: { - syncing + health: { + overall: { + status: syncing ? 'needsAttention' : 'ok', + message: [] + } + } } }; } diff --git a/logger/src/lib.rs b/logger/src/lib.rs index b1999b2a7..7b57f383f 100644 --- a/logger/src/lib.rs +++ b/logger/src/lib.rs @@ -67,10 +67,11 @@ pub fn setup_log(config: &Config) -> Result, 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") { diff --git a/parity/cli/config.toml b/parity/cli/config.toml index c3617077e..39e5686f6 100644 --- a/parity/cli/config.toml +++ b/parity/cli/config.toml @@ -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 diff --git a/parity/cli/mod.rs b/parity/cli/mod.rs index c82d861c7..08e38bf19 100644 --- a/parity/cli/mod.rs +++ b/parity/cli/mod.rs @@ -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 = None, or |c: &Config| otry!(c.misc).logging.clone().map(Some), flag_log_file: Option = None, @@ -590,6 +592,7 @@ struct VM { #[derive(Default, Debug, PartialEq, Deserialize)] struct Misc { + ntp_server: Option, logging: Option, log_file: Option, color: Option, @@ -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), diff --git a/parity/cli/usage.txt b/parity/cli/usage.txt index ae184d446..e6fd5fdd7 100644 --- a/parity/cli/usage.txt +++ b/parity/cli/usage.txt @@ -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 diff --git a/parity/configuration.rs b/parity/configuration.rs index e55ae7409..793b2171c 100644 --- a/parity/configuration.rs +++ b/parity/configuration.rs @@ -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![]), diff --git a/parity/dapps.rs b/parity/dapps.rs index 7e1cf82c1..e796488b1 100644 --- a/parity/dapps.rs +++ b/parity/dapps.rs @@ -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, } @@ -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, pub contract_client: Arc, pub remote: parity_reactor::TokioRemote, + pub pool: CpuPool, pub fetch: FetchClient, pub signer: Arc, pub ui_address: Option<(String, u16)>, @@ -152,20 +156,22 @@ pub fn new(configuration: Configuration, deps: Dependencies) -> Result Result, String> { +pub fn new_ui(enabled: bool, ntp_server: &str, deps: Dependencies) -> Result, 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, - _dapps_domain: String, + _dapps_domain: &str, ) -> Result { 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 { 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, - dapps_domain: String, + dapps_domain: &str, ) -> Result { 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 { 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, )) } diff --git a/parity/main.rs b/parity/main.rs index 0287e4bdd..21a7cc2d4 100644 --- a/parity/main.rs +++ b/parity/main.rs @@ -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; diff --git a/parity/rpc.rs b/parity/rpc.rs index 8b15db09e..b79e23b79 100644 --- a/parity/rpc.rs +++ b/parity/rpc.rs @@ -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>, @@ -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![]), diff --git a/parity/run.rs b/parity/run.rs index ff424e440..98a595b65 100644 --- a/parity/run.rs +++ b/parity/run.rs @@ -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, pub warp_sync: bool, pub public_node: bool, @@ -237,7 +236,7 @@ fn execute_light(cmd: RunCmd, can_restart: bool, logger: Arc) -> 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) -> on_demand: on_demand.clone(), }); - let sync = light_sync.clone(); + struct LightSyncStatus(Arc); + 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) -> }; 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) -> 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) -> R let (sync, client) = (sync_provider.clone(), client.clone()); let contract_client = Arc::new(::dapps::FullRegistrar { client: client.clone() }); + struct SyncStatus(Arc, Arc, 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) -> 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 { diff --git a/util/fetch/src/client.rs b/util/fetch/src/client.rs index 18c8d87d9..4aa85bd34 100644 --- a/util/fetch/src/client.rs +++ b/util/fetch/src/client.rs @@ -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 for Error { fn from(error: reqwest::Error) -> Self { Error::Fetch(error)