Merge: a log that survives the process, so the tablet can be debugged

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

# Conflicts:
#	apps/darkroom-android/Cargo.toml
#	apps/darkroom-desktop/Cargo.toml
#	platform/dr-plat/src/lib.rs
This commit is contained in:
2026-08-30 13:46:06 +02:00
11 changed files with 1325 additions and 19 deletions
+4 -3
View File
@@ -19,9 +19,10 @@ dr-ui.workspace = true
# For `account::set_data_dir`: only the platform entry point knows where Android
# lets this app keep files, and it must be set before any store is opened.
dr-sync.workspace = true
# For the panic hook, and for `crash::set_state_dir` — Android has no XDG
# directories, so the entry point is the only place that knows where a crash
# record may be written.
# For the panic hook, for `state::set_state_dir` and for
# `diagnostics::install`. Android has no XDG directories, so the entry point is
# the only place that knows where a crash record or a log file may be written,
# and both have to be in place before anything can fail.
dr-plat.workspace = true
# Directly, not just through dr-ui: `android_main` takes an `AndroidApp` and
# calls `slint::android::init`, both of which come from this crate. The backend
+52 -3
View File
@@ -6,8 +6,10 @@
//! * There are no command-line paths. Android's SAF hands out document URIs,
//! not filesystem paths (ARCH §6.9), so the viewer opens with an empty
//! browsing list and the library grid is the only way in.
//! * Logging goes to logcat. `env_logger` writes to stderr, which Android
//! discards.
//! * Logging goes to logcat *and* to a file. `env_logger` writes to stderr,
//! which Android discards; logcat replaces it, and a rotating file beside it
//! replaces the thing logcat cannot be — a record that outlives the session
//! and can be sent to somebody (NFR-OPS-1, [`dr_plat::diagnostics`]).
//!
//! The first of those has one exception, and it is the launch `Intent`: a
//! gallery, a file manager or the share sheet can name images to open, and
@@ -29,11 +31,39 @@ mod intents;
/// Android application entry point, called by android-activity's glue.
#[no_mangle]
fn android_main(app: slint::android::AndroidApp) {
android_logger::init_once(
// Before the logger, because the logger needs somewhere to write, and
// before any `log::` call at all, because records emitted before this line
// reach nothing.
//
// **The external directory, not the internal one, and the difference is
// the entire point of the file.** Both are app-private and both survive
// backgrounding — the volatile one is the *cache* directory, which is not
// in play here. What separates them is retrieval:
// `/data/data/<pkg>/files` needs `run-as` against a debuggable build or
// root to read, and `/sdcard/Android/data/<pkg>/files` is a plain
// `adb pull` from any build, needing no permission since API 19. A log
// nobody can get off the device does not do the job NFR-OPS-1 describes.
//
// The consequence is that anyone holding the tablet can read it, which is
// why `dr_plat::diagnostics` redacts at the sink and why configuration —
// the account list, and the credential reference beside it — stays on
// `internal_data_path` below rather than moving here (NFR-SEC-2).
let external = app.external_data_path();
if let Some(dir) = external.clone().or_else(|| app.internal_data_path()) {
dr_plat::set_state_dir(dir);
}
// `AndroidLogger` rather than `init_once`, so logcat can be *teed* rather
// than replaced: `init_once` installs itself as the global logger and
// there is only one of those. Everything that reached logcat before this
// change still reaches it, at the same level and under the same tag; the
// file is strictly additional.
let console = android_logger::AndroidLogger::new(
android_logger::Config::default()
.with_max_level(log::LevelFilter::Info)
.with_tag("DarkRoom"),
);
let logging = dr_plat::diagnostics::install(Box::new(console), log::LevelFilter::Info);
// Panics go to stderr, and Android discards stderr. Without this hook a
// worker thread that panics is invisible: the process survives, the
@@ -54,6 +84,25 @@ fn android_main(app: slint::android::AndroidApp) {
log::info!("DarkRoom v{}", env!("CARGO_PKG_VERSION"));
// Said in logcat as well as in the file, because the first thing anybody
// asked for a log needs is where it is — and on a device that is a path
// nobody can guess and a command nobody remembers.
match &logging {
dr_plat::Installed::ToFile(path) => {
log::info!("logging to {}", path.display());
if external.is_none() {
log::warn!(
"no external storage; the log is app-private and needs \
`adb shell run-as paris.tourolle.darkroom cat files/darkroom.log` \
on a debuggable build"
);
}
}
dr_plat::Installed::ConsoleOnly(why) => {
log::warn!("no log file this session, only logcat: {why}");
}
}
// Before anything opens a store: Android has no $HOME and no XDG
// directories, so the default guess resolves to a path the app cannot
// write. Nothing failed loudly — the session list went to a doomed path, so
+3 -3
View File
@@ -7,9 +7,9 @@ license.workspace = true
[dependencies]
dr-ui.workspace = true
# For the panic hook alone. Directly rather than through dr-ui, because it has
# to be installed before `dr_ui::run` — a panic during startup is exactly the
# one this exists to catch.
# For the panic hook and the log sink, directly rather than through dr-ui:
# both have to be installed before `dr_ui::run`, because a panic during startup
# is exactly the one they exist to catch and record (NFR-OPS-1, NFR-OPS-2).
dr-plat.workspace = true
anyhow.workspace = true
env_logger.workspace = true
+18 -2
View File
@@ -4,11 +4,21 @@
use std::path::PathBuf;
use dr_plat::diagnostics::Installed;
fn main() -> anyhow::Result<()> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(
// Built rather than `init`ed, so the same logger can be handed to the
// diagnostics tee: `env_logger` keeps writing to stderr exactly as before,
// and every record it accepts is also appended to the on-disk log
// (NFR-OPS-1). `filter()` is asked afterwards because the environment may
// have overridden the default below, and the file must not be quieter than
// the terminal.
let console = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(
"info,wgpu_core=warn,wgpu_hal=warn,zbus=warn,tracing=warn,calloop=warn,rawler=warn",
))
.init();
.build();
let level = console.filter();
let logging = dr_plat::diagnostics::install(Box::new(console), level);
// Immediately after the logger and before anything that could fail. Until
// now a panic on desktop went to stderr and died with the terminal, which
@@ -19,6 +29,12 @@ fn main() -> anyhow::Result<()> {
dr_plat::crash::install(env!("CARGO_PKG_VERSION"));
log::info!("DarkRoom v{}", env!("CARGO_PKG_VERSION"));
// First thing in the file, so a user asked for "the log" can find it
// without being told a path over the phone.
match &logging {
Installed::ToFile(path) => log::info!("logging to {}", path.display()),
Installed::ConsoleOnly(why) => log::warn!("no log file this session: {why}"),
}
let paths: Vec<PathBuf> = std::env::args().skip(1).map(PathBuf::from).collect();
if paths.is_empty() {