mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-05 08:29:40 +00:00
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
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:
1 parent
5e2d252fc6
commit
90988ebea4
11 files changed
+288
-17
No files matched your search
Generated
+1
@@ -4147,6 +4147,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"dbus-secret-service",
|
||||
"dbus-secret-service-keyring-store",
|
||||
"dirs",
|
||||
"file-format",
|
||||
"flexi_logger",
|
||||
"futures",
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user