app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 3e1716151601f0e8bff2f8d36c7e354c7e93f1f8
parent 7a5f0b4704f8403302bf1036a376df4a80e292a2
Author: triesap <tyson@radroots.org>
Date:   Sat, 18 Apr 2026 00:27:48 +0000

app: harden launcher logging verification

Diffstat:
MCargo.lock | 1+
Mcrates/launchers/desktop/Cargo.toml | 3+++
Mcrates/launchers/desktop/src/app.rs | 163++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mcrates/shared/core/src/logging.rs | 36+++++++++++++++++++++++++++++++++---
Mplatforms/macos/Scripts/test-macos-host.sh | 156++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
5 files changed, 305 insertions(+), 54 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -4667,6 +4667,7 @@ dependencies = [ "radroots_app_ui", "thiserror 2.0.18", "tracing", + "tracing-subscriber", ] [[package]] diff --git a/crates/launchers/desktop/Cargo.toml b/crates/launchers/desktop/Cargo.toml @@ -21,5 +21,8 @@ radroots_app_ui.workspace = true thiserror.workspace = true tracing.workspace = true +[dev-dependencies] +tracing-subscriber.workspace = true + [lints] workspace = true diff --git a/crates/launchers/desktop/src/app.rs b/crates/launchers/desktop/src/app.rs @@ -9,7 +9,7 @@ use thiserror::Error; use tracing::{error, info}; use crate::menus::install_native_app_menu; -use crate::runtime::DesktopAppRuntime; +use crate::runtime::{DesktopAppRuntime, DesktopAppRuntimeSummary}; use crate::window::{home_titlebar_options, open_home_window}; #[derive(Debug, Error)] @@ -26,17 +26,9 @@ pub fn launch() -> Result<(), AppLaunchError> { let snapshot = AppRuntimeSnapshot::from_config(build, &runtime_config); bootstrap_logging(&snapshot, runtime_config.local_log_root.as_path())?; install_panic_hook(); - emit_launch_event(&snapshot); let runtime = DesktopAppRuntime::bootstrap(); - if let Some(startup_issue) = runtime.summary().startup_issue { - error!( - target: "runtime", - event = "runtime.degraded", - startup_issue = %startup_issue, - "desktop runtime degraded" - ); - } + emit_runtime_events(&snapshot, &runtime.summary()); let app = Application::new().with_assets(gpui_component_assets::Assets); @@ -126,3 +118,154 @@ fn emit_launch_event(snapshot: &AppRuntimeSnapshot) { launch_event.message ); } + +fn emit_runtime_events(snapshot: &AppRuntimeSnapshot, runtime: &DesktopAppRuntimeSummary) { + emit_launch_event(snapshot); + + if let Some(startup_issue) = runtime.startup_issue.as_deref() { + emit_degraded_runtime_event(startup_issue); + } +} + +fn emit_degraded_runtime_event(startup_issue: &str) { + error!( + target: "runtime", + event = "runtime.degraded", + startup_issue = %startup_issue, + "desktop runtime degraded" + ); +} + +#[cfg(test)] +mod tests { + use std::sync::{Arc, Mutex}; + + use radroots_app_core::{ + APP_PROJECTION_SOURCE, AppBuildIdentity, AppRuntimeCapture, AppRuntimeMode, + AppRuntimeSnapshot, + }; + use radroots_app_models::TodayAgendaProjection; + use radroots_app_state::AppShellProjection; + use tracing::{ + Event, Level, Subscriber, + field::{Field, Visit}, + }; + use tracing_subscriber::{Layer, layer::Context, prelude::*, registry::LookupSpan}; + + use crate::runtime::DesktopAppRuntimeSummary; + + use super::emit_runtime_events; + + #[derive(Clone, Debug, Eq, PartialEq)] + struct CapturedEvent { + level: Level, + target: String, + event: Option<String>, + message: Option<String>, + startup_issue: Option<String>, + } + + #[derive(Default)] + struct EventFieldVisitor { + event: Option<String>, + message: Option<String>, + startup_issue: Option<String>, + } + + struct CaptureLayer { + events: Arc<Mutex<Vec<CapturedEvent>>>, + } + + impl EventFieldVisitor { + fn record_value(&mut self, field: &Field, value: String) { + match field.name() { + "event" => self.event = Some(value), + "message" => self.message = Some(value), + "startup_issue" => self.startup_issue = Some(value), + _ => {} + } + } + } + + impl Visit for EventFieldVisitor { + fn record_str(&mut self, field: &Field, value: &str) { + self.record_value(field, value.to_owned()); + } + + fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { + self.record_value(field, format!("{value:?}").trim_matches('"').to_owned()); + } + } + + impl<S> Layer<S> for CaptureLayer + where + S: Subscriber + for<'lookup> LookupSpan<'lookup>, + { + fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { + let mut visitor = EventFieldVisitor::default(); + event.record(&mut visitor); + self.events + .lock() + .expect("capture lock") + .push(CapturedEvent { + level: *event.metadata().level(), + target: event.metadata().target().to_owned(), + event: visitor.event, + message: visitor.message, + startup_issue: visitor.startup_issue, + }); + } + } + + fn test_snapshot() -> AppRuntimeSnapshot { + AppRuntimeSnapshot::from_capture( + AppBuildIdentity { + package_name: "radroots_app".to_owned(), + package_version: "0.1.0".to_owned(), + build_profile: "debug".to_owned(), + target_triple: "aarch64-apple-darwin".to_owned(), + projection_source: APP_PROJECTION_SOURCE.to_owned(), + git_commit: None, + }, + AppRuntimeMode::LocalhostDev, + AppRuntimeCapture { + host_locale: "en_US.UTF-8".to_owned(), + operating_system: "macos".to_owned(), + run_id: "app-localhost-dev-20260418T000000Z-deadbeefcafefeed".to_owned(), + }, + ) + } + + #[test] + fn degraded_runtime_emits_launch_and_degraded_events() { + let events = Arc::new(Mutex::new(Vec::new())); + let subscriber = tracing_subscriber::registry().with(CaptureLayer { + events: Arc::clone(&events), + }); + let summary = DesktopAppRuntimeSummary { + shell_projection: AppShellProjection::default(), + today_projection: TodayAgendaProjection::default(), + startup_issue: Some("desktop runtime roots require HOME for macos".to_owned()), + }; + + tracing::subscriber::with_default(subscriber, || { + emit_runtime_events(&test_snapshot(), &summary); + }); + + let events = events.lock().expect("events lock"); + assert_eq!(events.len(), 2); + assert_eq!(events[0].event.as_deref(), Some("runtime.launch")); + assert_eq!(events[0].target, "bootstrap"); + assert_eq!(events[1].event.as_deref(), Some("runtime.degraded")); + assert_eq!(events[1].level, Level::ERROR); + assert_eq!(events[1].target, "runtime"); + assert_eq!( + events[1].startup_issue.as_deref(), + Some("desktop runtime roots require HOME for macos") + ); + assert_eq!( + events[1].message.as_deref(), + Some("desktop runtime degraded") + ); + } +} diff --git a/crates/shared/core/src/logging.rs b/crates/shared/core/src/logging.rs @@ -238,7 +238,7 @@ pub fn init_logging(options: AppLoggingOptions) -> Result<(), AppLoggingError> { info!( target: "runtime", event = "logging.initialized", - file = %options.log_dir.join(format!("{}.jsonl", current_utc_day())).display(), + file = %current_day_log_path(&options.log_dir).display(), stdout = options.stdout, "logging initialized" ); @@ -271,6 +271,10 @@ fn build_file_appender(log_dir: &Path) -> Result<RollingFileAppender, AppLogging .build(log_dir)?) } +fn current_day_log_path(log_dir: &Path) -> PathBuf { + log_dir.join(format!("{}.jsonl", current_utc_day())) +} + fn current_utc_day() -> String { Utc::now().format("%Y-%m-%d").to_string() } @@ -298,7 +302,13 @@ fn prepare_latest_alias(log_dir: &Path) -> Result<(), AppLoggingError> { } #[cfg(unix)] - std::os::unix::fs::symlink(format!("{}.jsonl", current_utc_day()), &latest_path)?; + std::os::unix::fs::symlink( + current_day_log_path(log_dir) + .file_name() + .map(|value| value.to_owned()) + .unwrap_or_default(), + &latest_path, + )?; #[cfg(not(unix))] fs::write(&latest_path, [])?; @@ -356,6 +366,7 @@ fn target_category(target: &str) -> String { mod tests { use std::{ fs, + io::Write, path::{Path, PathBuf}, time::{SystemTime, UNIX_EPOCH}, }; @@ -369,7 +380,8 @@ mod tests { }; use super::{ - APP_LOG_PRODUCT, APP_LOG_SCHEMA_VERSION, app_runtime_log_dir, prepare_latest_alias, + APP_LOG_PRODUCT, APP_LOG_SCHEMA_VERSION, app_runtime_log_dir, build_file_appender, + current_day_log_path, prepare_latest_alias, }; fn temp_dir(name: &str) -> PathBuf { @@ -432,6 +444,24 @@ mod tests { } #[test] + fn rolling_file_appender_writes_to_active_day_log_file() { + let dir = temp_dir("rolling-file"); + fs::create_dir_all(&dir).expect("create dir"); + + let mut appender = build_file_appender(&dir).expect("build file appender"); + writeln!(appender, "{{\"event\":\"runtime.launch\"}}").expect("write structured record"); + appender.flush().expect("flush structured record"); + + let log_path = current_day_log_path(&dir); + let payload = fs::read_to_string(&log_path).expect("read current day log"); + + assert!(log_path.ends_with(format!("{}.jsonl", Utc::now().format("%Y-%m-%d")))); + assert!(payload.contains("\"event\":\"runtime.launch\"")); + + let _ = fs::remove_dir_all(&dir); + } + + #[test] fn structured_record_shape_remains_stable() { let snapshot = test_snapshot(); let payload = serde_json::to_value(json!({ diff --git a/platforms/macos/Scripts/test-macos-host.sh b/platforms/macos/Scripts/test-macos-host.sh @@ -18,6 +18,7 @@ require_command() { require_command /usr/libexec/PlistBuddy require_command mktemp +require_command readlink app_path="$("${script_dir}/build-macos-host.sh")" plist_path="${app_path}/Contents/Info.plist" @@ -67,11 +68,90 @@ release_app_path="$( } tmp_root="$(mktemp -d)" +runner_pid="" +degraded_runner_pid="" + +wait_for_log_event() { + local structured_log_file="$1" + local expected_event="$2" + local runner_pid="$3" + local event_verified=false + + for _ in $(seq 1 150); do + if [[ -f "${structured_log_file}" ]] && grep -q "\"event\":\"${expected_event}\"" "${structured_log_file}" 2>/dev/null; then + event_verified=true + break + fi + + if ! kill -0 "${runner_pid}" 2>/dev/null; then + wait "${runner_pid}" + exit $? + fi + + sleep 0.1 + done + + [[ "${event_verified}" == "true" ]] || { + echo "${expected_event} was not recorded by run-macos-host.sh" >&2 + exit 1 + } +} + +assert_latest_alias() { + local latest_log_path="$1" + + [[ -e "${latest_log_path}" ]] || { + echo "missing latest structured log alias: ${latest_log_path}" >&2 + exit 1 + } + + [[ "$(readlink "${latest_log_path}")" == "${date_utc}.jsonl" ]] || { + echo "latest structured log alias does not point at ${date_utc}.jsonl" >&2 + exit 1 + } +} + +assert_raw_logs_exist() { + local stdout_file="$1" + local stderr_file="$2" + + [[ -f "${stdout_file}" ]] || { + echo "missing raw stdout log: ${stdout_file}" >&2 + exit 1 + } + + [[ -f "${stderr_file}" ]] || { + echo "missing raw stderr log: ${stderr_file}" >&2 + exit 1 + } +} + +terminate_runner() { + local runner_pid="$1" + + if [[ -n "${runner_pid}" ]] && kill -0 "${runner_pid}" 2>/dev/null; then + kill "${runner_pid}" 2>/dev/null || true + fi + + set +e + wait "${runner_pid}" + local exit_code="$?" + set -e + [[ "${exit_code}" == "0" ]] || [[ "${exit_code}" == "143" ]] || [[ "${exit_code}" == "130" ]] || { + echo "unexpected runner exit code after termination: ${exit_code}" >&2 + exit 1 + } +} + cleanup() { if [[ -n "${runner_pid:-}" ]] && kill -0 "${runner_pid}" 2>/dev/null; then kill "${runner_pid}" 2>/dev/null || true wait "${runner_pid}" || true fi + if [[ -n "${degraded_runner_pid:-}" ]] && kill -0 "${degraded_runner_pid}" 2>/dev/null; then + kill "${degraded_runner_pid}" 2>/dev/null || true + wait "${degraded_runner_pid}" || true + fi rm -rf "${tmp_root}" } trap cleanup EXIT @@ -100,48 +180,42 @@ RADROOTS_APP_RUNTIME_CONFIG_JSON="$( "${script_dir}/run-macos-host.sh" & runner_pid="$!" -launch_verified=false -for _ in $(seq 1 150); do - if [[ -f "${structured_log_file}" ]] && grep -q '"event":"runtime.launch"' "${structured_log_file}" 2>/dev/null; then - launch_verified=true - break - fi - - if ! kill -0 "${runner_pid}" 2>/dev/null; then - wait "${runner_pid}" - exit $? - fi - - sleep 0.1 -done - -[[ "${launch_verified}" == "true" ]] || { - echo "runtime.launch was not recorded by run-macos-host.sh" >&2 - exit 1 -} - -[[ -e "${latest_log_path}" ]] || { - echo "missing latest structured log alias: ${latest_log_path}" >&2 - exit 1 -} - -[[ -f "${stdout_file}" ]] || { - echo "missing raw stdout log: ${stdout_file}" >&2 - exit 1 -} +wait_for_log_event "${structured_log_file}" "runtime.launch" "${runner_pid}" +assert_latest_alias "${latest_log_path}" +assert_raw_logs_exist "${stdout_file}" "${stderr_file}" +terminate_runner "${runner_pid}" +runner_pid="" -[[ -f "${stderr_file}" ]] || { - echo "missing raw stderr log: ${stderr_file}" >&2 - exit 1 -} +degraded_run_id="$(radroots_app_run_id "${runtime_mode}")" +degraded_log_root="${tmp_root}/degraded-logs" +degraded_structured_log_file="${degraded_log_root}/apps/local/app/app-macos-native/${date_utc}.jsonl" +degraded_latest_log_path="${degraded_log_root}/apps/local/app/app-macos-native/latest.jsonl" +degraded_stdout_file="${degraded_log_root}/apps/local/app/app-macos-native/raw/stdout.${date_utc}.log" +degraded_stderr_file="${degraded_log_root}/apps/local/app/app-macos-native/raw/stderr.${date_utc}.log" +degraded_runtime_config_json="$( + radroots_app_build_runtime_config_json \ + "${repo_root}" \ + "${runtime_mode}" \ + "${degraded_run_id}" \ + "${bundle_identifier}" \ + "${platform_name}" \ + "${degraded_log_root}" +)" -kill "${runner_pid}" 2>/dev/null || true -set +e -wait "${runner_pid}" -exit_code="$?" -set -e -[[ "${exit_code}" == "0" ]] || [[ "${exit_code}" == "143" ]] || [[ "${exit_code}" == "130" ]] || { - echo "unexpected runner exit code after termination: ${exit_code}" >&2 +env -u HOME \ + RADROOTS_APP_RUN_ID="${degraded_run_id}" \ + RADROOTS_APP_LOCAL_LOG_ROOT="${degraded_log_root}" \ + RADROOTS_APP_RUNTIME_CONFIG_JSON="${degraded_runtime_config_json}" \ + "${script_dir}/run-macos-host.sh" & +degraded_runner_pid="$!" + +wait_for_log_event "${degraded_structured_log_file}" "runtime.launch" "${degraded_runner_pid}" +wait_for_log_event "${degraded_structured_log_file}" "runtime.degraded" "${degraded_runner_pid}" +assert_latest_alias "${degraded_latest_log_path}" +assert_raw_logs_exist "${degraded_stdout_file}" "${degraded_stderr_file}" +grep -q '"startup_issue":"desktop runtime roots require HOME for macos"' "${degraded_structured_log_file}" || { + echo "runtime.degraded did not record the expected startup issue" >&2 exit 1 } -runner_pid="" +terminate_runner "${degraded_runner_pid}" +degraded_runner_pid=""