From 200b87f47aee0f10906ec1b2e9a235546f3c5b0a Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 25 Aug 2024 21:53:28 +0200 Subject: [PATCH] Refactored .NET server start In the dev and production environment gets the .NET server started by Rust. Also, the secret IPC key gets set by using a local env variable. --- app/MindWork AI Studio/Program.cs | 16 ++++- runtime/src/main.rs | 105 +++++++++++++++++++----------- 2 files changed, 82 insertions(+), 39 deletions(-) diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index 8a7638fd..30a6ef16 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -11,7 +11,13 @@ using System.Reflection; using Microsoft.Extensions.FileProviders; #endif -var rustApiPort = args.Length > 0 ? args[0] : "5000"; +if(args.Length == 0) +{ + Console.WriteLine("Please provide the port of the runtime API."); + return; +} + +var rustApiPort = args[0]; using var rust = new Rust(rustApiPort); var appPort = await rust.GetAppPort(); if(appPort == 0) @@ -20,6 +26,14 @@ if(appPort == 0) return; } +// Read the secret key for the IPC from the AI_STUDIO_SECRET_KEY environment variable: +var secretKey = Environment.GetEnvironmentVariable("AI_STUDIO_SECRET_KEY"); +if(string.IsNullOrWhiteSpace(secretKey)) +{ + Console.WriteLine("The AI_STUDIO_SECRET_KEY environment variable is not set."); + return; +} + var builder = WebApplication.CreateBuilder(); builder.Services.AddMudServices(config => { diff --git a/runtime/src/main.rs b/runtime/src/main.rs index d8a500b0..c514bc81 100644 --- a/runtime/src/main.rs +++ b/runtime/src/main.rs @@ -32,10 +32,6 @@ static DOTNET_SERVER: Lazy>>> = Lazy::new(|| Arc: // do not start the server in the development environment. static DOTNET_SERVER_PORT: Lazy = Lazy::new(|| get_available_port().unwrap()); -// Our runtime API server. We need it as a static variable because we need to -// shut it down when the app is closed. -static API_SERVER: Lazy>>>> = Lazy::new(|| Arc::new(Mutex::new(None))); - // 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 // is necessary because we cannot communicate the port to the .NET server in the development @@ -132,46 +128,85 @@ async fn main() { // Start the runtime API server in a separate thread. This is necessary // because the server is blocking, and we need to run the Tauri app in // parallel: - let api_server_spawn_clone = API_SERVER.clone(); + // tauri::async_runtime::spawn(async move { - let api_server = rocket::custom(figment) + _ = rocket::custom(figment) .mount("/", routes![dotnet_port, dotnet_ready]) .ignite().await.unwrap() .launch().await.unwrap(); - // We need to save the server to shut it down later: - *api_server_spawn_clone.lock().unwrap() = Some(api_server); + // + // Generate a secret key for the AES encryption for the IPC channel: + // + let mut secret_key = [0u8; 512]; // 512 bytes = 4096 bits + + // We use a cryptographically secure pseudo-random number generator + // to generate the secret key. ChaCha20Rng is the algorithm of our choice: + let mut rng = rand_chacha::ChaChaRng::from_entropy(); + + // Fill the secret key with random bytes: + rng.fill_bytes(&mut secret_key); + + // Convert the secret key to a hexadecimal string: + let secret_key = secret_key.iter().fold(String::new(), |mut out, b| { + _ = write!(out, "{b:02X}"); + out }); - - // Arc for the server process to stop it later: + info!("Secret key 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; - // Channel to communicate with the server process: - let (sender, mut receiver) = tauri::async_runtime::channel(100); + 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") - if is_prod() { - info!("Try to start the .NET server..."); - tauri::async_runtime::spawn(async move { - let api_port = *API_SERVER_PORT; - let (mut rx, child) = Command::new_sidecar("mindworkAIStudioServer") - .expect("Failed to create sidecar") - .args([format!("{api_port}").as_str()]) - .spawn() - .expect("Failed to spawn .NET server process."); + // 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()]) - let server_pid = child.pid(); - info!(".NET server process started with PID={server_pid}."); + // Provide the secret key for the IPC channel to the .NET server by using + // an environment variable. We must use a HashMap for this: + .envs(HashMap::from_iter(once(( + String::from("AI_STUDIO_SECRET_KEY"), + secret_key + )))) + .spawn() + .expect("Failed to spawn .NET server process.") + } - // Save the server process to stop it later: - *server_spawn_clone.lock().unwrap() = Some(child); + false => { + Command::new_sidecar("mindworkAIStudioServer") + .expect("Failed to create sidecar") - // TODO: Migrate to runtime API server: - 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, + // Provide the runtime API server port to the .NET server: + .args([format!("{api_port}").as_str()]) + + // Provide the secret key for the IPC channel to the .NET server by using + // an environment variable. We must use a HashMap for this: + .envs(HashMap::from_iter(once(( + String::from("AI_STUDIO_SECRET_KEY"), + secret_key + )))) + .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 { _ 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, @@ -400,12 +435,6 @@ fn stop_servers() { } else { warn!("The .NET server process was not started or is already stopped."); } - - if let Some(api_server) = API_SERVER.lock().unwrap().take() { - _ = api_server.shutdown(); - } else { - warn!("The API server was not started or is already stopped."); - } } #[tauri::command]