[tool] when writing to openssl as a part of macOS/iOS code-signing, flush the stdin stream before closing it (#150120)

Fixes https://github.com/flutter/flutter/issues/100584. Might help https://github.com/flutter/flutter/issues/137184.
This commit is contained in:
Andrew Kolos 2024-06-28 15:01:04 -07:00 committed by GitHub
parent 651a17db54
commit 28ff59513b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 166 additions and 7 deletions

View File

@ -244,31 +244,83 @@ abstract class ProcessUtils {
/// ```
///
/// However it did not catch a [SocketException] on Linux.
///
/// As part of making sure errors are caught, this function will call [flush]
/// on [stdin] to ensure that [line] is written to the pipe before this
/// function returns. This means completion will be blocked if the kernel
/// buffer of the pipe is full.
static Future<void> writelnToStdinGuarded({
required IOSink stdin,
required String line,
required void Function(Object, StackTrace) onError,
}) async {
await _writeToStdinGuarded(
stdin: stdin,
content: line,
onError: onError,
isLine: true,
);
}
/// Please see [writelnToStdinGuarded].
///
/// This calls `stdin.write` instead of `stdin.writeln`.
static Future<void> writeToStdinGuarded({
required IOSink stdin,
required String content,
required void Function(Object, StackTrace) onError,
}) async {
await _writeToStdinGuarded(
stdin: stdin,
content: content,
onError: onError,
isLine: false,
);
}
static Future<void> _writeToStdinGuarded({
required IOSink stdin,
required String content,
required void Function(Object, StackTrace) onError,
required bool isLine,
}) async {
final Completer<void> completer = Completer<void>();
void handleError(Object error, StackTrace stackTrace) {
try {
onError(error, stackTrace);
completer.complete();
} on Exception catch (e) {
completer.completeError(e);
}
}
void writeFlushAndComplete() {
stdin.writeln(line);
stdin.flush().whenComplete(() {
if (!completer.isCompleted) {
if (isLine) {
stdin.writeln(content);
} else {
stdin.write(content);
}
stdin.flush().then(
(_) {
completer.complete();
}
});
},
onError: handleError,
);
}
runZonedGuarded(
writeFlushAndComplete,
(Object error, StackTrace stackTrace) {
onError(error, stackTrace);
handleError(error, stackTrace);
// We may have already completed with an error in `handleError`.
if (!completer.isCompleted) {
completer.complete();
}
},
);
return completer.future;
}
}

View File

@ -231,7 +231,15 @@ Future<String?> _getCodeSigningIdentityDevelopmentTeam({
final Process opensslProcess = await processUtils.start(
const <String>['openssl', 'x509', '-subject']);
await (opensslProcess.stdin..write(signingCertificateStdout)).close();
await ProcessUtils.writeToStdinGuarded(
stdin: opensslProcess.stdin,
content: signingCertificateStdout,
onError: (Object? error, _) {
throw Exception('Unexpected error when writing to openssl: $error');
},
);
await opensslProcess.stdin.close();
final String opensslOutput = await utf8.decodeStream(opensslProcess.stdout);
// Fire and forget discard of the stderr stream so we don't hold onto resources.

View File

@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
@ -444,4 +446,37 @@ void main() {
);
});
});
group('writeToStdinGuarded', () {
testWithoutContext('handles any error thrown by stdin.flush', () async {
final _ThrowsOnFlushIOSink stdin = _ThrowsOnFlushIOSink();
Object? errorPassedToCallback;
await ProcessUtils.writeToStdinGuarded(
stdin: stdin,
content: 'message to stdin',
onError: (Object error, StackTrace stackTrace) {
errorPassedToCallback = error;
},
);
expect(
errorPassedToCallback,
isNotNull,
reason: 'onError callback should have been invoked.',
);
expect(errorPassedToCallback, const TypeMatcher<SocketException>());
});
});
}
class _ThrowsOnFlushIOSink extends MemoryIOSink {
@override
Future<Object?> flush() async {
throw const SocketException(
'Write failed',
osError: OSError('Broken pipe', 32),
);
}
}

View File

@ -15,6 +15,7 @@ import 'package:flutter_tools/src/ios/code_signing.dart';
import '../../src/common.dart';
import '../../src/fake_process_manager.dart';
import '../../src/fakes.dart';
const String kCertificates = '''
1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "iPhone Developer: Profile 1 (1111AAAA11)"
@ -581,6 +582,59 @@ void main() {
expect(developmentTeam, isNull);
expect(processManager, hasNoRemainingExpectations);
});
testWithoutContext('handles stdin pipe breaking on openssl process', () async {
final StreamSink<List<int>> stdinSink = ClosedStdinController();
final Completer<void> completer = Completer<void>();
const String certificates = '''
1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "iPhone Developer: Profile 1 (1111AAAA11)"
1 valid identities found''';
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>['which', 'security'],
),
const FakeCommand(
command: <String>['which', 'openssl'],
),
const FakeCommand(
command: <String>['security', 'find-identity', '-p', 'codesigning', '-v'],
stdout: certificates,
),
const FakeCommand(
command: <String>['security', 'find-certificate', '-c', '1111AAAA11', '-p'],
stdout: 'This is a fake certificate',
),
FakeCommand(
command: const <String>['openssl', 'x509', '-subject'],
stdin: IOSink(stdinSink),
stdout: 'subject= /CN=iPhone Developer: Profile 1 (1111AAAA11)/OU=3333CCCC33/O=My Team/C=US',
completer: completer,
),
]);
Future<Map<String, String>?> getCodeSigningIdentities() => getCodeSigningIdentityDevelopmentTeamBuildSetting(
buildSettings: const <String, String>{
'bogus': 'bogus',
},
platform: macosPlatform,
processManager: processManager,
logger: logger,
config: testConfig,
terminal: testTerminal,
);
await expectLater(
() => getCodeSigningIdentities(),
throwsA(
const TypeMatcher<Exception>().having(
(Exception e) => e.toString(),
'message',
equals('Exception: Unexpected error when writing to openssl: SocketException: Bad pipe'),
),
),
);
});
});
}

View File

@ -737,3 +737,13 @@ class FakeDevtoolsLauncher extends Fake implements DevtoolsLauncher {
closed = true;
}
}
class ClosedStdinController extends Fake implements StreamSink<List<int>> {
@override
Future<Object?> addStream(Stream<List<int>> stream) async => throw const SocketException('Bad pipe');
@override
Future<Object?> close() async {
return null;
}
}