163 lines
5.9 KiB
Rust
163 lines
5.9 KiB
Rust
//! The JNI half of `DevLogProvider`: reading this process's own log ring
|
|
//! for a `ContentProvider` that Dev Updater queries.
|
|
|
|
use android_view::jni::JNIEnv;
|
|
use android_view::jni::objects::{JClass, JObject, JString};
|
|
use android_view::jni::sys::{jlong, jobjectArray};
|
|
#[cfg(feature = "transcript-screen")]
|
|
use std::path::Path;
|
|
use std::{ptr, sync::OnceLock};
|
|
|
|
/// Gated with its one reader: the tabs demo links no `client-core` and so
|
|
/// has no ring to lay out, and an ungated constant is a warning in that
|
|
/// build (`iris-android-app` without `transcript-screen`).
|
|
#[cfg(feature = "transcript-screen")]
|
|
const FIELDS_PER_LINE: usize = 5;
|
|
|
|
/// The authority the provider registered itself under, once it has been
|
|
/// created. `None` until then, which is a state worth being able to say:
|
|
/// a provider Android never instantiated and one that is answering look
|
|
/// the same from inside this process otherwise.
|
|
static AUTHORITY: OnceLock<String> = OnceLock::new();
|
|
|
|
#[cfg(feature = "bench")]
|
|
pub fn authority() -> Option<&'static str> {
|
|
AUTHORITY.get().map(String::as_str)
|
|
}
|
|
|
|
/// The directory is taken here as well as in
|
|
/// `MainActivity.nativeSetFilesDir` because **the provider is often the
|
|
/// only thing running**: once the app has died, Dev Updater's query
|
|
/// starts the process for the provider alone, so no activity ever runs
|
|
/// and the panic hook's file would never be replayed into the ring. That
|
|
/// is precisely the run whose log is being asked for. Whichever of the
|
|
/// two arrives first does the replay; `set_crash_dir` deletes the file,
|
|
/// so the second finds nothing and says nothing.
|
|
///
|
|
/// # Safety
|
|
/// Called by the JVM with the arguments its `native` declaration names.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "system" fn Java_dev_iris_android_demo_DevLogProvider_nativeReady(
|
|
mut env: JNIEnv,
|
|
_class: JClass,
|
|
authority: JString,
|
|
files_dir: JString,
|
|
) {
|
|
#[cfg(feature = "transcript-screen")]
|
|
if let Some(dir) = string_arg(&mut env, &files_dir) {
|
|
crate::android::app_log::set_crash_dir(Path::new(&dir));
|
|
}
|
|
#[cfg(not(feature = "transcript-screen"))]
|
|
let _ = &files_dir;
|
|
let Some(authority) = string_arg(&mut env, &authority) else {
|
|
return;
|
|
};
|
|
log::info!("iris devlog: serving this app's log at content://{authority}");
|
|
let _ = AUTHORITY.set(authority);
|
|
}
|
|
|
|
fn string_arg(env: &mut JNIEnv, value: &JString) -> Option<String> {
|
|
if value.is_null() {
|
|
return None;
|
|
}
|
|
env.get_string(value).ok().map(Into::into)
|
|
}
|
|
|
|
/// `newest_seq` is `-1` for a ring nothing has been written to, which is
|
|
/// what tells a reader holding a cursor that this process **restarted**:
|
|
/// the ring is in memory, so a new process starts again at zero and a
|
|
/// stale cursor would otherwise skip everything silently.
|
|
///
|
|
/// # Safety
|
|
/// Called by the JVM with the arguments its `native` declaration names.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "system" fn Java_dev_iris_android_demo_DevLogProvider_nativeStatus(
|
|
mut env: JNIEnv,
|
|
_class: JClass,
|
|
) -> jobjectArray {
|
|
string_array(&mut env, &status_fields())
|
|
}
|
|
|
|
/// Inclusive of `since` because [`crate::client::log_ring::LogRing::since`]
|
|
/// is, and one definition of the cursor is what keeps the app's own
|
|
/// uploaded report and this provider describing the same lines.
|
|
///
|
|
/// # Safety
|
|
/// Called by the JVM with the arguments its `native` declaration names.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "system" fn Java_dev_iris_android_demo_DevLogProvider_nativeLinesSince(
|
|
mut env: JNIEnv,
|
|
_class: JClass,
|
|
since: jlong,
|
|
) -> jobjectArray {
|
|
// A negative cursor is a caller asking for everything, not an error to
|
|
// take the app down over: the provider is a diagnostic.
|
|
string_array(&mut env, &line_fields(since.max(0) as u64))
|
|
}
|
|
|
|
#[cfg(feature = "transcript-screen")]
|
|
fn status_fields() -> Vec<String> {
|
|
let ring = crate::client::log_ring::process_ring();
|
|
vec![
|
|
ring.len().to_string(),
|
|
ring.dropped().to_string(),
|
|
ring.newest_seq().map_or(-1, |seq| seq as i64).to_string(),
|
|
]
|
|
}
|
|
|
|
/// The tabs demo links no `client-core` and keeps no ring, so it holds
|
|
/// nothing and has never dropped anything -- which is the truth, not a
|
|
/// stand-in. The natives are still exported there, because a `native`
|
|
/// method Java declares and the library does not is an
|
|
/// `UnsatisfiedLinkError` the moment the class loads.
|
|
#[cfg(not(feature = "transcript-screen"))]
|
|
fn status_fields() -> Vec<String> {
|
|
vec!["0".to_string(), "0".to_string(), "-1".to_string()]
|
|
}
|
|
|
|
#[cfg(feature = "transcript-screen")]
|
|
fn line_fields(since: u64) -> Vec<String> {
|
|
let (lines, _next) = crate::client::log_ring::process_ring().since(since);
|
|
let mut fields = Vec::with_capacity(lines.len() * FIELDS_PER_LINE);
|
|
for line in lines {
|
|
fields.push(line.seq.to_string());
|
|
fields.push(line.at_ms.to_string());
|
|
fields.push(line.level.to_string());
|
|
fields.push(line.target);
|
|
fields.push(line.message);
|
|
}
|
|
fields
|
|
}
|
|
|
|
#[cfg(not(feature = "transcript-screen"))]
|
|
fn line_fields(_since: u64) -> Vec<String> {
|
|
Vec::new()
|
|
}
|
|
|
|
/// Null rather than a panic across the JNI boundary: `DevLogProvider`
|
|
/// reads it as "the provider could not answer" and returns no cursor,
|
|
/// which Dev Updater already draws as a distinct state. Taking the app
|
|
/// down to report that its diagnostic is unavailable would be worse than
|
|
/// the diagnostic being unavailable.
|
|
fn string_array(env: &mut JNIEnv, fields: &[String]) -> jobjectArray {
|
|
let null = ptr::null_mut();
|
|
let Ok(class) = env.find_class("java/lang/String") else {
|
|
return null;
|
|
};
|
|
let Ok(array) = env.new_object_array(fields.len() as i32, class, JObject::null()) else {
|
|
return null;
|
|
};
|
|
for (index, field) in fields.iter().enumerate() {
|
|
let Ok(value) = env.new_string(field) else {
|
|
return null;
|
|
};
|
|
if env
|
|
.set_object_array_element(&array, index as i32, value)
|
|
.is_err()
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
array.into_raw()
|
|
}
|