Skip to content
8 changes: 5 additions & 3 deletions eng/pipelines/libraries/helix-queues-setup.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,11 @@ jobs:
- $(helix_macos_x64)

# Android
# Always use the Ubuntu-based Android queue for internal validation as there is no internal equivalent of
# the Windows.11.Amd64.Android.Open queue.
- ${{ if or(eq(variables['System.TeamProject'], 'internal'), in(parameters.platform, 'android_x86', 'android_x64', 'linux_bionic_x64')) }}:
# Use the Ubuntu-based Android queue for x86/x64/bionic_x64 on all projects,
# and also for arm/arm64/bionic_arm/bionic_arm64 on non-public projects (no internal Windows Android queue).
- ${{ if in(parameters.platform, 'android_x86', 'android_x64', 'linux_bionic_x64') }}:
- Ubuntu.2204.Amd64.Android.29.Open
- ${{ if and(ne(variables['System.TeamProject'], 'public'), in(parameters.platform, 'android_arm', 'android_arm64', 'linux_bionic_arm', 'linux_bionic_arm64')) }}:
- Ubuntu.2204.Amd64.Android.29.Open
Comment thread
lewing marked this conversation as resolved.
- ${{ if and(eq(variables['System.TeamProject'], 'public'), in(parameters.platform, 'android_arm', 'android_arm64', 'linux_bionic_arm', 'linux_bionic_arm64')) }}:
- Windows.11.Amd64.Android.Open
Expand Down
112 changes: 108 additions & 4 deletions src/mono/wasm/Wasm.Build.Tests/BrowserRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
#nullable enable

using System;
using System.Diagnostics;
using System.Linq;
using System.IO;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Playwright;
Expand Down Expand Up @@ -124,6 +126,9 @@ public async Task<IBrowser> SpawnBrowserAsync(
chromeArgs = chromeArgs.Append("--headless").ToArray();
_testOutput.WriteLine($"Launching chrome ('{s_chromePath.Value}') via playwright with args = {string.Join(',', chromeArgs)}");

CheckBrowserDependencies(s_chromePath.Value);

Exception? lastException = null;
Comment thread
lewing marked this conversation as resolved.
int attempt = 0;
while (attempt < maxRetries)
Comment thread
lewing marked this conversation as resolved.
{
Expand All @@ -143,15 +148,82 @@ public async Task<IBrowser> SpawnBrowserAsync(
}
catch (System.TimeoutException ex)
{
lastException = ex;
attempt++;
_testOutput.WriteLine($"Attempt {attempt} failed with TimeoutException: {ex.Message}");
}
catch (PlaywrightException ex)
{
lastException = ex;
attempt++;
_testOutput.WriteLine($"Attempt {attempt} failed with PlaywrightException: {ex.Message}");
}
}
if (attempt == maxRetries)
throw new Exception($"Failed to launch browser after {maxRetries} attempts");
throw new Exception($"Failed to launch browser after {maxRetries} attempts", lastException);
Comment thread
lewing marked this conversation as resolved.
Outdated
return Browser!;
}

private void CheckBrowserDependencies(string chromePath)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return;

string output;
try
{
var psi = new ProcessStartInfo("ldd", chromePath)
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
};
Comment thread
lewing marked this conversation as resolved.
Outdated
using var process = Process.Start(psi);
if (process == null)
return;

// Read stdout/stderr asynchronously to avoid deadlocks
var stdoutTask = process.StandardOutput.ReadToEndAsync();
var stderrTask = process.StandardError.ReadToEndAsync();
Comment thread
lewing marked this conversation as resolved.
Outdated

if (!process.WaitForExit(10_000))
{
try { process.Kill(); } catch { }
Comment thread
lewing marked this conversation as resolved.
Outdated
_testOutput.WriteLine("Could not check browser dependencies: ldd timed out");
return;
}

output = stdoutTask.GetAwaiter().GetResult();
Comment thread
lewing marked this conversation as resolved.
Outdated
string stderr = stderrTask.GetAwaiter().GetResult();

if (process.ExitCode != 0)
{
_testOutput.WriteLine($"ldd exited with code {process.ExitCode}. stderr: {stderr}");
}
}
catch (Exception ex)
{
_testOutput.WriteLine($"Could not check browser dependencies: {ex.Message}");
return;
}

var missingLibs = output
.Split('\n')
.Where(line => line.Contains("not found"))
.Select(line => line.Trim())
.ToList();

if (missingLibs.Count > 0)
{
string message = $"Chrome binary at '{chromePath}' is missing {missingLibs.Count} shared library dependencies:\n"
+ string.Join("\n", missingLibs)
+ "\nThis will cause TargetClosedException when Playwright tries to launch Chrome."
+ "\nEnsure the Helix queue/container has Chrome's system dependencies installed (libgbm1, libnss3, libatk1.0-0, etc.).";
_testOutput.WriteLine($"WARNING: {message}");
throw new Exception(message);
}
}

// FIXME: options
public async Task<IPage> RunAsync(
ToolCommand cmd,
Expand All @@ -164,9 +236,41 @@ public async Task<IPage> RunAsync(
Func<string, string>? modifyBrowserUrl = null)
{
var urlString = await StartServerAndGetUrlAsync(cmd, args, onServerMessage);
var browser = await SpawnBrowserAsync(urlString, headless, locale: locale);
var context = await browser.NewContextAsync(new BrowserNewContextOptions { Locale = locale });
return await RunAsync(context, urlString, headless, onConsoleMessage, onError, modifyBrowserUrl);

// Retry the full browser session (launch + navigate) to handle
// intermittent Chrome crashes in Docker containers under memory pressure.
// Chrome can silently die (OOM killed) during navigation when concurrent
// test classes run wasm-opt builds alongside browser tests.
const int maxSessionRetries = 3;
for (int attempt = 0; ; attempt++)
{
try
{
var browser = await SpawnBrowserAsync(urlString, headless, locale: locale);
Comment thread
lewing marked this conversation as resolved.
Outdated
var context = await browser.NewContextAsync(new BrowserNewContextOptions { Locale = locale });
return await RunAsync(context, urlString, headless, onConsoleMessage, onError, modifyBrowserUrl);
}
catch (Exception ex) when (attempt + 1 < maxSessionRetries &&
ex is PlaywrightException)
{
_testOutput.WriteLine($"Browser session attempt {attempt + 1} failed with {ex.GetType().Name}: {ex.Message}");
_testOutput.WriteLine("Retrying with a fresh browser instance...");
try
{
if (Browser is not null)
{
await Browser.DisposeAsync();
Browser = null;
}
Playwright?.Dispose();
Playwright = null;
}
catch (Exception disposeEx)
{
_testOutput.WriteLine($"Browser cleanup failed: {disposeEx.Message}");
}
}
}
}

public async Task<IPage> RunAsync(
Expand Down
Loading