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 }