mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2025-07-04 07:02:56 +00:00
Improved dev experience & allow native .NET debugging (#460)
This commit is contained in:
parent
c0cf620fe3
commit
4ca1fd54d9
3
.gitignore
vendored
3
.gitignore
vendored
@ -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
|
||||||
|
@ -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="" />
|
27
app/.run/[2] Start .NET Server.run.xml
Normal file
27
app/.run/[2] Start .NET Server.run.xml
Normal 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>
|
@ -25,13 +25,13 @@ 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");
|
||||||
@ -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)
|
||||||
|
41
app/MindWork AI Studio/Tools/EnvFile.cs
Normal file
41
app/MindWork AI Studio/Tools/EnvFile.cs
Normal 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
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user