2024-05-12 12:34:29 +00:00
|
|
|
// Prevents an additional console window on Windows in release, DO NOT REMOVE!!
|
2024-03-28 21:26:48 +00:00
|
|
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
|
|
|
|
2024-05-12 12:40:06 +00:00
|
|
|
extern crate core;
|
|
|
|
|
|
|
|
use std::net::TcpListener;
|
|
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
|
2024-05-03 20:55:21 +00:00
|
|
|
use arboard::Clipboard;
|
2024-04-05 20:23:01 +00:00
|
|
|
use keyring::Entry;
|
2024-04-20 15:03:39 +00:00
|
|
|
use serde::Serialize;
|
2024-05-12 12:40:06 +00:00
|
|
|
use tauri::{Manager, Url, Window, WindowUrl};
|
|
|
|
use tauri::api::process::{Command, CommandChild, CommandEvent};
|
|
|
|
use tauri::utils::config::AppUrl;
|
|
|
|
use tokio::time;
|
|
|
|
use flexi_logger::{AdaptiveFormat, Logger};
|
|
|
|
use log::{debug, error, info, warn};
|
2024-04-05 20:23:01 +00:00
|
|
|
|
2024-03-28 21:26:48 +00:00
|
|
|
fn main() {
|
2024-05-12 12:40:06 +00:00
|
|
|
|
|
|
|
Logger::try_with_str("debug").expect("Cannot create logging")
|
|
|
|
.log_to_stdout()
|
|
|
|
.adaptive_format_for_stdout(AdaptiveFormat::Detailed)
|
|
|
|
.start().expect("Cannot start logging");
|
|
|
|
|
|
|
|
if is_dev() {
|
|
|
|
warn!("Running in development mode.");
|
|
|
|
} else {
|
|
|
|
info!("Running in production mode.");
|
|
|
|
}
|
|
|
|
|
|
|
|
let port = match is_dev() {
|
|
|
|
true => 5000,
|
|
|
|
false => get_available_port().unwrap(),
|
|
|
|
};
|
|
|
|
|
|
|
|
let url = match Url::parse(format!("http://localhost:{port}").as_str())
|
|
|
|
{
|
|
|
|
Ok(url) => url,
|
|
|
|
Err(msg) => {
|
|
|
|
error!("Error while parsing URL: {msg}");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let app_url = AppUrl::Url(WindowUrl::External(url.clone()));
|
|
|
|
let app_url_log = app_url.clone();
|
|
|
|
info!("Try to start the .NET server on {app_url_log}...");
|
|
|
|
|
|
|
|
// Arc for the server process to stop it later:
|
|
|
|
let server: Arc<Mutex<Option<CommandChild>>> = Arc::new(Mutex::new(None));
|
|
|
|
let server_spawn_clone = server.clone();
|
|
|
|
|
|
|
|
// Channel to communicate with the server process:
|
|
|
|
let (sender, mut receiver) = tauri::async_runtime::channel(100);
|
|
|
|
|
|
|
|
if is_prod() {
|
|
|
|
tauri::async_runtime::spawn(async move {
|
|
|
|
let (mut rx, child) = Command::new_sidecar("mindworkAIStudio")
|
|
|
|
.expect("Failed to create sidecar")
|
|
|
|
.args([format!("{port}").as_str()])
|
|
|
|
.spawn()
|
|
|
|
.expect("Failed to spawn .NET server process.");
|
|
|
|
|
|
|
|
let server_pid = child.pid();
|
|
|
|
debug!(".NET server process started with PID={server_pid}.");
|
|
|
|
|
|
|
|
// Save the server process to stop it later:
|
|
|
|
*server_spawn_clone.lock().unwrap() = Some(child);
|
|
|
|
|
|
|
|
info!("Waiting for .NET server to boot...");
|
|
|
|
while let Some(CommandEvent::Stdout(line)) = rx.recv().await {
|
|
|
|
let line_lower = line.to_lowercase();
|
|
|
|
let line_cleared = line_lower.trim();
|
|
|
|
match line_cleared
|
|
|
|
{
|
|
|
|
"rust/tauri server started" => _ = sender.send(ServerEvent::Started).await,
|
|
|
|
|
|
|
|
_ if line_cleared.contains("fail") || line_cleared.contains("error") || line_cleared.contains("exception") => _ = sender.send(ServerEvent::Error(line)).await,
|
|
|
|
_ if line_cleared.contains("warn") => _ = sender.send(ServerEvent::Warning(line)).await,
|
|
|
|
_ if line_cleared.contains("404") => _ = sender.send(ServerEvent::NotFound(line)).await,
|
|
|
|
_ => (),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let sending_stop_result = sender.send(ServerEvent::Stopped).await;
|
|
|
|
match sending_stop_result {
|
|
|
|
Ok(_) => (),
|
|
|
|
Err(e) => error!("Was not able to send the server stop message: {e}."),
|
|
|
|
}
|
|
|
|
});
|
2024-05-13 16:20:41 +00:00
|
|
|
} else {
|
|
|
|
warn!("Running in development mode, no .NET server will be started.");
|
2024-05-12 12:40:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
let main_window: Arc<Mutex<Option<Window>>> = Arc::new(Mutex::new(None));
|
|
|
|
let main_window_spawn_clone = main_window.clone();
|
|
|
|
let server_receive_clone = server.clone();
|
|
|
|
|
|
|
|
// Create a thread to handle server events:
|
|
|
|
tauri::async_runtime::spawn(async move {
|
2024-05-13 16:21:19 +00:00
|
|
|
info!("Start listening for server events...");
|
2024-05-12 12:40:06 +00:00
|
|
|
loop {
|
|
|
|
match receiver.recv().await {
|
|
|
|
Some(ServerEvent::Started) => {
|
|
|
|
info!("The .NET server was booted successfully.");
|
|
|
|
|
|
|
|
// Try to get the main window. If it is not available yet, wait for it:
|
|
|
|
let mut main_window_ready = false;
|
|
|
|
let mut main_window_status_reported = false;
|
|
|
|
while !main_window_ready
|
|
|
|
{
|
|
|
|
main_window_ready = {
|
|
|
|
let main_window = main_window_spawn_clone.lock().unwrap();
|
|
|
|
main_window.is_some()
|
|
|
|
};
|
|
|
|
|
|
|
|
if !main_window_ready {
|
|
|
|
if !main_window_status_reported {
|
|
|
|
info!("Waiting for main window to be ready, because .NET was faster than Tauri.");
|
|
|
|
main_window_status_reported = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
time::sleep(time::Duration::from_millis(100)).await;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let main_window = main_window_spawn_clone.lock().unwrap();
|
|
|
|
let js_location_change = format!("window.location = '{url}';");
|
|
|
|
let location_change_result = main_window.as_ref().unwrap().eval(js_location_change.as_str());
|
|
|
|
match location_change_result {
|
|
|
|
Ok(_) => info!("Location was changed to {url}."),
|
|
|
|
Err(e) => error!("Failed to change location to {url}: {e}."),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
Some(ServerEvent::NotFound(line)) => {
|
|
|
|
warn!("The .NET server issued a 404 error: {line}.");
|
|
|
|
},
|
|
|
|
|
|
|
|
Some(ServerEvent::Warning(line)) => {
|
|
|
|
warn!("The .NET server issued a warning: {line}.");
|
|
|
|
},
|
|
|
|
|
|
|
|
Some(ServerEvent::Error(line)) => {
|
|
|
|
error!("The .NET server issued an error: {line}.");
|
|
|
|
},
|
|
|
|
|
|
|
|
Some(ServerEvent::Stopped) => {
|
|
|
|
warn!("The .NET server was stopped.");
|
|
|
|
*server_receive_clone.lock().unwrap() = None;
|
|
|
|
},
|
|
|
|
|
|
|
|
None => {
|
|
|
|
debug!("Server event channel was closed.");
|
|
|
|
break;
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
info!("Starting Tauri app...");
|
2024-03-28 21:26:48 +00:00
|
|
|
tauri::Builder::default()
|
2024-05-12 12:40:06 +00:00
|
|
|
.setup(move |app| {
|
|
|
|
let window = app.get_window("main").expect("Failed to get main window.");
|
|
|
|
*main_window.lock().unwrap() = Some(window);
|
|
|
|
Ok(())
|
|
|
|
})
|
2024-05-03 20:55:21 +00:00
|
|
|
.invoke_handler(tauri::generate_handler![store_secret, get_secret, delete_secret, set_clipboard])
|
2024-03-28 21:26:48 +00:00
|
|
|
.run(tauri::generate_context!())
|
2024-05-12 12:40:06 +00:00
|
|
|
.expect("Error while running Tauri application");
|
|
|
|
|
|
|
|
info!("Tauri app was stopped.");
|
|
|
|
if is_prod() {
|
|
|
|
info!("Try to stop the .NET server as well...");
|
|
|
|
if let Some(server_process) = server.lock().unwrap().take() {
|
|
|
|
let server_kill_result = server_process.kill();
|
|
|
|
match server_kill_result {
|
|
|
|
Ok(_) => info!("The .NET server process was stopped."),
|
|
|
|
Err(e) => error!("Failed to stop the .NET server process: {e}."),
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
warn!("The .NET server process was not started or already stopped.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Enum for server events:
|
|
|
|
enum ServerEvent {
|
|
|
|
Started,
|
|
|
|
NotFound(String),
|
|
|
|
Warning(String),
|
|
|
|
Error(String),
|
|
|
|
Stopped,
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn is_dev() -> bool {
|
|
|
|
cfg!(dev)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn is_prod() -> bool {
|
|
|
|
!is_dev()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_available_port() -> Option<u16> {
|
|
|
|
TcpListener::bind(("127.0.0.1", 0))
|
|
|
|
.map(|listener| listener.local_addr().unwrap().port())
|
|
|
|
.ok()
|
2024-03-28 21:26:48 +00:00
|
|
|
}
|
2024-04-05 20:23:01 +00:00
|
|
|
|
|
|
|
#[tauri::command]
|
2024-04-20 15:03:39 +00:00
|
|
|
fn store_secret(destination: String, user_name: String, secret: String) -> StoreSecretResponse {
|
2024-05-12 12:40:06 +00:00
|
|
|
let service = format!("mindwork-ai-studio::{}", destination);
|
|
|
|
let entry = Entry::new(service.as_str(), user_name.as_str()).unwrap();
|
2024-04-20 15:03:39 +00:00
|
|
|
let result = entry.set_password(secret.as_str());
|
|
|
|
match result {
|
2024-05-12 12:40:06 +00:00
|
|
|
Ok(_) => {
|
|
|
|
info!("Secret for {service} and user {user_name} was stored successfully.");
|
|
|
|
StoreSecretResponse {
|
|
|
|
success: true,
|
|
|
|
issue: String::from(""),
|
|
|
|
}
|
2024-04-20 15:03:39 +00:00
|
|
|
},
|
|
|
|
|
2024-05-12 12:40:06 +00:00
|
|
|
Err(e) => {
|
|
|
|
error!("Failed to store secret for {service} and user {user_name}: {e}.");
|
|
|
|
StoreSecretResponse {
|
|
|
|
success: false,
|
|
|
|
issue: e.to_string(),
|
|
|
|
}
|
2024-04-20 15:03:39 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
struct StoreSecretResponse {
|
|
|
|
success: bool,
|
|
|
|
issue: String,
|
2024-04-05 20:23:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[tauri::command]
|
2024-04-20 15:03:39 +00:00
|
|
|
fn get_secret(destination: String, user_name: String) -> RequestedSecret {
|
2024-05-12 12:40:06 +00:00
|
|
|
let service = format!("mindwork-ai-studio::{}", destination);
|
|
|
|
let entry = Entry::new(service.as_str(), user_name.as_str()).unwrap();
|
2024-04-20 15:03:39 +00:00
|
|
|
let secret = entry.get_password();
|
|
|
|
match secret {
|
2024-05-12 12:40:06 +00:00
|
|
|
Ok(s) => {
|
|
|
|
info!("Secret for {service} and user {user_name} was retrieved successfully.");
|
|
|
|
RequestedSecret {
|
|
|
|
success: true,
|
|
|
|
secret: s,
|
|
|
|
issue: String::from(""),
|
|
|
|
}
|
2024-04-20 15:03:39 +00:00
|
|
|
},
|
|
|
|
|
2024-05-12 12:40:06 +00:00
|
|
|
Err(e) => {
|
|
|
|
error!("Failed to retrieve secret for {service} and user {user_name}: {e}.");
|
|
|
|
RequestedSecret {
|
|
|
|
success: false,
|
|
|
|
secret: String::from(""),
|
|
|
|
issue: e.to_string(),
|
|
|
|
}
|
2024-04-20 15:03:39 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
struct RequestedSecret {
|
|
|
|
success: bool,
|
|
|
|
secret: String,
|
|
|
|
issue: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
fn delete_secret(destination: String, user_name: String) -> DeleteSecretResponse {
|
2024-05-12 12:40:06 +00:00
|
|
|
let service = format!("mindwork-ai-studio::{}", destination);
|
|
|
|
let entry = Entry::new(service.as_str(), user_name.as_str()).unwrap();
|
2024-04-20 15:03:39 +00:00
|
|
|
let result = entry.delete_password();
|
|
|
|
match result {
|
2024-05-12 12:40:06 +00:00
|
|
|
Ok(_) => {
|
|
|
|
warn!("Secret for {service} and user {user_name} was deleted successfully.");
|
|
|
|
DeleteSecretResponse {
|
|
|
|
success: true,
|
|
|
|
issue: String::from(""),
|
|
|
|
}
|
2024-04-20 15:03:39 +00:00
|
|
|
},
|
|
|
|
|
2024-05-12 12:40:06 +00:00
|
|
|
Err(e) => {
|
|
|
|
error!("Failed to delete secret for {service} and user {user_name}: {e}.");
|
|
|
|
DeleteSecretResponse {
|
|
|
|
success: false,
|
|
|
|
issue: e.to_string(),
|
|
|
|
}
|
2024-04-20 15:03:39 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
struct DeleteSecretResponse {
|
|
|
|
success: bool,
|
|
|
|
issue: String,
|
2024-05-03 20:55:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
fn set_clipboard(text: String) -> SetClipboardResponse {
|
|
|
|
let clipboard_result = Clipboard::new();
|
|
|
|
let mut clipboard = match clipboard_result {
|
|
|
|
Ok(clipboard) => clipboard,
|
2024-05-12 12:40:06 +00:00
|
|
|
Err(e) => {
|
|
|
|
error!("Failed to get the clipboard instance: {e}.");
|
|
|
|
return SetClipboardResponse {
|
|
|
|
success: false,
|
|
|
|
issue: e.to_string(),
|
|
|
|
}
|
2024-05-03 20:55:21 +00:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
let set_text_result = clipboard.set_text(text);
|
|
|
|
match set_text_result {
|
2024-05-12 12:40:06 +00:00
|
|
|
Ok(_) => {
|
|
|
|
debug!("Text was set to the clipboard successfully.");
|
|
|
|
SetClipboardResponse {
|
|
|
|
success: true,
|
|
|
|
issue: String::from(""),
|
|
|
|
}
|
2024-05-03 20:55:21 +00:00
|
|
|
},
|
|
|
|
|
2024-05-12 12:40:06 +00:00
|
|
|
Err(e) => {
|
|
|
|
error!("Failed to set text to the clipboard: {e}.");
|
|
|
|
SetClipboardResponse {
|
|
|
|
success: false,
|
|
|
|
issue: e.to_string(),
|
|
|
|
}
|
2024-05-03 20:55:21 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
struct SetClipboardResponse {
|
|
|
|
success: bool,
|
|
|
|
issue: String,
|
2024-04-05 20:23:01 +00:00
|
|
|
}
|