mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-05 00:49:40 +00:00
Configured sidecar
- Add the sidecar to start the .NET server when running the app - Add proper logging - Add message handling for communication between .NET and Rust/Tauri - Add dynamic port allocation for the .NET server - Add transition to .NET app, i.e., a loading screen
This commit is contained in:
4 files changed
+726
-39
No files matched your search
+256
-33
@@ -1,30 +1,230 @@
|
||||
// Prevents an additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
extern crate core;
|
||||
|
||||
use std::net::TcpListener;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use arboard::Clipboard;
|
||||
use keyring::Entry;
|
||||
use serde::Serialize;
|
||||
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};
|
||||
|
||||
fn main() {
|
||||
|
||||
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}."),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 {
|
||||
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...");
|
||||
tauri::Builder::default()
|
||||
.setup(move |app| {
|
||||
let window = app.get_window("main").expect("Failed to get main window.");
|
||||
*main_window.lock().unwrap() = Some(window);
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![store_secret, get_secret, delete_secret, set_clipboard])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
.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()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn store_secret(destination: String, user_name: String, secret: String) -> StoreSecretResponse {
|
||||
let entry = Entry::new(&format!("mindwork-ai-studio::{}", destination), user_name.as_str()).unwrap();
|
||||
let service = format!("mindwork-ai-studio::{}", destination);
|
||||
let entry = Entry::new(service.as_str(), user_name.as_str()).unwrap();
|
||||
let result = entry.set_password(secret.as_str());
|
||||
match result {
|
||||
Ok(_) => StoreSecretResponse {
|
||||
success: true,
|
||||
issue: String::from(""),
|
||||
Ok(_) => {
|
||||
info!("Secret for {service} and user {user_name} was stored successfully.");
|
||||
StoreSecretResponse {
|
||||
success: true,
|
||||
issue: String::from(""),
|
||||
}
|
||||
},
|
||||
|
||||
Err(e) => StoreSecretResponse {
|
||||
success: false,
|
||||
issue: e.to_string(),
|
||||
Err(e) => {
|
||||
error!("Failed to store secret for {service} and user {user_name}: {e}.");
|
||||
StoreSecretResponse {
|
||||
success: false,
|
||||
issue: e.to_string(),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -37,19 +237,26 @@ struct StoreSecretResponse {
|
||||
|
||||
#[tauri::command]
|
||||
fn get_secret(destination: String, user_name: String) -> RequestedSecret {
|
||||
let entry = Entry::new(&format!("mindwork-ai-studio::{}", destination), user_name.as_str()).unwrap();
|
||||
let service = format!("mindwork-ai-studio::{}", destination);
|
||||
let entry = Entry::new(service.as_str(), user_name.as_str()).unwrap();
|
||||
let secret = entry.get_password();
|
||||
match secret {
|
||||
Ok(s) => RequestedSecret {
|
||||
success: true,
|
||||
secret: s,
|
||||
issue: String::from(""),
|
||||
Ok(s) => {
|
||||
info!("Secret for {service} and user {user_name} was retrieved successfully.");
|
||||
RequestedSecret {
|
||||
success: true,
|
||||
secret: s,
|
||||
issue: String::from(""),
|
||||
}
|
||||
},
|
||||
|
||||
Err(e) => RequestedSecret {
|
||||
success: false,
|
||||
secret: String::from(""),
|
||||
issue: e.to_string(),
|
||||
Err(e) => {
|
||||
error!("Failed to retrieve secret for {service} and user {user_name}: {e}.");
|
||||
RequestedSecret {
|
||||
success: false,
|
||||
secret: String::from(""),
|
||||
issue: e.to_string(),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -63,17 +270,24 @@ struct RequestedSecret {
|
||||
|
||||
#[tauri::command]
|
||||
fn delete_secret(destination: String, user_name: String) -> DeleteSecretResponse {
|
||||
let entry = Entry::new(&format!("mindwork-ai-studio::{}", destination), user_name.as_str()).unwrap();
|
||||
let service = format!("mindwork-ai-studio::{}", destination);
|
||||
let entry = Entry::new(service.as_str(), user_name.as_str()).unwrap();
|
||||
let result = entry.delete_password();
|
||||
match result {
|
||||
Ok(_) => DeleteSecretResponse {
|
||||
success: true,
|
||||
issue: String::from(""),
|
||||
Ok(_) => {
|
||||
warn!("Secret for {service} and user {user_name} was deleted successfully.");
|
||||
DeleteSecretResponse {
|
||||
success: true,
|
||||
issue: String::from(""),
|
||||
}
|
||||
},
|
||||
|
||||
Err(e) => DeleteSecretResponse {
|
||||
success: false,
|
||||
issue: e.to_string(),
|
||||
Err(e) => {
|
||||
error!("Failed to delete secret for {service} and user {user_name}: {e}.");
|
||||
DeleteSecretResponse {
|
||||
success: false,
|
||||
issue: e.to_string(),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -89,22 +303,31 @@ fn set_clipboard(text: String) -> SetClipboardResponse {
|
||||
let clipboard_result = Clipboard::new();
|
||||
let mut clipboard = match clipboard_result {
|
||||
Ok(clipboard) => clipboard,
|
||||
Err(e) => return SetClipboardResponse {
|
||||
success: false,
|
||||
issue: e.to_string(),
|
||||
Err(e) => {
|
||||
error!("Failed to get the clipboard instance: {e}.");
|
||||
return SetClipboardResponse {
|
||||
success: false,
|
||||
issue: e.to_string(),
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let set_text_result = clipboard.set_text(text);
|
||||
match set_text_result {
|
||||
Ok(_) => SetClipboardResponse {
|
||||
success: true,
|
||||
issue: String::from(""),
|
||||
Ok(_) => {
|
||||
debug!("Text was set to the clipboard successfully.");
|
||||
SetClipboardResponse {
|
||||
success: true,
|
||||
issue: String::from(""),
|
||||
}
|
||||
},
|
||||
|
||||
Err(e) => SetClipboardResponse {
|
||||
success: false,
|
||||
issue: e.to_string(),
|
||||
Err(e) => {
|
||||
error!("Failed to set text to the clipboard: {e}.");
|
||||
SetClipboardResponse {
|
||||
success: false,
|
||||
issue: e.to_string(),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user