Tauri update auf v2 und Rust update

This commit is contained in:
PaulKoudelka 2026-03-12 16:02:24 +01:00
parent 1c52d6f199
commit 58b93b9b0e
9 changed files with 454 additions and 316 deletions

3
.gitignore vendored
View File

@ -169,3 +169,6 @@ orleans.codegen.cs
# Ignore GitHub Copilot migration files:
**/copilot.data.migration.*.xml
# Tauri generated schemas/manifests
/runtime/gen/

View File

@ -5,7 +5,7 @@
9.0.13 (commit 9ecbfd4f3f)
1.94.0 (commit 4a4ef493e)
8.15.0
1.8.1
2.10.3
3eb367d4c9e, release
osx-arm64
144.0.7543.0

View File

@ -1,16 +1,16 @@
[package]
name = "mindwork-ai-studio"
version = "26.2.2"
edition = "2021"
edition = "2024"
description = "MindWork AI Studio"
authors = ["Thorsten Sommer"]
[build-dependencies]
tauri-build = { version = "1.5.6", features = [] }
tauri-build = { version = "2.5.6", features = [] }
[dependencies]
tauri = { version = "1.8.3", features = [ "http-all", "updater", "shell-sidecar", "shell-open", "dialog", "global-shortcut"] }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri = { version = "2.10.3", features = [] }
tauri-plugin-window-state = { version = "2.4.1" }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
keyring = { version = "3.6.2", features = ["apple-native", "windows-native", "sync-secret-service"] }
@ -46,6 +46,10 @@ sysinfo = "0.38.0"
# Fixes security vulnerability downstream, where the upstream is not fixed yet:
time = "0.3.47" # -> Rocket
bytes = "1.11.1" # -> almost every dependency
tauri-plugin-fs = "2"
tauri-plugin-http = "2"
tauri-plugin-shell = "2.3.5"
tauri-plugin-dialog = "2.6.0"
[target.'cfg(target_os = "linux")'.dependencies]
# See issue https://github.com/tauri-apps/tauri/issues/4470
@ -57,5 +61,9 @@ openssl = "0.10.75"
[target.'cfg(target_os = "windows")'.dependencies]
windows-registry = "0.6.1"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-global-shortcut = "2"
tauri-plugin-updater = "2.10.0"
[features]
custom-protocol = ["tauri/custom-protocol"]

View File

@ -53,6 +53,18 @@ fn update_cargo_toml(cargo_path: &str, version: &str) {
let cargo_toml_lines = cargo_toml.lines();
let mut new_cargo_toml = String::new();
// Return early when the version already matches to avoid unnecessary rewrites.
let current_version = cargo_toml.lines().find_map(|line| {
let trimmed = line.trim_start();
let rest = trimmed.strip_prefix("\"version\": ")?;
let quoted = rest.strip_prefix('"')?;
let end_idx = quoted.find('"')?;
Some(&quoted[..end_idx])
});
if current_version == Some(version) {
return;
}
for line in cargo_toml_lines {
if line.starts_with("version = ") {
new_cargo_toml.push_str(&format!("version = \"{version}\""));
@ -67,6 +79,19 @@ fn update_cargo_toml(cargo_path: &str, version: &str) {
fn update_tauri_conf(tauri_conf_path: &str, version: &str) {
let tauri_conf = std::fs::read_to_string(tauri_conf_path).unwrap();
// Return early when the version already matches to avoid unnecessary rewrites.
let current_version = tauri_conf.lines().find_map(|line| {
let trimmed = line.trim_start();
let rest = trimmed.strip_prefix("\"version\": ")?;
let quoted = rest.strip_prefix('"')?;
let end_idx = quoted.find('"')?;
Some(&quoted[..end_idx])
});
if current_version == Some(version) {
return;
}
let tauri_conf_lines = tauri_conf.lines();
let mut new_tauri_conf = String::new();
@ -75,7 +100,7 @@ fn update_tauri_conf(tauri_conf_path: &str, version: &str) {
// "version": "0.1.0-alpha.0"
// Please notice, that the version number line might have a leading tab, etc.
if line.contains("\"version\": ") {
new_tauri_conf.push_str(&format!("\t\"version\": \"{version}\""));
new_tauri_conf.push_str(&format!(" \"version\": \"{version}\","));
} else {
new_tauri_conf.push_str(line);
}

View File

@ -0,0 +1,47 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default capability for MindWork AI Studio",
"windows": [
"main"
],
"permissions": [
"core:default",
"shell:allow-open",
{
"identifier": "shell:allow-spawn",
"allow": [
{
"name": "mindworkAIStudioServer",
"sidecar": true,
"args": true
},
{
"name": "qdrant",
"sidecar": true,
"args": true
}
]
},
{
"identifier": "http:default",
"allow": [
{
"url": "http://localhost"
},
{
"url": "http://localhost/*"
}
]
},
"fs:default",
{
"identifier": "fs:scope",
"allow": [
{
"path": "$RESOURCE/resources/**"
}
]
}
]
}

View File

@ -9,9 +9,12 @@ use rocket::serde::json::Json;
use rocket::serde::Serialize;
use serde::Deserialize;
use strum_macros::Display;
use tauri::updater::UpdateResponse;
use tauri::{FileDropEvent, GlobalShortcutManager, UpdaterEvent, RunEvent, Manager, PathResolver, Window, WindowEvent, generate_context};
use tauri::api::dialog::blocking::FileDialogBuilder;
use tauri::{DragDropEvent,RunEvent, Manager, WindowEvent, generate_context};
use tauri::path::PathResolver;
use tauri::WebviewWindow;
use tauri_plugin_dialog::{DialogExt, FileDialogBuilder};
use tauri_plugin_updater::{UpdaterExt, Update};
use tauri_plugin_global_shortcut::GlobalShortcutExt;
use tokio::sync::broadcast;
use tokio::time;
use crate::api_token::APIToken;
@ -24,10 +27,10 @@ use crate::qdrant::{cleanup_qdrant, start_qdrant_server, stop_qdrant_server};
use crate::dotnet::create_startup_env_file;
/// The Tauri main window.
static MAIN_WINDOW: Lazy<Mutex<Option<Window>>> = Lazy::new(|| Mutex::new(None));
static MAIN_WINDOW: Lazy<Mutex<Option<WebviewWindow>>> = 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 CHECK_UPDATE_RESPONSE: Lazy<Mutex<Option<Update>>> = Lazy::new(|| Mutex::new(None));
/// The event broadcast sender for Tauri events.
static EVENT_BROADCAST: Lazy<Mutex<Option<broadcast::Sender<Event>>>> = Lazy::new(|| Mutex::new(None));
@ -76,10 +79,14 @@ pub fn start_tauri() {
});
let app = tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(tauri_plugin_updater::Builder::new().build())
.setup(move |app| {
// Get the main window:
let window = app.get_window("main").expect("Failed to get main window.");
let window = app.get_webview_window("main").expect("Failed to get main window.");
// Register a callback for window events, such as file drops. We have to use
// this handler in addition to the app event handler, because file drop events
@ -100,12 +107,12 @@ pub fn start_tauri() {
*MAIN_WINDOW.lock().unwrap() = Some(window);
info!(Source = "Bootloader Tauri"; "Setup is running.");
let data_path = app.path_resolver().app_local_data_dir().unwrap();
let data_path = app.path().app_local_data_dir().unwrap();
let data_path = data_path.join("data");
// Get and store the data and config directories:
DATA_DIRECTORY.set(data_path.to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the data directory.")).unwrap();
CONFIG_DIRECTORY.set(app.path_resolver().app_config_dir().unwrap().to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the config directory.")).unwrap();
CONFIG_DIRECTORY.set(app.path().app_config_dir().unwrap().to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the config directory.")).unwrap();
cleanup_qdrant();
cleanup_dotnet_server();
@ -114,13 +121,13 @@ pub fn start_tauri() {
#[cfg(debug_assertions)]
create_startup_env_file();
} else {
start_dotnet_server();
start_dotnet_server(app.handle().clone());
}
start_qdrant_server();
start_qdrant_server(app.handle().clone());
info!(Source = "Bootloader Tauri"; "Reconfigure the file logger to use the app data directory {data_path:?}");
switch_to_file_logging(data_path).map_err(|e| error!("Failed to switch logging to file: {e}")).unwrap();
set_pdfium_path(app.path_resolver());
set_pdfium_path(app.path());
Ok(())
})
@ -129,7 +136,7 @@ pub fn start_tauri() {
.expect("Error while running Tauri application");
// The app event handler:
app.run(|app_handle, event| {
app.run(|_app_handle, event| {
if !matches!(event, RunEvent::MainEventsCleared) {
debug!(Source = "Tauri"; "Tauri event received: location=app event handler , event={event:?}");
}
@ -149,54 +156,6 @@ pub fn start_tauri() {
}
}
RunEvent::Updater(updater_event) => {
match updater_event {
UpdaterEvent::UpdateAvailable { body, date, version } => {
let body_len = body.len();
info!(Source = "Tauri"; "Updater: update available: body size={body_len} time={date:?} version={version}");
}
UpdaterEvent::Pending => {
info!(Source = "Tauri"; "Updater: update is pending!");
}
UpdaterEvent::DownloadProgress { chunk_length, content_length: _ } => {
trace!(Source = "Tauri"; "Updater: downloading chunk of {chunk_length} bytes");
}
UpdaterEvent::Downloaded => {
info!(Source = "Tauri"; "Updater: update has been downloaded!");
warn!(Source = "Tauri"; "Try to stop the .NET server now...");
if is_prod() {
stop_dotnet_server();
stop_qdrant_server();
} else {
warn!(Source = "Tauri"; "Development environment detected; do not stop the .NET server.");
}
}
UpdaterEvent::Updated => {
info!(Source = "Tauri"; "Updater: app has been updated");
warn!(Source = "Tauri"; "Try to restart the app now...");
if is_prod() {
app_handle.restart();
} else {
warn!(Source = "Tauri"; "Development environment detected; do not restart the app.");
}
}
UpdaterEvent::AlreadyUpToDate => {
info!(Source = "Tauri"; "Updater: app is already up to date");
}
UpdaterEvent::Error(error) => {
warn!(Source = "Tauri"; "Updater: failed to update: {error}");
}
}
}
RunEvent::ExitRequested { .. } => {
warn!(Source = "Tauri"; "Run event: exit was requested.");
stop_qdrant_server();
@ -303,23 +262,21 @@ impl Event {
/// Creates an Event instance from a Tauri WindowEvent.
pub fn from_window_event(window_event: &WindowEvent) -> Self {
match window_event {
WindowEvent::FileDrop(drop_event) => {
WindowEvent::DragDrop(drop_event) => {
match drop_event {
FileDropEvent::Hovered(files) => Event::new(TauriEventType::FileDropHovered,
files.iter().map(|f| f.to_string_lossy().to_string()).collect(),
DragDropEvent::Enter { paths, .. } => Event::new(
TauriEventType::FileDropHovered,
paths.iter().map(|p| p.display().to_string()).collect(),
),
FileDropEvent::Dropped(files) => Event::new(TauriEventType::FileDropDropped,
files.iter().map(|f| f.to_string_lossy().to_string()).collect(),
DragDropEvent::Drop { paths, .. } => Event::new(
TauriEventType::FileDropDropped,
paths.iter().map(|p| p.display().to_string()).collect(),
),
FileDropEvent::Cancelled => Event::new(TauriEventType::FileDropCanceled,
Vec::new(),
),
DragDropEvent::Leave => Event::new(TauriEventType::FileDropCanceled, Vec::new()),
_ => Event::new(TauriEventType::Unknown,
Vec::new(),
),
_ => Event::new(TauriEventType::Unknown, Vec::new()),
}
},
@ -402,46 +359,67 @@ pub async fn check_for_update(_token: APIToken) -> Json<CheckUpdateResponse> {
});
}
let app_handle = MAIN_WINDOW.lock().unwrap().as_ref().unwrap().app_handle();
let response = app_handle.updater().check().await;
match response {
Ok(update_response) => match update_response.is_update_available() {
true => {
*CHECK_UPDATE_RESPONSE.lock().unwrap() = Some(update_response.clone());
let new_version = update_response.latest_version();
info!(Source = "Updater"; "An update to version '{new_version}' is available.");
let changelog = update_response.body();
Json(CheckUpdateResponse {
update_is_available: true,
error: false,
new_version: new_version.to_string(),
changelog: match changelog {
Some(c) => c.to_string(),
None => String::from(""),
},
})
},
false => {
info!(Source = "Updater"; "No updates are available.");
Json(CheckUpdateResponse {
let app_handle = {
let main_window = MAIN_WINDOW.lock().unwrap();
match main_window.as_ref() {
Some(window) => window.app_handle().clone(),
None => {
error!(Source = "Updater"; "Cannot check updates: main window not available.");
return Json(CheckUpdateResponse {
update_is_available: false,
error: false,
error: true,
new_version: String::from(""),
changelog: String::from(""),
})
},
},
});
}
}
};
let response = match app_handle.updater() {
Ok(updater) => updater.check().await,
Err(e) => {
warn!(Source = "Updater"; "Failed to check for updates: {e}.");
warn!(Source = "Updater"; "Failed to get updater instance: {e}");
return Json(CheckUpdateResponse {
update_is_available: false,
error: true,
new_version: String::from(""),
changelog: String::from(""),
});
}
};
match response {
Ok(Some(update)) => {
let body_len = update.body.as_ref().map_or(0, |body| body.len());
let date = update.date;
let new_version = update.version.clone();
info!(Source = "Tauri"; "Updater: update available: body size={body_len} time={date:?} version={new_version}");
let changelog = update.body.clone().unwrap_or_default();
*CHECK_UPDATE_RESPONSE.lock().unwrap() = Some(update);
Json(CheckUpdateResponse {
update_is_available: true,
error: false,
new_version,
changelog,
})
}
Ok(None) => {
info!(Source = "Tauri"; "Updater: app is already up to date");
Json(CheckUpdateResponse {
update_is_available: false,
error: false,
new_version: String::from(""),
changelog: String::from(""),
})
}
Err(e) => {
warn!(Source = "Tauri"; "Updater: failed to update: {e}");
Json(CheckUpdateResponse {
update_is_available: false,
error: true,
new_version: String::from(""),
changelog: String::from(""),
})
},
}
}
}
@ -463,9 +441,51 @@ pub async fn install_update(_token: APIToken) {
}
let cloned_response_option = CHECK_UPDATE_RESPONSE.lock().unwrap().clone();
let app_handle = MAIN_WINDOW
.lock()
.unwrap()
.as_ref()
.map(|window| window.app_handle().clone());
match cloned_response_option {
Some(update_response) => {
update_response.download_and_install().await.unwrap();
info!(Source = "Tauri"; "Updater: update is pending!");
let result = update_response.download_and_install(
|chunk_length, _content_length| {
trace!(Source = "Tauri"; "Updater: downloading chunk of {chunk_length} bytes");
},
|| {
info!(Source = "Tauri"; "Updater: update has been downloaded!");
warn!(Source = "Tauri"; "Try to stop the .NET server now...");
if is_prod() {
stop_dotnet_server();
stop_qdrant_server();
} else {
warn!(Source = "Tauri"; "Development environment detected; do not stop the .NET server.");
}
},
).await;
match result {
Ok(_) => {
info!(Source = "Tauri"; "Updater: app has been updated");
warn!(Source = "Tauri"; "Try to restart the app now...");
if is_prod() {
if let Some(handle) = app_handle {
handle.restart();
} else {
warn!(Source = "Tauri"; "Cannot restart after update: main window not available.");
}
} else {
warn!(Source = "Tauri"; "Development environment detected; do not restart the app.");
}
}
Err(e) => {
warn!(Source = "Tauri"; "Updater: failed to update: {e}");
}
}
},
None => {
@ -477,29 +497,43 @@ pub async fn install_update(_token: APIToken) {
/// Let the user select a directory.
#[post("/select/directory?<title>", data = "<previous_directory>")]
pub fn select_directory(_token: APIToken, title: &str, previous_directory: Option<Json<PreviousDirectory>>) -> Json<DirectorySelectionResponse> {
let folder_path = match previous_directory {
Some(previous) => {
let previous_path = previous.path.as_str();
FileDialogBuilder::new()
.set_title(title)
.set_directory(previous_path)
.pick_folder()
},
let main_window_lock = MAIN_WINDOW.lock().unwrap();
let main_window = match main_window_lock.as_ref() {
Some(window) => window,
None => {
FileDialogBuilder::new()
.set_title(title)
.pick_folder()
},
error!(Source = "Tauri"; "Cannot open directory dialog: main window not available.");
return Json(DirectorySelectionResponse {
user_cancelled: true,
selected_directory: String::from(""),
});
}
};
let mut dialog = main_window.app_handle().dialog().file().set_title(title);
if let Some(previous) = previous_directory {
dialog = dialog.set_directory(previous.path.clone());
}
let folder_path = dialog.blocking_pick_folder();
match folder_path {
Some(path) => {
info!("User selected directory: {path:?}");
Json(DirectorySelectionResponse {
user_cancelled: false,
selected_directory: path.to_str().unwrap().to_string(),
})
match path.into_path() {
Ok(pb) => {
info!("User selected directory: {pb:?}");
Json(DirectorySelectionResponse {
user_cancelled: false,
selected_directory: pb.to_string_lossy().to_string(),
})
}
Err(e) => {
error!(Source = "Tauri"; "Failed to convert directory path: {e}");
Json(DirectorySelectionResponse {
user_cancelled: true,
selected_directory: String::new(),
})
}
}
},
None => {
@ -548,33 +582,47 @@ pub struct DirectorySelectionResponse {
pub fn select_file(_token: APIToken, payload: Json<SelectFileOptions>) -> Json<FileSelectionResponse> {
// Create a new file dialog builder:
let file_dialog = FileDialogBuilder::new();
let file_dialog = MAIN_WINDOW
.lock()
.unwrap()
.as_ref()
.map(|w| w.app_handle().dialog().file().set_title(&payload.title));
// Set the title of the file dialog:
let file_dialog = file_dialog.set_title(&payload.title);
// Set the file type filter if provided:
let file_dialog = apply_filter(file_dialog, &payload.filter);
// Set the previous file path if provided:
let file_dialog = match &payload.previous_file {
Some(previous) => {
let previous_path = previous.file_path.as_str();
file_dialog.set_directory(previous_path)
},
None => file_dialog,
let Some(mut file_dialog) = file_dialog else {
error!(Source = "Tauri"; "Cannot open file dialog: main window not available.");
return Json(FileSelectionResponse {
user_cancelled: true,
selected_file_path: String::from(""),
});
};
// Set the file type filter if provided:
file_dialog = apply_filter(file_dialog, &payload.filter);
// Set the previous file path if provided:
if let Some(previous) = &payload.previous_file {
let previous_path = previous.file_path.as_str();
file_dialog = file_dialog.set_directory(previous_path);
}
// Show the file dialog and get the selected file path:
let file_path = file_dialog.pick_file();
let file_path = file_dialog.blocking_pick_file();
match file_path {
Some(path) => {
info!("User selected file: {path:?}");
Json(FileSelectionResponse {
user_cancelled: false,
selected_file_path: path.to_str().unwrap().to_string(),
})
Some(path) => match path.into_path() {
Ok(pb) => {
info!("User selected file: {pb:?}");
Json(FileSelectionResponse {
user_cancelled: false,
selected_file_path: pb.to_string_lossy().to_string(),
})
}
Err(e) => {
error!(Source = "Tauri"; "Failed to convert file path: {e}");
Json(FileSelectionResponse {
user_cancelled: true,
selected_file_path: String::new(),
})
}
},
None => {
@ -592,32 +640,38 @@ pub fn select_file(_token: APIToken, payload: Json<SelectFileOptions>) -> Json<F
pub fn select_files(_token: APIToken, payload: Json<SelectFileOptions>) -> Json<FilesSelectionResponse> {
// Create a new file dialog builder:
let file_dialog = FileDialogBuilder::new();
let file_dialog = MAIN_WINDOW
.lock()
.unwrap()
.as_ref()
.map(|w| w.app_handle().dialog().file().set_title(&payload.title));
// Set the title of the file dialog:
let file_dialog = file_dialog.set_title(&payload.title);
// Set the file type filter if provided:
let file_dialog = apply_filter(file_dialog, &payload.filter);
// Set the previous file path if provided:
let file_dialog = match &payload.previous_file {
Some(previous) => {
let previous_path = previous.file_path.as_str();
file_dialog.set_directory(previous_path)
},
None => file_dialog,
let Some(mut file_dialog) = file_dialog else {
error!(Source = "Tauri"; "Cannot open file dialog: main window not available.");
return Json(FilesSelectionResponse {
user_cancelled: true,
selected_file_paths: Vec::new(),
});
};
// Set the file type filter if provided:
file_dialog = apply_filter(file_dialog, &payload.filter);
// Set the previous file path if provided:
if let Some(previous) = &payload.previous_file {
let previous_path = previous.file_path.as_str();
file_dialog = file_dialog.set_directory(previous_path);
}
// Show the file dialog and get the selected file path:
let file_paths = file_dialog.pick_files();
let file_paths = file_dialog.blocking_pick_files();
match file_paths {
Some(paths) => {
info!("User selected {} files.", paths.len());
let converted: Vec<String> = paths.into_iter().filter_map(|p| p.into_path().ok()).map(|pb| pb.to_string_lossy().to_string()).collect();
info!("User selected {} files.", converted.len());
Json(FilesSelectionResponse {
user_cancelled: false,
selected_file_paths: paths.iter().map(|p| p.to_str().unwrap().to_string()).collect(),
selected_file_paths: converted,
})
}
@ -635,33 +689,47 @@ pub fn select_files(_token: APIToken, payload: Json<SelectFileOptions>) -> Json<
pub fn save_file(_token: APIToken, payload: Json<SaveFileOptions>) -> Json<FileSaveResponse> {
// Create a new file dialog builder:
let file_dialog = FileDialogBuilder::new();
let file_dialog = MAIN_WINDOW
.lock()
.unwrap()
.as_ref()
.map(|w| w.app_handle().dialog().file().set_title(&payload.title));
// Set the title of the file dialog:
let file_dialog = file_dialog.set_title(&payload.title);
// Set the file type filter if provided:
let file_dialog = apply_filter(file_dialog, &payload.filter);
// Set the previous file path if provided:
let file_dialog = match &payload.name_file {
Some(previous) => {
let previous_path = previous.file_path.as_str();
file_dialog.set_directory(previous_path)
},
None => file_dialog,
let Some(mut file_dialog) = file_dialog else {
error!(Source = "Tauri"; "Cannot open save dialog: main window not available.");
return Json(FileSaveResponse {
user_cancelled: true,
save_file_path: String::from(""),
});
};
// Set the file type filter if provided:
file_dialog = apply_filter(file_dialog, &payload.filter);
// Set the previous file path if provided:
if let Some(previous) = &payload.name_file {
let previous_path = previous.file_path.as_str();
file_dialog = file_dialog.set_directory(previous_path);
}
// Displays the file dialogue box and select the file:
let file_path = file_dialog.save_file();
let file_path = file_dialog.blocking_save_file();
match file_path {
Some(path) => {
info!("User selected file for writing operation: {path:?}");
Json(FileSaveResponse {
user_cancelled: false,
save_file_path: path.to_str().unwrap().to_string(),
})
Some(path) => match path.into_path() {
Ok(pb) => {
info!("User selected file for writing operation: {pb:?}");
Json(FileSaveResponse {
user_cancelled: false,
save_file_path: pb.to_string_lossy().to_string(),
})
}
Err(e) => {
error!(Source = "Tauri"; "Failed to convert save file path: {e}");
Json(FileSaveResponse {
user_cancelled: true,
save_file_path: String::new(),
})
}
},
None => {
@ -680,7 +748,7 @@ pub struct PreviousFile {
}
/// Applies an optional file type filter to a FileDialogBuilder.
fn apply_filter(file_dialog: FileDialogBuilder, filter: &Option<FileTypeFilter>) -> FileDialogBuilder {
fn apply_filter<R: tauri::Runtime>(file_dialog: FileDialogBuilder<R>, filter: &Option<FileTypeFilter>) -> FileDialogBuilder<R> {
match filter {
Some(f) => file_dialog.add_filter(
&f.filter_name,
@ -730,29 +798,23 @@ pub struct ShortcutResponse {
/// Internal helper function to register a shortcut with its callback.
/// This is used by both `register_shortcut` and `resume_shortcuts` to
/// avoid code duplication.
fn register_shortcut_with_callback(
shortcut_manager: &mut impl GlobalShortcutManager,
fn register_shortcut_with_callback<R: tauri::Runtime>(
app_handle: &tauri::AppHandle<R>,
shortcut: &str,
shortcut_id: Shortcut,
event_sender: broadcast::Sender<Event>,
) -> Result<(), tauri::Error> {
//
// Match the shortcut registration to transform the Tauri result into the Rust result:
//
match shortcut_manager.register(shortcut, move || {
) -> Result<(), tauri_plugin_global_shortcut::Error> {
let shortcut_manager = app_handle.global_shortcut();
shortcut_manager.on_shortcut(shortcut, move |_app, _shortcut, _event| {
info!(Source = "Tauri"; "Global shortcut triggered for '{}'.", shortcut_id);
let event = Event::new(TauriEventType::GlobalShortcutPressed, vec![shortcut_id.to_string()]);
let sender = event_sender.clone();
tauri::async_runtime::spawn(async move {
match sender.send(event) {
Ok(_) => {}
Err(error) => error!(Source = "Tauri"; "Failed to send global shortcut event: {error}"),
if let Err(error) = sender.send(event) {
error!(Source = "Tauri"; "Failed to send global shortcut event: {error}");
}
});
}) {
Ok(_) => Ok(()),
Err(e) => Err(e.into()),
}
})
}
/// Registers or updates a global shortcut. If the shortcut string is empty,
@ -785,7 +847,8 @@ pub fn register_shortcut(_token: APIToken, payload: Json<RegisterShortcutRequest
}
};
let mut shortcut_manager = main_window.app_handle().global_shortcut_manager();
let app_handle = main_window.app_handle();
let shortcut_manager = app_handle.global_shortcut();
let mut registered_shortcuts = REGISTERED_SHORTCUTS.lock().unwrap();
// Unregister the old shortcut if one exists for this name:
@ -824,7 +887,7 @@ pub fn register_shortcut(_token: APIToken, payload: Json<RegisterShortcutRequest
drop(event_broadcast_lock);
// Register the new shortcut:
match register_shortcut_with_callback(&mut shortcut_manager, &new_shortcut, id, event_sender) {
match register_shortcut_with_callback(app_handle, &new_shortcut, id, event_sender) {
Ok(_) => {
info!(Source = "Tauri"; "Global shortcut '{new_shortcut}' registered successfully for '{}'.", id);
registered_shortcuts.insert(id, new_shortcut);
@ -934,7 +997,8 @@ pub fn suspend_shortcuts(_token: APIToken) -> Json<ShortcutResponse> {
}
};
let mut shortcut_manager = main_window.app_handle().global_shortcut_manager();
let app_handle = main_window.app_handle();
let shortcut_manager = app_handle.global_shortcut();
let registered_shortcuts = REGISTERED_SHORTCUTS.lock().unwrap();
// Unregister all shortcuts from the OS (but keep them in our map):
@ -970,7 +1034,7 @@ pub fn resume_shortcuts(_token: APIToken) -> Json<ShortcutResponse> {
}
};
let mut shortcut_manager = main_window.app_handle().global_shortcut_manager();
let app_handle = main_window.app_handle();
let registered_shortcuts = REGISTERED_SHORTCUTS.lock().unwrap();
// Get the event broadcast sender for the shortcut callbacks:
@ -995,7 +1059,7 @@ pub fn resume_shortcuts(_token: APIToken) -> Json<ShortcutResponse> {
continue;
}
match register_shortcut_with_callback(&mut shortcut_manager, shortcut, *shortcut_id, event_sender.clone()) {
match register_shortcut_with_callback(&app_handle, shortcut, *shortcut_id, event_sender.clone()) {
Ok(_) => {
info!(Source = "Tauri"; "Re-registered shortcut '{shortcut}' for '{}'.", shortcut_id);
success_count += 1;
@ -1056,15 +1120,31 @@ fn validate_shortcut_syntax(shortcut: &str) -> bool {
has_key
}
fn set_pdfium_path(path_resolver: PathResolver) {
let pdfium_relative_source_path = String::from("resources/libraries/");
let pdfium_source_path = path_resolver.resolve_resource(pdfium_relative_source_path);
if pdfium_source_path.is_none() {
error!(Source = "Bootloader Tauri"; "Failed to set the PDFium library path.");
return;
}
fn set_pdfium_path<R: tauri::Runtime>(path_resolver: &PathResolver<R>) {
let resource_dir = match path_resolver.resource_dir() {
Ok(path) => path,
Err(error) => {
error!(Source = "Bootloader Tauri"; "Failed to resolve resource dir: {error}");
return;
}
};
let pdfium_source_path = pdfium_source_path.unwrap();
let pdfium_source_path = pdfium_source_path.to_str().unwrap().to_string();
*PDFIUM_LIB_PATH.lock().unwrap() = Some(pdfium_source_path.clone());
}
let candidate_paths = [
resource_dir.join("resources").join("libraries"),
resource_dir.join("libraries"),
];
let pdfium_source_path = candidate_paths
.iter()
.find(|path| path.exists())
.map(|path| path.to_string_lossy().to_string());
match pdfium_source_path {
Some(path) => {
*PDFIUM_LIB_PATH.lock().unwrap() = Some(path);
}
None => {
error!(Source = "Bootloader Tauri"; "Failed to set the PDFium library path.");
}
}
}

View File

@ -6,8 +6,9 @@ use base64::prelude::BASE64_STANDARD;
use log::{error, info, warn};
use once_cell::sync::Lazy;
use rocket::get;
use tauri::api::process::{Command, CommandChild, CommandEvent};
use tauri::Url;
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
use tauri_plugin_shell::ShellExt;
use crate::api_token::APIToken;
use crate::runtime_api_token::API_TOKEN;
use crate::app_window::change_location_to;
@ -130,14 +131,14 @@ pub fn create_startup_env_file() {
}
/// Starts the .NET server in a separate process.
pub fn start_dotnet_server() {
pub fn start_dotnet_server<R: tauri::Runtime>(app_handle: tauri::AppHandle<R>) {
// 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;
let dotnet_server_environment = HashMap::from_iter([
let dotnet_server_environment: HashMap<String, String> = 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.get().unwrap().to_string()),
@ -148,11 +149,13 @@ pub fn start_dotnet_server() {
info!("Try to start the .NET server...");
let server_spawn_clone = DOTNET_SERVER.clone();
tauri::async_runtime::spawn(async move {
let (mut rx, child) = Command::new_sidecar("mindworkAIStudioServer")
.expect("Failed to create sidecar")
.envs(dotnet_server_environment)
.spawn()
.expect("Failed to spawn .NET server process.");
let shell = app_handle.shell();
let (mut rx, child) = shell
.sidecar("mindworkAIStudioServer")
.expect("Failed to create sidecar")
.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}.");
log_potential_stale_process(Path::new(DATA_DIRECTORY.get().unwrap()).join(PID_FILE_NAME), server_pid, SIDECAR_TYPE);
@ -163,10 +166,13 @@ pub fn start_dotnet_server() {
// Log the output of the .NET server:
// NOTE: Log events are sent via structured HTTP API calls.
// This loop serves for fundamental output (e.g., startup errors).
while let Some(CommandEvent::Stdout(line)) = rx.recv().await {
let line = sanitize_stdout_line(line.trim_end());
if !line.trim().is_empty() {
info!(Source = ".NET Server (stdout)"; "{line}");
while let Some(event) = rx.recv().await {
if let CommandEvent::Stdout(line) = event {
let line_utf8 = String::from_utf8_lossy(&line).to_string();
let line = sanitize_stdout_line(line_utf8.trim_end());
if !line.trim().is_empty() {
info!(Source = ".NET Server (stdout)"; "{line}");
}
}
}
});

View File

@ -10,7 +10,6 @@ use once_cell::sync::Lazy;
use rocket::get;
use rocket::serde::json::Json;
use rocket::serde::Serialize;
use tauri::api::process::{Command, CommandChild, CommandEvent};
use crate::api_token::{APIToken};
use crate::environment::DATA_DIRECTORY;
use crate::certificate_factory::generate_certificate;
@ -18,6 +17,8 @@ use std::path::PathBuf;
use tempfile::{TempDir, Builder};
use crate::stale_process_cleanup::{kill_stale_process, log_potential_stale_process};
use crate::sidecar_types::SidecarType;
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
use tauri_plugin_shell::ShellExt;
// Qdrant server process started in a separate process and can communicate
// via HTTP or gRPC with the .NET server and the runtime process
@ -63,7 +64,7 @@ pub fn qdrant_port(_token: APIToken) -> Json<ProvideQdrantInfo> {
}
/// Starts the Qdrant server in a separate process.
pub fn start_qdrant_server(){
pub fn start_qdrant_server<R: tauri::Runtime>(app_handle: tauri::AppHandle<R>){
let base_path = DATA_DIRECTORY.get().unwrap();
let path = Path::new(base_path).join("databases").join("qdrant");
@ -78,7 +79,7 @@ pub fn start_qdrant_server(){
let snapshot_path = path.join("snapshots").to_str().unwrap().to_string();
let init_path = path.join(".qdrant-initalized").to_str().unwrap().to_string();
let qdrant_server_environment = HashMap::from_iter([
let qdrant_server_environment: HashMap<String, String> = HashMap::from_iter([
(String::from("QDRANT__SERVICE__HTTP_PORT"), QDRANT_SERVER_PORT_HTTP.to_string()),
(String::from("QDRANT__SERVICE__GRPC_PORT"), QDRANT_SERVER_PORT_GRPC.to_string()),
(String::from("QDRANT_INIT_FILE_PATH"), init_path),
@ -92,7 +93,9 @@ pub fn start_qdrant_server(){
let server_spawn_clone = QDRANT_SERVER.clone();
tauri::async_runtime::spawn(async move {
let (mut rx, child) = Command::new_sidecar("qdrant")
let shell = app_handle.shell();
let (mut rx, child) = shell
.sidecar("qdrant")
.expect("Failed to create sidecar for Qdrant")
.args(["--config-path", "resources/databases/qdrant/config.yaml"])
.envs(qdrant_server_environment)
@ -110,7 +113,8 @@ pub fn start_qdrant_server(){
while let Some(event) = rx.recv().await {
match event {
CommandEvent::Stdout(line) => {
let line = line.trim_end();
let line_utf8 = String::from_utf8_lossy(&line).to_string();
let line = line_utf8.trim_end();
if line.contains("INFO") || line.contains("info") {
info!(Source = "Qdrant Server"; "{line}");
} else if line.contains("WARN") || line.contains("warning") {
@ -123,7 +127,8 @@ pub fn start_qdrant_server(){
},
CommandEvent::Stderr(line) => {
error!(Source = "Qdrant Server (stderr)"; "{line}");
let line_utf8 = String::from_utf8_lossy(&line).to_string();
error!(Source = "Qdrant Server (stderr)"; "{line_utf8}");
},
_ => {}

View File

@ -1,44 +1,47 @@
{
"build": {
"devPath": "ui/",
"distDir": "ui/",
"withGlobalTauri": false
"frontendDist": "ui/"
},
"package": {
"productName": "MindWork AI Studio",
"version": "26.2.2"
},
"tauri": {
"allowlist": {
"all": false,
"shell": {
"sidecar": true,
"all": false,
"open": true,
"scope": [
{
"name": "../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer",
"sidecar": true,
"args": true
},
{
"name": "target/databases/qdrant/qdrant",
"sidecar": true,
"args": true
}
]
},
"http" : {
"all": true,
"request": true,
"scope": [
"http://localhost"
]
},
"fs": {
"scope": ["$RESOURCE/resources/*"]
}
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"externalBin": [
"../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer",
"target/databases/qdrant/qdrant"
],
"resources": [
"resources/databases/qdrant/config.yaml",
"resources/libraries/*"
],
"macOS": {
"exceptionDomain": "localhost"
},
"createUpdaterArtifacts": "v1Compatible"
},
"productName": "MindWork AI Studio",
"mainBinaryName": "MindWork AI Studio",
"version": "26.2.2",
"identifier": "com.github.mindwork-ai.ai-studio",
"plugins": {
"updater": {
"windows": {
"installMode": "passive"
},
"endpoints": [
"https://github.com/MindWorkAI/AI-Studio/releases/latest/download/latest.json"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDM3MzE4MTM4RTNDMkM0NEQKUldSTnhNTGpPSUV4TjFkczFxRFJOZWgydzFQN1dmaFlKbXhJS1YyR1RKS1RnR09jYUpMaGsrWXYK"
}
},
"app": {
"withGlobalTauri": false,
"windows": [
{
"fullscreen": false,
@ -46,51 +49,12 @@
"title": "MindWork AI Studio",
"width": 1920,
"height": 1080,
"fileDropEnabled": true
"dragDropEnabled": true,
"useHttpsScheme": true
}
],
"security": {
"csp": null,
"dangerousRemoteDomainIpcAccess": [
{
"domain": "localhost",
"windows": ["main"],
"enableTauriAPI": true
}
]
},
"bundle": {
"active": true,
"targets": "all",
"identifier": "com.github.mindwork-ai.ai-studio",
"externalBin": [
"../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer",
"target/databases/qdrant/qdrant"
],
"resources": [
"resources/*"
],
"macOS": {
"exceptionDomain": "localhost"
},
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
},
"updater": {
"active": true,
"endpoints": [
"https://github.com/MindWorkAI/AI-Studio/releases/latest/download/latest.json"
],
"dialog": false,
"windows": {
"installMode": "passive"
},
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDM3MzE4MTM4RTNDMkM0NEQKUldSTnhNTGpPSUV4TjFkczFxRFJOZWgydzFQN1dmaFlKbXhJS1YyR1RKS1RnR09jYUpMaGsrWXYK"
"csp": null
}
}
}