mirror of
				https://github.com/MindWorkAI/AI-Studio.git
				synced 2025-10-31 05:00:21 +00:00 
			
		
		
		
	Refactored .NET server
This commit is contained in:
		
							parent
							
								
									a744c1e16e
								
							
						
					
					
						commit
						90e037b59b
					
				
							
								
								
									
										170
									
								
								runtime/src/dotnet.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								runtime/src/dotnet.rs
									
									
									
									
									
										Normal file
									
								
							| @ -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<Arc<Mutex<Option<CommandChild>>>> = 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<u16> = Lazy::new(|| get_available_port().unwrap()); | ||||
| 
 | ||||
| static DOTNET_INITIALIZED: Lazy<Mutex<bool>> = 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: '<YYYY-MM-dd HH:mm:ss.fff> [<log level>] <source>: <message>'.
 | ||||
|                 // 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."); | ||||
|     } | ||||
| } | ||||
| @ -1,5 +1,6 @@ | ||||
| pub mod encryption; | ||||
| pub mod log; | ||||
| pub mod environment; | ||||
| pub mod dotnet; | ||||
| pub mod network; | ||||
| pub mod api_token; | ||||
| @ -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<Arc<Mutex<Option<CommandChild>>>> = 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<u16> = 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<Mutex<Option<Window>>> = Lazy::new(|| Mutex::new(None)) | ||||
| // The update response coming from the Tauri updater.
 | ||||
| static CHECK_UPDATE_RESPONSE: Lazy<Mutex<Option<UpdateResponse<tauri::Wry>>>> = Lazy::new(|| Mutex::new(None)); | ||||
| 
 | ||||
| 
 | ||||
| static DOTNET_INITIALIZED: Lazy<Mutex<bool>> = 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: '<YYYY-MM-dd HH:mm:ss.fff> [<log level>] <source>: <message>'.
 | ||||
|                 // 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<CheckUpdateResponse> { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user