2021-02-19 21:04:58 +00:00
|
|
|
// With the default subsystem, 'console', windows creates an additional console window for the program.
|
|
|
|
// This is silently ignored on non-windows systems.
|
|
|
|
// See https://msdn.microsoft.com/en-us/library/4cc7ya5b.aspx for more details.
|
2019-12-08 11:32:51 +00:00
|
|
|
#![windows_subsystem = "windows"]
|
|
|
|
|
2021-04-16 21:12:06 +00:00
|
|
|
use std::path::Path;
|
2021-01-30 13:18:37 +00:00
|
|
|
use std::sync::{Arc, Mutex};
|
2021-03-10 21:21:50 +00:00
|
|
|
use std::time::Duration;
|
2021-06-09 18:36:36 +00:00
|
|
|
use std::{env, thread};
|
2021-01-30 13:18:37 +00:00
|
|
|
|
2021-06-09 18:36:36 +00:00
|
|
|
use getopts::{Matches, Options};
|
2021-02-21 20:56:56 +00:00
|
|
|
#[allow(unused_imports)]
|
2021-03-29 12:58:35 +00:00
|
|
|
use log::{debug, error, info, trace, warn, LevelFilter};
|
2021-04-17 14:45:49 +00:00
|
|
|
use simplelog::*;
|
2021-03-10 21:21:50 +00:00
|
|
|
#[cfg(windows)]
|
2021-06-09 18:36:36 +00:00
|
|
|
use winapi::um::wincon::{AttachConsole, FreeConsole, ATTACH_PARENT_PROCESS};
|
2021-05-09 22:49:01 +00:00
|
|
|
extern crate lazy_static;
|
2021-03-10 21:21:50 +00:00
|
|
|
|
2021-09-21 13:25:42 +00:00
|
|
|
use std::fs::{File, OpenOptions};
|
|
|
|
use std::io::{Seek, SeekFrom, Write};
|
2021-06-09 18:36:36 +00:00
|
|
|
use std::process::exit;
|
2021-05-09 21:33:11 +00:00
|
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
2021-06-09 18:36:36 +00:00
|
|
|
|
|
|
|
use alfis::event::Event;
|
2021-11-20 15:11:05 +00:00
|
|
|
use alfis::eventbus::{post, register};
|
2021-06-09 18:36:36 +00:00
|
|
|
use alfis::keystore::create_key;
|
|
|
|
use alfis::{
|
|
|
|
dns_utils, Block, Bytes, Chain, Context, Keystore, Miner, Network, Settings, Transaction, ALFIS_DEBUG, DB_NAME, ORIGIN_DIFFICULTY
|
|
|
|
};
|
2019-12-01 21:45:25 +00:00
|
|
|
|
2021-03-19 14:20:18 +00:00
|
|
|
#[cfg(feature = "webgui")]
|
2021-03-10 21:21:50 +00:00
|
|
|
mod web_ui;
|
2021-01-30 13:18:37 +00:00
|
|
|
|
2021-03-06 21:40:19 +00:00
|
|
|
const SETTINGS_FILENAME: &str = "alfis.toml";
|
2021-03-06 20:28:06 +00:00
|
|
|
const LOG_TARGET_MAIN: &str = "alfis::Main";
|
2021-01-14 17:34:43 +00:00
|
|
|
|
2019-12-01 21:45:25 +00:00
|
|
|
fn main() {
|
2022-01-03 21:06:41 +00:00
|
|
|
#[allow(unused_assignments, unused_mut)]
|
2021-10-25 15:22:50 +00:00
|
|
|
let mut console_attached = true;
|
2021-02-19 21:04:58 +00:00
|
|
|
// When linked with the windows subsystem windows won't automatically attach
|
|
|
|
// to the console of the parent process, so we do it explicitly. This fails silently if the parent has no console.
|
|
|
|
#[cfg(windows)]
|
|
|
|
unsafe {
|
2021-10-25 15:22:50 +00:00
|
|
|
console_attached = AttachConsole(ATTACH_PARENT_PROCESS) != 0;
|
2021-03-29 12:58:35 +00:00
|
|
|
#[cfg(feature = "webgui")]
|
2021-03-18 14:09:26 +00:00
|
|
|
winapi::um::shellscalingapi::SetProcessDpiAwareness(2);
|
2021-02-19 21:04:58 +00:00
|
|
|
}
|
|
|
|
|
2021-02-19 15:41:43 +00:00
|
|
|
let args: Vec<String> = env::args().collect();
|
|
|
|
let program = args[0].clone();
|
|
|
|
|
|
|
|
let mut opts = Options::new();
|
2021-02-27 17:57:15 +00:00
|
|
|
opts.optflag("h", "help", "Print this help menu");
|
2021-03-19 14:20:18 +00:00
|
|
|
opts.optflag("n", "nogui", "Run without graphic user interface (default for no gui builds)");
|
2021-04-17 18:37:20 +00:00
|
|
|
opts.optflag("v", "version", "Print version and exit");
|
2021-02-27 17:57:15 +00:00
|
|
|
opts.optflag("d", "debug", "Show trace messages, more than debug");
|
2021-04-17 14:45:49 +00:00
|
|
|
opts.optflag("b", "blocks", "List blocks from DB and exit");
|
2021-04-03 12:57:56 +00:00
|
|
|
opts.optflag("g", "generate", "Generate new config file. Generated config will be printed to console.");
|
2021-05-09 21:33:11 +00:00
|
|
|
opts.optopt("k", "gen-key", "Generate new keys and save them to file.", "FILE");
|
2021-04-17 14:45:49 +00:00
|
|
|
opts.optopt("l", "log", "Write log to file", "FILE");
|
2021-09-21 13:25:42 +00:00
|
|
|
opts.optopt("s", "status", "Write status to file", "FILE");
|
2021-04-03 12:57:56 +00:00
|
|
|
opts.optopt("c", "config", "Path to config file", "FILE");
|
2021-04-16 21:12:06 +00:00
|
|
|
opts.optopt("w", "work-dir", "Path to working directory", "DIRECTORY");
|
2021-04-03 12:57:56 +00:00
|
|
|
opts.optopt("u", "upgrade", "Path to config file that you want to upgrade. Upgraded config will be printed to console.", "FILE");
|
2021-02-19 15:41:43 +00:00
|
|
|
|
|
|
|
let opt_matches = match opts.parse(&args[1..]) {
|
|
|
|
Ok(m) => m,
|
2021-06-09 18:36:36 +00:00
|
|
|
Err(f) => panic!("{}", f.to_string())
|
2021-02-19 15:41:43 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
if opt_matches.opt_present("h") {
|
|
|
|
let brief = format!("Usage: {} [options]", program);
|
2021-04-03 12:57:56 +00:00
|
|
|
println!("{}", opts.usage(&brief));
|
2021-04-17 18:37:20 +00:00
|
|
|
exit(0);
|
|
|
|
}
|
|
|
|
|
|
|
|
if opt_matches.opt_present("v") {
|
|
|
|
println!("ALFIS v{}", env!("CARGO_PKG_VERSION"));
|
|
|
|
exit(0);
|
2021-02-19 15:41:43 +00:00
|
|
|
}
|
|
|
|
|
2021-04-03 12:57:56 +00:00
|
|
|
if opt_matches.opt_present("g") {
|
|
|
|
println!("{}", include_str!("../alfis.toml"));
|
2021-04-17 18:37:20 +00:00
|
|
|
exit(0);
|
2021-04-03 12:57:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
match opt_matches.opt_str("u") {
|
|
|
|
None => {}
|
|
|
|
Some(path) => {
|
|
|
|
if let Some(settings) = Settings::load(&path) {
|
|
|
|
let string = toml::to_string(&settings).unwrap();
|
|
|
|
println!("{}", &string);
|
|
|
|
} else {
|
|
|
|
println!("Error loading config for upgrade!");
|
|
|
|
}
|
2021-04-03 19:09:55 +00:00
|
|
|
return;
|
2021-04-03 12:57:56 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-03-19 14:20:18 +00:00
|
|
|
#[cfg(feature = "webgui")]
|
2021-02-19 15:41:43 +00:00
|
|
|
let no_gui = opt_matches.opt_present("n");
|
2021-03-19 14:20:18 +00:00
|
|
|
#[cfg(not(feature = "webgui"))]
|
2021-03-19 10:37:49 +00:00
|
|
|
let no_gui = true;
|
2021-02-19 15:41:43 +00:00
|
|
|
|
2021-04-21 12:38:37 +00:00
|
|
|
if let Some(path) = opt_matches.opt_str("w") {
|
2021-12-25 17:40:36 +00:00
|
|
|
env::set_current_dir(Path::new(&path)).unwrap_or_else(|_| panic!("Unable to change working directory to '{}'", &path));
|
2021-04-21 12:38:37 +00:00
|
|
|
}
|
2021-02-22 09:11:22 +00:00
|
|
|
let config_name = match opt_matches.opt_str("c") {
|
2021-06-09 18:36:36 +00:00
|
|
|
None => SETTINGS_FILENAME.to_owned(),
|
|
|
|
Some(path) => path
|
2021-02-22 09:11:22 +00:00
|
|
|
};
|
2021-04-16 21:12:06 +00:00
|
|
|
|
2021-10-25 15:22:50 +00:00
|
|
|
setup_logger(&opt_matches, console_attached);
|
2021-09-21 13:25:42 +00:00
|
|
|
if let Some(status) = opt_matches.opt_str("s") {
|
|
|
|
register(move |_, event| {
|
2021-12-25 17:40:36 +00:00
|
|
|
// TODO optimize for same data
|
|
|
|
if let Event::NetworkStatus { blocks, domains, keys, nodes } = event {
|
|
|
|
match File::create(Path::new(&status)) {
|
|
|
|
Ok(mut f) => {
|
|
|
|
let data = format!("{{ \"blocks\":{}, \"domains\":{}, \"keys\":{}, \"nodes\":{} }}", blocks, domains, keys, nodes);
|
|
|
|
f.write_all(data.as_bytes()).expect("Error writing status file!");
|
|
|
|
let _ = f.flush();
|
2021-09-21 13:25:42 +00:00
|
|
|
}
|
2021-12-25 17:40:36 +00:00
|
|
|
Err(_) => { error!("Error writing status file!"); }
|
2021-09-21 13:25:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
true
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-03-06 20:28:06 +00:00
|
|
|
info!(target: LOG_TARGET_MAIN, "Starting ALFIS {}", env!("CARGO_PKG_VERSION"));
|
2021-02-20 15:28:10 +00:00
|
|
|
|
2021-12-25 17:40:36 +00:00
|
|
|
let settings = Settings::load(&config_name).unwrap_or_else(|| panic!("Cannot load settings from {}!", &config_name));
|
2021-04-26 19:49:01 +00:00
|
|
|
debug!(target: LOG_TARGET_MAIN, "Loaded settings: {:?}", &settings);
|
2021-05-09 21:33:11 +00:00
|
|
|
let chain: Chain = Chain::new(&settings, DB_NAME);
|
2021-04-17 14:45:49 +00:00
|
|
|
if opt_matches.opt_present("b") {
|
2021-04-20 18:54:45 +00:00
|
|
|
for i in 1..(chain.get_height() + 1) {
|
2021-03-10 21:21:50 +00:00
|
|
|
if let Some(block) = chain.get_block(i) {
|
2021-03-06 20:28:06 +00:00
|
|
|
info!(target: LOG_TARGET_MAIN, "{:?}", &block);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
2021-05-21 21:32:46 +00:00
|
|
|
info!("Blocks count: {}, domains count: {}, users count: {}", chain.get_height(), chain.get_domains_count(), chain.get_users_count());
|
2021-02-19 15:41:43 +00:00
|
|
|
let settings_copy = settings.clone();
|
2021-05-14 12:14:45 +00:00
|
|
|
let mut keys = Vec::new();
|
2021-12-25 17:40:36 +00:00
|
|
|
if !settings.key_files.is_empty() {
|
2021-05-14 12:14:45 +00:00
|
|
|
for name in &settings.key_files {
|
|
|
|
match Keystore::from_file(name, "") {
|
2021-06-09 18:36:36 +00:00
|
|
|
None => {
|
|
|
|
warn!("Error loading keyfile from {}", name);
|
|
|
|
}
|
2021-05-14 12:14:45 +00:00
|
|
|
Some(keystore) => {
|
|
|
|
info!("Successfully loaded keyfile {}", name);
|
|
|
|
keys.push(keystore);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let context = Context::new(env!("CARGO_PKG_VERSION").to_owned(), settings, keys, chain);
|
2021-03-23 17:55:11 +00:00
|
|
|
let context: Arc<Mutex<Context>> = Arc::new(Mutex::new(context));
|
2021-05-09 21:33:11 +00:00
|
|
|
|
|
|
|
// If we just need to generate keys
|
|
|
|
if let Some(filename) = opt_matches.opt_str("k") {
|
|
|
|
info!(target: LOG_TARGET_MAIN, "Generating keys...");
|
|
|
|
let mining = Arc::new(AtomicBool::new(true));
|
|
|
|
let mining_copy = Arc::clone(&mining);
|
|
|
|
let context_copy = Arc::clone(&context);
|
2021-05-09 22:49:01 +00:00
|
|
|
// Register key-mined event listener
|
|
|
|
register(move |_uuid, e| {
|
2021-06-09 18:36:36 +00:00
|
|
|
if matches!(e, Event::KeyCreated { .. }) {
|
2021-05-09 22:49:01 +00:00
|
|
|
let context_copy = Arc::clone(&context_copy);
|
|
|
|
let mining_copy = Arc::clone(&mining_copy);
|
|
|
|
let filename = filename.clone();
|
|
|
|
thread::spawn(move || {
|
2021-05-14 12:14:45 +00:00
|
|
|
if let Some(keystore) = context_copy.lock().unwrap().get_keystore_mut() {
|
2021-05-09 22:49:01 +00:00
|
|
|
keystore.save(&filename, "");
|
|
|
|
mining_copy.store(false, Ordering::Relaxed);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
false
|
|
|
|
} else {
|
|
|
|
true
|
|
|
|
}
|
|
|
|
});
|
2021-05-09 21:33:11 +00:00
|
|
|
// Start key mining
|
|
|
|
create_key(context);
|
|
|
|
|
|
|
|
let delay = Duration::from_secs(1);
|
|
|
|
while mining.load(Ordering::Relaxed) {
|
|
|
|
thread::sleep(delay);
|
|
|
|
}
|
|
|
|
exit(0);
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Ok(mut context) = context.lock() {
|
|
|
|
context.chain.check_chain(settings_copy.check_blocks);
|
|
|
|
match context.chain.get_block(1) {
|
2021-06-09 18:36:36 +00:00
|
|
|
None => {
|
|
|
|
info!(target: LOG_TARGET_MAIN, "No blocks found in DB");
|
|
|
|
}
|
|
|
|
Some(block) => {
|
|
|
|
trace!(target: LOG_TARGET_MAIN, "Loaded DB with origin {:?}", &block.hash);
|
|
|
|
}
|
2021-05-09 21:33:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-20 15:11:05 +00:00
|
|
|
let dns_server_ok = if settings_copy.dns.threads > 0 {
|
|
|
|
dns_utils::start_dns_server(&context, &settings_copy)
|
|
|
|
} else {
|
|
|
|
true
|
|
|
|
};
|
2020-04-18 19:31:40 +00:00
|
|
|
|
2021-03-23 10:41:50 +00:00
|
|
|
let mut miner_obj = Miner::new(Arc::clone(&context));
|
2021-01-14 17:34:43 +00:00
|
|
|
miner_obj.start_mining_thread();
|
|
|
|
let miner: Arc<Mutex<Miner>> = Arc::new(Mutex::new(miner_obj));
|
2020-04-18 19:31:40 +00:00
|
|
|
|
2021-03-23 10:41:50 +00:00
|
|
|
let mut network = Network::new(Arc::clone(&context));
|
2022-01-16 22:39:41 +00:00
|
|
|
let network = thread::Builder::new().name(String::from("Network")).spawn(move || {
|
2021-05-29 22:33:13 +00:00
|
|
|
// Give UI some time to appear :)
|
|
|
|
thread::sleep(Duration::from_millis(1000));
|
|
|
|
network.start();
|
2022-01-16 22:39:41 +00:00
|
|
|
}).expect("Could not start network thread!");
|
2021-02-05 21:24:28 +00:00
|
|
|
|
2021-01-14 17:34:43 +00:00
|
|
|
create_genesis_if_needed(&context, &miner);
|
2021-02-19 15:41:43 +00:00
|
|
|
if no_gui {
|
2021-04-15 10:21:41 +00:00
|
|
|
print_my_domains(&context);
|
2022-01-16 22:39:41 +00:00
|
|
|
let _ = network.join();
|
2021-02-19 15:41:43 +00:00
|
|
|
} else {
|
2021-11-20 15:11:05 +00:00
|
|
|
if !dns_server_ok {
|
|
|
|
thread::spawn(|| {
|
|
|
|
thread::sleep(Duration::from_millis(500));
|
|
|
|
post(Event::Error { text: String::from("Error starting DNS-server. Please, check that it’s port is not busy.") });
|
|
|
|
});
|
|
|
|
}
|
2021-03-19 14:20:18 +00:00
|
|
|
#[cfg(feature = "webgui")]
|
2021-12-25 17:40:36 +00:00
|
|
|
web_ui::run_interface(Arc::clone(&context), miner);
|
2021-02-19 15:41:43 +00:00
|
|
|
}
|
2021-02-19 21:04:58 +00:00
|
|
|
|
|
|
|
// Without explicitly detaching the console cmd won't redraw it's prompt.
|
|
|
|
#[cfg(windows)]
|
|
|
|
unsafe {
|
|
|
|
FreeConsole();
|
|
|
|
}
|
2021-02-19 15:41:43 +00:00
|
|
|
}
|
|
|
|
|
2021-04-17 14:45:49 +00:00
|
|
|
/// Sets up logger in accordance with command line options
|
2021-10-25 15:22:50 +00:00
|
|
|
fn setup_logger(opt_matches: &Matches, console_attached: bool) {
|
2021-04-17 14:45:49 +00:00
|
|
|
let mut level = LevelFilter::Info;
|
2021-04-21 15:05:07 +00:00
|
|
|
if opt_matches.opt_present("d") || env::var(ALFIS_DEBUG).is_ok() {
|
2021-04-17 14:45:49 +00:00
|
|
|
level = LevelFilter::Trace;
|
|
|
|
}
|
|
|
|
let config = ConfigBuilder::new()
|
|
|
|
.add_filter_ignore_str("mio::poll")
|
2021-09-05 17:05:30 +00:00
|
|
|
.add_filter_ignore_str("rustls::client")
|
|
|
|
.add_filter_ignore_str("ureq::")
|
2021-09-09 16:18:03 +00:00
|
|
|
.set_thread_level(LevelFilter::Error)
|
2021-04-17 14:45:49 +00:00
|
|
|
.set_location_level(LevelFilter::Off)
|
|
|
|
.set_target_level(LevelFilter::Error)
|
|
|
|
.set_time_level(LevelFilter::Error)
|
2021-09-09 16:18:03 +00:00
|
|
|
.set_time_format_str("%F %T%.3f")
|
2021-04-17 14:45:49 +00:00
|
|
|
.set_time_to_local(true)
|
2021-11-15 16:38:12 +00:00
|
|
|
.set_level_padding(LevelPadding::Right)
|
2021-04-17 14:45:49 +00:00
|
|
|
.build();
|
|
|
|
match opt_matches.opt_str("l") {
|
|
|
|
None => {
|
2021-10-25 15:22:50 +00:00
|
|
|
if console_attached {
|
|
|
|
if let Err(e) = TermLogger::init(level, config, TerminalMode::Stdout, ColorChoice::Auto) {
|
|
|
|
println!("Unable to initialize logger!\n{}", e);
|
|
|
|
}
|
2021-04-17 14:45:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Some(path) => {
|
|
|
|
let file = match OpenOptions::new().write(true).create(true).open(&path) {
|
|
|
|
Ok(mut file) => {
|
|
|
|
file.seek(SeekFrom::End(0)).unwrap();
|
|
|
|
file
|
|
|
|
}
|
|
|
|
Err(e) => {
|
|
|
|
println!("Could not open log file '{}' for writing!\n{}", &path, e);
|
|
|
|
exit(1);
|
|
|
|
}
|
|
|
|
};
|
2021-10-25 15:22:50 +00:00
|
|
|
if console_attached {
|
|
|
|
CombinedLogger::init(vec![
|
|
|
|
TermLogger::new(level, config.clone(), TerminalMode::Stdout, ColorChoice::Auto),
|
|
|
|
WriteLogger::new(level, config, file),
|
|
|
|
])
|
|
|
|
.unwrap();
|
2021-12-25 17:40:36 +00:00
|
|
|
} else if let Err(e) = WriteLogger::init(level, config, file) {
|
|
|
|
println!("Unable to initialize logger!\n{}", e);
|
2021-10-25 15:22:50 +00:00
|
|
|
}
|
2021-04-17 14:45:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Gets own domains by current loaded keystore and writes them to log
|
2021-04-15 10:21:41 +00:00
|
|
|
fn print_my_domains(context: &Arc<Mutex<Context>>) {
|
|
|
|
let context = context.lock().unwrap();
|
2021-05-14 12:14:45 +00:00
|
|
|
let domains = context.chain.get_my_domains(context.get_keystore());
|
2021-04-15 10:21:41 +00:00
|
|
|
debug!("Domains: {:?}", &domains);
|
|
|
|
}
|
|
|
|
|
2021-04-17 14:45:49 +00:00
|
|
|
/// Creates genesis (origin) block if `origin` is empty in config and we don't have any blocks in DB
|
2021-01-14 17:34:43 +00:00
|
|
|
fn create_genesis_if_needed(context: &Arc<Mutex<Context>>, miner: &Arc<Mutex<Miner>>) {
|
2021-02-13 22:37:44 +00:00
|
|
|
// If there is no origin in settings and no blockchain in DB, generate genesis block
|
|
|
|
let context = context.lock().unwrap();
|
2021-03-10 21:21:50 +00:00
|
|
|
let last_block = context.get_chain().last_block();
|
2021-02-13 22:37:44 +00:00
|
|
|
let origin = context.settings.origin.clone();
|
2021-04-10 07:47:21 +00:00
|
|
|
if origin.is_empty() && last_block.is_none() {
|
2021-05-14 12:14:45 +00:00
|
|
|
if let Some(keystore) = context.get_keystore() {
|
2021-03-23 17:55:11 +00:00
|
|
|
// If blockchain is empty, we are going to mine a Genesis block
|
2021-05-04 14:47:03 +00:00
|
|
|
let transaction = Transaction::origin(Chain::get_zones_hash(), keystore.get_public(), keystore.get_encryption_public());
|
|
|
|
let block = Block::new(Some(transaction), keystore.get_public(), Bytes::default(), ORIGIN_DIFFICULTY);
|
2021-03-23 17:55:11 +00:00
|
|
|
miner.lock().unwrap().add_block(block, keystore.clone());
|
|
|
|
}
|
2021-01-17 23:18:35 +00:00
|
|
|
}
|
|
|
|
}
|
2021-01-14 17:34:43 +00:00
|
|
|
|
2021-02-19 15:41:43 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use alfis::dns::protocol::{DnsRecord, TransientTtl};
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn record_to_string() {
|
|
|
|
let record = DnsRecord::A {
|
|
|
|
domain: "google.com".to_string(),
|
|
|
|
addr: "127.0.0.1".parse().unwrap(),
|
|
|
|
ttl: TransientTtl(300)
|
|
|
|
};
|
|
|
|
println!("Record is {:?}", &record);
|
|
|
|
println!("Record in JSON is {}", serde_json::to_string(&record).unwrap());
|
|
|
|
}
|
|
|
|
}
|