Improved dev experience & allow native .NET debugging (#460)

This commit is contained in:
Thorsten Sommer 2025-05-17 13:36:28 +02:00 committed by GitHub
parent c0cf620fe3
commit 4ca1fd54d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 139 additions and 42 deletions

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
# Ignore any startup.env file:
startup.env
# Ignore pdfium library: # Ignore pdfium library:
libpdfium.dylib libpdfium.dylib
libpdfium.so libpdfium.so

View File

@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Tauri Dev" type="ShConfigurationType"> <configuration default="false" name="[1] Start Tauri" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="cargo tauri dev --no-watch" /> <option name="SCRIPT_TEXT" value="cargo tauri dev --no-watch" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" /> <option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="" /> <option name="SCRIPT_PATH" value="" />

View File

@ -0,0 +1,27 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="[2] Start .NET Server" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/MindWork AI Studio/bin/Debug/net9.0/osx-arm64/mindworkAIStudio" />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/MindWork AI Studio" />
<option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="ENV_FILE_PATHS" value="" />
<option name="REDIRECT_INPUT_PATH" value="" />
<option name="PTY_MODE" value="Auto" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="AUTO_ATTACH_CHILDREN" value="0" />
<option name="MIXED_MODE_DEBUG" value="0" />
<option name="RUNTIME_TYPE" value="coreclr" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/MindWork AI Studio/MindWork AI Studio.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net9.0" />
<method v="2">
<option name="Build" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Collect I18N content" run_configuration_type="ShConfigurationType" />
</method>
</configuration>
</component>

View File

@ -25,14 +25,14 @@ internal sealed class Program
public static IServiceProvider SERVICE_PROVIDER = null!; public static IServiceProvider SERVICE_PROVIDER = null!;
public static ILoggerFactory LOGGER_FACTORY = null!; public static ILoggerFactory LOGGER_FACTORY = null!;
public static async Task Main(string[] args) public static async Task Main()
{ {
if(args.Length == 0) #if DEBUG
{ // Read the environment variables from the .env file:
Console.WriteLine("Error: Please provide the port of the runtime API."); var envFilePath = Path.Combine("..", "..", "startup.env");
return; await EnvFile.Apply(envFilePath);
} #endif
// Read the secret key for the IPC from the AI_STUDIO_SECRET_KEY environment variable: // Read the secret key for the IPC from the AI_STUDIO_SECRET_KEY environment variable:
var secretPasswordEncoded = Environment.GetEnvironmentVariable("AI_STUDIO_SECRET_PASSWORD"); var secretPasswordEncoded = Environment.GetEnvironmentVariable("AI_STUDIO_SECRET_PASSWORD");
if(string.IsNullOrWhiteSpace(secretPasswordEncoded)) if(string.IsNullOrWhiteSpace(secretPasswordEncoded))
@ -58,6 +58,13 @@ internal sealed class Program
return; return;
} }
var rustApiPort = Environment.GetEnvironmentVariable("AI_STUDIO_API_PORT");
if(string.IsNullOrWhiteSpace(rustApiPort))
{
Console.WriteLine("Error: The AI_STUDIO_API_PORT environment variable is not set.");
return;
}
var apiToken = Environment.GetEnvironmentVariable("AI_STUDIO_API_TOKEN"); var apiToken = Environment.GetEnvironmentVariable("AI_STUDIO_API_TOKEN");
if(string.IsNullOrWhiteSpace(apiToken)) if(string.IsNullOrWhiteSpace(apiToken))
{ {
@ -67,7 +74,6 @@ internal sealed class Program
API_TOKEN = apiToken; API_TOKEN = apiToken;
var rustApiPort = args[0];
using var rust = new RustService(rustApiPort, certificateFingerprint); using var rust = new RustService(rustApiPort, certificateFingerprint);
var appPort = await rust.GetAppPort(); var appPort = await rust.GetAppPort();
if(appPort == 0) if(appPort == 0)

View File

@ -0,0 +1,41 @@
#if DEBUG
using System.Text;
namespace AIStudio.Tools;
/// <summary>
/// Read environment variables for the application from an .env file.
/// </summary>
/// <remarks>
/// We consider this feature a security issue. Therefore, it is only
/// available in DEBUG mode. To ensure this, we remove the code
/// from any release build.
/// </remarks>
public static class EnvFile
{
public static async Task Apply(string filePath)
{
if(!File.Exists(filePath))
{
Console.WriteLine($"Error: The .env file '{filePath}' does not exist.");
return;
}
var lines = await File.ReadAllLinesAsync(filePath, Encoding.UTF8);
foreach (var line in lines)
{
if (string.IsNullOrWhiteSpace(line) || line.Trim().StartsWith('#'))
continue;
var parts = line.Split(['='], 2);
if (parts.Length != 2)
continue;
var key = parts[0].Trim();
var value = parts[1].Trim();
Environment.SetEnvironmentVariable(key, value);
}
}
}
#endif

View File

@ -34,54 +34,65 @@ pub fn dotnet_port(_token: APIToken) -> String {
format!("{dotnet_server_port}") format!("{dotnet_server_port}")
} }
/// Creates the startup environment file for the .NET server in the development
/// environment. The file is created in the root directory of the repository.
/// Creating that env file on a production environment would be a security
/// issue, since it contains the secret password and salt in plain text.
/// Anyone could read that file and decrypt the secret communication
/// between the .NET server and the Tauri app.
///
/// Therefore, we not only create the file in the development environment
/// but also remove that code from any production build.
#[cfg(debug_assertions)]
pub fn create_startup_env_file() {
// 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 api_port = *API_SERVER_PORT;
warn!(Source = "Bootloader .NET"; "Development environment detected; create the startup env file at '../startup.env'.");
let env_file_path = std::path::PathBuf::from("..").join("startup.env");
let mut env_file = std::fs::File::create(env_file_path).unwrap();
let env_file_content = format!(
"AI_STUDIO_SECRET_PASSWORD={secret_password}\n\
AI_STUDIO_SECRET_KEY_SALT={secret_key_salt}\n\
AI_STUDIO_CERTIFICATE_FINGERPRINT={cert_fingerprint}\n\
AI_STUDIO_API_PORT={api_port}\n\
AI_STUDIO_API_TOKEN={api_token}",
cert_fingerprint = CERTIFICATE_FINGERPRINT.get().unwrap(),
api_token = API_TOKEN.to_hex_text()
);
std::io::Write::write_all(&mut env_file, env_file_content.as_bytes()).unwrap();
info!(Source = "Bootloader .NET"; "The startup env file was created successfully.");
}
/// Starts the .NET server in a separate process. /// Starts the .NET server in a separate process.
pub fn start_dotnet_server() { pub fn start_dotnet_server() {
// Get the secret password & salt and convert it to a base64 string: // Get the secret password & salt and convert it to a base64 string:
let secret_password = BASE64_STANDARD.encode(ENCRYPTION.secret_password); let secret_password = BASE64_STANDARD.encode(ENCRYPTION.secret_password);
let secret_key_salt = BASE64_STANDARD.encode(ENCRYPTION.secret_key_salt); let secret_key_salt = BASE64_STANDARD.encode(ENCRYPTION.secret_key_salt);
let api_port = *API_SERVER_PORT;
let dotnet_server_environment = HashMap::from_iter([ let dotnet_server_environment = HashMap::from_iter([
(String::from("AI_STUDIO_SECRET_PASSWORD"), secret_password), (String::from("AI_STUDIO_SECRET_PASSWORD"), secret_password),
(String::from("AI_STUDIO_SECRET_KEY_SALT"), secret_key_salt), (String::from("AI_STUDIO_SECRET_KEY_SALT"), secret_key_salt),
(String::from("AI_STUDIO_CERTIFICATE_FINGERPRINT"), CERTIFICATE_FINGERPRINT.get().unwrap().to_string()), (String::from("AI_STUDIO_CERTIFICATE_FINGERPRINT"), CERTIFICATE_FINGERPRINT.get().unwrap().to_string()),
(String::from("AI_STUDIO_API_PORT"), format!("{api_port}")),
(String::from("AI_STUDIO_API_TOKEN"), API_TOKEN.to_hex_text().to_string()), (String::from("AI_STUDIO_API_TOKEN"), API_TOKEN.to_hex_text().to_string()),
]); ]);
info!("Try to start the .NET server..."); 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 { tauri::async_runtime::spawn(async move {
let api_port = *API_SERVER_PORT; let (mut rx, child) = Command::new_sidecar("mindworkAIStudioServer")
let (mut rx, child) = match is_dev() { .expect("Failed to create sidecar")
true => { .envs(dotnet_server_environment)
// We are in the development environment, so we try to start a process .spawn()
// with `dotnet run` in the `../app/MindWork AI Studio` directory. But .expect("Failed to spawn .NET server process.");
// 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(); let server_pid = child.pid();
info!(Source = "Bootloader .NET"; "The .NET server process started with PID={server_pid}."); info!(Source = "Bootloader .NET"; "The .NET server process started with PID={server_pid}.");
@ -140,7 +151,7 @@ pub async fn dotnet_ready(_token: APIToken) {
// held. // held.
{ {
let mut initialized = DOTNET_INITIALIZED.lock().unwrap(); let mut initialized = DOTNET_INITIALIZED.lock().unwrap();
if *initialized { if !is_dev() && *initialized {
error!("Anyone tried to initialize the runtime twice. This is not intended."); error!("Anyone tried to initialize the runtime twice. This is not intended.");
return; return;
} }

View File

@ -13,6 +13,9 @@ use mindwork_ai_studio::log::init_logging;
use mindwork_ai_studio::metadata::MetaData; use mindwork_ai_studio::metadata::MetaData;
use mindwork_ai_studio::runtime_api::start_runtime_api; use mindwork_ai_studio::runtime_api::start_runtime_api;
#[cfg(debug_assertions)]
use mindwork_ai_studio::dotnet::create_startup_env_file;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let metadata = MetaData::init_from_string(include_str!("../../metadata.txt")); let metadata = MetaData::init_from_string(include_str!("../../metadata.txt"));
@ -44,6 +47,12 @@ async fn main() {
generate_certificate(); generate_certificate();
start_runtime_api(); start_runtime_api();
start_dotnet_server();
if is_dev() {
create_startup_env_file();
} else {
start_dotnet_server();
}
start_tauri(); start_tauri();
} }