diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
index 085f6e43..d77b7dc1 100644
--- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
+++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
@@ -6994,6 +6994,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2273492381"] = "We must generate
-- Configuration plugin ID:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2301484629"] = "Configuration plugin ID:"
+-- dirs determines the platform-specific local application data directory. AI Studio uses it so the Flatpak startup log is written to the same application data directory that Tauri uses.
+UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2325338322"] = "dirs determines the platform-specific local application data directory. AI Studio uses it so the Flatpak startup log is written to the same application data directory that Tauri uses."
+
-- The C# language is used for the implementation of the user interface and the backend. To implement the user interface with C#, the Blazor technology from ASP.NET Core is used. All these technologies are integrated into the .NET SDK.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2329884315"] = "The C# language is used for the implementation of the user interface and the backend. To implement the user interface with C#, the Blazor technology from ASP.NET Core is used. All these technologies are integrated into the .NET SDK."
diff --git a/app/MindWork AI Studio/Pages/Information.razor b/app/MindWork AI Studio/Pages/Information.razor
index 45e47d0d..965017e9 100644
--- a/app/MindWork AI Studio/Pages/Information.razor
+++ b/app/MindWork AI Studio/Pages/Information.razor
@@ -304,6 +304,7 @@
+
diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
index a1c92af5..9a16a851 100644
--- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
+++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
@@ -6996,6 +6996,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2273492381"] = "Wir müssen Zufa
-- Configuration plugin ID:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2301484629"] = "Konfigurations-Plugin-ID:"
+-- dirs determines the platform-specific local application data directory. AI Studio uses it so the Flatpak startup log is written to the same application data directory that Tauri uses.
+UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2325338322"] = "dirs bestimmt das plattformspezifische lokale Anwendungsdatenverzeichnis. AI Studio verwendet es, damit das Flatpak-Startprotokoll in dasselbe Verzeichnis geschrieben wird, das auch Tauri verwendet."
+
-- The C# language is used for the implementation of the user interface and the backend. To implement the user interface with C#, the Blazor technology from ASP.NET Core is used. All these technologies are integrated into the .NET SDK.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2329884315"] = "Die Programmiersprache C# wird für die Umsetzung der Benutzeroberfläche und des Backends verwendet. Für die Entwicklung der Benutzeroberfläche mit C# kommt die Blazor-Technologie aus ASP.NET Core zum Einsatz. Alle diese Technologien sind im .NET SDK integriert."
diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
index 451125dc..610b5aaf 100644
--- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
+++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
@@ -6996,6 +6996,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2273492381"] = "We must generate
-- Configuration plugin ID:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2301484629"] = "Configuration plugin ID:"
+-- dirs determines the platform-specific local application data directory. AI Studio uses it so the Flatpak startup log is written to the same application data directory that Tauri uses.
+UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2325338322"] = "dirs determines the platform-specific local application data directory. AI Studio uses it so the Flatpak startup log is written to the same application data directory that Tauri uses."
+
-- The C# language is used for the implementation of the user interface and the backend. To implement the user interface with C#, the Blazor technology from ASP.NET Core is used. All these technologies are integrated into the .NET SDK.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2329884315"] = "The C# language is used for the implementation of the user interface and the backend. To implement the user interface with C#, the Blazor technology from ASP.NET Core is used. All these technologies are integrated into the .NET SDK."
diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs
index 29d4c562..3e775326 100644
--- a/app/MindWork AI Studio/Program.cs
+++ b/app/MindWork AI Studio/Program.cs
@@ -9,6 +9,7 @@ using AIStudio.Tools.PluginSystem.Assistants;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
+using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.Logging.Console;
@@ -111,6 +112,32 @@ internal sealed class Program
options.FormatterName = TerminalLogger.FORMATTER_NAME;
}).AddConsoleFormatter();
+ if(runtimeInfo.LinuxPackageType == "flatpak")
+ {
+ try
+ {
+ var tauriDataDirectory = await rust.GetDataDirectory();
+ if(string.IsNullOrWhiteSpace(tauriDataDirectory))
+ throw new InvalidOperationException("Rust returned an empty Tauri data directory.");
+
+ var dataProtectionKeysDirectory = Path.Combine(tauriDataDirectory, "data-protection-keys");
+ Directory.CreateDirectory(dataProtectionKeysDirectory);
+ var writeTestPath = Path.Combine(dataProtectionKeysDirectory, $".write-test-{Guid.NewGuid():N}");
+ using (new FileStream(writeTestPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 1, FileOptions.DeleteOnClose))
+ {
+ }
+
+ builder.Services.AddDataProtection()
+ .PersistKeysToFileSystem(new DirectoryInfo(dataProtectionKeysDirectory))
+ .SetApplicationName("org.mindworkai.AIStudio");
+ }
+ catch(Exception exception)
+ {
+ Console.WriteLine($"Error: Failed to configure Flatpak data-protection keys in the Tauri data directory: {exception.Message}");
+ return;
+ }
+ }
+
builder.Services.AddMudExtensions();
builder.Services.AddMudServices(config =>
{
diff --git a/app/MindWork AI Studio/Tools/Services/RustService.App.cs b/app/MindWork AI Studio/Tools/Services/RustService.App.cs
index 9fd0227f..974d9c19 100644
--- a/app/MindWork AI Studio/Tools/Services/RustService.App.cs
+++ b/app/MindWork AI Studio/Tools/Services/RustService.App.cs
@@ -113,7 +113,7 @@ public sealed partial class RustService
var response = await this.http.GetAsync("/system/directories/data");
if (!response.IsSuccessStatusCode)
{
- this.logger!.LogError($"Failed to get the data directory from Rust: '{response.StatusCode}'");
+ this.logger?.LogError($"Failed to get the data directory from Rust: '{response.StatusCode}'");
return string.Empty;
}
diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock
index 606bbdb8..69cad297 100644
--- a/runtime/Cargo.lock
+++ b/runtime/Cargo.lock
@@ -4147,6 +4147,7 @@ dependencies = [
"cfg-if",
"dbus-secret-service",
"dbus-secret-service-keyring-store",
+ "dirs",
"file-format",
"flexi_logger",
"futures",
diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml
index c5fc79dc..a6bc6e0a 100644
--- a/runtime/Cargo.toml
+++ b/runtime/Cargo.toml
@@ -24,6 +24,7 @@ tokio-stream = { version = "0.1.18", features = ["sync"] }
futures = "0.3.32"
async-stream = "0.3.6"
flexi_logger = "0.31.9"
+dirs = "6.0.0"
log = { version = "0.4.33", features = ["kv"] }
once_cell = "1.21.4"
axum = { version = "0.8.9", features = ["http2", "json", "query", "tokio"] }
diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs
index 274d378f..3fa86892 100644
--- a/runtime/src/app_window.rs
+++ b/runtime/src/app_window.rs
@@ -12,7 +12,7 @@ use log::{debug, error, info, trace, warn};
use once_cell::sync::Lazy;
use pdfium_render::prelude::Pdfium;
use serde::{Deserialize, Serialize};
-use tauri::{DragDropEvent,RunEvent, Manager, WindowEvent, generate_context};
+use tauri::{DragDropEvent,RunEvent, Manager, WindowEvent};
use tauri::path::PathResolver;
use tauri::WebviewWindow;
use tauri_plugin_updater::{UpdaterExt, Update};
@@ -52,7 +52,7 @@ static EVENT_BROADCAST: Lazy>>> = Lazy::ne
static APPROVED_APP_URL: Lazy>> = Lazy::new(|| Mutex::new(None));
/// Starts the Tauri app.
-pub fn start_tauri() {
+pub fn start_tauri(tauri_context: tauri::Context) {
info!("Starting Tauri app...");
// Create the event broadcast channel:
@@ -179,7 +179,7 @@ pub fn start_tauri() {
Ok(())
})
.plugin(tauri_plugin_window_state::Builder::default().build())
- .build(generate_context!())
+ .build(tauri_context)
.expect("Error while running Tauri application");
// The app event handler:
diff --git a/runtime/src/log.rs b/runtime/src/log.rs
index 22741f0e..fc9a39e3 100644
--- a/runtime/src/log.rs
+++ b/runtime/src/log.rs
@@ -2,7 +2,8 @@ use std::collections::BTreeMap;
use std::env::{current_dir, temp_dir};
use std::error::Error;
use std::fmt::Debug;
-use std::path::{absolute, PathBuf};
+use std::fs::{create_dir_all, OpenOptions};
+use std::path::{absolute, Path, PathBuf};
use std::sync::OnceLock;
use flexi_logger::{DeferredNow, Duplicate, FileSpec, Logger, LoggerHandle};
use flexi_logger::writers::FileLogWriter;
@@ -11,7 +12,9 @@ use log::kv::{Key, Value, VisitSource};
use axum::Json;
use serde::{Deserialize, Serialize};
use crate::api_token::APIToken;
-use crate::environment::is_dev;
+use crate::environment::{is_dev, is_flatpak};
+
+const FLATPAK_PERSISTENT_DATA_DIRECTORY: &str = "/var/data";
static LOGGER: OnceLock = OnceLock::new();
@@ -20,7 +23,7 @@ static LOG_STARTUP_PATH: OnceLock = OnceLock::new();
static LOG_APP_PATH: OnceLock = OnceLock::new();
/// Initialize the logging system.
-pub fn init_logging() {
+pub fn init_logging(bundle_identifier: &str) {
//
// Configure the LOGGER:
@@ -54,14 +57,15 @@ pub fn init_logging() {
false => "AI Studio Events",
};
+ let (startup_log_directory, fallback_warning) = get_startup_log_path(bundle_identifier);
let log_path = FileSpec::default()
- .directory(get_startup_log_path())
+ .directory(startup_log_directory)
.basename(log_basename)
.suppress_timestamp()
.suffix("log");
// Store the startup log path:
- let _ = LOG_STARTUP_PATH.set(convert_log_path_to_string(&log_path));
+ store_startup_log_path(&LOG_STARTUP_PATH, &log_path);
let runtime_logger = Logger::try_with_str(log_config).expect("Cannot create logging")
.log_to_file(log_path)
@@ -78,6 +82,14 @@ pub fn init_logging() {
};
LOGGER.set(runtime_logger).expect("Cannot set LOGGER");
+
+ if let Some(fallback_warning) = fallback_warning {
+ log::warn!("{fallback_warning}");
+ }
+}
+
+fn store_startup_log_path(storage: &OnceLock, log_path: &FileSpec) {
+ let _ = storage.set(convert_log_path_to_string(log_path));
}
fn convert_log_path_to_string(log_path: &FileSpec) -> String {
@@ -106,25 +118,123 @@ fn convert_log_path_to_string(log_path: &FileSpec) -> String {
}
}
+fn get_startup_log_path(bundle_identifier: &str) -> (PathBuf, Option) {
+ if is_flatpak() {
+ return select_flatpak_startup_log_path(
+ bundle_identifier,
+ dirs::data_local_dir(),
+ PathBuf::from(FLATPAK_PERSISTENT_DATA_DIRECTORY),
+ temp_dir(),
+ ensure_log_directory_is_writable,
+ ).unwrap_or_else(|error| panic!("Cannot prepare a Flatpak startup log directory: {error}"));
+ }
+
+ (get_non_flatpak_startup_log_path(
+ home_directory(),
+ current_dir().ok(),
+ temp_dir(),
+ ), None)
+}
+
// Note: Rust plans to remove the deprecation flag for std::env::home_dir() in Rust 1.86.0.
#[allow(deprecated)]
-fn get_startup_log_path() -> String {
- match std::env::home_dir() {
+fn home_directory() -> Option {
+ std::env::home_dir()
+}
+
+fn get_non_flatpak_startup_log_path(
+ home_directory: Option,
+ working_directory: Option,
+ temporary_directory: PathBuf,
+) -> PathBuf {
+ match home_directory {
// Case: We could determine the home directory:
- Some(home_dir) => home_dir.to_str().unwrap().to_string(),
+ Some(home_directory) => home_directory,
// Case: We could not determine the home directory. Let's try to use the working directory:
- None => match current_dir() {
+ None => match working_directory {
// Case: We could determine the working directory:
- Ok(working_directory) => working_directory.to_str().unwrap().to_string(),
+ Some(working_directory) => working_directory,
// Case: We could not determine the working directory. Let's use the temporary directory:
- Err(_) => temp_dir().to_str().unwrap().to_string(),
+ None => temporary_directory,
},
}
}
+fn select_flatpak_startup_log_path(
+ bundle_identifier: &str,
+ data_local_directory: Option,
+ persistent_data_directory: PathBuf,
+ temporary_directory: PathBuf,
+ mut ensure_writable: F,
+) -> Result<(PathBuf, Option), String>
+where
+ F: FnMut(&Path) -> Result<(), String>,
+{
+ let standard_directory = data_local_directory.map(|directory| directory.join(bundle_identifier).join("data"));
+ let persistent_fallback = persistent_data_directory.join(bundle_identifier).join("data");
+ let temporary_fallback = temporary_directory.join(bundle_identifier).join("data");
+ let mut failures = Vec::new();
+
+ if let Some(standard_directory) = standard_directory {
+ match ensure_writable(&standard_directory) {
+ Ok(()) => return Ok((standard_directory, None)),
+ Err(error) => failures.push(format!("standard path failed: {error}")),
+ }
+ } else {
+ failures.push(String::from("standard path failed: dirs::data_local_dir() returned no path"));
+ }
+
+ match ensure_writable(&persistent_fallback) {
+ Ok(()) => {
+ let warning = format!(
+ "The standard Flatpak startup log directory was unavailable; using persistent fallback '{}'. {}",
+ persistent_fallback.display(),
+ failures.join("; "),
+ );
+
+ return Ok((persistent_fallback, Some(warning)));
+ },
+
+ Err(error) => failures.push(format!("persistent fallback failed: {error}")),
+ }
+
+ match ensure_writable(&temporary_fallback) {
+ Ok(()) => {
+ let warning = format!(
+ "The standard and persistent Flatpak startup log directories were unavailable; using temporary fallback '{}'. {}",
+ temporary_fallback.display(),
+ failures.join("; "),
+ );
+
+ Ok((temporary_fallback, Some(warning)))
+ },
+
+ Err(error) => {
+ failures.push(format!("temporary fallback failed: {error}"));
+ Err(failures.join("; "))
+ },
+ }
+}
+
+fn ensure_log_directory_is_writable(directory: &Path) -> Result<(), String> {
+ create_dir_all(directory).map_err(|error| format!("could not create '{}': {error}", directory.display()))?;
+ let log_file_path = directory.join(if cfg!(unix) {
+ ".AI Studio Events.log"
+ } else {
+ "AI Studio Events.log"
+ });
+
+ OpenOptions::new()
+ .create(true)
+ .append(true)
+ .open(&log_file_path)
+ .map(|_| ())
+ .map_err(|error| format!("could not write '{}': {error}", log_file_path.display()))
+}
+
/// Switch the logging system to a file-based output inside the given directory.
pub fn switch_to_file_logging(logger_path: PathBuf) -> Result<(), Box>{
let log_path = FileSpec::default()
@@ -316,4 +426,124 @@ pub struct LogEvent {
pub struct LogEventResponse {
success: bool,
issue: String,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ const BUNDLE_IDENTIFIER: &str = "org.mindworkai.AIStudio";
+
+ #[test]
+ fn flatpak_standard_path_matches_tauri_local_data_path() {
+ let base_directory = PathBuf::from("/var/data");
+ let expected = base_directory.join(BUNDLE_IDENTIFIER).join("data");
+
+ let (selected, warning) = select_flatpak_startup_log_path(
+ BUNDLE_IDENTIFIER,
+ Some(base_directory),
+ PathBuf::from("/persistent"),
+ PathBuf::from("/temporary"),
+ |_| Ok(()),
+ ).unwrap();
+
+ assert_eq!(selected, expected);
+ assert!(warning.is_none());
+ }
+
+ #[test]
+ fn flatpak_uses_persistent_fallback_when_standard_path_is_unwritable() {
+ let standard = PathBuf::from("/standard").join(BUNDLE_IDENTIFIER).join("data");
+ let persistent = PathBuf::from("/var/data").join(BUNDLE_IDENTIFIER).join("data");
+
+ let (selected, warning) = select_flatpak_startup_log_path(
+ BUNDLE_IDENTIFIER,
+ Some(PathBuf::from("/standard")),
+ PathBuf::from("/var/data"),
+ PathBuf::from("/temporary"),
+ |candidate| {
+ if candidate == standard {
+ Err(String::from("read-only"))
+ } else {
+ Ok(())
+ }
+ },
+ ).unwrap();
+
+ assert_eq!(selected, persistent);
+ assert!(warning.unwrap().contains("persistent fallback"));
+ }
+
+ #[test]
+ fn flatpak_uses_temporary_fallback_when_persistent_path_is_unwritable() {
+ let temporary = PathBuf::from("/tmp").join(BUNDLE_IDENTIFIER).join("data");
+
+ let (selected, warning) = select_flatpak_startup_log_path(
+ BUNDLE_IDENTIFIER,
+ None,
+ PathBuf::from("/var/data"),
+ PathBuf::from("/tmp"),
+ |candidate| {
+ if candidate == temporary {
+ Ok(())
+ } else {
+ Err(String::from("read-only"))
+ }
+ },
+ ).unwrap();
+
+ assert_eq!(selected, temporary);
+ assert!(warning.unwrap().contains("temporary fallback"));
+ }
+
+ #[test]
+ fn non_flatpak_path_selection_keeps_existing_fallback_order() {
+ let home = PathBuf::from("/home/user");
+ let working = PathBuf::from("/working");
+ let temporary = PathBuf::from("/tmp");
+
+ assert_eq!(
+ get_non_flatpak_startup_log_path(Some(home.clone()), Some(working.clone()), temporary.clone()),
+ home,
+ );
+ assert_eq!(
+ get_non_flatpak_startup_log_path(None, Some(working.clone()), temporary.clone()),
+ working,
+ );
+ assert_eq!(
+ get_non_flatpak_startup_log_path(None, None, temporary.clone()),
+ temporary,
+ );
+ }
+
+ #[test]
+ fn startup_log_path_storage_uses_selected_fallback_path() {
+ let temporary = PathBuf::from("/tmp").join(BUNDLE_IDENTIFIER).join("data");
+ let (selected, _) = select_flatpak_startup_log_path(
+ BUNDLE_IDENTIFIER,
+ None,
+ PathBuf::from("/var/data"),
+ PathBuf::from("/tmp"),
+ |candidate| {
+ if candidate == temporary {
+ Ok(())
+ } else {
+ Err(String::from("unavailable"))
+ }
+ },
+ ).unwrap();
+ let log_path = FileSpec::default()
+ .directory(selected)
+ .basename(".AI Studio Events")
+ .suppress_timestamp()
+ .suffix("log");
+ let storage = OnceLock::new();
+
+ store_startup_log_path(&storage, &log_path);
+
+ assert_eq!(
+ storage.get().unwrap(),
+ "/tmp/org.mindworkai.AIStudio/data/.AI Studio Events.log",
+ );
+ }
}
\ No newline at end of file
diff --git a/runtime/src/main.rs b/runtime/src/main.rs
index b41b4c64..9461e97b 100644
--- a/runtime/src/main.rs
+++ b/runtime/src/main.rs
@@ -28,8 +28,10 @@ use mindwork_ai_studio::secret::init_secret_store;
// change requires explicit Linux startup tests; compiling successfully is not sufficient.
fn main() {
let metadata = MetaData::init_from_string(include_str!("../../metadata.txt"));
+ let tauri_context = tauri::generate_context!();
+ let bundle_identifier = tauri_context.config().identifier.clone();
- init_logging();
+ init_logging(&bundle_identifier);
info!("Starting MindWork AI Studio:");
let working_directory = std::env::current_dir().unwrap();
@@ -59,5 +61,5 @@ fn main() {
generate_runtime_certificate();
start_runtime_api();
- start_tauri();
+ start_tauri(tauri_context);
}
\ No newline at end of file