openethereum/parity/main.rs

605 lines
18 KiB
Rust
Raw Normal View History

2016-02-05 13:40:41 +01:00
// Copyright 2015, 2016 Ethcore (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/>.
//! Ethcore client application.
#![warn(missing_docs)]
2016-03-11 11:16:49 +01:00
#![cfg_attr(feature="dev", feature(plugin))]
#![cfg_attr(feature="dev", plugin(clippy))]
2016-04-06 10:07:24 +02:00
#![cfg_attr(feature="dev", allow(useless_format))]
#![cfg_attr(feature="dev", allow(match_bool))]
2016-04-21 13:12:43 +02:00
2016-01-23 23:53:20 +01:00
extern crate docopt;
extern crate num_cpus;
2016-01-23 23:53:20 +01:00
extern crate rustc_serialize;
2015-12-22 22:19:50 +01:00
extern crate ethcore_util as util;
2016-01-07 16:08:12 +01:00
extern crate ethcore;
2016-01-29 15:56:06 +01:00
extern crate ethsync;
2016-02-18 12:42:01 +01:00
#[macro_use]
2016-01-31 11:08:04 +01:00
extern crate log as rlog;
2016-01-09 23:21:57 +01:00
extern crate env_logger;
extern crate ctrlc;
2016-02-05 13:49:36 +01:00
extern crate fdlimit;
#[cfg(not(windows))]
2016-02-18 13:10:04 +01:00
extern crate daemonize;
2016-02-23 20:14:37 +01:00
extern crate time;
2016-02-25 14:09:39 +01:00
extern crate number_prefix;
2016-03-09 14:11:15 +01:00
extern crate rpassword;
2016-04-10 15:12:20 +02:00
extern crate semver;
2016-04-13 18:03:57 +02:00
extern crate ethcore_ipc as ipc;
extern crate ethcore_ipc_nano as nanoipc;
#[macro_use]
extern crate hyper; // for price_info.rs
extern crate json_ipc_server as jsonipc;
2016-03-28 00:49:35 +02:00
extern crate ethcore_ipc_hypervisor as hypervisor;
2016-04-21 13:12:43 +02:00
extern crate ethcore_rpc;
extern crate ethcore_signer;
extern crate ansi_term;
#[macro_use]
extern crate lazy_static;
extern crate regex;
#[cfg(feature = "dapps")]
extern crate ethcore_dapps;
2016-04-21 13:12:43 +02:00
#[macro_use]
mod die;
2016-04-10 15:12:20 +02:00
mod upgrade;
2016-04-21 13:12:43 +02:00
mod setup_log;
mod rpc;
mod dapps;
mod informant;
mod io_handler;
2016-04-21 15:41:25 +02:00
mod cli;
mod configuration;
mod migration;
2016-05-27 13:03:00 +02:00
mod signer;
mod rpc_apis;
mod url;
use std::io::{Write, Read, BufReader, BufRead};
use std::ops::Deref;
use std::sync::Arc;
use std::path::Path;
2016-05-23 18:42:59 +02:00
use std::fs::File;
use std::str::{FromStr, from_utf8};
use std::thread::sleep;
use std::time::Duration;
use rustc_serialize::hex::FromHex;
use ctrlc::CtrlC;
use util::{H256, ToPretty, PayloadInfo, Bytes, Colour, Applyable, version, journaldb};
use util::panics::{MayPanic, ForwardPanic, PanicHandler};
use ethcore::client::{BlockID, BlockChainClient, ClientConfig, get_db_path, BlockImportError,
ChainNotify, Mode};
2016-07-01 21:40:54 +02:00
use ethcore::error::{ImportError};
use ethcore::service::ClientService;
use ethcore::spec::Spec;
use ethsync::EthSync;
2016-05-31 21:31:42 +02:00
use ethcore::miner::{Miner, MinerService, ExternalMiner};
use migration::migrate;
use informant::Informant;
use util::{Mutex, Condvar};
2016-03-28 00:49:35 +02:00
2016-04-21 13:12:43 +02:00
use die::*;
use cli::print_version;
2016-04-21 13:12:43 +02:00
use rpc::RpcServer;
use signer::{SignerServer, new_token};
use dapps::WebappServer;
use io_handler::ClientIoHandler;
2016-07-05 18:18:35 +02:00
use configuration::{Policy, Configuration};
2016-04-21 15:41:25 +02:00
fn main() {
let conf = Configuration::parse();
execute(conf);
}
2015-12-22 22:19:50 +01:00
2016-04-21 15:41:25 +02:00
fn execute(conf: Configuration) {
if conf.args.flag_version {
print_version();
return;
}
if conf.args.cmd_signer {
execute_signer(conf);
return;
}
let spec = conf.spec();
let client_config = conf.client_config(&spec);
execute_upgrades(&conf, &spec, &client_config);
2016-04-21 15:41:25 +02:00
if conf.args.cmd_daemon {
daemonize(&conf);
2016-04-21 15:41:25 +02:00
}
2016-04-21 15:41:25 +02:00
if conf.args.cmd_account {
execute_account_cli(conf);
return;
}
2016-06-21 17:50:22 +02:00
if conf.args.cmd_wallet {
execute_wallet_cli(conf);
return;
}
2016-05-23 09:51:36 +02:00
if conf.args.cmd_export {
execute_export(conf);
return;
}
if conf.args.cmd_import {
execute_import(conf);
return;
}
execute_client(conf, spec, client_config);
2016-02-10 18:11:10 +01:00
}
#[cfg(not(windows))]
fn daemonize(conf: &Configuration) {
use daemonize::Daemonize;
Daemonize::new()
.pid_file(conf.args.arg_pid_file.clone())
.chown_pid_file(true)
.start()
.unwrap_or_else(|e| die!("Couldn't daemonize; {}", e));
}
#[cfg(windows)]
fn daemonize(_conf: &Configuration) {
}
fn execute_upgrades(conf: &Configuration, spec: &Spec, client_config: &ClientConfig) {
2016-04-21 15:41:25 +02:00
match ::upgrade::upgrade(Some(&conf.path())) {
Ok(upgrades_applied) if upgrades_applied > 0 => {
println!("Executed {} upgrade scripts - ok", upgrades_applied);
},
Err(e) => {
die!("Error upgrading parity data: {:?}", e);
},
_ => {},
}
let db_path = get_db_path(Path::new(&conf.path()), client_config.pruning, spec.genesis_header().hash());
let result = migrate(&db_path, client_config.pruning);
if let Err(err) = result {
die_with_message(&format!("{}", err));
}
2016-04-21 15:41:25 +02:00
}
fn execute_client(conf: Configuration, spec: Spec, client_config: ClientConfig) {
2016-04-21 15:41:25 +02:00
// Setup panic handler
let panic_handler = PanicHandler::new_in_arc();
// Setup logging
let logger = setup_log::setup_log(&conf.args.flag_logging, conf.have_color(), &conf.args.flag_log_file);
2016-04-21 15:41:25 +02:00
// Raise fdlimit
unsafe { ::fdlimit::raise_fd_limit(); }
info!("Starting {}", format!("{}", version()).apply(Colour::White.bold()));
info!("Using state DB journalling strategy {}", match client_config.pruning {
journaldb::Algorithm::Archive => "archive",
journaldb::Algorithm::EarlyMerge => "light",
journaldb::Algorithm::OverlayRecent => "fast",
journaldb::Algorithm::RefCounted => "basic",
}.apply(Colour::White.bold()));
// Display warning about using experimental journaldb types
match client_config.pruning {
journaldb::Algorithm::EarlyMerge | journaldb::Algorithm::RefCounted => {
warn!("Your chosen strategy is {}! You can re-run with --pruning to change.", "unstable".apply(Colour::Red.bold()));
}
_ => {}
}
2016-04-21 15:41:25 +02:00
2016-06-22 21:32:26 +02:00
// Display warning about using unlock with signer
2016-06-24 14:20:39 +02:00
if conf.signer_enabled() && conf.args.flag_unlock.is_some() {
2016-06-22 21:32:26 +02:00
warn!("Using Trusted Signer and --unlock is not recommended!");
warn!("NOTE that Signer will not ask you to confirm transactions from unlocked account.");
}
2016-07-05 18:18:35 +02:00
// Check fork settings.
if conf.policy() != Policy::None {
warn!("Value given for --policy, yet no proposed forks exist. Ignoring.");
2016-07-05 18:18:35 +02:00
}
let net_settings = conf.net_settings(&spec);
let sync_config = conf.sync_config(&spec);
2016-04-21 15:41:25 +02:00
// Secret Store
let account_service = Arc::new(conf.account_service());
// Miner
let miner = Miner::new(conf.miner_options(), conf.gas_pricer(), conf.spec(), Some(account_service.clone()));
2016-06-26 22:48:09 +02:00
miner.set_author(conf.author().unwrap_or_default());
2016-04-21 15:41:25 +02:00
miner.set_gas_floor_target(conf.gas_floor_target());
2016-06-23 14:29:16 +02:00
miner.set_gas_ceil_target(conf.gas_ceil_target());
2016-04-21 15:41:25 +02:00
miner.set_extra_data(conf.extra_data());
miner.set_transactions_limit(conf.args.flag_tx_queue_size);
2016-04-21 15:41:25 +02:00
2016-05-31 21:38:07 +02:00
// Build client
let service = ClientService::start(
client_config,
spec,
Path::new(&conf.path()),
miner.clone(),
2016-05-31 21:38:07 +02:00
).unwrap_or_else(|e| die_with_error("Client", e));
panic_handler.forward_from(&service);
let client = service.client();
2016-04-21 17:32:53 +02:00
let external_miner = Arc::new(ExternalMiner::default());
2016-04-21 19:19:42 +02:00
let network_settings = Arc::new(conf.network_settings());
2016-04-21 15:41:25 +02:00
// Sync
let sync = EthSync::new(sync_config, client.clone(), net_settings)
.unwrap_or_else(|e| die_with_error("Sync", ethcore::error::Error::Util(e)));
service.set_notify(&(sync.clone() as Arc<ChainNotify>));
// if network is active by default
if match conf.mode() { Mode::Dark(..) => false, _ => !conf.args.flag_no_network } {
sync.start();
}
2016-04-21 15:41:25 +02:00
let deps_for_rpc_apis = Arc::new(rpc_apis::Dependencies {
signer_port: conf.signer_port(),
signer_queue: Arc::new(rpc_apis::ConfirmationsQueue::default()),
2016-04-21 15:41:25 +02:00
client: client.clone(),
sync: sync.clone(),
secret_store: account_service.clone(),
miner: miner.clone(),
2016-04-21 17:32:53 +02:00
external_miner: external_miner.clone(),
2016-04-21 19:19:42 +02:00
logger: logger.clone(),
settings: network_settings.clone(),
allow_pending_receipt_query: !conf.args.flag_geth,
net_service: sync.clone(),
2016-04-21 15:41:25 +02:00
});
let dependencies = rpc::Dependencies {
panic_handler: panic_handler.clone(),
apis: deps_for_rpc_apis.clone(),
};
// Setup http rpc
let rpc_server = rpc::new_http(rpc::HttpConfiguration {
enabled: network_settings.rpc_enabled,
interface: conf.rpc_interface(),
port: network_settings.rpc_port,
apis: conf.rpc_apis(),
cors: conf.rpc_cors(),
}, &dependencies);
// setup ipc rpc
let _ipc_server = rpc::new_ipc(conf.ipc_settings(), &dependencies);
debug!("IPC: {}", conf.ipc_settings());
if conf.args.flag_webapp { println!("WARNING: Flag -w/--webapp is deprecated. Dapps server is now on by default. Ignoring."); }
let dapps_server = dapps::new(dapps::Configuration {
2016-06-24 12:14:46 +02:00
enabled: conf.dapps_enabled(),
interface: conf.dapps_interface(),
port: conf.args.flag_dapps_port,
user: conf.args.flag_dapps_user.clone(),
pass: conf.args.flag_dapps_pass.clone(),
dapps_path: conf.directories().dapps,
}, dapps::Dependencies {
2016-04-23 12:29:12 +02:00
panic_handler: panic_handler.clone(),
apis: deps_for_rpc_apis.clone(),
2016-04-21 15:41:25 +02:00
});
2016-05-27 13:03:00 +02:00
// Set up a signer
let signer_server = signer::start(signer::Configuration {
2016-06-24 14:29:15 +02:00
enabled: conf.signer_enabled(),
2016-05-27 13:03:00 +02:00
port: conf.args.flag_signer_port,
signer_path: conf.directories().signer,
2016-05-27 13:03:00 +02:00
}, signer::Dependencies {
panic_handler: panic_handler.clone(),
apis: deps_for_rpc_apis.clone(),
2016-05-27 13:03:00 +02:00
});
2016-04-21 15:41:25 +02:00
// Register IO handler
let io_handler = Arc::new(ClientIoHandler {
2016-04-21 15:41:25 +02:00
client: service.client(),
info: Informant::new(conf.have_color()),
2016-04-21 15:41:25 +02:00
sync: sync.clone(),
accounts: account_service.clone(),
});
2016-06-17 12:58:28 +02:00
service.register_io_handler(io_handler).expect("Error registering IO handler");
2016-04-21 15:41:25 +02:00
if conf.args.cmd_ui {
2016-06-24 12:14:46 +02:00
if !conf.dapps_enabled() {
die_with_message("Cannot use UI command with Dapps turned off.");
}
url::open(&format!("http://{}:{}/", conf.dapps_interface(), conf.args.flag_dapps_port));
}
2016-04-21 15:41:25 +02:00
// Handle exit
2016-05-27 13:03:00 +02:00
wait_for_exit(panic_handler, rpc_server, dapps_server, signer_server);
2016-04-21 15:41:25 +02:00
}
2016-05-02 15:29:50 +02:00
fn flush_stdout() {
2016-05-04 15:22:22 +02:00
::std::io::stdout().flush().expect("stdout is flushable; qed");
2016-05-02 15:29:50 +02:00
}
2016-05-23 18:42:59 +02:00
enum DataFormat {
Hex,
Binary,
}
2016-05-23 09:51:36 +02:00
fn execute_export(conf: Configuration) {
// Setup panic handler
let panic_handler = PanicHandler::new_in_arc();
2016-06-29 17:16:58 +02:00
// Setup logging
let _logger = setup_log::setup_log(&conf.args.flag_logging, conf.have_color(), &conf.args.flag_log_file);
2016-05-23 09:51:36 +02:00
// Raise fdlimit
unsafe { ::fdlimit::raise_fd_limit(); }
let spec = conf.spec();
let client_config = conf.client_config(&spec);
// Build client
let service = ClientService::start(
client_config, spec, Path::new(&conf.path()), Arc::new(Miner::with_spec(conf.spec()))
2016-05-23 09:51:36 +02:00
).unwrap_or_else(|e| die_with_error("Client", e));
panic_handler.forward_from(&service);
let client = service.client();
// we have a client!
2016-05-24 13:31:03 +02:00
let parse_block_id = |s: &str, arg: &str| -> u64 {
2016-05-23 09:51:36 +02:00
if s == "latest" {
client.chain_info().best_block_number
} else if let Ok(n) = s.parse::<u64>() {
n
} else if let Ok(h) = H256::from_str(s) {
client.block_number(BlockID::Hash(h)).unwrap_or_else(|| {
2016-05-24 13:31:03 +02:00
die!("Unknown block hash passed to {} parameter: {:?}", arg, s);
2016-05-23 09:51:36 +02:00
})
} else {
2016-05-24 13:31:03 +02:00
die!("Invalid {} parameter given: {:?}", arg, s);
2016-05-23 09:51:36 +02:00
}
};
2016-05-24 13:31:03 +02:00
let from = parse_block_id(&conf.args.flag_from, "--from");
let to = parse_block_id(&conf.args.flag_to, "--to");
let format = match conf.args.flag_format {
Some(x) => match x.deref() {
"binary" | "bin" => DataFormat::Binary,
"hex" => DataFormat::Hex,
x => die!("Invalid --format parameter given: {:?}", x),
},
None if conf.args.arg_file.is_none() => DataFormat::Hex,
None => DataFormat::Binary,
2016-05-23 18:42:59 +02:00
};
2016-05-23 09:51:36 +02:00
2016-05-23 18:42:59 +02:00
let mut out: Box<Write> = if let Some(f) = conf.args.arg_file {
Box::new(File::create(&f).unwrap_or_else(|_| die!("Cannot write to file given: {}", f)))
} else {
Box::new(::std::io::stdout())
};
for i in from..(to + 1) {
let b = client.deref().block(BlockID::Number(i)).unwrap();
match format {
DataFormat::Binary => { out.write(&b).expect("Couldn't write to stream."); }
DataFormat::Hex => { out.write_fmt(format_args!("{}", b.pretty())).expect("Couldn't write to stream."); }
}
2016-05-23 09:51:36 +02:00
}
}
fn execute_import(conf: Configuration) {
// Setup panic handler
let panic_handler = PanicHandler::new_in_arc();
2016-06-29 17:16:58 +02:00
// Setup logging
let _logger = setup_log::setup_log(&conf.args.flag_logging, conf.have_color(), &conf.args.flag_log_file);
// Raise fdlimit
unsafe { ::fdlimit::raise_fd_limit(); }
let spec = conf.spec();
let client_config = conf.client_config(&spec);
// Build client
let service = ClientService::start(
client_config, spec, Path::new(&conf.path()), Arc::new(Miner::with_spec(conf.spec()))
).unwrap_or_else(|e| die_with_error("Client", e));
panic_handler.forward_from(&service);
let client = service.client();
let mut instream: Box<Read> = if let Some(ref f) = conf.args.arg_file {
let f = File::open(f).unwrap_or_else(|_| die!("Cannot open the file given: {}", f));
Box::new(f)
} else {
Box::new(::std::io::stdin())
};
2016-06-15 23:12:43 +02:00
const READAHEAD_BYTES: usize = 8;
let mut first_bytes: Bytes = vec![0; READAHEAD_BYTES];
let mut first_read = 0;
let format = match conf.args.flag_format {
Some(ref x) => match x.deref() {
"binary" | "bin" => DataFormat::Binary,
"hex" => DataFormat::Hex,
x => die!("Invalid --format parameter given: {:?}", x),
},
None => {
// autodetect...
first_read = instream.read(&mut(first_bytes[..])).unwrap_or_else(|_| die!("Error reading from the file/stream."));
match first_bytes[0] {
0xf9 => {
println!("Autodetected binary data format.");
DataFormat::Binary
}
_ => {
println!("Autodetected hex data format.");
DataFormat::Hex
}
}
}
};
let informant = Informant::new(conf.have_color());
let do_import = |bytes| {
while client.queue_info().is_full() { sleep(Duration::from_secs(1)); }
match client.import_block(bytes) {
Ok(_) => {}
2016-07-01 21:40:54 +02:00
Err(BlockImportError::Import(ImportError::AlreadyInChain)) => { trace!("Skipping block already in chain."); }
Err(e) => die!("Cannot import block: {:?}", e)
}
informant.tick(client.deref(), None);
};
match format {
DataFormat::Binary => {
loop {
2016-06-15 23:12:43 +02:00
let mut bytes: Bytes = if first_read > 0 {first_bytes.clone()} else {vec![0; READAHEAD_BYTES]};
let n = if first_read > 0 {first_read} else {instream.read(&mut(bytes[..])).unwrap_or_else(|_| die!("Error reading from the file/stream."))};
if n == 0 { break; }
first_read = 0;
let s = PayloadInfo::from(&(bytes[..])).unwrap_or_else(|e| die!("Invalid RLP in the file/stream: {:?}", e)).total();
bytes.resize(s, 0);
2016-06-15 23:12:43 +02:00
instream.read_exact(&mut(bytes[READAHEAD_BYTES..])).unwrap_or_else(|_| die!("Error reading from the file/stream."));
do_import(bytes);
}
}
DataFormat::Hex => {
for line in BufReader::new(instream).lines() {
let s = line.unwrap_or_else(|_| die!("Error reading from the file/stream."));
let s = if first_read > 0 {from_utf8(&first_bytes).unwrap().to_owned() + &(s[..])} else {s};
first_read = 0;
let bytes = FromHex::from_hex(&(s[..])).unwrap_or_else(|_| die!("Invalid hex in file/stream."));
do_import(bytes);
}
}
}
client.flush_queue();
}
fn execute_signer(conf: Configuration) {
if !conf.args.cmd_new_token {
die!("Unknown command.");
}
let path = conf.directories().signer;
new_token(path).unwrap_or_else(|e| {
die!("Error generating token: {:?}", e)
});
}
2016-04-21 15:41:25 +02:00
fn execute_account_cli(conf: Configuration) {
2016-06-20 15:20:55 +02:00
use ethcore::ethstore::{EthStore, import_accounts};
use ethcore::ethstore::dir::DiskDirectory;
use ethcore::account_provider::AccountProvider;
2016-04-21 15:41:25 +02:00
use rpassword::read_password;
let dir = Box::new(DiskDirectory::create(conf.keys_path()).unwrap());
let iterations = conf.keys_iterations();
let secret_store = AccountProvider::new(Box::new(EthStore::open_with_iterations(dir, iterations).unwrap()));
2016-04-21 15:41:25 +02:00
if conf.args.cmd_new {
println!("Please note that password is NOT RECOVERABLE.");
print!("Type password: ");
2016-05-02 15:29:50 +02:00
flush_stdout();
2016-04-21 15:41:25 +02:00
let password = read_password().unwrap();
print!("Repeat password: ");
2016-05-02 15:29:50 +02:00
flush_stdout();
2016-04-21 15:41:25 +02:00
let password_repeat = read_password().unwrap();
if password != password_repeat {
println!("Passwords do not match!");
return;
}
println!("New account address:");
let new_address = secret_store.new_account(&password).unwrap();
println!("{:?}", new_address);
return;
}
2016-04-21 15:41:25 +02:00
if conf.args.cmd_list {
println!("Known addresses:");
for addr in &secret_store.accounts() {
2016-04-21 15:41:25 +02:00
println!("{:?}", addr);
}
return;
}
if conf.args.cmd_import {
let to = DiskDirectory::create(conf.keys_path()).unwrap();
let mut imported = 0;
for path in &conf.args.arg_path {
let from = DiskDirectory::at(path);
imported += import_accounts(&from, &to).unwrap_or_else(|e| die!("Could not import accounts {}", e)).len();
}
println!("Imported {} keys", imported);
2016-04-21 15:41:25 +02:00
}
}
2016-06-21 17:50:22 +02:00
fn execute_wallet_cli(conf: Configuration) {
2016-06-23 12:04:54 +02:00
use ethcore::ethstore::{PresaleWallet, EthStore};
2016-06-21 17:50:22 +02:00
use ethcore::ethstore::dir::DiskDirectory;
use ethcore::account_provider::AccountProvider;
let wallet_path = conf.args.arg_path.first().unwrap();
let filename = conf.args.flag_password.first().unwrap();
let mut file = File::open(filename).unwrap_or_else(|_| die!("{} Unable to read password file.", filename));
let mut file_content = String::new();
file.read_to_string(&mut file_content).unwrap_or_else(|_| die!("{} Unable to read password file.", filename));
let dir = Box::new(DiskDirectory::create(conf.keys_path()).unwrap());
let iterations = conf.keys_iterations();
let store = AccountProvider::new(Box::new(EthStore::open_with_iterations(dir, iterations).unwrap()));
// remove eof
let pass = &file_content[..file_content.len() - 1];
let wallet = PresaleWallet::open(wallet_path).unwrap_or_else(|_| die!("Unable to open presale wallet."));
let kp = wallet.decrypt(pass).unwrap_or_else(|_| die!("Invalid password"));
let address = store.insert_account(kp.secret().clone(), pass).unwrap();
println!("Imported account: {}", address);
}
2016-05-27 13:03:00 +02:00
fn wait_for_exit(
panic_handler: Arc<PanicHandler>,
_rpc_server: Option<RpcServer>,
_dapps_server: Option<WebappServer>,
_signer_server: Option<SignerServer>
) {
2016-04-21 15:41:25 +02:00
let exit = Arc::new(Condvar::new());
// Handle possible exits
let e = exit.clone();
CtrlC::set_handler(move || { e.notify_all(); });
// Handle panics
let e = exit.clone();
panic_handler.on_panic(move |_reason| { e.notify_all(); });
// Wait for signal
let mutex = Mutex::new(());
let _ = exit.wait(&mut mutex.lock());
2016-04-21 15:41:25 +02:00
info!("Finishing work, please wait...");
2016-02-10 18:11:10 +01:00
}
/// Parity needs at least 1 test to generate coverage reports correctly.
#[test]
fn if_works() {
}