desktop: robust startup + operation logging (portability troubleshooting)
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 6s
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 9s
CI & Build / Python tests (push) Successful in 13s
CI & Build / Build & push image (push) Successful in 32s
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 6s
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 9s
CI & Build / Python tests (push) Successful in 13s
CI & Build / Build & push image (push) Successful in 32s
There was essentially no logging — useless for proving the app renders across different environments. Add real observability: - tauri-plugin-log -> stdout (so `2>&1 | tee` captures a run) AND a persistent file in the app log dir (grabbable after the fact on any machine). Level Info. - Startup diagnostics: app version, OS/arch, the Linux display/session stack (XDG_SESSION_TYPE, desktop, Wayland/X11, GDK_BACKEND), the WebKit render- hardening vars actually in effect, resolved log + data dirs, DB open/migrate result, and note/label counts. - log_event command + a frontend logEvent() helper: boot line (data source + WebKit user-agent) from main.ts, first-route config/session/destination from the router guard, and — via the bridge invoke() wrapper — every failed Tauri command named with its error, so a broken basic function is self-identifying. Task 2040. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
@@ -24,6 +24,10 @@ rusqlite = { version = "0.32", features = ["bundled"] }
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
# RFC3339 timestamps for created_at/updated_at/remind_at (Date.parse-able on the JS side).
|
||||
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
||||
# Startup + operation logging to stdout AND a persistent file, so portability
|
||||
# issues are diagnosable from any environment. `log` is the facade the code uses.
|
||||
tauri-plugin-log = "2"
|
||||
log = "0.4"
|
||||
|
||||
# Tauri's default release profile: smaller, faster shipped binaries.
|
||||
[profile.release]
|
||||
|
||||
@@ -11,21 +11,41 @@ mod local;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
use tauri_plugin_log::{Target, TargetKind};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
harden_linux_webkit_rendering();
|
||||
|
||||
tauri::Builder::default()
|
||||
// Logging first, so startup diagnostics (and any setup error) are captured to
|
||||
// stdout AND a persistent file from the very beginning — the basis for
|
||||
// troubleshooting portability across environments.
|
||||
.plugin(
|
||||
tauri_plugin_log::Builder::new()
|
||||
.level(log::LevelFilter::Info)
|
||||
.targets([
|
||||
Target::new(TargetKind::Stdout),
|
||||
Target::new(TargetKind::LogDir { file_name: None }),
|
||||
])
|
||||
.build(),
|
||||
)
|
||||
.setup(|app| {
|
||||
use tauri::Manager;
|
||||
log_environment(app);
|
||||
// The on-device store lives in the platform app-data dir (e.g. Linux
|
||||
// ~/.local/share/com.fabledsword.thoughtsync/thoughtsync.db), created on
|
||||
// first launch. This is what makes the app work with no server or login.
|
||||
let dir = app.path().app_data_dir()?;
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
app.manage(local::open(&dir.join("thoughtsync.db"))?);
|
||||
let db_path = dir.join("thoughtsync.db");
|
||||
log::info!("opening local store: {}", db_path.display());
|
||||
let db = local::open(&db_path)?;
|
||||
log::info!("local store ready — {}", local::summary(&db));
|
||||
app.manage(db);
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
log_event,
|
||||
integration::integration_status,
|
||||
integration::integrate_desktop,
|
||||
integration::unintegrate_desktop,
|
||||
@@ -70,6 +90,59 @@ pub fn run() {
|
||||
.expect("error while running the ThoughtSync desktop app");
|
||||
}
|
||||
|
||||
/// Frontend logging bridge: routes boot milestones and errors from the webview into
|
||||
/// the same stdout + file log as the Rust side (see frontend/src/desktop/bridge.ts).
|
||||
#[tauri::command]
|
||||
fn log_event(level: String, message: String) {
|
||||
match level.as_str() {
|
||||
"error" => log::error!(target: "frontend", "{message}"),
|
||||
"warn" => log::warn!(target: "frontend", "{message}"),
|
||||
"debug" => log::debug!(target: "frontend", "{message}"),
|
||||
_ => log::info!(target: "frontend", "{message}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Log the app version and the environment that determines whether the window
|
||||
/// renders — the first thing to check when a build works on one machine but not
|
||||
/// another.
|
||||
fn log_environment(app: &tauri::App) {
|
||||
use tauri::Manager;
|
||||
log::info!(
|
||||
"ThoughtSync desktop v{} starting ({} {})",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
std::env::consts::OS,
|
||||
std::env::consts::ARCH,
|
||||
);
|
||||
match app.path().app_log_dir() {
|
||||
Ok(d) => log::info!("log directory: {}", d.display()),
|
||||
Err(e) => log::warn!("could not resolve log dir: {e}"),
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
log_linux_graphics_env();
|
||||
}
|
||||
|
||||
/// The Linux display + graphics stack, and the WebKit render-hardening vars actually
|
||||
/// in effect (set by harden_linux_webkit_rendering, which runs before the logger, so
|
||||
/// we report the resulting environment rather than logging from inside it).
|
||||
#[cfg(target_os = "linux")]
|
||||
fn log_linux_graphics_env() {
|
||||
let v = |k: &str| std::env::var(k).unwrap_or_else(|_| "(unset)".to_string());
|
||||
log::info!(
|
||||
"display: session_type={} desktop={} wayland={} x11={} gdk_backend={}",
|
||||
v("XDG_SESSION_TYPE"),
|
||||
v("XDG_CURRENT_DESKTOP"),
|
||||
v("WAYLAND_DISPLAY"),
|
||||
v("DISPLAY"),
|
||||
v("GDK_BACKEND"),
|
||||
);
|
||||
log::info!(
|
||||
"webkit hardening: dmabuf_disabled={} compositing_disabled={} nv_explicit_sync_disabled={}",
|
||||
v("WEBKIT_DISABLE_DMABUF_RENDERER"),
|
||||
v("WEBKIT_DISABLE_COMPOSITING_MODE"),
|
||||
v("__NV_DISABLE_EXPLICIT_SYNC"),
|
||||
);
|
||||
}
|
||||
|
||||
/// WebKitGTK's GPU-accelerated rendering (the DMA-BUF renderer + EGL compositing) fails
|
||||
/// to initialize on a wide range of Linux GPU/driver/Wayland setups — "Could not create
|
||||
/// default EGL display: EGL_BAD_PARAMETER" → a black/blank window. This is a well-known
|
||||
|
||||
@@ -24,3 +24,17 @@ pub fn open(path: &Path) -> rusqlite::Result<Db> {
|
||||
schema::migrate(&conn)?;
|
||||
Ok(Db(Mutex::new(conn)))
|
||||
}
|
||||
|
||||
/// A one-line count summary of the store, for the startup log.
|
||||
pub fn summary(db: &Db) -> String {
|
||||
let conn = match db.0.lock() {
|
||||
Ok(c) => c,
|
||||
Err(_) => return "counts unavailable (lock poisoned)".to_string(),
|
||||
};
|
||||
let count = |sql: &str| conn.query_row(sql, [], |r| r.get::<_, i64>(0)).unwrap_or(-1);
|
||||
format!(
|
||||
"{} notes, {} labels",
|
||||
count("SELECT COUNT(*) FROM notes"),
|
||||
count("SELECT COUNT(*) FROM labels"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,18 @@ export function isDesktop(): boolean {
|
||||
export function invoke<T>(cmd: string, args?: Record<string, unknown>): Promise<T> {
|
||||
const tauri = window.__TAURI__;
|
||||
if (!tauri) return Promise.reject(new Error("Not running in the desktop app."));
|
||||
return tauri.core.invoke<T>(cmd, args);
|
||||
return tauri.core.invoke<T>(cmd, args).catch((err: unknown) => {
|
||||
// Every failed command names itself in the log — so a broken "basic function"
|
||||
// in some environment is diagnosable. Skip log_event to avoid recursion.
|
||||
if (cmd !== "log_event") logEvent("error", `invoke '${cmd}' failed: ${String(err)}`);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
/** Route a frontend message into the desktop log (stdout + file). No-op on web. */
|
||||
export function logEvent(level: "info" | "warn" | "error" | "debug", message: string): void {
|
||||
if (!isDesktop()) return;
|
||||
void invoke("log_event", { level, message }).catch(() => {});
|
||||
}
|
||||
|
||||
// Desktop (Linux AppImage) self-integration: add/remove an applications-menu entry.
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createApp } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import { isDesktop, logEvent } from "./desktop/bridge";
|
||||
import "./style.css";
|
||||
|
||||
const app = createApp(App);
|
||||
@@ -9,6 +10,10 @@ app.use(createPinia()); // before router: the nav guard reads the session store
|
||||
app.use(router);
|
||||
app.mount("#app");
|
||||
|
||||
// Boot line (desktop only): which data source, and the WebKit user-agent — the
|
||||
// renderer identity that matters most when a build behaves differently per machine.
|
||||
logEvent("info", `boot: source=${isDesktop() ? "local (offline core)" : "rest"} ua=${navigator.userAgent}`);
|
||||
|
||||
// Register the service worker so the app is installable (PWA). It's a
|
||||
// progressive enhancement — installability only, not offline-first (see
|
||||
// public/sw.js) — so failures are non-fatal and intentionally ignored.
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import { useSessionStore } from "../stores/session";
|
||||
import { useConfigStore } from "../stores/config";
|
||||
import { logEvent } from "../desktop/bridge";
|
||||
|
||||
// One-time boot diagnostic: the first navigation is where config + session resolve,
|
||||
// so it's the moment that tells us whether the app got past its startup gate.
|
||||
let bootLogged = false;
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
@@ -56,6 +61,13 @@ router.beforeEach(async (to) => {
|
||||
if (!session.loaded) {
|
||||
await session.fetchMe();
|
||||
}
|
||||
if (!bootLogged) {
|
||||
bootLogged = true;
|
||||
logEvent(
|
||||
"info",
|
||||
`first route: config(site=${config.siteName}) session(user=${session.user?.email ?? "none"}) -> ${String(to.name ?? to.path)}`,
|
||||
);
|
||||
}
|
||||
if (to.meta.requiresAuth && !session.user) {
|
||||
return { name: "login", query: to.fullPath !== "/" ? { redirect: to.fullPath } : undefined };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user