git.lucas.co / cce-browser
web browser (Servo)
git clone https://git.lucas.co/cce-browser.git

examples/wpe_crash.rs (10K)

  1 //! Proves what a tab does when its WebProcess hangs or dies.
  2 //!
  3 //! Serves a page that spins its main thread forever on the first visit and
  4 //! loads normally on the next, and checks, against the real engine:
  5 //!
  6 //! * a hung page is reported unresponsive once a click goes unanswered —
  7 //!   including after a pointer move, which WebKit's own timer misses;
  8 //! * a page that answers is not;
  9 //! * "Wait" quiets the question, and stopping kills the process;
 10 //! * the dead tab shows the error page **under its own URL**, so the URL
 11 //!   bar and the saved session still mean the real page;
 12 //! * a reload from there fetches the real page again;
 13 //! * a page that spins is never stopped without asking, but a process that
 14 //!   is hung *and idle* — what a deadlock looks like, simulated with SIGSTOP —
 15 //!   is stopped and reloaded on its own;
 16 //! * a page waiting in `alert()` is not taken for hung;
 17 //! * a WebProcess that dies outright (SIGKILL) gets the crash page.
 18 //!
 19 //! `cargo run --release -p cce-browser --example wpe_crash`
 20 
 21 #[cfg(not(feature = "wpe"))]
 22 fn main() {
 23     eprintln!("build with --features wpe");
 24 }
 25 
 26 #[cfg(feature = "wpe")]
 27 #[derive(Debug, Clone, Copy)]
 28 pub enum EditingCommand { Copy, Cut, Paste }
 29 
 30 #[cfg(feature = "wpe")]
 31 #[path = "../src/pages.rs"]
 32 mod pages;
 33 #[cfg(feature = "wpe")]
 34 #[path = "../src/downloads.rs"]
 35 mod downloads;
 36 #[cfg(feature = "wpe")]
 37 // The host's vi channel and scripts.
 38 #[path = "../src/vi.rs"]
 39 #[allow(dead_code)]
 40 mod vi;
 41 
 42 #[path = "../src/wpe/mod.rs"]
 43 mod wpe;
 44 
 45 /// Spins once it has painted, so there is a frame to be stuck on.
 46 #[cfg(feature = "wpe")]
 47 const HANG: &str = "<!doctype html><title>hang</title><p>spinning\
 48 <script>setTimeout(() => { for (;;) {} }, 300)</script>";
 49 #[cfg(feature = "wpe")]
 50 const FINE: &str = "<!doctype html><title>recovered</title><p>fine";
 51 /// Opens an alert on the first click.
 52 #[cfg(feature = "wpe")]
 53 const ALERT: &str = "<!doctype html><title>alert</title><p>click me\
 54 <script>addEventListener('mousedown', () => alert('hi'), {once: true})</script>";
 55 
 56 /// The first request gets the hanging page, every later one the fine one.
 57 #[cfg(feature = "wpe")]
 58 fn serve() -> u16 {
 59     use std::io::{Read, Write};
 60     let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
 61     let port = listener.local_addr().unwrap().port();
 62     std::thread::spawn(move || {
 63         let mut served = 0;
 64         for stream in listener.incoming() {
 65             let Ok(mut s) = stream else { continue };
 66             let mut buf = [0u8; 4096];
 67             let n = s.read(&mut buf).unwrap_or(0);
 68             let req = String::from_utf8_lossy(&buf[..n]).to_string();
 69             let body = if req.starts_with("GET /alert") {
 70                 ALERT
 71             } else if req.starts_with("GET /page") {
 72                 served += 1;
 73                 if served == 1 { HANG } else { FINE }
 74             } else {
 75                 let _ = write!(s, "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
 76                 continue;
 77             };
 78             let _ = write!(
 79                 s,
 80                 "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
 81                 body.len(),
 82                 body
 83             );
 84         }
 85     });
 86     port
 87 }
 88 
 89 /// Every `WPEWebProcess` under this process (they sit below bwrap).
 90 #[cfg(feature = "wpe")]
 91 fn web_processes() -> Vec<i32> {
 92     let me = std::process::id() as i32;
 93     let parent_of = |pid: i32| -> Option<i32> {
 94         let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
 95         stat.rsplit_once(')')?.1.split_whitespace().nth(1)?.parse().ok()
 96     };
 97     let mut out = Vec::new();
 98     for e in std::fs::read_dir("/proc").into_iter().flatten().flatten() {
 99         let Ok(pid) = e.file_name().to_string_lossy().parse::<i32>() else { continue };
100         let comm = std::fs::read_to_string(format!("/proc/{pid}/comm")).unwrap_or_default();
101         if comm.trim() != "WPEWebProcess" {
102             continue;
103         }
104         let mut p = pid;
105         while let Some(pp) = parent_of(p) {
106             if pp == me {
107                 out.push(pid);
108                 break;
109             }
110             if pp <= 1 {
111                 break;
112             }
113             p = pp;
114         }
115     }
116     out
117 }
118 
119 #[cfg(feature = "wpe")]
120 fn main() {
121     let port = serve();
122     let page = url::Url::parse(&format!("http://127.0.0.1:{port}/page")).unwrap();
123     let mut host = wpe::WebKitHost::new(page.clone(), (800, 600));
124     host.focus(true);
125     let settle = |h: &mut wpe::WebKitHost, ms: u64| {
126         let end = std::time::Instant::now() + std::time::Duration::from_millis(ms);
127         while std::time::Instant::now() < end {
128             h.pump();
129             // Nothing draws here; say so, or the readback waits forever.
130             h.frame_drawn();
131             std::thread::sleep(std::time::Duration::from_millis(20));
132         }
133     };
134 
135     use cce_ui::widget::MouseButton;
136     let mut ok = true;
137     let mut check = |what: &str, pass: bool, detail: String| {
138         ok &= pass;
139         println!("{what:<44} {} {detail}", if pass { "OK   " } else { "WRONG" });
140     };
141 
142     settle(&mut host, 1500);
143     check("the hanging page loaded", host.title().as_deref() == Some("hang"), format!("{:?}", host.title()));
144     check("not unresponsive before any input", !host.active_unresponsive(), String::new());
145 
146     // Move, then click: the order a person does it in, and the one WebKit's
147     // own timer misses — the click queues behind the unanswered move and is
148     // never sent. The host's ping is what catches it.
149     host.mouse_move(100.0, 100.0);
150     host.mouse_button_ui(MouseButton::Left, true, 100.0, 100.0);
151     host.mouse_button_ui(MouseButton::Left, false, 100.0, 100.0);
152     settle(&mut host, 4000);
153     check("unanswered input reports a hang", host.active_unresponsive(), String::new());
154 
155     // Well past the deadlock watch: a spinning page burns a core, so it is
156     // left to the person to stop.
157     settle(&mut host, 7000);
158     check(
159         "a busy page is never stopped unasked",
160         host.active_unresponsive() && host.title().as_deref() == Some("hang"),
161         format!("{:?}", host.title()),
162     );
163 
164     host.wait_unresponsive();
165     check("waiting quiets the question", !host.active_unresponsive(), String::new());
166 
167     host.stop_unresponsive();
168     settle(&mut host, 1500);
169     let title = host.title().unwrap_or_default();
170     check("a stopped page shows the error page", title == "This page was stopped", format!("{title:?}"));
171     check("under its own URL", host.url().as_ref() == Some(&page), format!("{:?}", host.url().map(|u| u.to_string())));
172     check("and is no longer unresponsive", !host.active_unresponsive(), String::new());
173 
174     host.reload();
175     settle(&mut host, 1500);
176     let title = host.title().unwrap_or_default();
177     check("reload fetches the real page", title == "recovered", format!("{title:?}"));
178 
179     // A page that answers is never asked about.
180     host.mouse_move(120.0, 120.0);
181     host.mouse_button_ui(MouseButton::Left, true, 120.0, 120.0);
182     host.mouse_button_ui(MouseButton::Left, false, 120.0, 120.0);
183     settle(&mut host, 4000);
184     check("a live page is not reported", !host.active_unresponsive(), String::new());
185 
186     // A deadlock, simulated: every page process frozen, so nothing answers
187     // and nothing burns CPU.
188     let frozen = web_processes();
189     for &pid in &frozen {
190         unsafe { libc_kill(pid, 19) }; // SIGSTOP
191     }
192     host.mouse_move(130.0, 130.0);
193     host.mouse_button_ui(MouseButton::Left, true, 130.0, 130.0);
194     host.mouse_button_ui(MouseButton::Left, false, 130.0, 130.0);
195     settle(&mut host, 4000);
196     check("an idle hang is reported first", host.active_unresponsive(), String::new());
197     settle(&mut host, 6000);
198     let title = host.title().unwrap_or_default();
199     check(
200         "then stopped and reloaded on its own",
201         title == "recovered" && !host.active_unresponsive(),
202         format!("{title:?}"),
203     );
204     check("under the same URL", host.url().as_ref() == Some(&page), String::new());
205     // Whatever was frozen and not stopped (the spare) goes back to work.
206     for &pid in &frozen {
207         unsafe { libc_kill(pid, 18) }; // SIGCONT
208     }
209     settle(&mut host, 500);
210 
211     // A page blocked in alert() is waiting on the chrome, not hung — even
212     // though the ping sent with the click that opened it goes unanswered.
213     let alert = url::Url::parse(&format!("http://127.0.0.1:{port}/alert")).unwrap();
214     host.load(alert);
215     settle(&mut host, 1500);
216     host.mouse_move(140.0, 140.0);
217     host.mouse_button_ui(MouseButton::Left, true, 140.0, 140.0);
218     host.mouse_button_ui(MouseButton::Left, false, 140.0, 140.0);
219     settle(&mut host, 4500);
220     check(
221         "a page in alert() is not hung",
222         host.pending_dialog().is_some() && !host.active_unresponsive(),
223         format!("dialog={:?}", host.pending_dialog().map(|d| d.message)),
224     );
225     host.respond_dialog(true, None);
226     check("nor right after it is answered", !host.active_unresponsive(), String::new());
227     settle(&mut host, 1000);
228     check("and it is still the same page", host.title().as_deref() == Some("alert"), format!("{:?}", host.title()));
229     host.load(page.clone());
230     settle(&mut host, 1500);
231 
232     let victims = web_processes();
233     // SIGKILL rather than SIGSEGV: JavaScriptCore installs its own SEGV
234     // handler, and a sent one is not a fault it will die of.
235     for &pid in &victims {
236         unsafe { libc_kill(pid, 9) };
237     }
238     settle(&mut host, 1500);
239     let title = host.title().unwrap_or_default();
240     check(
241         "a crashed process shows the crash page",
242         title == "This page crashed",
243         format!("{title:?} after SIGKILL to {victims:?}"),
244     );
245     check("still under its own URL", host.url().as_ref() == Some(&page), String::new());
246 
247     host.reload();
248     settle(&mut host, 1500);
249     let title = host.title().unwrap_or_default();
250     check("and reloads from there", title == "recovered", format!("{title:?}"));
251 
252     println!("\ncrash: {}", if ok { "OK" } else { "BROKEN" });
253     std::process::exit(if ok { 0 } else { 1 });
254 }
255 
256 #[cfg(feature = "wpe")]
257 extern "C" {
258     #[link_name = "kill"]
259     fn libc_kill(pid: i32, sig: i32) -> i32;
260 }