diff --git a/runtime/src/dotnet.rs b/runtime/src/dotnet.rs new file mode 100644 index 00000000..0b1b9a07 --- /dev/null +++ b/runtime/src/dotnet.rs @@ -0,0 +1,170 @@ +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use base64::Engine; +use base64::prelude::BASE64_STANDARD; +use log::{debug, error, info, warn}; +use once_cell::sync::Lazy; +use rocket::get; +use tauri::api::process::{Command, CommandChild, CommandEvent}; +use tauri::Url; +use crate::api_token::{APIToken, API_TOKEN}; +use crate::app_window::change_location_to; +use crate::encryption::ENCRYPTION; +use crate::environment::is_dev; +use crate::network::get_available_port; + +// The .NET server is started in a separate process and communicates with this +// runtime process via IPC. However, we do net start the .NET server in +// the development environment. +static DOTNET_SERVER: Lazy>>> = Lazy::new(|| Arc::new(Mutex::new(None))); + +// The .NET server port is relevant for the production environment only, sine we +// do not start the server in the development environment. +static DOTNET_SERVER_PORT: Lazy = Lazy::new(|| get_available_port().unwrap()); + +static DOTNET_INITIALIZED: Lazy> = Lazy::new(|| Mutex::new(false)); + +#[get("/system/dotnet/port")] +pub fn dotnet_port(_token: APIToken) -> String { + let dotnet_server_port = *DOTNET_SERVER_PORT; + format!("{dotnet_server_port}") +} + +pub fn start_dotnet_server(api_server_port: u16, certificate_fingerprint: String) { + + // Get the secret password & salt and convert it to a base64 string: + let secret_password = BASE64_STANDARD.encode(ENCRYPTION.secret_password); + let secret_key_salt = BASE64_STANDARD.encode(ENCRYPTION.secret_key_salt); + + let dotnet_server_environment = HashMap::from_iter([ + (String::from("AI_STUDIO_SECRET_PASSWORD"), secret_password), + (String::from("AI_STUDIO_SECRET_KEY_SALT"), secret_key_salt), + (String::from("AI_STUDIO_CERTIFICATE_FINGERPRINT"), certificate_fingerprint), + (String::from("AI_STUDIO_API_TOKEN"), API_TOKEN.to_hex_text().to_string()), + ]); + + info!("Try to start the .NET server..."); + let server_spawn_clone = DOTNET_SERVER.clone(); + tauri::async_runtime::spawn(async move { + let api_port = api_server_port; + + let (mut rx, child) = match is_dev() { + true => { + // We are in the development environment, so we try to start a process + // with `dotnet run` in the `../app/MindWork AI Studio` directory. But + // we cannot issue a sidecar because we cannot use any command for the + // sidecar (see Tauri configuration). Thus, we use a standard Rust process: + warn!(Source = "Bootloader .NET"; "Development environment detected; start .NET server using 'dotnet run'."); + Command::new("dotnet") + + // Start the .NET server in the `../app/MindWork AI Studio` directory. + // We provide the runtime API server port to the .NET server: + .args(["run", "--project", "../app/MindWork AI Studio", "--", format!("{api_port}").as_str()]) + + .envs(dotnet_server_environment) + .spawn() + .expect("Failed to spawn .NET server process.") + } + + false => { + Command::new_sidecar("mindworkAIStudioServer") + .expect("Failed to create sidecar") + + // Provide the runtime API server port to the .NET server: + .args([format!("{api_port}").as_str()]) + + .envs(dotnet_server_environment) + .spawn() + .expect("Failed to spawn .NET server process.") + } + }; + + let server_pid = child.pid(); + info!(Source = "Bootloader .NET"; "The .NET server process started with PID={server_pid}."); + + // Save the server process to stop it later: + *server_spawn_clone.lock().unwrap() = Some(child); + + // Log the output of the .NET server: + while let Some(CommandEvent::Stdout(line)) = rx.recv().await { + + // Remove newline characters from the end: + let line = line.trim_end(); + + // Starts the line with '=>'? + if line.starts_with("=>") { + // Yes. This means that the line is a log message from the .NET server. + // The format is: ' [] : '. + // We try to parse this line and log it with the correct log level: + let line = line.trim_start_matches("=>").trim(); + let parts = line.split_once(": ").unwrap(); + let left_part = parts.0.trim(); + let message = parts.1.trim(); + let parts = left_part.split_once("] ").unwrap(); + let level = parts.0.split_once("[").unwrap().1.trim(); + let source = parts.1.trim(); + match level { + "Trace" => debug!(Source = ".NET Server", Comp = source; "{message}"), + "Debug" => debug!(Source = ".NET Server", Comp = source; "{message}"), + "Information" => info!(Source = ".NET Server", Comp = source; "{message}"), + "Warning" => warn!(Source = ".NET Server", Comp = source; "{message}"), + "Error" => error!(Source = ".NET Server", Comp = source; "{message}"), + "Critical" => error!(Source = ".NET Server", Comp = source; "{message}"), + + _ => error!(Source = ".NET Server", Comp = source; "{message} (unknown log level '{level}')"), + } + } else { + let lower_line = line.to_lowercase(); + if lower_line.contains("error") { + error!(Source = ".NET Server"; "{line}"); + } else if lower_line.contains("warning") { + warn!(Source = ".NET Server"; "{line}"); + } else { + info!(Source = ".NET Server"; "{line}"); + } + } + } + }); +} + +#[get("/system/dotnet/ready")] +pub async fn dotnet_ready(_token: APIToken) { + + // We create a manual scope for the lock to be released as soon as possible. + // This is necessary because we cannot await any function while the lock is + // held. + { + let mut initialized = DOTNET_INITIALIZED.lock().unwrap(); + if *initialized { + error!("Anyone tried to initialize the runtime twice. This is not intended."); + return; + } + + info!("The .NET server was booted successfully."); + *initialized = true; + } + + let dotnet_server_port = *DOTNET_SERVER_PORT; + let url = match Url::parse(format!("http://localhost:{dotnet_server_port}").as_str()) + { + Ok(url) => url, + Err(msg) => { + error!("Error while parsing URL for navigating to the app: {msg}"); + return; + } + }; + + change_location_to(url.as_str()).await; +} + +pub fn stop_dotnet_server() { + if let Some(server_process) = DOTNET_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 is already stopped."); + } +} \ No newline at end of file diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 2fce4733..e1353cb0 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1,5 +1,6 @@ pub mod encryption; pub mod log; pub mod environment; +pub mod dotnet; pub mod network; pub mod api_token; \ No newline at end of file diff --git a/runtime/src/main.rs b/runtime/src/main.rs index 49a6f97c..24d970b4 100644 --- a/runtime/src/main.rs +++ b/runtime/src/main.rs @@ -36,14 +36,6 @@ use mindwork_ai_studio::environment::{is_dev, is_prod}; use mindwork_ai_studio::log::{init_logging, switch_to_file_logging}; -// The .NET server is started in a separate process and communicates with this -// runtime process via IPC. However, we do net start the .NET server in -// the development environment. -static DOTNET_SERVER: Lazy>>> = Lazy::new(|| Arc::new(Mutex::new(None))); - -// The .NET server port is relevant for the production environment only, sine we -// do not start the server in the development environment. -static DOTNET_SERVER_PORT: Lazy = Lazy::new(|| get_available_port().unwrap()); // The port used for the runtime API server. In the development environment, we use a fixed // port, in the production environment we use the next available port. This differentiation @@ -63,9 +55,6 @@ static MAIN_WINDOW: Lazy>> = Lazy::new(|| Mutex::new(None)) // The update response coming from the Tauri updater. static CHECK_UPDATE_RESPONSE: Lazy>>> = Lazy::new(|| Mutex::new(None)); - -static DOTNET_INITIALIZED: Lazy> = Lazy::new(|| Mutex::new(false)); - #[tokio::main] async fn main() { @@ -178,100 +167,7 @@ async fn main() { .launch().await.unwrap(); }); - // Get the secret password & salt and convert it to a base64 string: - let secret_password = BASE64_STANDARD.encode(ENCRYPTION.secret_password); - let secret_key_salt = BASE64_STANDARD.encode(ENCRYPTION.secret_key_salt); - - let dotnet_server_environment = HashMap::from_iter([ - (String::from("AI_STUDIO_SECRET_PASSWORD"), secret_password), - (String::from("AI_STUDIO_SECRET_KEY_SALT"), secret_key_salt), - (String::from("AI_STUDIO_CERTIFICATE_FINGERPRINT"), certificate_fingerprint), - (String::from("AI_STUDIO_API_TOKEN"), API_TOKEN.to_hex_text().to_string()), - ]); - info!("Secret password for the IPC channel was generated successfully."); - info!("Try to start the .NET server..."); - let server_spawn_clone = DOTNET_SERVER.clone(); - tauri::async_runtime::spawn(async move { - let api_port = *API_SERVER_PORT; - - let (mut rx, child) = match is_dev() { - true => { - // We are in the development environment, so we try to start a process - // with `dotnet run` in the `../app/MindWork AI Studio` directory. But - // we cannot issue a sidecar because we cannot use any command for the - // sidecar (see Tauri configuration). Thus, we use a standard Rust process: - warn!(Source = "Bootloader .NET"; "Development environment detected; start .NET server using 'dotnet run'."); - Command::new("dotnet") - - // Start the .NET server in the `../app/MindWork AI Studio` directory. - // We provide the runtime API server port to the .NET server: - .args(["run", "--project", "../app/MindWork AI Studio", "--", format!("{api_port}").as_str()]) - - .envs(dotnet_server_environment) - .spawn() - .expect("Failed to spawn .NET server process.") - } - - false => { - Command::new_sidecar("mindworkAIStudioServer") - .expect("Failed to create sidecar") - - // Provide the runtime API server port to the .NET server: - .args([format!("{api_port}").as_str()]) - - .envs(dotnet_server_environment) - .spawn() - .expect("Failed to spawn .NET server process.") - } - }; - - let server_pid = child.pid(); - info!(Source = "Bootloader .NET"; "The .NET server process started with PID={server_pid}."); - - // Save the server process to stop it later: - *server_spawn_clone.lock().unwrap() = Some(child); - - // Log the output of the .NET server: - while let Some(CommandEvent::Stdout(line)) = rx.recv().await { - - // Remove newline characters from the end: - let line = line.trim_end(); - - // Starts the line with '=>'? - if line.starts_with("=>") { - // Yes. This means that the line is a log message from the .NET server. - // The format is: ' [] : '. - // We try to parse this line and log it with the correct log level: - let line = line.trim_start_matches("=>").trim(); - let parts = line.split_once(": ").unwrap(); - let left_part = parts.0.trim(); - let message = parts.1.trim(); - let parts = left_part.split_once("] ").unwrap(); - let level = parts.0.split_once("[").unwrap().1.trim(); - let source = parts.1.trim(); - match level { - "Trace" => debug!(Source = ".NET Server", Comp = source; "{message}"), - "Debug" => debug!(Source = ".NET Server", Comp = source; "{message}"), - "Information" => info!(Source = ".NET Server", Comp = source; "{message}"), - "Warning" => warn!(Source = ".NET Server", Comp = source; "{message}"), - "Error" => error!(Source = ".NET Server", Comp = source; "{message}"), - "Critical" => error!(Source = ".NET Server", Comp = source; "{message}"), - - _ => error!(Source = ".NET Server", Comp = source; "{message} (unknown log level '{level}')"), - } - } else { - let lower_line = line.to_lowercase(); - if lower_line.contains("error") { - error!(Source = ".NET Server"; "{line}"); - } else if lower_line.contains("warning") { - warn!(Source = ".NET Server"; "{line}"); - } else { - info!(Source = ".NET Server"; "{line}"); - } - } - } - }); info!("Starting Tauri app..."); let app = tauri::Builder::default() @@ -373,82 +269,6 @@ async fn main() { } -#[get("/system/dotnet/port")] -fn dotnet_port(_token: APIToken) -> String { - let dotnet_server_port = *DOTNET_SERVER_PORT; - format!("{dotnet_server_port}") -} - - -#[get("/system/dotnet/ready")] -async fn dotnet_ready(_token: APIToken) { - - // We create a manual scope for the lock to be released as soon as possible. - // This is necessary because we cannot await any function while the lock is - // held. - { - let mut initialized = DOTNET_INITIALIZED.lock().unwrap(); - if *initialized { - error!("Anyone tried to initialize the runtime twice. This is not intended."); - return; - } - - info!("The .NET server was booted successfully."); - *initialized = true; - } - - // 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; - let main_window_spawn_clone = &MAIN_WINDOW; - 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(Duration::from_millis(100)).await; - } - } - - let main_window = main_window_spawn_clone.lock().unwrap(); - let dotnet_server_port = *DOTNET_SERVER_PORT; - let url = match Url::parse(format!("http://localhost:{dotnet_server_port}").as_str()) - { - Ok(url) => url, - Err(msg) => { - error!("Error while parsing URL for navigating to the app: {msg}"); - return; - } - }; - - 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!("The app location was changed to {url}."), - Err(e) => error!("Failed to change the app location to {url}: {e}."), - } -} - - -fn stop_servers() { - if let Some(server_process) = DOTNET_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 is already stopped."); - } -} #[get("/updates/check")] async fn check_for_update(_token: APIToken) -> Json {