#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
use holochain_client::AdminWebsocket;
use holochain_launcher_utils::window_builder::{happ_window_builder, UISource};
use tauri::utils::config::AppUrl;
use tauri::WindowUrl;
use tauri::{CustomMenuItem, Menu, Submenu};
use std::collections::HashMap;
use std::path::PathBuf;
use tauri::Manager;
use tauri::RunEvent;
use tauri::Window;
use tauri::WindowEvent;
use url::Url;
use lair_keystore_api::dependencies::sodoken;
use lair_keystore_api::{LairClient, ipc_keystore_connect};
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
pub fn launch_tauri(
ui_source: UISource,
app_id: String,
local_storage_dir: PathBuf,
watch: bool,
passphrase: sodoken::BufRead
) -> () {
let mut context = tauri::generate_context!();
if let UISource::Port(port) = ui_source {
let url = format!("http://localhost:{}", port).parse().unwrap();
let window_url = WindowUrl::External(url);
context.config_mut().build.dist_dir = AppUrl::Url(window_url.clone());
context.config_mut().build.dev_path = AppUrl::Url(window_url.clone());
}
let builder = match ui_source {
UISource::Path(_) => tauri::Builder::default(),
UISource::Port(port) => tauri::Builder::default()
.plugin(tauri_plugin_localhost::Builder::new(port).build())
};
let builder_result = builder
.invoke_handler(tauri::generate_handler![
crate::commands::sign_zome_call::sign_zome_call,
holochain_launcher_utils::shared_commands::open_url_cmd,
]) .setup(move |app| {
let pwd = std::env::current_dir().unwrap();
let dot_hc_path = pwd.join(".hc");
let dot_hc_content = match std::fs::read_to_string(dot_hc_path) {
Ok(p) => p,
Err(e) => {
println!("[hc launch] ERROR: Failed to read content of .hc file: {}", e);
panic!("Failed to read content of .hc file: {}", e);
}
};
let mut windows: Vec<Window> = vec![];
let mut lair_clients: HashMap<String, LairClient> = HashMap::new();
let mut app_counter = 0;
let (windows, ui_source, lair_clients, app) = tokio::task::block_in_place(|| {
tauri::async_runtime::block_on(async move {
for tmp_directory_path in dot_hc_content.lines() {
let dot_hc_live_path: PathBuf = pwd.join(format!(".hc_live_{}", app_counter)).into();
let admin_port = match std::fs::read_to_string(dot_hc_live_path) {
Ok(p) => p,
Err(e) => {
println!("[hc launch] ERROR: Failed to read content of .hc_live file: {}", e);
panic!("Failed to read content of .hc_live file: {}", e);
}
};
let admin_port_string = admin_port.clone();
let app_port = match get_app_websocket(admin_port_string).await {
Ok(ws) => ws,
Err(e) => {
println!("[hc launch] ERROR: Failed to get app websocket port: {}", e);
panic!("Failed to get app websocket port.");
}
};
let admin_port = match admin_port.trim().parse::<u16>() {
Ok(u) => u,
Err(e) => {
println!("[hc launch] ERROR: Failed to convert admin port from String to u16: {}", e);
panic!("Failed to convert admin port from String to u16: {}", e);
}
};
let sanitized_app_id = app_id.clone().replace("-", "--").replace(" ", "-").replace(".", "_");
let window_label = format!("Conductor-{}-{}", app_counter, sanitized_app_id);
let window_title = format!("Conductor {} - {}", app_counter, sanitized_app_id);
let window_width = 1000.0;
let window_height = 700.0;
let app_handle = app.handle();
let mut window_builder = match ui_source.clone() {
UISource::Path(ui_path) => {
happ_window_builder(
&app_handle,
app_id.clone(),
window_label.clone(),
window_title.clone(),
UISource::Path(ui_path.clone()),
local_storage_dir.clone().join(format!("Conductor-{}-{}", app_counter, sanitized_app_id)),
app_port,
admin_port,
false,
)
},
UISource::Port(ui_port) => {
happ_window_builder(
&app_handle,
app_id.clone(),
window_label.clone(),
window_title.clone(),
UISource::Port(ui_port),
local_storage_dir.clone().join(format!("Conductor-{}-{}", app_counter, sanitized_app_id)),
app_port,
admin_port,
false,
)
}
};
window_builder = window_builder.inner_size(window_width, window_height);
let window = match window_builder
.menu(Menu::new().add_submenu(Submenu::new(
"Settings",
Menu::new().add_item(CustomMenuItem::new("show-devtools", "Show DevTools")),
)))
.build() {
Ok(window) => window,
Err(e) => {
println!("[hc launch] ERROR: Failed to build window: {}", e);
panic!("Failed to build Window: {:?}", e);
}
};
let a = app.handle().clone();
let window_label_clone = window_label.clone();
window.on_menu_event(move |_| {
if let Some(w) = a.get_window(window_label_clone.as_str()) {
w.open_devtools();
}
});
window.on_window_event(|event| match event {
WindowEvent::CloseRequested { api: _, .. } => (),
_ => (),
});
windows.push(window);
let connection_url = match read_lair_yaml(PathBuf::from(tmp_directory_path)) {
Some(url) => url,
None => {
println!("[hc launch] ERROR: No connectionUrl found in lair-keystore-config.yaml");
panic!("No connectionUrl found in lair-keystore-config.yaml")
}
};
let connection_url = Url::parse(connection_url.as_str()).unwrap();
let client =
match ipc_keystore_connect(connection_url.clone(), passphrase.clone()).await {
Ok(client) => client,
Err(e) => {
println!("[hc launch] ERROR: Failed to connect to lair client: {:?}", e);
panic!("Failed to connect to lair client");
}
};
lair_clients.insert(window_label.to_string(), client);
app_counter += 1;
}
(windows, ui_source, lair_clients, app)
})
});
app.manage(lair_clients);
match (watch, ui_source) {
(true, UISource::Path(ui_path)) => {
println!("[hc launch] Watching file changes in folder {:?}", ui_path.as_path());
let _watch_handle = std::thread::spawn(move || {
let (tx_watcher, rx_watcher) = std::sync::mpsc::channel();
let mut watcher = match RecommendedWatcher::new(tx_watcher, Config::default()) {
Ok(w) => w,
Err(e) => panic!("Failed to create file system watcher: {:?}", e),
};
match watcher.watch(ui_path.as_path(), RecursiveMode::Recursive) {
Ok(()) => (),
Err(e) => {
println!("[hc launch] ERROR: Failed to watch: {:?}", e);
panic!("Failed to watch.");
}
};
for res in rx_watcher {
match res {
Ok(event) => {
println!("event: {:?}", event);
for window in &windows {
window.eval("location.reload()").unwrap();
}
}
Err(e) => println!("[hc launch] ERROR: watch error: {:?}", e),
}
}
});
},
(true, UISource::Port(_port)) => println!("[hc launch] WARNING: The --watch flag has no effect if the UI is served from localhost."),
_ => (),
}
Ok(())
})
.build(context);
match builder_result {
Ok(builder) => {
builder.run(move |_app_handle, event| {
if let RunEvent::ExitRequested { api, .. } = event {
api.prevent_exit();
}
});
}
Err(e) => eprintln!("[hc launch] ERROR: Failed to build tauri windows: {:?}", e),
}
}
async fn get_app_websocket(admin_port: String) -> Result<u16, String> {
let mut ws = match AdminWebsocket::connect(format!("ws://localhost:{}", admin_port)).await {
Ok(ws) => ws,
Err(e) => return Err(format!("Could not connect to admin websocket: {:?}", e)),
};
let app_interface_port = {
let app_interfaces = ws
.list_app_interfaces()
.await
.or(Err(format!("Could not list app websocket interfaces.")))?;
if app_interfaces.len() > 0 {
app_interfaces[0]
} else {
let free_port = portpicker::pick_unused_port().expect("No ports free");
ws.attach_app_interface(free_port)
.await
.or(Err(format!("Could not attach app websocket interface.")))?;
free_port
}
};
Ok(app_interface_port)
}
fn read_lair_yaml(path: PathBuf) -> Option<String> {
let yaml_content = match std::fs::read_to_string(
PathBuf::from(path)
.join("keystore")
.join("lair-keystore-config.yaml"),
) {
Ok(content) => content,
Err(e) => {
println!("[hc launch] ERROR: Failed to read lair-keystore-config.yaml: {:?}", e);
panic!("Failed to read lair-keystore-config.yaml");
}
};
for line in yaml_content.lines() {
match line.contains("connectionUrl") {
true => return Some(line[15..].to_string()),
false => (),
}
}
None
}