514 lines
18 KiB
Rust
514 lines
18 KiB
Rust
#![windows_subsystem = "windows"]
|
|
|
|
use std::{error::Error, io::Write, path::PathBuf, process, str::FromStr};
|
|
|
|
use iced::{executor, Application, Command, Settings, Subscription};
|
|
use tracing::{error, info};
|
|
use tracing_subscriber::filter::LevelFilter;
|
|
extern crate serde;
|
|
extern crate serde_json;
|
|
|
|
use liana::{config::Config as DaemonConfig, miniscript::bitcoin};
|
|
use liana_ui::{component::text, font, image, theme, widget::Element};
|
|
|
|
use liana_gui::{
|
|
app::{
|
|
self,
|
|
config::{default_datadir, ConfigError},
|
|
App,
|
|
},
|
|
installer::{self, Installer},
|
|
launcher::{self, Launcher},
|
|
loader::{self, Loader},
|
|
logger::Logger,
|
|
VERSION,
|
|
};
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
enum Arg {
|
|
ConfigPath(PathBuf),
|
|
DatadirPath(PathBuf),
|
|
Network(bitcoin::Network),
|
|
}
|
|
|
|
fn parse_args(args: Vec<String>) -> Result<Vec<Arg>, Box<dyn Error>> {
|
|
let mut res = Vec::new();
|
|
|
|
if args.len() > 1 && (args[1] == "--version" || args[1] == "-v") {
|
|
eprintln!("{}", VERSION);
|
|
process::exit(1);
|
|
}
|
|
|
|
for (i, arg) in args.iter().enumerate() {
|
|
if arg == "--conf" {
|
|
if let Some(a) = args.get(i + 1) {
|
|
res.push(Arg::ConfigPath(PathBuf::from(a)));
|
|
} else {
|
|
return Err("missing arg to --conf".into());
|
|
}
|
|
} else if arg == "--datadir" {
|
|
if let Some(a) = args.get(i + 1) {
|
|
res.push(Arg::DatadirPath(PathBuf::from(a)));
|
|
} else {
|
|
return Err("missing arg to --datadir".into());
|
|
}
|
|
} else if arg.contains("--") {
|
|
let network = bitcoin::Network::from_str(args[i].trim_start_matches("--"))?;
|
|
res.push(Arg::Network(network));
|
|
}
|
|
}
|
|
|
|
Ok(res)
|
|
}
|
|
|
|
pub struct GUI {
|
|
state: State,
|
|
logger: Logger,
|
|
// if set up, it overrides the level filter of the logger.
|
|
log_level: Option<LevelFilter>,
|
|
}
|
|
|
|
enum State {
|
|
Launcher(Box<Launcher>),
|
|
Installer(Box<Installer>),
|
|
Loader(Box<Loader>),
|
|
App(App),
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum Message {
|
|
CtrlC,
|
|
Launch(Box<launcher::Message>),
|
|
Install(Box<installer::Message>),
|
|
Load(Box<loader::Message>),
|
|
Run(Box<app::Message>),
|
|
Event(iced_native::Event),
|
|
}
|
|
|
|
async fn ctrl_c() -> Result<(), ()> {
|
|
if let Err(e) = tokio::signal::ctrl_c().await {
|
|
error!("{}", e);
|
|
};
|
|
info!("Signal received, exiting");
|
|
Ok(())
|
|
}
|
|
|
|
impl Application for GUI {
|
|
type Executor = executor::Default;
|
|
type Message = Message;
|
|
type Flags = (Config, Option<LevelFilter>);
|
|
type Theme = theme::Theme;
|
|
|
|
fn title(&self) -> String {
|
|
match self.state {
|
|
State::Installer(_) => String::from("Liana Installer"),
|
|
_ => String::from("Liana"),
|
|
}
|
|
}
|
|
|
|
fn new((config, log_level): (Config, Option<LevelFilter>)) -> (GUI, Command<Self::Message>) {
|
|
let logger = Logger::setup(log_level.unwrap_or(LevelFilter::INFO));
|
|
match config {
|
|
Config::Launcher(datadir_path) => {
|
|
let launcher = Launcher::new(datadir_path);
|
|
(
|
|
Self {
|
|
state: State::Launcher(Box::new(launcher)),
|
|
logger,
|
|
log_level,
|
|
},
|
|
Command::perform(ctrl_c(), |_| Message::CtrlC),
|
|
)
|
|
}
|
|
Config::Install(datadir_path, network) => {
|
|
if !datadir_path.exists() {
|
|
// datadir is created right before launching the installer
|
|
// so logs can go in <datadir_path>/installer.log
|
|
if let Err(e) = create_datadir(&datadir_path) {
|
|
error!("Failed to create datadir: {}", e);
|
|
} else {
|
|
info!(
|
|
"Created a fresh data directory at {}",
|
|
&datadir_path.to_string_lossy()
|
|
);
|
|
}
|
|
}
|
|
logger.set_installer_mode(
|
|
datadir_path.clone(),
|
|
log_level.unwrap_or(LevelFilter::INFO),
|
|
);
|
|
let (install, command) = Installer::new(datadir_path, network);
|
|
(
|
|
Self {
|
|
state: State::Installer(Box::new(install)),
|
|
logger,
|
|
log_level,
|
|
},
|
|
Command::batch(vec![
|
|
command.map(|msg| Message::Install(Box::new(msg))),
|
|
Command::perform(ctrl_c(), |_| Message::CtrlC),
|
|
]),
|
|
)
|
|
}
|
|
Config::Run(datadir_path, cfg, network) => {
|
|
logger.set_running_mode(
|
|
datadir_path.clone(),
|
|
network,
|
|
log_level.unwrap_or_else(|| cfg.log_level().unwrap_or(LevelFilter::INFO)),
|
|
);
|
|
let (loader, command) = Loader::new(datadir_path, cfg, network, None);
|
|
(
|
|
Self {
|
|
state: State::Loader(Box::new(loader)),
|
|
logger,
|
|
log_level,
|
|
},
|
|
Command::batch(vec![
|
|
command.map(|msg| Message::Load(Box::new(msg))),
|
|
Command::perform(ctrl_c(), |_| Message::CtrlC),
|
|
]),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
|
|
match (&mut self.state, message) {
|
|
(_, Message::CtrlC)
|
|
| (
|
|
_,
|
|
Message::Event(iced_native::Event::Window(
|
|
iced_native::window::Event::CloseRequested,
|
|
)),
|
|
) => {
|
|
match &mut self.state {
|
|
State::Loader(s) => s.stop(),
|
|
State::Launcher(s) => s.stop(),
|
|
State::Installer(s) => s.stop(),
|
|
State::App(s) => s.stop(),
|
|
};
|
|
iced::window::close()
|
|
}
|
|
(State::Launcher(l), Message::Launch(msg)) => match *msg {
|
|
launcher::Message::Install(datadir_path) => {
|
|
self.logger.set_installer_mode(
|
|
datadir_path.clone(),
|
|
self.log_level.unwrap_or(LevelFilter::INFO),
|
|
);
|
|
let (install, command) =
|
|
Installer::new(datadir_path, bitcoin::Network::Bitcoin);
|
|
self.state = State::Installer(Box::new(install));
|
|
command.map(|msg| Message::Install(Box::new(msg)))
|
|
}
|
|
launcher::Message::Run(datadir_path, cfg, network) => {
|
|
self.logger.set_running_mode(
|
|
datadir_path.clone(),
|
|
network,
|
|
self.log_level
|
|
.unwrap_or_else(|| cfg.log_level().unwrap_or(LevelFilter::INFO)),
|
|
);
|
|
let (loader, command) = Loader::new(datadir_path, cfg, network, None);
|
|
self.state = State::Loader(Box::new(loader));
|
|
command.map(|msg| Message::Load(Box::new(msg)))
|
|
}
|
|
_ => l.update(*msg).map(|msg| Message::Launch(Box::new(msg))),
|
|
},
|
|
(State::Installer(i), Message::Install(msg)) => {
|
|
if let installer::Message::Exit(path, internal_bitcoind) = *msg {
|
|
let cfg = app::Config::from_file(&path).unwrap();
|
|
let daemon_cfg =
|
|
DaemonConfig::from_file(cfg.daemon_config_path.clone()).unwrap();
|
|
let datadir_path = daemon_cfg
|
|
.data_dir
|
|
.as_ref()
|
|
.expect("Installer must have set it")
|
|
.clone();
|
|
|
|
self.logger.set_running_mode(
|
|
datadir_path.clone(),
|
|
daemon_cfg.bitcoin_config.network,
|
|
self.log_level
|
|
.unwrap_or_else(|| cfg.log_level().unwrap_or(LevelFilter::INFO)),
|
|
);
|
|
self.logger.remove_install_log_file(datadir_path.clone());
|
|
let (loader, command) = Loader::new(
|
|
datadir_path,
|
|
cfg,
|
|
daemon_cfg.bitcoin_config.network,
|
|
internal_bitcoind,
|
|
);
|
|
self.state = State::Loader(Box::new(loader));
|
|
command.map(|msg| Message::Load(Box::new(msg)))
|
|
} else {
|
|
i.update(*msg).map(|msg| Message::Install(Box::new(msg)))
|
|
}
|
|
}
|
|
(State::Loader(loader), Message::Load(msg)) => match *msg {
|
|
loader::Message::View(loader::ViewMessage::SwitchNetwork) => {
|
|
self.state =
|
|
State::Launcher(Box::new(Launcher::new(loader.datadir_path.clone())));
|
|
Command::none()
|
|
}
|
|
loader::Message::Synced(Ok((wallet, cache, daemon, bitcoind))) => {
|
|
let (app, command) = App::new(
|
|
cache,
|
|
wallet,
|
|
loader.gui_config.clone(),
|
|
daemon,
|
|
loader.datadir_path.clone(),
|
|
bitcoind,
|
|
);
|
|
self.state = State::App(app);
|
|
command.map(|msg| Message::Run(Box::new(msg)))
|
|
}
|
|
_ => loader.update(*msg).map(|msg| Message::Load(Box::new(msg))),
|
|
},
|
|
(State::App(i), Message::Run(msg)) => {
|
|
i.update(*msg).map(|msg| Message::Run(Box::new(msg)))
|
|
}
|
|
_ => Command::none(),
|
|
}
|
|
}
|
|
|
|
fn subscription(&self) -> Subscription<Self::Message> {
|
|
Subscription::batch(vec![
|
|
match &self.state {
|
|
State::Installer(v) => v.subscription().map(|msg| Message::Install(Box::new(msg))),
|
|
State::Loader(v) => v.subscription().map(|msg| Message::Load(Box::new(msg))),
|
|
State::App(v) => v.subscription().map(|msg| Message::Run(Box::new(msg))),
|
|
State::Launcher(v) => v.subscription().map(|msg| Message::Launch(Box::new(msg))),
|
|
},
|
|
iced_native::subscription::events().map(Self::Message::Event),
|
|
])
|
|
.with_filter(|(event, _status)| {
|
|
matches!(
|
|
event,
|
|
iced::Event::Window(iced_native::window::Event::CloseRequested)
|
|
)
|
|
})
|
|
}
|
|
|
|
fn view(&self) -> Element<Self::Message> {
|
|
match &self.state {
|
|
State::Installer(v) => v.view().map(|msg| Message::Install(Box::new(msg))),
|
|
State::App(v) => v.view().map(|msg| Message::Run(Box::new(msg))),
|
|
State::Launcher(v) => v.view().map(|msg| Message::Launch(Box::new(msg))),
|
|
State::Loader(v) => v.view().map(|msg| Message::Load(Box::new(msg))),
|
|
}
|
|
}
|
|
|
|
fn scale_factor(&self) -> f64 {
|
|
1.0
|
|
}
|
|
}
|
|
|
|
fn create_datadir(datadir_path: &std::path::Path) -> Result<(), Box<dyn std::error::Error>> {
|
|
#[cfg(unix)]
|
|
return {
|
|
use std::fs::DirBuilder;
|
|
use std::os::unix::fs::DirBuilderExt;
|
|
|
|
let mut builder = DirBuilder::new();
|
|
builder.mode(0o700).recursive(true).create(datadir_path)?;
|
|
Ok(())
|
|
};
|
|
|
|
// TODO: permissions on Windows..
|
|
#[cfg(not(unix))]
|
|
return {
|
|
std::fs::create_dir_all(datadir_path)?;
|
|
Ok(())
|
|
};
|
|
}
|
|
|
|
pub enum Config {
|
|
Run(PathBuf, app::Config, bitcoin::Network),
|
|
Launcher(PathBuf),
|
|
Install(PathBuf, bitcoin::Network),
|
|
}
|
|
|
|
impl Config {
|
|
pub fn new(
|
|
datadir_path: PathBuf,
|
|
network: Option<bitcoin::Network>,
|
|
) -> Result<Self, Box<dyn Error>> {
|
|
if let Some(network) = network {
|
|
let mut path = datadir_path.clone();
|
|
path.push(network.to_string());
|
|
path.push(app::config::DEFAULT_FILE_NAME);
|
|
match app::Config::from_file(&path) {
|
|
Ok(cfg) => Ok(Config::Run(datadir_path, cfg, network)),
|
|
Err(ConfigError::NotFound) => Ok(Config::Install(datadir_path, network)),
|
|
Err(e) => Err(format!("Failed to read configuration file: {}", e).into()),
|
|
}
|
|
} else if !datadir_path.exists()
|
|
|| (!datadir_path.join("bitcoin").exists()
|
|
&& !datadir_path.join("testnet").exists()
|
|
&& !datadir_path.join("signet").exists()
|
|
&& !datadir_path.join("regtest").exists())
|
|
{
|
|
Ok(Config::Install(datadir_path, bitcoin::Network::Bitcoin))
|
|
} else {
|
|
Ok(Config::Launcher(datadir_path))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn main() -> Result<(), Box<dyn Error>> {
|
|
let args = parse_args(std::env::args().collect())?;
|
|
let config = match args.as_slice() {
|
|
[] => {
|
|
let datadir_path = default_datadir().unwrap();
|
|
Config::new(datadir_path, None)
|
|
}
|
|
[Arg::Network(network)] => {
|
|
let datadir_path = default_datadir().unwrap();
|
|
Config::new(datadir_path, Some(*network))
|
|
}
|
|
[Arg::ConfigPath(path)] => {
|
|
let cfg = app::Config::from_file(path)?;
|
|
if let Some(daemon_config_path) = cfg.daemon_config_path.clone() {
|
|
let daemon_cfg = DaemonConfig::from_file(Some(daemon_config_path))?;
|
|
let datadir_path = daemon_cfg
|
|
.data_dir
|
|
.unwrap_or_else(|| default_datadir().unwrap());
|
|
Ok(Config::Run(
|
|
datadir_path,
|
|
cfg,
|
|
daemon_cfg.bitcoin_config.network,
|
|
))
|
|
} else {
|
|
Err("Application cannot guess network".into())
|
|
}
|
|
}
|
|
[Arg::ConfigPath(path), Arg::Network(network)]
|
|
| [Arg::Network(network), Arg::ConfigPath(path)] => {
|
|
let cfg = app::Config::from_file(path)?;
|
|
if let Some(daemon_config_path) = cfg.daemon_config_path.clone() {
|
|
let daemon_cfg = DaemonConfig::from_file(Some(daemon_config_path))?;
|
|
let datadir_path = daemon_cfg
|
|
.data_dir
|
|
.unwrap_or_else(|| default_datadir().unwrap());
|
|
Ok(Config::Run(
|
|
datadir_path,
|
|
cfg,
|
|
daemon_cfg.bitcoin_config.network,
|
|
))
|
|
} else {
|
|
Ok(Config::Run(default_datadir().unwrap(), cfg, *network))
|
|
}
|
|
}
|
|
[Arg::DatadirPath(datadir_path)] => Config::new(datadir_path.clone(), None),
|
|
[Arg::DatadirPath(datadir_path), Arg::Network(network)]
|
|
| [Arg::Network(network), Arg::DatadirPath(datadir_path)] => {
|
|
Config::new(datadir_path.clone(), Some(*network))
|
|
}
|
|
_ => {
|
|
return Err("Unknown args combination".into());
|
|
}
|
|
}?;
|
|
|
|
let log_level = if let Ok(l) = std::env::var("LOG_LEVEL") {
|
|
Some(LevelFilter::from_str(&l)?)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
setup_panic_hook();
|
|
|
|
let mut settings = Settings::with_flags((config, log_level));
|
|
settings.id = Some("liana-gui".to_string());
|
|
settings.window.icon = Some(image::liana_app_icon());
|
|
settings.default_text_size = text::P1_SIZE.into();
|
|
settings.default_font = Some(font::REGULAR_BYTES);
|
|
settings.exit_on_close_request = false;
|
|
|
|
if let Err(e) = GUI::run(settings) {
|
|
return Err(format!("Failed to launch UI: {}", e).into());
|
|
};
|
|
Ok(())
|
|
}
|
|
|
|
// A panic in any thread should stop the main thread, and print the panic.
|
|
fn setup_panic_hook() {
|
|
std::panic::set_hook(Box::new(move |panic_info| {
|
|
let file = panic_info
|
|
.location()
|
|
.map(|l| l.file())
|
|
.unwrap_or_else(|| "'unknown'");
|
|
let line = panic_info
|
|
.location()
|
|
.map(|l| l.line().to_string())
|
|
.unwrap_or_else(|| "'unknown'".to_string());
|
|
|
|
let bt = backtrace::Backtrace::new();
|
|
let info = panic_info
|
|
.payload()
|
|
.downcast_ref::<&str>()
|
|
.map(|s| s.to_string())
|
|
.or_else(|| panic_info.payload().downcast_ref::<String>().cloned());
|
|
error!(
|
|
"panic occurred at line {} of file {}: {:?}\n{:?}",
|
|
line, file, info, bt
|
|
);
|
|
|
|
std::io::stdout().flush().expect("Flushing stdout");
|
|
std::process::exit(1);
|
|
}));
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_parse_args() {
|
|
assert!(parse_args(vec!["--meth".into()]).is_err());
|
|
assert!(parse_args(vec!["--datadir".into()]).is_err());
|
|
assert!(parse_args(vec!["--conf".into()]).is_err());
|
|
assert_eq!(
|
|
Some(vec![
|
|
Arg::DatadirPath(PathBuf::from(".")),
|
|
Arg::ConfigPath(PathBuf::from("hello.toml")),
|
|
]),
|
|
parse_args(
|
|
"--datadir . --conf hello.toml"
|
|
.split(' ')
|
|
.map(|a| a.to_string())
|
|
.collect()
|
|
)
|
|
.ok()
|
|
);
|
|
assert_eq!(
|
|
Some(vec![Arg::Network(bitcoin::Network::Regtest)]),
|
|
parse_args(vec!["--regtest".into()]).ok()
|
|
);
|
|
assert_eq!(
|
|
Some(vec![
|
|
Arg::DatadirPath(PathBuf::from("hello")),
|
|
Arg::Network(bitcoin::Network::Testnet)
|
|
]),
|
|
parse_args(
|
|
"--datadir hello --testnet"
|
|
.split(' ')
|
|
.map(|a| a.to_string())
|
|
.collect()
|
|
)
|
|
.ok()
|
|
);
|
|
assert_eq!(
|
|
Some(vec![
|
|
Arg::Network(bitcoin::Network::Testnet),
|
|
Arg::DatadirPath(PathBuf::from("hello"))
|
|
]),
|
|
parse_args(
|
|
"--testnet --datadir hello"
|
|
.split(' ')
|
|
.map(|a| a.to_string())
|
|
.collect()
|
|
)
|
|
.ok()
|
|
);
|
|
}
|
|
}
|