Unverified Commit fac41dde authored by Christopher Fujino's avatar Christopher Fujino Committed by GitHub

[flutter_tools] catch SocketException writing to ios-deploy stdin (#139784)

Fixes https://github.com/flutter/flutter/issues/139709

This adds a static helper method `ProcessUtils.writelnToStdinGuarded()`, which will asynchronously write to a sub-process's STDIN `IOSink` and catch errors.

In talking with Brian, it sounds like this is the best and most reliable way to catch `SocketException`s during these writes *to sub-process file descriptors* specifically (with a "real" hard drive file, the future returned by `.flush()` should complete with the write error).

Also, as I note in the dartdoc to `writelnToStdinGuarded()`, the behavior seems to be different between macOS and linux.

Moving forward, in any place where we want to catch exceptions writing to STDIN, we will want to use this new helper.
parent 815dc96e
...@@ -233,6 +233,52 @@ abstract class ProcessUtils { ...@@ -233,6 +233,52 @@ abstract class ProcessUtils {
List<String> cli, { List<String> cli, {
Map<String, String>? environment, Map<String, String>? environment,
}); });
/// Write [line] to [stdin] and catch any errors with [onError].
///
/// Specifically with [Process] file descriptors, an exception that is
/// thrown as part of a write can be most reliably caught with a
/// [ZoneSpecification] error handler.
///
/// On some platforms, the following code appears to work:
///
/// ```dart
/// stdin.writeln(line);
/// try {
/// await stdin.flush(line);
/// } catch (err) {
/// // handle error
/// }
/// ```
///
/// However it did not catch a [SocketException] on Linux.
static Future<void> writelnToStdinGuarded({
required IOSink stdin,
required String line,
required void Function(Object, StackTrace) onError,
}) async {
final Completer<void> completer = Completer<void>();
void writeFlushAndComplete() {
stdin.writeln(line);
stdin.flush().whenComplete(() {
if (!completer.isCompleted) {
completer.complete();
}
});
}
runZonedGuarded(
writeFlushAndComplete,
(Object error, StackTrace stackTrace) {
onError(error, stackTrace);
if (!completer.isCompleted) {
completer.complete();
}
},
);
return completer.future;
}
} }
class _DefaultProcessUtils implements ProcessUtils { class _DefaultProcessUtils implements ProcessUtils {
......
...@@ -598,29 +598,66 @@ class IOSDeployDebugger { ...@@ -598,29 +598,66 @@ class IOSDeployDebugger {
if (!debuggerAttached) { if (!debuggerAttached) {
return; return;
} }
try { // Stop the app, which will prompt the backtrace to be printed for all
// Stop the app, which will prompt the backtrace to be printed for all threads in the stdoutSubscription handler. // threads in the stdoutSubscription handler.
_iosDeployProcess?.stdin.writeln(_signalStop); await stdinWriteln(
} on SocketException catch (error) { _signalStop,
// Best effort, try to detach, but maybe the app already exited or already detached. onError: (Object error, _) {
_logger.printTrace('Could not stop app from debugger: $error'); _logger.printTrace('Could not stop the app: $error');
} },
);
// Wait for logging to finish on process exit. // Wait for logging to finish on process exit.
return logLines.drain(); return logLines.drain();
} }
void detach() { Future<void>? _stdinWriteFuture;
if (!debuggerAttached) {
/// Queue write of [line] to STDIN of [_iosDeployProcess].
///
/// No-op if [_iosDeployProcess] is null.
///
/// This write will not happen until the flush of any previous writes have
/// completed, because calling [IOSink.flush()] before a previous flush has
/// completed will throw a [StateError].
///
/// This method needs to keep track of the [_stdinWriteFuture] from previous
/// calls because the future returned by [detach] is not always await-ed.
Future<void> stdinWriteln(String line, {required void Function(Object, StackTrace) onError}) async {
final Process? process = _iosDeployProcess;
if (process == null) {
return; return;
} }
try { Future<void> writeln() {
// Detach lldb from the app process. return ProcessUtils.writelnToStdinGuarded(
_iosDeployProcess?.stdin.writeln('process detach'); stdin: process.stdin,
} on SocketException catch (error) { line: line,
// Best effort, try to detach, but maybe the app already exited or already detached. onError: onError,
_logger.printTrace('Could not detach from debugger: $error'); );
}
if (_stdinWriteFuture != null) {
_stdinWriteFuture = _stdinWriteFuture!.then<void>((_) => writeln());
} else {
_stdinWriteFuture = writeln();
} }
return _stdinWriteFuture;
}
Future<void> detach() async {
if (!debuggerAttached) {
return;
}
return stdinWriteln(
'process detach',
onError: (Object error, _) {
// Best effort, try to detach, but maybe the app already exited or already detached.
_logger.printTrace('Could not detach from debugger: $error');
}
);
} }
} }
......
...@@ -9,12 +9,14 @@ import 'package:file/memory.dart'; ...@@ -9,12 +9,14 @@ import 'package:file/memory.dart';
import 'package:file_testing/file_testing.dart'; import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/process.dart'; import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/ios/ios_deploy.dart'; import 'package:flutter_tools/src/ios/ios_deploy.dart';
import 'package:test/fake.dart';
import '../../src/common.dart'; import '../../src/common.dart';
import '../../src/fake_process_manager.dart'; import '../../src/fake_process_manager.dart';
...@@ -386,7 +388,26 @@ void main () { ...@@ -386,7 +388,26 @@ void main () {
); );
expect(stdin.stream.transform<String>(const Utf8Decoder()), emits('process detach')); expect(stdin.stream.transform<String>(const Utf8Decoder()), emits('process detach'));
await iosDeployDebugger.launchAndAttach(); await iosDeployDebugger.launchAndAttach();
iosDeployDebugger.detach(); await iosDeployDebugger.detach();
});
testWithoutContext('detach handles broken pipe', () async {
final StreamSink<List<int>> stdinSink = _ClosedStdinController();
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
FakeCommand(
command: const <String>['ios-deploy'],
stdout: '(lldb) run\nsuccess',
stdin: IOSink(stdinSink),
),
]);
final BufferLogger logger = BufferLogger.test();
final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test(
processManager: processManager,
logger: logger,
);
await iosDeployDebugger.launchAndAttach();
await iosDeployDebugger.detach();
expect(logger.traceText, contains('Could not detach from debugger'));
}); });
testWithoutContext('stop with backtrace', () async { testWithoutContext('stop with backtrace', () async {
...@@ -399,18 +420,28 @@ void main () { ...@@ -399,18 +420,28 @@ void main () {
], ],
stdout: stdout:
'(lldb) run\nsuccess\nLog on attach\n(lldb) Process 6156 stopped\n* thread #1, stop reason = Assertion failed:\n(lldb) Process 6156 detached', '(lldb) run\nsuccess\nLog on attach\n(lldb) Process 6156 stopped\n* thread #1, stop reason = Assertion failed:\n(lldb) Process 6156 detached',
stdin: IOSink(stdin.sink), stdin: IOSink(stdin),
), ),
]); ]);
final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test( final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test(
processManager: processManager, processManager: processManager,
); );
await iosDeployDebugger.launchAndAttach(); await iosDeployDebugger.launchAndAttach();
await iosDeployDebugger.stopAndDumpBacktrace(); List<String>? stdinLines;
expect(await stdinStream.take(3).toList(), <String>[
// These two futures will deadlock if await-ed sequentially
await Future.wait(<Future<void>>[
iosDeployDebugger.stopAndDumpBacktrace(),
stdinStream.take(5).toList().then<void>(
(List<String> lines) => stdinLines = lines,
),
]);
expect(stdinLines, const <String>[
'thread backtrace all', 'thread backtrace all',
'\n', '\n',
'process detach', 'process detach',
'\n',
'process signal SIGSTOP',
]); ]);
}); });
...@@ -616,6 +647,11 @@ process continue ...@@ -616,6 +647,11 @@ process continue
}); });
} }
class _ClosedStdinController extends Fake implements StreamSink<List<int>> {
@override
Future<Object?> addStream(Stream<List<int>> stream) async => throw const SocketException('Bad pipe');
}
IOSDeploy setUpIOSDeploy(ProcessManager processManager, { IOSDeploy setUpIOSDeploy(ProcessManager processManager, {
Artifacts? artifacts, Artifacts? artifacts,
}) { }) {
......
...@@ -1001,7 +1001,7 @@ class FakeIOSDeployDebugger extends Fake implements IOSDeployDebugger { ...@@ -1001,7 +1001,7 @@ class FakeIOSDeployDebugger extends Fake implements IOSDeployDebugger {
Stream<String> logLines = const Stream<String>.empty(); Stream<String> logLines = const Stream<String>.empty();
@override @override
void detach() { Future<void> detach() async {
detached = true; detached = true;
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment