Skip to content

Commit d848262

Browse files
authored
[tool] Guard process writes to frontend server in ResidentCompiler (#152358)
Contributes to fixing flutter/flutter#137184. Cleaned up version of earlier PR, flutter/flutter#152187. This PR guards all the writes to `Process::stdin` by wrapping them with `ProcessUtils.writelnToStdinUnsafe`. This way, if any writes fail, we should at least get a stacktrace in our crash reporting.
1 parent c4e1996 commit d848262

File tree

14 files changed

+121
-71
lines changed

14 files changed

+121
-71
lines changed

packages/flutter_tools/lib/src/base/process.dart

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,11 @@ abstract class ProcessUtils {
228228

229229
/// Write [line] to [stdin] and catch any errors with [onError].
230230
///
231-
/// Specifically with [Process] file descriptors, an exception that is
232-
/// thrown as part of a write can be most reliably caught with a
231+
/// Concurrent calls to this method will result in an exception due to its
232+
/// dependence on [IOSink.flush] (see https://github.com/dart-lang/sdk/issues/25277).
233+
///
234+
/// Context: specifically with [Process] file descriptors, an exception that
235+
/// is thrown as part of a write can be most reliably caught with a
233236
/// [ZoneSpecification] error handler.
234237
///
235238
/// On some platforms, the following code appears to work:
@@ -278,6 +281,10 @@ abstract class ProcessUtils {
278281
);
279282
}
280283

284+
/// See [writelnToStdinGuarded].
285+
///
286+
/// In the event that the write or flush fails, this will throw an exception
287+
/// that preserves the stack trace of the callsite.
281288
static Future<void> writelnToStdinUnsafe({
282289
required IOSink stdin,
283290
required String line,
@@ -289,6 +296,10 @@ abstract class ProcessUtils {
289296
);
290297
}
291298

299+
/// See [writeToStdinGuarded].
300+
///
301+
/// In the event that the write or flush fails, this will throw an exception
302+
/// that preserves the stack trace of the callsite.
292303
static Future<void> writeToStdinUnsafe({
293304
required IOSink stdin,
294305
required String content,

packages/flutter_tools/lib/src/compile.dart

Lines changed: 83 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'dart:typed_data';
77

88
import 'package:meta/meta.dart';
99
import 'package:package_config/package_config.dart';
10+
import 'package:pool/pool.dart';
1011
import 'package:process/process.dart';
1112
import 'package:usage/uuid/uuid.dart';
1213

@@ -16,6 +17,7 @@ import 'base/file_system.dart';
1617
import 'base/io.dart';
1718
import 'base/logger.dart';
1819
import 'base/platform.dart';
20+
import 'base/process.dart';
1921
import 'build_info.dart';
2022
import 'convert.dart';
2123

@@ -589,7 +591,7 @@ abstract class ResidentCompiler {
589591
/// Should be invoked when results of compilation are accepted by the client.
590592
///
591593
/// Either [accept] or [reject] should be called after every [recompile] call.
592-
void accept();
594+
Future<void> accept();
593595

594596
/// Should be invoked when results of compilation are rejected by the client.
595597
///
@@ -599,7 +601,7 @@ abstract class ResidentCompiler {
599601
/// Should be invoked when frontend server compiler should forget what was
600602
/// accepted previously so that next call to [recompile] produces complete
601603
/// kernel file.
602-
void reset();
604+
Future<void> reset();
603605

604606
Future<Object> shutdown();
605607
}
@@ -742,11 +744,9 @@ class DefaultResidentCompiler implements ResidentCompiler {
742744
final String inputKey = Uuid().generateV4();
743745

744746
if (nativeAssets != null && nativeAssets.isNotEmpty) {
745-
server.stdin.writeln('native-assets $nativeAssets');
746-
_logger.printTrace('<- native-assets $nativeAssets');
747+
await _writelnToServerStdin('native-assets $nativeAssets', printTrace: true);
747748
}
748-
server.stdin.writeln('recompile $mainUri $inputKey');
749-
_logger.printTrace('<- recompile $mainUri $inputKey');
749+
await _writelnToServerStdin('recompile $mainUri $inputKey', printTrace: true);
750750
final List<Uri>? invalidatedFiles = request.invalidatedFiles;
751751
if (invalidatedFiles != null) {
752752
for (final Uri fileUri in invalidatedFiles) {
@@ -761,8 +761,7 @@ class DefaultResidentCompiler implements ResidentCompiler {
761761
_logger.printTrace(message);
762762
}
763763
}
764-
server.stdin.writeln(inputKey);
765-
_logger.printTrace('<- $inputKey');
764+
await _writelnToServerStdin(inputKey, printTrace: true);
766765

767766
return _stdoutHandler.compilerOutput?.future;
768767
}
@@ -899,12 +898,10 @@ class DefaultResidentCompiler implements ResidentCompiler {
899898
}));
900899

901900
if (nativeAssetsUri != null && nativeAssetsUri.isNotEmpty) {
902-
_server?.stdin.writeln('native-assets $nativeAssetsUri');
903-
_logger.printTrace('<- native-assets $nativeAssetsUri');
901+
await _writelnToServerStdin('native assets $nativeAssetsUri', printTrace: true);
904902
}
905903

906-
_server?.stdin.writeln('compile $scriptUri');
907-
_logger.printTrace('<- compile $scriptUri');
904+
await _writelnToServerStdin('compile $scriptUri', printTrace: true);
908905

909906
return _stdoutHandler.compilerOutput?.future;
910907
}
@@ -945,24 +942,24 @@ class DefaultResidentCompiler implements ResidentCompiler {
945942
}
946943

947944
final String inputKey = Uuid().generateV4();
948-
server.stdin
949-
..writeln('compile-expression $inputKey')
950-
..writeln(request.expression);
951-
request.definitions?.forEach(server.stdin.writeln);
952-
server.stdin.writeln(inputKey);
953-
request.definitionTypes?.forEach(server.stdin.writeln);
954-
server.stdin.writeln(inputKey);
955-
request.typeDefinitions?.forEach(server.stdin.writeln);
956-
server.stdin.writeln(inputKey);
957-
request.typeBounds?.forEach(server.stdin.writeln);
958-
server.stdin.writeln(inputKey);
959-
request.typeDefaults?.forEach(server.stdin.writeln);
960-
server.stdin
961-
..writeln(inputKey)
962-
..writeln(request.libraryUri ?? '')
963-
..writeln(request.klass ?? '')
964-
..writeln(request.method ?? '')
965-
..writeln(request.isStatic);
945+
await _writelnToServerStdinAll(<String>[
946+
'compile-expression $inputKey',
947+
request.expression,
948+
...?request.definitions,
949+
inputKey,
950+
...?request.definitionTypes,
951+
inputKey,
952+
...?request.typeDefinitions,
953+
inputKey,
954+
...?request.typeBounds,
955+
inputKey,
956+
...?request.typeDefaults,
957+
inputKey,
958+
request.libraryUri ?? '',
959+
request.klass ?? '',
960+
request.method ?? '',
961+
request.isStatic.toString(),
962+
]);
966963

967964
return _stdoutHandler.compilerOutput?.future;
968965
}
@@ -1000,27 +997,28 @@ class DefaultResidentCompiler implements ResidentCompiler {
1000997
}
1001998

1002999
final String inputKey = Uuid().generateV4();
1003-
server.stdin
1004-
..writeln('compile-expression-to-js $inputKey')
1005-
..writeln(request.libraryUri ?? '')
1006-
..writeln(request.line)
1007-
..writeln(request.column);
1008-
request.jsModules?.forEach((String k, String v) { server.stdin.writeln('$k:$v'); });
1009-
server.stdin.writeln(inputKey);
1010-
request.jsFrameValues?.forEach((String k, String v) { server.stdin.writeln('$k:$v'); });
1011-
server.stdin
1012-
..writeln(inputKey)
1013-
..writeln(request.moduleName ?? '')
1014-
..writeln(request.expression ?? '');
1000+
await _writelnToServerStdinAll(<String>[
1001+
'compile-expression-to-js $inputKey',
1002+
request.libraryUri ?? '',
1003+
request.line.toString(),
1004+
request.column.toString(),
1005+
for (final MapEntry<String, String> entry in request.jsModules?.entries ?? <MapEntry<String, String>>[])
1006+
'${entry.key}:${entry.value}',
1007+
inputKey,
1008+
for (final MapEntry<String, String> entry in request.jsFrameValues?.entries ?? <MapEntry<String, String>>[])
1009+
'${entry.key}:${entry.value}',
1010+
inputKey,
1011+
request.moduleName ?? '',
1012+
request.expression ?? ''
1013+
]);
10151014

10161015
return _stdoutHandler.compilerOutput?.future;
10171016
}
10181017

10191018
@override
1020-
void accept() {
1019+
Future<void> accept() async {
10211020
if (_compileRequestNeedsConfirmation) {
1022-
_server?.stdin.writeln('accept');
1023-
_logger.printTrace('<- accept');
1021+
await _writelnToServerStdin('accept', printTrace: true);
10241022
}
10251023
_compileRequestNeedsConfirmation = false;
10261024
}
@@ -1041,16 +1039,14 @@ class DefaultResidentCompiler implements ResidentCompiler {
10411039
return Future<CompilerOutput?>.value();
10421040
}
10431041
_stdoutHandler.reset(expectSources: false);
1044-
_server?.stdin.writeln('reject');
1045-
_logger.printTrace('<- reject');
1042+
await _writelnToServerStdin('reject', printTrace: true);
10461043
_compileRequestNeedsConfirmation = false;
10471044
return _stdoutHandler.compilerOutput?.future;
10481045
}
10491046

10501047
@override
1051-
void reset() {
1052-
_server?.stdin.writeln('reset');
1053-
_logger.printTrace('<- reset');
1048+
Future<void> reset() async {
1049+
await _writelnToServerStdin('reset', printTrace: true);
10541050
}
10551051

10561052
@override
@@ -1064,6 +1060,43 @@ class DefaultResidentCompiler implements ResidentCompiler {
10641060
server.kill();
10651061
return server.exitCode;
10661062
}
1063+
1064+
Future<void> _writelnToServerStdin(String line, {
1065+
bool printTrace = false,
1066+
}) async {
1067+
await _writelnToServerStdinAll(<String>[line], printTrace: printTrace);
1068+
}
1069+
1070+
// TODO(andrewkolos): Concurrent calls to ProcessUtils.writelnToStdinUnsafe
1071+
// against the same stdin will result in an exception. To guard against this,
1072+
// we need to force calls to run serially. Ideally, this wouldn't be
1073+
// necessary since we shouldn't have multiple concurrent writes to the
1074+
// compiler process.
1075+
// However, we do. See https://github.com/flutter/flutter/issues/152577.
1076+
final Pool _serverStdinWritePool = Pool(1);
1077+
Future<void> _writelnToServerStdinAll(List<String> lines, {
1078+
bool printTrace = false,
1079+
}) async {
1080+
final Process? server = _server;
1081+
if (server == null) {
1082+
return;
1083+
}
1084+
final PoolResource request = await _serverStdinWritePool.request();
1085+
try {
1086+
await ProcessUtils.writelnToStdinUnsafe(
1087+
stdin: server.stdin,
1088+
line: lines.join('\n'),
1089+
);
1090+
1091+
for (final String line in lines) {
1092+
if (printTrace) {
1093+
_logger.printTrace('<- $line');
1094+
}
1095+
}
1096+
} finally {
1097+
request.release();
1098+
}
1099+
}
10671100
}
10681101

10691102
/// Convert a file URI into a multi-root scheme URI if provided, otherwise

packages/flutter_tools/lib/src/devfs.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,7 @@ class DevFS {
585585
bool assetBuildFailed = false;
586586
int syncedBytes = 0;
587587
if (fullRestart) {
588-
generator.reset();
588+
await generator.reset();
589589
}
590590
// On a full restart, or on an initial compile for the attach based workflow,
591591
// this will produce a full dill. Subsequent invocations will produce incremental
@@ -614,6 +614,12 @@ class DevFS {
614614
// before processing bundle.
615615
_logger.printTrace('Processing bundle.');
616616
// await null to give time for telling the compiler to compile.
617+
// TODO(andrewkolos): This is a hack. Adding any more awaits to the compiler's
618+
// recompile method will cause this to be insufficent.
619+
// https://github.com/flutter/flutter/issues/151255.
620+
await null;
621+
await null;
622+
await null;
617623
await null;
618624

619625
// The tool writes the assets into the AssetBundle working dir so that they

packages/flutter_tools/lib/src/isolated/devfs_web.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1020,7 +1020,7 @@ class WebDevFS implements DevFS {
10201020
await _validateTemplateFile('flutter_bootstrap.js');
10211021
final DateTime candidateCompileTime = DateTime.now();
10221022
if (fullRestart) {
1023-
generator.reset();
1023+
await generator.reset();
10241024
}
10251025

10261026
// The tool generates an entrypoint file in a temp directory to handle

packages/flutter_tools/lib/src/isolated/resident_web_runner.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
334334
appFailedToStart();
335335
return 1;
336336
}
337-
device!.generator!.accept();
337+
await device!.generator!.accept();
338338
cacheInitialDillCompilation();
339339
} else {
340340
final WebBuilder webBuilder = WebBuilder(
@@ -418,7 +418,7 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
418418
// Full restart is always false for web, since the extra recompile is wasteful.
419419
final UpdateFSReport report = await _updateDevFS();
420420
if (report.success) {
421-
device!.generator!.accept();
421+
await device!.generator!.accept();
422422
} else {
423423
status.stop();
424424
await device!.generator!.reject();

packages/flutter_tools/lib/src/resident_runner.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -595,7 +595,7 @@ class FlutterDevice {
595595

596596
Future<void> updateReloadStatus(bool wasReloadSuccessful) async {
597597
if (wasReloadSuccessful) {
598-
generator?.accept();
598+
await generator?.accept();
599599
} else {
600600
await generator?.reject();
601601
}

packages/flutter_tools/lib/src/run_hot.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ class HotRunner extends ResidentRunner {
298298
// VM must have accepted the kernel binary, there will be no reload
299299
// report, so we let incremental compiler know that source code was accepted.
300300
if (device!.generator != null) {
301-
device.generator!.accept();
301+
await device.generator!.accept();
302302
}
303303
final List<FlutterView> views = await device.vmService!.getFlutterViews();
304304
for (final FlutterView view in views) {
@@ -626,7 +626,7 @@ class HotRunner extends ResidentRunner {
626626
// VM must have accepted the kernel binary, there will be no reload
627627
// report, so we let incremental compiler know that source code was accepted.
628628
if (device!.generator != null) {
629-
device.generator!.accept();
629+
await device.generator!.accept();
630630
}
631631
}
632632
// Check if the isolate is paused and resume it.

packages/flutter_tools/lib/src/test/runner.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,7 @@ class SpawnPlugin extends PlatformPlugin {
647647
fs: globals.fs,
648648
nativeAssetsYaml: nativeAssetsYaml,
649649
);
650-
residentCompiler.accept();
650+
await residentCompiler.accept();
651651

652652
globals.printTrace('Compiling ${sourceFile.absolute.uri} took ${compilerTime.elapsedMilliseconds}ms');
653653
testTimeRecorder?.stop(TestTimePhases.Compile, testTimeRecorderStopwatch!);

packages/flutter_tools/lib/src/test/test_compiler.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,11 @@ class TestCompiler {
188188
// compiler to avoid reusing compiler that might have gotten into
189189
// a weird state.
190190
if (outputPath == null || compilerOutput!.errorCount > 0) {
191-
request.result.complete();
192191
await _shutdown();
192+
request.result.complete();
193193
} else {
194+
await compiler!.accept();
195+
await compiler!.reset();
194196
if (shouldCopyDillFile) {
195197
final String path = request.mainUri.toFilePath(windows: globals.platform.isWindows);
196198
final File outputFile = globals.fs.file(outputPath);
@@ -209,8 +211,6 @@ class TestCompiler {
209211
} else {
210212
request.result.complete(outputPath);
211213
}
212-
compiler!.accept();
213-
compiler!.reset();
214214
}
215215
globals.printTrace('Compiling ${request.mainUri} took ${compilerTime.elapsedMilliseconds}ms');
216216
testTimeRecorder?.stop(TestTimePhases.Compile, testTimeRecorderStopwatch!);

packages/flutter_tools/test/general.shard/compile_incremental_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,7 @@ Future<void> _accept(
521521
MemoryIOSink frontendServerStdIn,
522522
String expected,
523523
) async {
524-
generator.accept();
524+
await generator.accept();
525525
final String commands = frontendServerStdIn.getAndClear();
526526
final RegExp re = RegExp(expected);
527527
expect(commands, matches(re));

0 commit comments

Comments
 (0)