Skip to content

Commit 0b78110

Browse files
authored
Work around the glibc bug that causes rare Chrome crashes (flutter#67466)
Work around the glibc bug that causes rare Chrome crashes
1 parent 396b64c commit 0b78110

File tree

3 files changed

+154
-30
lines changed

3 files changed

+154
-30
lines changed

dev/try_builders.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@
165165
"repo":"flutter",
166166
"task_name":"linux_web_tests",
167167
"enabled":true,
168-
"run_if":["dev/", "packages/flutter/", "packages/flutter_goldens_client/", "packages/flutter_goldens/", "packages/flutter_test/", "packages/flutter_tools/lib/src/test/", "packages/flutter_web_plugins/", "bin/"]
168+
"run_if":["dev/", "packages/", "bin/"]
169169
},
170170
{
171171
"name":"Linux web_integration_tests",

packages/flutter_tools/lib/src/web/chrome.dart

Lines changed: 76 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ const String kWindowsExecutable = r'Google\Chrome\Application\chrome.exe';
3535
/// The expected Edge executable name on Windows.
3636
const String kWindowsEdgeExecutable = r'Microsoft\Edge\Application\msedge.exe';
3737

38+
/// Used by [ChromiumLauncher] to detect a glibc bug and retry launching the
39+
/// browser.
40+
///
41+
/// Once every few thousands of launches we hit this glibc bug:
42+
///
43+
/// https://sourceware.org/bugzilla/show_bug.cgi?id=19329.
44+
///
45+
/// When this happens Chrome spits out something like the following then exits with code 127:
46+
///
47+
/// Inconsistency detected by ld.so: ../elf/dl-tls.c: 493: _dl_allocate_tls_init: Assertion `listp->slotinfo[cnt].gen <= GL(dl_tls_generation)' failed!
48+
const String _kGlibcError = 'Inconsistency detected by ld.so';
49+
3850
typedef BrowserFinder = String Function(Platform, FileSystem);
3951

4052
/// Find the chrome executable on the current platform.
@@ -168,6 +180,12 @@ class ChromiumLauncher {
168180
}
169181

170182
final String chromeExecutable = _browserFinder(_platform, _fileSystem);
183+
184+
if (_logger.isVerbose) {
185+
final ProcessResult versionResult = await _processManager.run(<String>[chromeExecutable, '--version']);
186+
_logger.printTrace('Using ${versionResult.stdout}');
187+
}
188+
171189
final Directory userDataDir = _fileSystem.systemTempDirectory
172190
.createTempSync('flutter_tools_chrome_device.');
173191

@@ -204,27 +222,7 @@ class ChromiumLauncher {
204222
url,
205223
];
206224

207-
final Process process = await _processManager.start(args);
208-
209-
process.stdout
210-
.transform(utf8.decoder)
211-
.transform(const LineSplitter())
212-
.listen((String line) {
213-
_logger.printTrace('[CHROME]: $line');
214-
});
215-
216-
// Wait until the DevTools are listening before trying to connect. This is
217-
// only required for flutter_test --platform=chrome and not flutter run.
218-
await process.stderr
219-
.transform(utf8.decoder)
220-
.transform(const LineSplitter())
221-
.map((String line) {
222-
_logger.printTrace('[CHROME]:$line');
223-
return line;
224-
})
225-
.firstWhere((String line) => line.startsWith('DevTools listening'), orElse: () {
226-
return 'Failed to spawn stderr';
227-
});
225+
final Process process = await _spawnChromiumProcess(args);
228226

229227
// When the process exits, copy the user settings back to the provided data-dir.
230228
if (cacheDir != null) {
@@ -241,6 +239,63 @@ class ChromiumLauncher {
241239
), skipCheck);
242240
}
243241

242+
Future<Process> _spawnChromiumProcess(List<String> args) async {
243+
// Keep attempting to launch the browser until one of:
244+
// - Chrome launched successfully, in which case we just return from the loop.
245+
// - The tool detected an unretriable Chrome error, in which case we throw ToolExit.
246+
while (true) {
247+
final Process process = await _processManager.start(args);
248+
249+
process.stdout
250+
.transform(utf8.decoder)
251+
.transform(const LineSplitter())
252+
.listen((String line) {
253+
_logger.printTrace('[CHROME]: $line');
254+
});
255+
256+
// Wait until the DevTools are listening before trying to connect. This is
257+
// only required for flutter_test --platform=chrome and not flutter run.
258+
bool hitGlibcBug = false;
259+
await process.stderr
260+
.transform(utf8.decoder)
261+
.transform(const LineSplitter())
262+
.map((String line) {
263+
_logger.printTrace('[CHROME]:$line');
264+
if (line.contains(_kGlibcError)) {
265+
hitGlibcBug = true;
266+
}
267+
return line;
268+
})
269+
.firstWhere((String line) => line.startsWith('DevTools listening'), orElse: () {
270+
if (hitGlibcBug) {
271+
_logger.printTrace(
272+
'Encountered glibc bug https://sourceware.org/bugzilla/show_bug.cgi?id=19329. '
273+
'Will try launching browser again.',
274+
);
275+
return null;
276+
}
277+
_logger.printTrace('Failed to launch browser. Command used to launch it: ${args.join(' ')}');
278+
throw ToolExit(
279+
'Failed to launch browser. Make sure you are using an up-to-date '
280+
'Chrome or Edge. Otherwise, consider using -d web-server instead '
281+
'and filing an issue at https://github.com/flutter/flutter/issues.',
282+
);
283+
});
284+
285+
if (!hitGlibcBug) {
286+
return process;
287+
}
288+
289+
// A precaution that avoids accumulating browser processes, in case the
290+
// glibc bug doesn't cause the browser to quit and we keep looping and
291+
// launching more processes.
292+
unawaited(process.exitCode.timeout(const Duration(seconds: 1), onTimeout: () {
293+
process.kill();
294+
return null;
295+
}));
296+
}
297+
}
298+
244299
// This is a JSON file which contains configuration from the browser session,
245300
// such as window position. It is located under the Chrome data-dir folder.
246301
String get _preferencesPath => _fileSystem.path.join('Default', 'preferences');

packages/flutter_tools/test/general.shard/web/chrome_test.dart

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ void main() {
8181
processManager,
8282
chromeLauncher,
8383
),
84-
throwsToolExit(),
84+
throwsToolExit(message: 'Only one instance of chrome can be started'),
8585
);
8686
});
8787

@@ -209,13 +209,17 @@ void main() {
209209
localStorageContentsDirectory.childFile('LOCK').writeAsBytesSync(<int>[]);
210210
localStorageContentsDirectory.childFile('LOG').writeAsStringSync('contents');
211211

212-
processManager.addCommand(FakeCommand(command: const <String>[
213-
'example_chrome',
214-
'--user-data-dir=/.tmp_rand0/flutter_tools_chrome_device.rand0',
215-
'--remote-debugging-port=1234',
216-
...kChromeArgs,
217-
'example_url',
218-
], completer: exitCompleter));
212+
processManager.addCommand(FakeCommand(
213+
command: const <String>[
214+
'example_chrome',
215+
'--user-data-dir=/.tmp_rand0/flutter_tools_chrome_device.rand0',
216+
'--remote-debugging-port=1234',
217+
...kChromeArgs,
218+
'example_url',
219+
],
220+
completer: exitCompleter,
221+
stderr: kDevtoolsStderr,
222+
));
219223

220224
await chromeLauncher.launch(
221225
'example_url',
@@ -244,6 +248,71 @@ void main() {
244248
expect(storageDir.childFile('LOG'), exists);
245249
expect(storageDir.childFile('LOG').readAsStringSync(), 'contents');
246250
});
251+
252+
testWithoutContext('can retry launch when glibc bug happens', () async {
253+
const List<String> args = <String>[
254+
'example_chrome',
255+
'--user-data-dir=/.tmp_rand0/flutter_tools_chrome_device.rand0',
256+
'--remote-debugging-port=1234',
257+
...kChromeArgs,
258+
'--headless',
259+
'--disable-gpu',
260+
'--no-sandbox',
261+
'--window-size=2400,1800',
262+
'example_url',
263+
];
264+
265+
// Pretend to hit glibc bug 3 times.
266+
for (int i = 0; i < 3; i++) {
267+
processManager.addCommand(const FakeCommand(
268+
command: args,
269+
stderr: 'Inconsistency detected by ld.so: ../elf/dl-tls.c: 493: '
270+
'_dl_allocate_tls_init: Assertion `listp->slotinfo[cnt].gen '
271+
'<= GL(dl_tls_generation)\' failed!',
272+
));
273+
}
274+
275+
// Succeed on the 4th try.
276+
processManager.addCommand(const FakeCommand(
277+
command: args,
278+
stderr: kDevtoolsStderr,
279+
));
280+
281+
expect(
282+
() async => await chromeLauncher.launch(
283+
'example_url',
284+
skipCheck: true,
285+
headless: true,
286+
),
287+
returnsNormally,
288+
);
289+
});
290+
291+
testWithoutContext('gives up retrying when a non-glibc error happens', () async {
292+
processManager.addCommand(const FakeCommand(
293+
command: <String>[
294+
'example_chrome',
295+
'--user-data-dir=/.tmp_rand0/flutter_tools_chrome_device.rand0',
296+
'--remote-debugging-port=1234',
297+
...kChromeArgs,
298+
'--headless',
299+
'--disable-gpu',
300+
'--no-sandbox',
301+
'--window-size=2400,1800',
302+
'example_url',
303+
],
304+
stderr: 'nothing in the std error indicating glibc error',
305+
));
306+
307+
expect(
308+
() async => await chromeLauncher.launch(
309+
'example_url',
310+
skipCheck: true,
311+
headless: true,
312+
),
313+
throwsToolExit(message: 'Failed to launch browser.'),
314+
);
315+
});
247316
}
248317

249318
class MockFileSystemUtils extends Mock implements FileSystemUtils {}

0 commit comments

Comments
 (0)