mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2025-04-28 21:39:46 +00:00
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.
This commit is contained in:
parent
2e6de53095
commit
200b87f47a
@ -11,7 +11,13 @@ using System.Reflection;
|
|||||||
using Microsoft.Extensions.FileProviders;
|
using Microsoft.Extensions.FileProviders;
|
||||||
#endif
|
#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);
|
using var rust = new Rust(rustApiPort);
|
||||||
var appPort = await rust.GetAppPort();
|
var appPort = await rust.GetAppPort();
|
||||||
if(appPort == 0)
|
if(appPort == 0)
|
||||||
@ -20,6 +26,14 @@ if(appPort == 0)
|
|||||||
return;
|
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();
|
var builder = WebApplication.CreateBuilder();
|
||||||
builder.Services.AddMudServices(config =>
|
builder.Services.AddMudServices(config =>
|
||||||
{
|
{
|
||||||
|
@ -32,10 +32,6 @@ static DOTNET_SERVER: Lazy<Arc<Mutex<Option<CommandChild>>>> = Lazy::new(|| Arc:
|
|||||||
// do not start the server in the development environment.
|
// do not start the server in the development environment.
|
||||||
static DOTNET_SERVER_PORT: Lazy<u16> = Lazy::new(|| get_available_port().unwrap());
|
static DOTNET_SERVER_PORT: Lazy<u16> = 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<Arc<Mutex<Option<rocket::Rocket<rocket::Ignite>>>>> = Lazy::new(|| Arc::new(Mutex::new(None)));
|
|
||||||
|
|
||||||
// The port used for the runtime API server. In the development environment, we use a fixed
|
// 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
|
// 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
|
// 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
|
// 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
|
// because the server is blocking, and we need to run the Tauri app in
|
||||||
// parallel:
|
// parallel:
|
||||||
let api_server_spawn_clone = API_SERVER.clone();
|
//
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
let api_server = rocket::custom(figment)
|
_ = rocket::custom(figment)
|
||||||
.mount("/", routes![dotnet_port, dotnet_ready])
|
.mount("/", routes![dotnet_port, dotnet_ready])
|
||||||
.ignite().await.unwrap()
|
.ignite().await.unwrap()
|
||||||
.launch().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
|
||||||
});
|
});
|
||||||
|
info!("Secret key for the IPC channel was generated successfully.");
|
||||||
// Arc for the server process to stop it later:
|
info!("Try to start the .NET server...");
|
||||||
let server_spawn_clone = DOTNET_SERVER.clone();
|
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 (mut rx, child) = match is_dev() {
|
||||||
let (sender, mut receiver) = tauri::async_runtime::channel(100);
|
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() {
|
// Start the .NET server in the `../app/MindWork AI Studio` directory.
|
||||||
info!("Try to start the .NET server...");
|
// We provide the runtime API server port to the .NET server:
|
||||||
tauri::async_runtime::spawn(async move {
|
.args(["run", "--project", "../app/MindWork AI Studio", "--", format!("{api_port}").as_str()])
|
||||||
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.");
|
|
||||||
|
|
||||||
let server_pid = child.pid();
|
// Provide the secret key for the IPC channel to the .NET server by using
|
||||||
info!(".NET server process started with PID={server_pid}.");
|
// 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:
|
false => {
|
||||||
*server_spawn_clone.lock().unwrap() = Some(child);
|
Command::new_sidecar("mindworkAIStudioServer")
|
||||||
|
.expect("Failed to create sidecar")
|
||||||
|
|
||||||
// TODO: Migrate to runtime API server:
|
// Provide the runtime API server port to the .NET server:
|
||||||
while let Some(CommandEvent::Stdout(line)) = rx.recv().await {
|
.args([format!("{api_port}").as_str()])
|
||||||
let line_lower = line.to_lowercase();
|
|
||||||
let line_cleared = line_lower.trim();
|
// Provide the secret key for the IPC channel to the .NET server by using
|
||||||
match line_cleared
|
// an environment variable. We must use a HashMap for this:
|
||||||
{
|
.envs(HashMap::from_iter(once((
|
||||||
"rust/tauri server started" => _ = sender.send(ServerEvent::Started).await,
|
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("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("warn") => _ = sender.send(ServerEvent::Warning(line)).await,
|
||||||
@ -400,12 +435,6 @@ fn stop_servers() {
|
|||||||
} else {
|
} else {
|
||||||
warn!("The .NET server process was not started or is already stopped.");
|
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]
|
#[tauri::command]
|
||||||
|
Loading…
Reference in New Issue
Block a user