From 9fa80d4290971e1e37f089c893d9458e47fc0685 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 11 Feb 2026 19:02:07 +0800 Subject: [PATCH] desktop: use tracing for logging --- packages/desktop/src-tauri/Cargo.lock | 102 ++++++++++++++++- packages/desktop/src-tauri/Cargo.toml | 4 + packages/desktop/src-tauri/src/cli.rs | 104 ++++-------------- packages/desktop/src-tauri/src/constants.rs | 1 - packages/desktop/src-tauri/src/job_object.rs | 12 +- packages/desktop/src-tauri/src/lib.rs | 83 +++++++------- .../desktop/src-tauri/src/linux_display.rs | 15 ++- packages/desktop/src-tauri/src/logging.rs | 83 ++++++++++++++ packages/desktop/src-tauri/src/main.rs | 4 +- packages/desktop/src-tauri/src/server.rs | 10 +- 10 files changed, 264 insertions(+), 154 deletions(-) create mode 100644 packages/desktop/src-tauri/src/logging.rs diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock index 537a7c9c566..8ce97b2b724 100644 --- a/packages/desktop/src-tauri/Cargo.lock +++ b/packages/desktop/src-tauri/Cargo.lock @@ -535,8 +535,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link 0.2.1", ] @@ -2491,6 +2493,15 @@ dependencies = [ "syn 2.0.110", ] +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "matches" version = "0.1.10" @@ -2691,6 +2702,15 @@ dependencies = [ "zbus", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -3065,6 +3085,7 @@ dependencies = [ name = "opencode-desktop" version = "0.0.0" dependencies = [ + "chrono", "comrak", "dirs", "futures", @@ -3096,6 +3117,9 @@ dependencies = [ "tauri-plugin-window-state", "tauri-specta", "tokio", + "tracing", + "tracing-appender", + "tracing-subscriber", "uuid", "webkit2gtk", "windows 0.61.3", @@ -4412,6 +4436,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shared_child" version = "1.1.1" @@ -5472,6 +5505,15 @@ dependencies = [ "syn 2.0.110", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "tiff" version = "0.10.3" @@ -5745,20 +5787,32 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" +dependencies = [ + "crossbeam-channel", + "thiserror 2.0.17", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -5767,11 +5821,41 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -5964,6 +6048,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "version-compare" version = "0.2.1" diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml index 2d6f13eca07..e9ba55b039a 100644 --- a/packages/desktop/src-tauri/Cargo.toml +++ b/packages/desktop/src-tauri/Cargo.toml @@ -47,6 +47,10 @@ specta = "=2.0.0-rc.22" specta-typescript = "0.0.9" tauri-specta = { version = "=2.0.0-rc.21", features = ["derive", "typescript"] } dirs = "6.0.0" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing-appender = "0.2" +chrono = "0.4" [target.'cfg(target_os = "linux")'.dependencies] gtk = "0.18.2" diff --git a/packages/desktop/src-tauri/src/cli.rs b/packages/desktop/src-tauri/src/cli.rs index 70ac973f034..b9e1ed4bd50 100644 --- a/packages/desktop/src-tauri/src/cli.rs +++ b/packages/desktop/src-tauri/src/cli.rs @@ -6,10 +6,7 @@ use tauri_plugin_shell::{ use tauri_plugin_store::StoreExt; use tokio::sync::oneshot; -use crate::{ - LogState, - constants::{MAX_LOG_ENTRIES, SETTINGS_STORE, WSL_ENABLED_KEY}, -}; +use crate::constants::{SETTINGS_STORE, WSL_ENABLED_KEY}; const CLI_INSTALL_DIR: &str = ".opencode/bin"; const CLI_BINARY_NAME: &str = "opencode"; @@ -29,7 +26,7 @@ pub async fn get_config(app: &AppHandle) -> Option { create_command(app, "debug config", &[]) .output() .await - .inspect_err(|e| eprintln!("Failed to read OC config: {e}")) + .inspect_err(|e| tracing::warn!("Failed to read OC config: {e}")) .ok() .and_then(|out| String::from_utf8(out.stdout.to_vec()).ok()) .and_then(|s| serde_json::from_str::(&s).ok()) @@ -104,12 +101,12 @@ pub fn install_cli(app: tauri::AppHandle) -> Result { pub fn sync_cli(app: tauri::AppHandle) -> Result<(), String> { if cfg!(debug_assertions) { - println!("Skipping CLI sync for debug build"); + tracing::debug!("Skipping CLI sync for debug build"); return Ok(()); } if !is_cli_installed() { - println!("No CLI installation found, skipping sync"); + tracing::info!("No CLI installation found, skipping sync"); return Ok(()); } @@ -132,21 +129,21 @@ pub fn sync_cli(app: tauri::AppHandle) -> Result<(), String> { let app_version = app.package_info().version.clone(); if cli_version >= app_version { - println!( - "CLI version {} is up to date (app version: {}), skipping sync", - cli_version, app_version + tracing::info!( + %cli_version, %app_version, + "CLI is up to date, skipping sync" ); return Ok(()); } - println!( - "CLI version {} is older than app version {}, syncing", - cli_version, app_version + tracing::info!( + %cli_version, %app_version, + "CLI is older than app version, syncing" ); install_cli(app)?; - println!("Synced installed CLI"); + tracing::info!("Synced installed CLI"); Ok(()) } @@ -207,7 +204,7 @@ pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, St if cfg!(windows) { if is_wsl_enabled(app) { - println!("WSL is enabled, spawning CLI server in WSL."); + tracing::info!("WSL is enabled, spawning CLI server in WSL"); let version = app.package_info().version.to_string(); let mut script = vec![ "set -e".to_string(), @@ -280,38 +277,9 @@ pub fn serve( port: u32, password: &str, ) -> (CommandChild, oneshot::Receiver) { - let log_state = app.state::(); - let log_state_clone = log_state.inner().clone(); - let (exit_tx, exit_rx) = oneshot::channel::(); - println!("spawning sidecar on port {port}"); - - if let Ok(mut logs) = log_state_clone.0.lock() { - let args = - format!("--print-logs --log-level WARN serve --hostname {hostname} --port {port}"); - - #[cfg(target_os = "windows")] - { - logs.push_back(format!("[SPAWN] sidecar=opencode-cli args=\"{args}\"\n")); - } - - #[cfg(not(target_os = "windows"))] - { - let sidecar = get_sidecar_path(app); - let shell = get_user_shell(); - let cmd = if shell.ends_with("/nu") { - format!("^\"{}\" {}", sidecar.display(), args) - } else { - format!("\"{}\" {}", sidecar.display(), args) - }; - logs.push_back(format!("[SPAWN] shell=\"{shell}\" argv=\"-il -c {cmd}\"\n")); - } - - while logs.len() > MAX_LOG_ENTRIES { - logs.pop_front(); - } - } + tracing::info!(port, "Spawning sidecar"); let envs = [ ("OPENCODE_SERVER_USERNAME", "opencode".to_string()), @@ -332,50 +300,22 @@ pub fn serve( match event { CommandEvent::Stdout(line_bytes) => { let line = String::from_utf8_lossy(&line_bytes); - print!("{line}"); - - // Store log in shared state - if let Ok(mut logs) = log_state_clone.0.lock() { - logs.push_back(format!("[STDOUT] {}", line)); - // Keep only the last MAX_LOG_ENTRIES - while logs.len() > MAX_LOG_ENTRIES { - logs.pop_front(); - } - } + tracing::info!(target: "sidecar", "{line}"); } CommandEvent::Stderr(line_bytes) => { let line = String::from_utf8_lossy(&line_bytes); - eprint!("{line}"); - - // Store log in shared state - if let Ok(mut logs) = log_state_clone.0.lock() { - logs.push_back(format!("[STDERR] {}", line)); - // Keep only the last MAX_LOG_ENTRIES - while logs.len() > MAX_LOG_ENTRIES { - logs.pop_front(); - } - } + tracing::info!(target: "sidecar", "{line}"); } CommandEvent::Error(err) => { - eprintln!("{err}"); - - if let Ok(mut logs) = log_state_clone.0.lock() { - logs.push_back(format!("[ERROR] {err}\n")); - while logs.len() > MAX_LOG_ENTRIES { - logs.pop_front(); - } - } + tracing::error!(target: "sidecar", "{err}"); } CommandEvent::Terminated(payload) => { - if let Ok(mut logs) = log_state_clone.0.lock() { - logs.push_back(format!( - "[EXIT] code={:?} signal={:?}\n", - payload.code, payload.signal - )); - while logs.len() > MAX_LOG_ENTRIES { - logs.pop_front(); - } - } + tracing::info!( + target: "sidecar", + code = ?payload.code, + signal = ?payload.signal, + "Sidecar terminated" + ); if let Some(tx) = exit_tx.take() { let _ = tx.send(payload); diff --git a/packages/desktop/src-tauri/src/constants.rs b/packages/desktop/src-tauri/src/constants.rs index cdf05fb458b..9d50d00e202 100644 --- a/packages/desktop/src-tauri/src/constants.rs +++ b/packages/desktop/src-tauri/src/constants.rs @@ -4,7 +4,6 @@ pub const SETTINGS_STORE: &str = "opencode.settings.dat"; pub const DEFAULT_SERVER_URL_KEY: &str = "defaultServerUrl"; pub const WSL_ENABLED_KEY: &str = "wslEnabled"; pub const UPDATER_ENABLED: bool = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some(); -pub const MAX_LOG_ENTRIES: usize = 200; pub fn window_state_flags() -> StateFlags { StateFlags::all() - StateFlags::DECORATIONS - StateFlags::VISIBLE diff --git a/packages/desktop/src-tauri/src/job_object.rs b/packages/desktop/src-tauri/src/job_object.rs index 220aa5db66d..8d774b14cd9 100644 --- a/packages/desktop/src-tauri/src/job_object.rs +++ b/packages/desktop/src-tauri/src/job_object.rs @@ -15,9 +15,9 @@ use std::io::{Error, Result}; use std::sync::Mutex; use windows::Win32::Foundation::{CloseHandle, HANDLE}; use windows::Win32::System::JobObjects::{ - AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, - JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation, - SetInformationJobObject, + AssignProcessToJobObject, CreateJobObjectW, JobObjectExtendedLimitInformation, + SetInformationJobObject, JOBOBJECT_EXTENDED_LIMIT_INFORMATION, + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, }; use windows::Win32::System::Threading::{OpenProcess, PROCESS_SET_QUOTA, PROCESS_TERMINATE}; @@ -111,7 +111,7 @@ impl JobObjectState { error: Mutex::new(None), }, Err(e) => { - eprintln!("Failed to create job object: {e}"); + tracing::error!("Failed to create job object: {e}"); Self { job: Mutex::new(None), error: Mutex::new(Some(format!("Failed to create job object: {e}"))), @@ -123,11 +123,11 @@ impl JobObjectState { pub fn assign_pid(&self, pid: u32) { if let Some(job) = self.job.lock().unwrap().as_ref() { if let Err(e) = job.assign_pid(pid) { - eprintln!("Failed to assign process {pid} to job object: {e}"); + tracing::error!(pid, "Failed to assign process to job object: {e}"); *self.error.lock().unwrap() = Some(format!("Failed to assign process to job object: {e}")); } else { - println!("Assigned process {pid} to job object for automatic cleanup"); + tracing::info!(pid, "Assigned process to job object for automatic cleanup"); } } } diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index 3e7902804ea..2c570b7a77d 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -4,6 +4,7 @@ mod constants; mod job_object; #[cfg(target_os = "linux")] pub mod linux_display; +mod logging; mod markdown; mod server; mod window_customizer; @@ -16,7 +17,6 @@ use futures::{ #[cfg(windows)] use job_object::*; use std::{ - collections::VecDeque, env, net::TcpListener, path::PathBuf, @@ -85,14 +85,11 @@ impl ServerState { } } -#[derive(Clone)] -struct LogState(Arc>>); - #[tauri::command] #[specta::specta] fn kill_sidecar(app: AppHandle) { let Some(server_state) = app.try_state::() else { - println!("Server not running"); + tracing::info!("Server not running"); return; }; @@ -102,24 +99,17 @@ fn kill_sidecar(app: AppHandle) { .expect("Failed to acquire mutex lock") .take() else { - println!("Server state missing"); + tracing::info!("Server state missing"); return; }; let _ = server_state.kill(); - println!("Killed server"); + tracing::info!("Killed server"); } -async fn get_logs(app: AppHandle) -> Result { - let log_state = app.try_state::().ok_or("Log state not found")?; - - let logs = log_state - .0 - .lock() - .map_err(|_| "Failed to acquire log lock")?; - - Ok(logs.iter().cloned().collect::>().join("")) +fn get_logs() -> String { + logging::tail() } #[tauri::command] @@ -715,10 +705,18 @@ pub fn run() { .plugin(tauri_plugin_decorum::init()) .invoke_handler(builder.invoke_handler()) .setup(move |app| { - let app = app.handle().clone(); + let handle = app.handle().clone(); + + let log_dir = app + .path() + .app_log_dir() + .expect("failed to resolve app log dir"); + // Hold the guard in managed state so it lives for the app's lifetime, + // ensuring all buffered logs are flushed on shutdown. + handle.manage(logging::init(&log_dir)); - builder.mount_events(&app); - tauri::async_runtime::spawn(initialize(app)); + builder.mount_events(&handle); + tauri::async_runtime::spawn(initialize(handle)); Ok(()) }); @@ -732,7 +730,7 @@ pub fn run() { .expect("error while running tauri application") .run(|app, event| { if let RunEvent::Exit = event { - println!("Received Exit"); + tracing::info!("Received Exit"); kill_sidecar(app.clone()); } @@ -780,9 +778,8 @@ fn test_export_types() { #[derive(tauri_specta::Event, serde::Deserialize, specta::Type)] struct LoadingWindowComplete; -// #[tracing::instrument(skip_all)] async fn initialize(app: AppHandle) { - println!("Initializing app"); + tracing::info!("Initializing app"); let (init_tx, init_rx) = watch::channel(InitStep::ServerWaiting); @@ -795,7 +792,7 @@ async fn initialize(app: AppHandle) { let loading_window_complete = event_once_fut::(&app); - println!("Main and loading windows created"); + tracing::info!("Main and loading windows created"); let sqlite_enabled = option_env!("OPENCODE_SQLITE").is_some(); @@ -806,7 +803,7 @@ async fn initialize(app: AppHandle) { async move { let mut sqlite_exists = sqlite_file_exists(); - println!("Setting up server connection"); + tracing::info!("Setting up server connection"); let server_connection = setup_server_connection(app.clone()).await; // we delay spawning this future so that the timeout is created lazily @@ -831,16 +828,13 @@ async fn initialize(app: AppHandle) { if let Some(err) = err { let _ = child.kill(); - let logs = get_logs(app.clone()) - .await - .unwrap_or_else(|e| format!("[DESKTOP] Failed to read sidecar logs: {e}\n")); - return Err(format!( - "Failed to spawn OpenCode Server ({err}). Logs:\n{logs}" + "Failed to spawn OpenCode Server ({err}). Logs:\n{}", + get_logs() )); } - println!("CLI health check OK"); + tracing::info!("CLI health check OK"); #[cfg(windows)] { @@ -868,11 +862,11 @@ async fn initialize(app: AppHandle) { if let Some(cli_health_check) = cli_health_check { if sqlite_enabled { - println!("Does sqlite file exist: {sqlite_exists}"); + tracing::debug!(sqlite_exists, "Checking sqlite file existence"); if !sqlite_exists { - println!( - "Sqlite file not found at {}, waiting for it to be generated", - opencode_db_path().expect("failed to get db path").display() + tracing::info!( + path = %opencode_db_path().expect("failed to get db path").display(), + "Sqlite file not found, waiting for it to be generated" ); let _ = init_tx.send(InitStep::SqliteWaiting); @@ -897,7 +891,7 @@ async fn initialize(app: AppHandle) { .await .is_err() { - println!("Loading task timed out, showing loading window"); + tracing::debug!("Loading task timed out, showing loading window"); let app = app.clone(); let loading_window = LoadingWindow::create(&app).expect("Failed to create loading window"); sleep(Duration::from_secs(1)).await; @@ -910,14 +904,14 @@ async fn initialize(app: AppHandle) { let _ = loading_task.await; - println!("Loading done, completing initialisation"); + tracing::info!("Loading done, completing initialisation"); let _ = init_tx.send(InitStep::Done); if loading_window.is_some() { loading_window_complete.await; - println!("Loading window completed"); + tracing::info!("Loading window completed"); } MainWindow::create(&app).expect("Failed to create main window"); @@ -931,9 +925,6 @@ fn setup_app(app: &tauri::AppHandle, init_rx: watch::Receiver) { #[cfg(any(target_os = "linux", all(debug_assertions, windows)))] app.deep_link().register_all().ok(); - // Initialize log state - app.manage(LogState(Arc::new(Mutex::new(VecDeque::new())))); - #[cfg(windows)] app.manage(JobObjectState::new()); @@ -943,7 +934,7 @@ fn setup_app(app: &tauri::AppHandle, init_rx: watch::Receiver) { fn spawn_cli_sync_task(app: AppHandle) { tokio::spawn(async move { if let Err(e) = sync_cli(app) { - eprintln!("Failed to sync CLI: {e}"); + tracing::error!("Failed to sync CLI: {e}"); } }); } @@ -963,12 +954,12 @@ enum ServerConnection { async fn setup_server_connection(app: AppHandle) -> ServerConnection { let custom_url = get_saved_server_url(&app).await; - println!("Attempting server connection to custom url: {custom_url:?}"); + tracing::info!(?custom_url, "Attempting server connection"); if let Some(url) = custom_url && server::check_health_or_ask_retry(&app, &url).await { - println!("Connected to custom server: {}", url); + tracing::info!(%url, "Connected to custom server"); return ServerConnection::Existing { url: url.clone() }; } @@ -976,15 +967,15 @@ async fn setup_server_connection(app: AppHandle) -> ServerConnection { let hostname = "127.0.0.1"; let local_url = format!("http://{hostname}:{local_port}"); - println!("Checking health of server '{}'", local_url); + tracing::debug!(url = %local_url, "Checking health of local server"); if server::check_health(&local_url, None).await { - println!("Health check OK, using existing server"); + tracing::info!(url = %local_url, "Health check OK, using existing server"); return ServerConnection::Existing { url: local_url }; } let password = uuid::Uuid::new_v4().to_string(); - println!("Spawning new local server"); + tracing::info!("Spawning new local server"); let (child, health_check) = server::spawn_local_server(app, hostname.to_string(), local_port, password.clone()); diff --git a/packages/desktop/src-tauri/src/linux_display.rs b/packages/desktop/src-tauri/src/linux_display.rs index 24fb27a4bfb..0179cf8bbb3 100644 --- a/packages/desktop/src-tauri/src/linux_display.rs +++ b/packages/desktop/src-tauri/src/linux_display.rs @@ -14,7 +14,11 @@ struct DisplayConfig { } fn dir() -> Option { - Some(dirs::data_dir()?.join(if cfg!(debug_assertions) { "ai.opencode.desktop.dev" } else { "ai.opencode.desktop" })) + Some(dirs::data_dir()?.join(if cfg!(debug_assertions) { + "ai.opencode.desktop.dev" + } else { + "ai.opencode.desktop" + })) } fn path() -> Option { @@ -22,13 +26,12 @@ fn path() -> Option { } pub fn read_wayland() -> Option { - let raw = std::fs::read_to_string(dbg!(path()?)).ok()?; + let raw = std::fs::read_to_string(path()?).ok()?; let root = serde_json::from_str::(&raw) .ok()? - .get(LINUX_DISPLAY_CONFIG_KEY).cloned()?; - serde_json::from_value::(root) - .ok()? - .wayland + .get(LINUX_DISPLAY_CONFIG_KEY) + .cloned()?; + serde_json::from_value::(root).ok()?.wayland } pub fn write_wayland(app: &AppHandle, value: bool) -> Result<(), String> { diff --git a/packages/desktop/src-tauri/src/logging.rs b/packages/desktop/src-tauri/src/logging.rs new file mode 100644 index 00000000000..f794f9c1bc4 --- /dev/null +++ b/packages/desktop/src-tauri/src/logging.rs @@ -0,0 +1,83 @@ +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::path::{Path, PathBuf}; +use tracing_appender::non_blocking::WorkerGuard; +use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt}; + +const MAX_LOG_AGE_DAYS: u64 = 7; +const TAIL_LINES: usize = 1000; + +static LOG_PATH: std::sync::OnceLock = std::sync::OnceLock::new(); + +pub fn init(log_dir: &Path) -> WorkerGuard { + std::fs::create_dir_all(log_dir).expect("failed to create log directory"); + + cleanup(log_dir); + + let timestamp = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S"); + let filename = format!("opencode-desktop_{timestamp}.log"); + let log_path = log_dir.join(&filename); + + LOG_PATH + .set(log_path.clone()) + .expect("logging already initialized"); + + let file = File::create(&log_path).expect("failed to create log file"); + let (non_blocking, guard) = tracing_appender::non_blocking(file); + + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| { + if cfg!(debug_assertions) { + EnvFilter::new("opencode_lib=debug,opencode_desktop=debug,sidecar=debug") + } else { + EnvFilter::new("opencode_lib=info,opencode_desktop=info,sidecar=info") + } + }); + + tracing_subscriber::registry() + .with(filter) + .with(fmt::layer().with_writer(std::io::stderr)) + .with( + fmt::layer() + .with_writer(non_blocking) + .with_ansi(false), + ) + .init(); + + guard +} + +pub fn tail() -> String { + let Some(path) = LOG_PATH.get() else { + return String::new(); + }; + + let Ok(file) = File::open(path) else { + return String::new(); + }; + + let lines: Vec = BufReader::new(file) + .lines() + .map_while(Result::ok) + .collect(); + + let start = lines.len().saturating_sub(TAIL_LINES); + lines[start..].join("\n") +} + +fn cleanup(log_dir: &Path) { + let cutoff = std::time::SystemTime::now() + - std::time::Duration::from_secs(MAX_LOG_AGE_DAYS * 24 * 60 * 60); + + let Ok(entries) = std::fs::read_dir(log_dir) else { + return; + }; + + for entry in entries.flatten() { + if let Ok(meta) = entry.metadata() + && let Ok(modified) = meta.modified() + && modified < cutoff + { + let _ = std::fs::remove_file(entry.path()); + } + } +} diff --git a/packages/desktop/src-tauri/src/main.rs b/packages/desktop/src-tauri/src/main.rs index af88342c3b2..9eb86cdacc8 100644 --- a/packages/desktop/src-tauri/src/main.rs +++ b/packages/desktop/src-tauri/src/main.rs @@ -43,7 +43,7 @@ fn configure_display_backend() -> Option { set_env_if_absent("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); return Some( "Wayland session detected; forcing X11 backend to avoid compositor protocol errors. \ - Set OC_ALLOW_WAYLAND=1 to keep native Wayland." + Set OC_ALLOW_WAYLAND=1 to keep native Wayland." .into(), ); } @@ -86,7 +86,7 @@ fn main() { #[cfg(target_os = "linux")] { if let Some(backend_note) = configure_display_backend() { - eprintln!("{backend_note:?}"); + eprintln!("{backend_note}"); } } diff --git a/packages/desktop/src-tauri/src/server.rs b/packages/desktop/src-tauri/src/server.rs index 9d09512ce74..81e0595af71 100644 --- a/packages/desktop/src-tauri/src/server.rs +++ b/packages/desktop/src-tauri/src/server.rs @@ -93,14 +93,14 @@ pub fn set_wsl_config(app: AppHandle, config: WslConfig) -> Result<(), String> { pub async fn get_saved_server_url(app: &tauri::AppHandle) -> Option { if let Some(url) = get_default_server_url(app.clone()).ok().flatten() { - println!("Using desktop-specific custom URL: {url}"); + tracing::info!(%url, "Using desktop-specific custom URL"); return Some(url); } if let Some(cli_config) = cli::get_config(app).await && let Some(url) = get_server_url_from_config(&cli_config) { - println!("Using custom server URL from config: {url}"); + tracing::info!(%url, "Using custom server URL from config"); return Some(url); } @@ -124,7 +124,7 @@ pub fn spawn_local_server( tokio::time::sleep(Duration::from_millis(100)).await; if check_health(&url, Some(&password)).await { - println!("Server ready after {:?}", timestamp.elapsed()); + tracing::info!(elapsed = ?timestamp.elapsed(), "Server ready"); return Ok(()); } } @@ -216,7 +216,7 @@ fn normalize_hostname_for_url(hostname: &str) -> String { fn get_server_url_from_config(config: &cli::Config) -> Option { let server = config.server.as_ref()?; let port = server.port?; - println!("server.port found in OC config: {port}"); + tracing::debug!(port, "server.port found in OC config"); let hostname = server .hostname .as_ref() @@ -227,7 +227,7 @@ fn get_server_url_from_config(config: &cli::Config) -> Option { } pub async fn check_health_or_ask_retry(app: &AppHandle, url: &str) -> bool { - println!("Checking health for {url}"); + tracing::debug!(%url, "Checking health"); loop { if check_health(url, None).await { return true;