Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-11-26 01:10:39 +00:00

688 lines
24 KiB
Dart

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert' show JsonEncoder, LineSplitter, json, utf8;
import 'dart:io' as io;
import 'dart:math' as math;
import 'package:path/path.dart' as path;
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
/// The number of samples used to extract metrics, such as noise, means,
/// max/min values.
///
/// Keep this constant in sync with the same constant defined in `dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart`.
const int _kMeasuredSampleCount = 10;
/// Options passed to Chrome when launching it.
class ChromeOptions {
ChromeOptions({
this.userDataDirectory,
this.url,
this.windowWidth = 1024,
this.windowHeight = 1024,
this.headless,
this.debugPort,
this.enableWasmGC = false,
this.silent = false,
});
/// If not null passed as `--user-data-dir`.
final String? userDataDirectory;
/// If not null launches a Chrome tab at this URL.
final String? url;
/// The width of the Chrome window.
///
/// This is important for screenshots and benchmarks.
final int windowWidth;
/// The height of the Chrome window.
///
/// This is important for screenshots and benchmarks.
final int windowHeight;
/// Launches code in "headless" mode, which allows running Chrome in
/// environments without a display, such as LUCI.
final bool? headless;
/// The port Chrome will use for its debugging protocol.
///
/// If null, Chrome is launched without debugging. When running in headless
/// mode without a debug port, Chrome quits immediately. For most tests it is
/// typical to set [headless] to true and set a non-null debug port.
final int? debugPort;
/// Whether to enable experimental WasmGC flags
final bool enableWasmGC;
/// Disables Chrome stdio outputs.
final bool silent;
}
/// A function called when the Chrome process encounters an error.
typedef ChromeErrorCallback = void Function(String);
/// Manages a single Chrome process.
class Chrome {
Chrome._(this._chromeProcess, this._onError, this._debugConnection) {
// If the Chrome process quits before it was asked to quit, notify the
// error listener.
_chromeProcess.exitCode.then((int exitCode) {
if (!_isStopped) {
_onError('Chrome process exited prematurely with exit code $exitCode');
}
});
}
/// Launches Chrome with the given [options].
///
/// The [onError] callback is called with an error message when the Chrome
/// process encounters an error. In particular, [onError] is called when the
/// Chrome process exits prematurely, i.e. before [stop] is called.
static Future<Chrome> launch(
ChromeOptions options, {
String? workingDirectory,
required ChromeErrorCallback onError,
}) async {
if (!io.Platform.isWindows) {
final io.ProcessResult versionResult = io.Process.runSync(
_findSystemChromeExecutable(),
const <String>['--version'],
);
print('Launching ${versionResult.stdout}');
} else {
print('Launching Chrome...');
}
final String jsFlags = options.enableWasmGC
? <String>['--experimental-wasm-gc', '--experimental-wasm-type-reflection'].join(' ')
: '';
final withDebugging = options.debugPort != null;
final args = <String>[
if (options.userDataDirectory != null) '--user-data-dir=${options.userDataDirectory}',
if (options.url != null) options.url!,
if (io.Platform.environment['CHROME_NO_SANDBOX'] == 'true') '--no-sandbox',
if (options.headless ?? false) '--headless',
if (withDebugging) '--remote-debugging-port=${options.debugPort}',
'--window-size=${options.windowWidth},${options.windowHeight}',
'--disable-extensions',
'--disable-popup-blocking',
// Indicates that the browser is in "browse without sign-in" (Guest session) mode.
'--bwsi',
'--no-first-run',
'--no-default-browser-check',
'--disable-default-apps',
'--disable-translate',
if (jsFlags.isNotEmpty) '--js-flags=$jsFlags',
];
final io.Process chromeProcess = await _spawnChromiumProcess(
_findSystemChromeExecutable(),
args,
silent: options.silent,
workingDirectory: workingDirectory,
);
WipConnection? debugConnection;
if (withDebugging) {
debugConnection = await _connectToChromeDebugPort(options.debugPort!, options.url);
}
return Chrome._(chromeProcess, onError, debugConnection);
}
/// Connects to an existing Chrome process with the given [options].
///
/// The [onError] callback is called with an error message when the Chrome
/// process encounters an error. In particular, [onError] is called when the
/// Chrome process exits prematurely, i.e. before [stop] is called.
static Future<Chrome> connect(
io.Process chromeProcess,
ChromeOptions options, {
String? workingDirectory,
required ChromeErrorCallback onError,
}) async {
final withDebugging = options.debugPort != null;
WipConnection? debugConnection;
if (withDebugging) {
debugConnection = await _connectToChromeDebugPort(options.debugPort!, options.url);
}
return Chrome._(chromeProcess, onError, debugConnection);
}
final io.Process _chromeProcess;
final ChromeErrorCallback _onError;
final WipConnection? _debugConnection;
bool _isStopped = false;
Completer<void>? _tracingCompleter;
StreamSubscription<WipEvent>? _tracingSubscription;
List<Map<String, dynamic>>? _tracingData;
/// Starts recording a performance trace.
///
/// If there is already a tracing session in progress, throws an error. Call
/// [endRecordingPerformance] before starting a new tracing session.
///
/// The [label] is for debugging convenience.
Future<void> beginRecordingPerformance(String label) async {
if (_tracingCompleter != null) {
throw StateError(
'Cannot start a new performance trace. A tracing session labeled '
'"$label" is already in progress.',
);
}
_tracingCompleter = Completer<void>();
_tracingData = <Map<String, dynamic>>[];
// Subscribe to tracing events prior to calling "Tracing.start". Otherwise,
// we'll miss tracing data.
_tracingSubscription = _debugConnection?.onNotification.listen((WipEvent event) {
// We receive data as a sequence of "Tracing.dataCollected" followed by
// "Tracing.tracingComplete" at the end. Until "Tracing.tracingComplete"
// is received, the data may be incomplete.
if (event.method == 'Tracing.tracingComplete') {
_tracingCompleter!.complete();
_tracingSubscription!.cancel();
_tracingSubscription = null;
} else if (event.method == 'Tracing.dataCollected') {
final dynamic value = event.params?['value'];
if (value is! List) {
throw FormatException(
'"Tracing.dataCollected" returned malformed data. '
'Expected a List but got: ${value.runtimeType}',
);
}
_tracingData?.addAll(
(event.params?['value'] as List<dynamic>).cast<Map<String, dynamic>>(),
);
}
});
await _debugConnection?.sendCommand('Tracing.start', <String, dynamic>{
// The choice of categories is as follows:
//
// blink:
// provides everything on the UI thread, including scripting,
// style recalculations, layout, painting, and some compositor
// work.
// blink.user_timing:
// provides marks recorded using window.performance. We use marks
// to find frames that the benchmark cares to measure.
// gpu:
// provides tracing data from the GPU data
// disabled due to https://bugs.chromium.org/p/chromium/issues/detail?id=1068259
// TODO(yjbanov): extract useful GPU data
'categories': 'blink,blink.user_timing',
'transferMode': 'SendAsStream',
});
}
/// Stops a performance tracing session started by [beginRecordingPerformance].
///
/// Returns all the collected tracing data unfiltered.
Future<List<Map<String, dynamic>>?> endRecordingPerformance() async {
await _debugConnection!.sendCommand('Tracing.end');
await _tracingCompleter!.future;
final List<Map<String, dynamic>>? data = _tracingData;
_tracingCompleter = null;
_tracingData = null;
return data;
}
Future<void> reloadPage({bool ignoreCache = false}) async {
await _debugConnection?.page.reload(ignoreCache: ignoreCache);
}
/// Stops the Chrome process.
void stop() {
_isStopped = true;
_tracingSubscription?.cancel();
_chromeProcess.kill();
}
}
String _findSystemChromeExecutable() {
// On some environments, such as the Dart HHH tester, Chrome resides in a
// non-standard location and is provided via the following environment
// variable.
final String? envExecutable = io.Platform.environment['CHROME_EXECUTABLE'];
if (envExecutable != null) {
return envExecutable;
}
if (io.Platform.isLinux) {
final io.ProcessResult which = io.Process.runSync('which', <String>['google-chrome']);
if (which.exitCode != 0) {
throw Exception('Failed to locate system Chrome installation.');
}
return (which.stdout as String).trim();
} else if (io.Platform.isMacOS) {
return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
} else if (io.Platform.isWindows) {
const kWindowsExecutable = r'Google\Chrome\Application\chrome.exe';
final List<String> kWindowsPrefixes = <String?>[
io.Platform.environment['LOCALAPPDATA'],
io.Platform.environment['PROGRAMFILES'],
io.Platform.environment['PROGRAMFILES(X86)'],
].whereType<String>().toList();
final String windowsPrefix = kWindowsPrefixes.firstWhere((String prefix) {
final String expectedPath = path.join(prefix, kWindowsExecutable);
return io.File(expectedPath).existsSync();
}, orElse: () => '.');
return path.join(windowsPrefix, kWindowsExecutable);
} else {
throw Exception('Web benchmarks cannot run on ${io.Platform.operatingSystem}.');
}
}
/// Waits for Chrome to print DevTools URI and connects to it.
Future<WipConnection> _connectToChromeDebugPort(int port, String? tabUrl) async {
final Uri devtoolsUri = await _getRemoteDebuggerUrl(Uri.parse('http://localhost:$port'));
print('Connecting to DevTools: $devtoolsUri');
final chromeConnection = ChromeConnection('localhost', port);
final Iterable<ChromeTab> tabs = (await chromeConnection.getTabs()).where((ChromeTab tab) {
return tab.url.startsWith(tabUrl ?? 'http://localhost');
});
final ChromeTab tab = tabs.single;
final WipConnection debugConnection = await tab.connect();
print('Connected to Chrome tab: ${tab.title} (${tab.url})');
return debugConnection;
}
/// Gets the Chrome debugger URL for the web page being benchmarked.
Future<Uri> _getRemoteDebuggerUrl(Uri base) async {
final client = io.HttpClient();
final io.HttpClientRequest request = await client.getUrl(base.resolve('/json/list'));
final io.HttpClientResponse response = await request.close();
final jsonObject = await json.fuse(utf8).decoder.bind(response).single as List<dynamic>?;
if (jsonObject == null || jsonObject.isEmpty) {
return base;
}
return base.resolve((jsonObject.first as Map<String, dynamic>)['webSocketDebuggerUrl'] as String);
}
/// Summarizes a Blink trace down to a few interesting values.
class BlinkTraceSummary {
BlinkTraceSummary._({
required this.averageBeginFrameTime,
required this.averageUpdateLifecyclePhasesTime,
}) : averageTotalUIFrameTime = averageBeginFrameTime + averageUpdateLifecyclePhasesTime;
static BlinkTraceSummary? fromJson(List<Map<String, dynamic>> traceJson) {
try {
// Convert raw JSON data to BlinkTraceEvent objects sorted by timestamp.
List<BlinkTraceEvent> events =
traceJson.map<BlinkTraceEvent>(BlinkTraceEvent.fromJson).toList()
..sort((BlinkTraceEvent a, BlinkTraceEvent b) => a.ts! - b.ts!);
Exception noMeasuredFramesFound() => Exception(
'No measured frames found in benchmark tracing data. This likely '
'indicates a bug in the benchmark. For example, the benchmark failed '
"to pump enough frames. It may also indicate a change in Chrome's "
'tracing data format. Check if Chrome version changed recently and '
'adjust the parsing code accordingly.',
);
// Use the pid from the first "measured_frame" event since the event is
// emitted by the script running on the process we're interested in.
//
// We previously tried using the "CrRendererMain" event. However, for
// reasons unknown, Chrome in the devicelab refuses to emit this event
// sometimes, causing to flakes.
final BlinkTraceEvent firstMeasuredFrameEvent = events.firstWhere(
(BlinkTraceEvent event) => event.isBeginMeasuredFrame,
orElse: () => throw noMeasuredFramesFound(),
);
final int tabPid = firstMeasuredFrameEvent.pid!;
// Filter out data from unrelated processes
events = events.where((BlinkTraceEvent element) => element.pid == tabPid).toList();
// Extract frame data.
final frames = <BlinkFrame>[];
var skipCount = 0;
var frame = BlinkFrame();
for (final event in events) {
if (event.isBeginFrame) {
frame.beginFrame = event;
} else if (event.isUpdateAllLifecyclePhases) {
frame.updateAllLifecyclePhases = event;
if (frame.endMeasuredFrame != null) {
frames.add(frame);
} else {
skipCount += 1;
}
frame = BlinkFrame();
} else if (event.isBeginMeasuredFrame) {
frame.beginMeasuredFrame = event;
} else if (event.isEndMeasuredFrame) {
frame.endMeasuredFrame = event;
}
}
print('Extracted ${frames.length} measured frames.');
print('Skipped $skipCount non-measured frames.');
if (frames.isEmpty) {
throw noMeasuredFramesFound();
}
// Compute averages and summarize.
return BlinkTraceSummary._(
averageBeginFrameTime: _computeAverageDuration(
frames.map((BlinkFrame frame) => frame.beginFrame).whereType<BlinkTraceEvent>().toList(),
),
averageUpdateLifecyclePhasesTime: _computeAverageDuration(
frames
.map((BlinkFrame frame) => frame.updateAllLifecyclePhases)
.whereType<BlinkTraceEvent>()
.toList(),
),
);
} catch (_) {
final traceFile = io.File('./chrome-trace.json');
io.stderr.writeln(
'Failed to interpret the Chrome trace contents. The trace was saved in ${traceFile.path}',
);
traceFile.writeAsStringSync(const JsonEncoder.withIndent(' ').convert(traceJson));
rethrow;
}
}
/// The average duration of "WebViewImpl::beginFrame" events.
///
/// This event contains all of scripting time of an animation frame, plus an
/// unknown small amount of work browser does before and after scripting.
final Duration averageBeginFrameTime;
/// The average duration of "WebViewImpl::updateAllLifecyclePhases" events.
///
/// This event contains style, layout, painting, and compositor computations,
/// which are not included in the scripting time. This event does not
/// include GPU time, which happens on a separate thread.
final Duration averageUpdateLifecyclePhasesTime;
/// The average sum of [averageBeginFrameTime] and
/// [averageUpdateLifecyclePhasesTime].
///
/// This value contains the vast majority of work the UI thread performs in
/// any given animation frame.
final Duration averageTotalUIFrameTime;
@override
String toString() =>
'$BlinkTraceSummary('
'averageBeginFrameTime: ${averageBeginFrameTime.inMicroseconds / 1000}ms, '
'averageUpdateLifecyclePhasesTime: ${averageUpdateLifecyclePhasesTime.inMicroseconds / 1000}ms)';
}
/// Contains events pertaining to a single frame in the Blink trace data.
class BlinkFrame {
/// Corresponds to 'WebViewImpl::beginFrame' event.
BlinkTraceEvent? beginFrame;
/// Corresponds to 'WebViewImpl::updateAllLifecyclePhases' event.
BlinkTraceEvent? updateAllLifecyclePhases;
/// Corresponds to 'measured_frame' begin event.
BlinkTraceEvent? beginMeasuredFrame;
/// Corresponds to 'measured_frame' end event.
BlinkTraceEvent? endMeasuredFrame;
}
/// Takes a list of events that have non-null [BlinkTraceEvent.tdur] computes
/// their average as a [Duration] value.
Duration _computeAverageDuration(List<BlinkTraceEvent> events) {
// Compute the sum of "tdur" fields of the last _kMeasuredSampleCount events.
final double sum = events.skip(math.max(events.length - _kMeasuredSampleCount, 0)).fold(0.0, (
double previousValue,
BlinkTraceEvent event,
) {
if (event.tdur == null) {
throw FormatException('Trace event lacks "tdur" field: $event');
}
return previousValue + event.tdur!;
});
final int sampleCount = math.min(events.length, _kMeasuredSampleCount);
return Duration(microseconds: sum ~/ sampleCount);
}
/// An event collected by the Blink tracer (in Chrome accessible using chrome://tracing).
///
/// See also:
/// * https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview
class BlinkTraceEvent {
/// Parses an event from its JSON representation.
///
/// Sample event encoded as JSON (the data is bogus, this just shows the format):
///
/// ```json
/// {
/// "name": "myName",
/// "cat": "category,list",
/// "ph": "B",
/// "ts": 12345,
/// "pid": 123,
/// "tid": 456,
/// "args": {
/// "someArg": 1,
/// "anotherArg": {
/// "value": "my value"
/// }
/// }
/// }
/// ```
///
/// For detailed documentation of the format see:
///
/// https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview
BlinkTraceEvent.fromJson(Map<String, dynamic> json)
: args = json['args'] as Map<String, dynamic>,
cat = json['cat'] as String,
name = json['name'] as String,
ph = json['ph'] as String,
pid = _readInt(json, 'pid'),
tid = _readInt(json, 'tid'),
ts = _readInt(json, 'ts'),
tts = _readInt(json, 'tts'),
tdur = _readInt(json, 'tdur');
/// Event-specific data.
final Map<String, dynamic> args;
/// Event category.
final String cat;
/// Event name.
final String name;
/// Event "phase".
final String ph;
/// Process ID of the process that emitted the event.
final int? pid;
/// Thread ID of the thread that emitted the event.
final int? tid;
/// Timestamp in microseconds using tracer clock.
final int? ts;
/// Timestamp in microseconds using thread clock.
final int? tts;
/// Event duration in microseconds.
final int? tdur;
/// A "begin frame" event contains all of the scripting time of an animation
/// frame (JavaScript, WebAssembly), plus a negligible amount of internal
/// browser overhead.
///
/// This event does not include non-UI thread scripting, such as web workers,
/// service workers, and CSS Paint paintlets.
///
/// WebViewImpl::beginFrame was used in earlier versions of Chrome, kept
/// for compatibility.
///
/// This event is a duration event that has its `tdur` populated.
bool get isBeginFrame {
return ph == 'X' &&
(name == 'WebViewImpl::beginFrame' ||
name == 'WebFrameWidgetBase::BeginMainFrame' ||
name == 'WebFrameWidgetImpl::BeginMainFrame');
}
/// An "update all lifecycle phases" event contains UI thread computations
/// related to an animation frame that's outside the scripting phase.
///
/// This event includes style recalculation, layer tree update, layout,
/// painting, and parts of compositing work.
///
/// WebViewImpl::updateAllLifecyclePhases was used in earlier versions of
/// Chrome, kept for compatibility.
///
/// This event is a duration event that has its `tdur` populated.
bool get isUpdateAllLifecyclePhases {
return ph == 'X' &&
(name == 'WebViewImpl::updateAllLifecyclePhases' ||
name == 'WebFrameWidgetImpl::UpdateLifecycle');
}
/// Whether this is the beginning of a "measured_frame" event.
///
/// This event is a custom event emitted by our benchmark test harness.
///
/// See also:
/// * `recorder.dart`, which emits this event.
bool get isBeginMeasuredFrame => ph == 'b' && name == 'measured_frame';
/// Whether this is the end of a "measured_frame" event.
///
/// This event is a custom event emitted by our benchmark test harness.
///
/// See also:
/// * `recorder.dart`, which emits this event.
bool get isEndMeasuredFrame => ph == 'e' && name == 'measured_frame';
@override
String toString() =>
'$BlinkTraceEvent('
'args: ${json.encode(args)}, '
'cat: $cat, '
'name: $name, '
'ph: $ph, '
'pid: $pid, '
'tid: $tid, '
'ts: $ts, '
'tts: $tts, '
'tdur: $tdur)';
}
/// Read an integer out of [json] stored under [key].
///
/// Since JSON does not distinguish between `int` and `double`, extra
/// validation and conversion is needed.
///
/// Returns null if the value is null.
int? _readInt(Map<String, dynamic> json, String key) {
final jsonValue = json[key] as num?;
return jsonValue?.toInt();
}
/// Used by [Chrome.launch] to detect a glibc bug and retry launching the
/// browser.
///
/// Once every few thousands of launches we hit this glibc bug:
///
/// https://sourceware.org/bugzilla/show_bug.cgi?id=19329.
///
/// When this happens Chrome spits out something like the following then exits with code 127:
///
/// Inconsistency detected by ld.so: ../elf/dl-tls.c: 493: _dl_allocate_tls_init: Assertion `listp->slotinfo[cnt].gen <= GL(dl_tls_generation)' failed!
const String _kGlibcError = 'Inconsistency detected by ld.so';
Future<io.Process> _spawnChromiumProcess(
String executable,
List<String> args, {
required bool silent,
String? workingDirectory,
}) async {
// Keep attempting to launch the browser until one of:
// - Chrome launched successfully, in which case we just return from the loop.
// - The tool detected an unretryable Chrome error, in which case we throw ToolExit.
while (true) {
final io.Process process = await io.Process.start(
executable,
args,
workingDirectory: workingDirectory,
);
process.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen((String line) {
if (!silent) {
print('[CHROME STDOUT]: $line');
}
});
// Wait until the DevTools are listening before trying to connect. This is
// only required for flutter_test --platform=chrome and not flutter run.
var hitGlibcBug = false;
await process.stderr
.transform(utf8.decoder)
.transform(const LineSplitter())
.map((String line) {
if (!silent) {
print('[CHROME STDERR]:$line');
}
if (line.contains(_kGlibcError)) {
hitGlibcBug = true;
}
return line;
})
.firstWhere(
(String line) => line.startsWith('DevTools listening'),
orElse: () {
if (hitGlibcBug) {
print(
'Encountered glibc bug https://sourceware.org/bugzilla/show_bug.cgi?id=19329. '
'Will try launching browser again.',
);
return '';
}
print('Failed to launch browser. Command used to launch it: ${args.join(' ')}');
throw Exception(
'Failed to launch browser. Make sure you are using an up-to-date '
'Chrome or Edge. Otherwise, consider using -d web-server instead '
'and filing an issue at https://github.com/flutter/flutter/issues.',
);
},
);
if (!hitGlibcBug) {
return process;
}
// A precaution that avoids accumulating browser processes, in case the
// glibc bug doesn't cause the browser to quit and we keep looping and
// launching more processes.
unawaited(
process.exitCode.timeout(
const Duration(seconds: 1),
onTimeout: () {
process.kill();
return 0;
},
),
);
}
}