Fix Chrome headless launch failures with --enable-unsafe-swiftshader (#915)

* Fix Chrome headless launch failures by adding --disable-gpu flag

Fixes silent Chrome crashes in headless mode when GPU drivers are unavailable or restricted (common in VMs, containers, and cloud environments).

## Changes Made

- **Auto-add `--disable-gpu` flag**: Automatically includes `--disable-gpu` when launching Chrome in headless mode to prevent GPU initialization crashes
- **Improved error reporting**: Enhanced error messages to include Chrome's exit code when it crashes before writing DevToolsActivePort
- **Better user guidance**: Added helpful hints in error messages suggesting `--no-sandbox` and `--disable-gpu` flags for troubleshooting
- **Updated tests**: Added test coverage for the new `--disable-gpu` flag behavior

## Implementation Details

The `--disable-gpu` flag is only added in headless mode (when `options.headless && !has_extensions`), preserving GPU acceleration for non-headless usage. The error handling now captures Chrome's exit code and provides actionable debugging information when Chrome fails silently.

Fixes #914

* Use --enable-unsafe-swiftshader instead of --disable-gpu for Playwright parity

--disable-gpu disables all GPU acceleration and breaks WebGL on Chrome 130+.
Playwright uses --enable-unsafe-swiftshader to enable CPU-based software
rendering via SwiftShader, which prevents GPU-driver crashes while preserving
WebGL support. This matches the behavior from v0.19 (Playwright-based daemon).

---------

Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
This commit is contained in:
Chris Tate
2026-03-18 16:30:09 -05:00
committed by GitHub
co-authored by ctate
parent 5b5dffe0a2
commit 486e1b341f
+30 -4
View File
@@ -133,6 +133,11 @@ fn build_chrome_args(options: &LaunchOptions) -> Result<ChromeArgs, String> {
// injected in headless mode). Skip --headless when extensions are loaded.
if options.headless && !has_extensions {
args.push("--headless=new".to_string());
// Enable SwiftShader software rendering in headless mode. This
// prevents silent crashes in environments where GPU drivers are
// missing or restricted (VMs, containers, some cloud machines)
// while preserving WebGL support. Playwright uses the same flag.
args.push("--enable-unsafe-swiftshader".to_string());
}
if let Some(ref proxy) = options.proxy {
@@ -303,9 +308,17 @@ fn wait_for_devtools_active_port(
let poll_interval = Duration::from_millis(50);
while std::time::Instant::now() <= deadline {
if let Ok(Some(_status)) = child.try_wait() {
// If Chrome already exited, stop waiting.
break;
if let Ok(Some(status)) = child.try_wait() {
// Chrome exited before writing DevToolsActivePort -- report the
// exit code so the caller can surface it alongside stderr output.
let code = status
.code()
.map(|c| format!("{}", c))
.unwrap_or_else(|| "unknown".to_string());
return Err(format!(
"Chrome exited early (exit code: {}) without writing DevToolsActivePort",
code
));
}
if let Some((port, ws_path)) = read_devtools_active_port(user_data_dir) {
@@ -364,7 +377,10 @@ fn chrome_launch_error(message: &str, stderr_lines: &[String]) -> String {
if relevant.is_empty() {
if stderr_lines.is_empty() {
return format!("{} (no stderr output from Chrome)", message);
return format!(
"{} (no stderr output from Chrome)\nHint: try passing --args \"--no-sandbox\" if Chrome crashes silently in your environment",
message
);
}
let last_lines: Vec<&String> = stderr_lines.iter().rev().take(5).collect();
return format!(
@@ -794,6 +810,8 @@ mod tests {
fn test_chrome_launch_error_no_stderr() {
let msg = chrome_launch_error("Chrome exited", &[]);
assert!(msg.contains("no stderr output"));
assert!(msg.contains("Hint:"));
assert!(msg.contains("--no-sandbox"));
}
#[test]
@@ -831,6 +849,10 @@ mod tests {
};
let result = build_chrome_args(&opts).unwrap();
assert!(result.args.iter().any(|a| a == "--headless=new"));
assert!(result
.args
.iter()
.any(|a| a == "--enable-unsafe-swiftshader"));
assert!(result.args.iter().any(|a| a == "--window-size=1280,720"));
// Temp dir created when no profile
assert!(result.temp_user_data_dir.is_some());
@@ -847,6 +869,10 @@ mod tests {
};
let result = build_chrome_args(&opts).unwrap();
assert!(!result.args.iter().any(|a| a.contains("--headless")));
assert!(!result
.args
.iter()
.any(|a| a == "--enable-unsafe-swiftshader"));
assert!(!result.args.iter().any(|a| a.starts_with("--window-size=")));
// Temp dir created when no profile
assert!(result.temp_user_data_dir.is_some());