Fixed initial logging setup for Flatpak (#874)
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-07-19 22:48:30 +02:00
1 parent 5e2d252fc6
commit 90988ebea4
11 files changed
+288 -17

No files matched your search

+3 -3
View File
@@ -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<Mutex<Option<broadcast::Sender<Event>>>> = Lazy::ne
static APPROVED_APP_URL: Lazy<Mutex<Option<tauri::Url>>> = Lazy::new(|| Mutex::new(None));
/// Starts the Tauri app.
pub fn start_tauri() {
pub fn start_tauri(tauri_context: tauri::Context<tauri::Wry>) {
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:
+241 -11
View File
@@ -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<RuntimeLoggerHandle> = OnceLock::new();
@@ -20,7 +23,7 @@ static LOG_STARTUP_PATH: OnceLock<String> = OnceLock::new();
static LOG_APP_PATH: OnceLock<String> = 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<String>, 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<String>) {
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<PathBuf> {
std::env::home_dir()
}
fn get_non_flatpak_startup_log_path(
home_directory: Option<PathBuf>,
working_directory: Option<PathBuf>,
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<F>(
bundle_identifier: &str,
data_local_directory: Option<PathBuf>,
persistent_data_directory: PathBuf,
temporary_directory: PathBuf,
mut ensure_writable: F,
) -> Result<(PathBuf, Option<String>), 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<dyn Error>>{
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",
);
}
}
+4 -2
View File
@@ -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);
}