Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ a restart of the studio. One studio at a time can use it: a second one's
browser sources fail saying so. Each page renders in a process of its own, so
a page that crashes fails only its own source, with the reason, while the
stream carries on. A crash of the browser itself fails every browser source.
Either shows at once, since the browser's processes dump no core.
Comment thread
kixelated marked this conversation as resolved.
Resolved
**Reload** in Sources restarts a page, failed or not. A page's dropdown
lists, such as a `<select>`'s, draw over it and take clicks under
**Interact**, but it opens no other windows (`window.open` and new-tab links
Expand Down
1 change: 0 additions & 1 deletion quest/m0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ repository.
- [Virtual camera](/quest/m0/vcam/README.md) - a canvas's program exposed as a system camera on Linux, Windows, and macOS
- [Recover crashed recordings](/quest/m0/recording-repair.md) - a recording a crash left unfinished is repaired beside the original at launch
- [Premultiplied source sampling](/quest/m0/premultiply.md) - scaled transparent sources draw without edge fringes
- [A browser renderer that dies on its first page](/quest/m0/browser-first-crash.md) - a renderer lost during the first navigation fails its source
- [Browser sources without an X display](/quest/m0/cef-headless.md) - CEF runs headless, so Wayland sessions and tests need no X server
- [Signing identities are available](/quest/m0/signing.md) - the Apple and Windows code-signing credentials packaging and the macOS camera need
- [Packaging](/quest/m0/packaging.md) - signed installers for macOS and Windows and a Flatpak for Linux
Expand Down
22 changes: 0 additions & 22 deletions quest/m0/browser-first-crash.md

This file was deleted.

19 changes: 19 additions & 0 deletions rs/moq-scene/src/browser/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, mpsc};

use cef::*;
use rustix::process::{Resource, Rlimit, getrlimit, setrlimit};

use super::outbox::Commands;
use super::protocol::{Area, Channel, Command, Event, Format, Id, Layer, Open, Plane, Raster, Start, Texture};
Expand Down Expand Up @@ -110,6 +111,24 @@ fn serve() -> Result<(), String> {
/// Run CEF on the profile `start` names, opening and closing pages as the
/// studio asks, until it closes the socket and the last page closes.
fn browse(shared: &Arc<Shared>, start: Start, reader: Channel, painted_tx: mpsc::Sender<()>) -> Result<(), String> {
// A crashed process is reported only once it is gone, and one dumping
// core is not gone yet: systemd-coredump took 20 to 70 seconds over a
// renderer, which left its page Starting meanwhile. CEF's subprocesses
// inherit the limit. 1, not 0: the kernel ignores the limit for a pipe
// core_pattern (systemd-coredump, Apport) except at 1, where it skips the
// handler, and a core file needs a page. A socket core_pattern (`@`), or
// a pipe under a hard limit of 0, which forbids 1, relies instead on the
// handler honouring the limit, as systemd-coredump does.
let core = getrlimit(Resource::Core);
setrlimit(
Resource::Core,
Rlimit {
current: Some(core.maximum.map_or(1, |maximum| maximum.min(1))),
maximum: core.maximum,
},
)
.map_err(|err| format!("cannot turn off core dumps: {err}"))?;

let _ = api_hash(sys::CEF_API_VERSION_LAST, 0);
let args = args::Args::new();
let mut app = HelperApp::new(HelperProcess::new(), start.raster);
Expand Down
112 changes: 96 additions & 16 deletions rs/moq-studio/tests/browser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,12 +341,12 @@ async fn a_page_that_closes_itself_fails_alone() {
.await;
}

/// A page whose renderer crashes fails its own source and leaves the canvas,
/// while everything else keeps drawing. A crash of the helper every page
/// shares fails them all and nothing else, and a reload starts a new one.
/// A page whose renderer crashes on its first navigation fails its own
/// source, while everything else keeps drawing. A crash of the helper every
/// page shares fails them all and nothing else, and a reload starts a new one.
#[tokio::test(flavor = "multi_thread")]
#[ignore = "needs CEF and a Vulkan device; run by just render-sw"]
async fn a_crash_drops_only_its_source() {
async fn a_crash_while_loading_drops_only_its_source() {
local(async {
let crash = Source::Browser {
url: "chrome://crash".into(),
Expand Down Expand Up @@ -377,6 +377,38 @@ async fn a_crash_drops_only_its_source() {
.await;
}

/// A page whose renderer crashes after it has drawn fails its source and
/// leaves the canvas, and a reload draws it again.
#[tokio::test(flavor = "multi_thread")]
#[ignore = "needs CEF and a Vulkan device; run by just render-sw"]
async fn a_crash_after_drawing_leaves_the_canvas() {
local(async {
let red = page("<body style='background:red'></body>");
let mut studio = Studio::new(&[("blue", blue()), ("page", red)]).await;
studio.until_center([255, 0, 0, 255], &[]).await;

// Abort the page's renderer, and any spare one CEF keeps ready, which
// shows no page, as a failed assertion would: a crash that dumps core.
// (V8 takes a SIGSEGV that no fault raised and carries on.)
let helper = helper().expect("the page's helper is running");
let renderers = renderers(helper);
assert!(!renderers.is_empty(), "the page has a renderer");
for renderer in renderers {
let crashed = std::process::Command::new("kill")
.args(["-ABRT", &renderer.to_string()])
.status();
assert!(crashed.unwrap().success());
}
let reason = studio.until_failed("page").await;
assert!(reason.contains("renderer"), "{reason}");
studio.until_center(BLUE, &["page"]).await;

studio.reload("page");
studio.until_center([255, 0, 0, 255], &[]).await;
})
.await;
}

/// Cookies, even one without an expiry, survive a reload of the page and a
/// restart of the studio, since every page keeps them in one profile on disk.
/// A second studio on the profile while it is in use fails its pages saying
Expand Down Expand Up @@ -455,16 +487,64 @@ async fn serve(html: &'static str) -> String {
/// The one helper this test process still runs: a child started with the
/// helper's argument.
fn helper() -> Option<u32> {
let ours = std::process::id().to_string();
std::fs::read_dir("/proc").ok()?.flatten().find_map(|entry| {
let pid: u32 = entry.file_name().to_str()?.parse().ok()?;
let stat = std::fs::read_to_string(entry.path().join("stat")).ok()?;
// The parent is the second field after the parenthesized command.
let parent = stat.rsplit_once(')')?.1.split_whitespace().nth(1)?;
let cmdline = std::fs::read(entry.path().join("cmdline")).ok()?;
let helper = cmdline
.split(|byte| *byte == 0)
.any(|arg| arg == b"--moq-browser-helper");
(parent == ours && helper).then_some(pid)
})
let ours = std::process::id();
processes()
.into_iter()
.find(|process| process.parent == ours && process.args.iter().any(|arg| arg == b"--moq-browser-helper"))
.map(|process| process.pid)
}

/// The renderers under `helper`. CEF forks them from its zygote, whose
/// arguments they keep, so they are told apart by the compositor thread every
/// renderer runs.
fn renderers(helper: u32) -> Vec<u32> {
let processes = processes();
let mut family = vec![helper];
let mut at = 0;
while let Some(&parent) = family.get(at) {
family.extend(
processes
.iter()
.filter(|process| process.parent == parent)
.map(|process| process.pid),
);
at += 1;
}
family
.into_iter()
.filter(|pid| {
let threads = std::fs::read_dir(format!("/proc/{pid}/task"))
.into_iter()
.flatten()
.flatten();
threads
.filter_map(|thread| std::fs::read_to_string(thread.path().join("comm")).ok())
.any(|name| name.trim_end() == "Compositor")
})
.collect()
}

struct Process {
pid: u32,
parent: u32,
args: Vec<Vec<u8>>,
}

/// Every process running.
fn processes() -> Vec<Process> {
let Ok(entries) = std::fs::read_dir("/proc") else {
return Vec::new();
};
entries
.flatten()
.filter_map(|entry| {
let pid = entry.file_name().to_str()?.parse().ok()?;
let stat = std::fs::read_to_string(entry.path().join("stat")).ok()?;
// The parent is the second field after the parenthesized command.
let parent = stat.rsplit_once(')')?.1.split_whitespace().nth(1)?.parse().ok()?;
let cmdline = std::fs::read(entry.path().join("cmdline")).ok()?;
let args = cmdline.split(|byte| *byte == 0).map(<[u8]>::to_vec).collect();
Some(Process { pid, parent, args })
})
.collect()
}