fix: adb logcat with invalid utf8 crashes shorebird preview (#3460)
This commit is contained in:
@@ -17,7 +17,6 @@ import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
||||
import 'package:shorebird_cli/src/logging/shorebird_logger.dart';
|
||||
import 'package:shorebird_cli/src/metadata/metadata.dart';
|
||||
import 'package:shorebird_cli/src/patch_diff_checker.dart';
|
||||
import 'package:shorebird_cli/src/platform/apple/plist.dart';
|
||||
import 'package:shorebird_cli/src/platform/platform.dart';
|
||||
import 'package:shorebird_cli/src/release_type.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_artifacts.dart';
|
||||
|
||||
@@ -394,10 +394,7 @@ This is only applicable when previewing Android releases.''',
|
||||
|
||||
await process.run('chmod', ['+x', executableFile.path]);
|
||||
|
||||
final proc = await process.start(executableFile.path, []);
|
||||
proc.stdout.listen((log) => logger.info(utf8.decode(log)));
|
||||
proc.stderr.listen((log) => logger.err(utf8.decode(log)));
|
||||
return proc.exitCode;
|
||||
return startAndForwardOutput(executableFile.path);
|
||||
}
|
||||
|
||||
/// Downloads and runs the given [release] the given [appId] on Windows.
|
||||
@@ -463,10 +460,7 @@ This is only applicable when previewing Android releases.''',
|
||||
(file) => file.path.endsWith('.exe'),
|
||||
);
|
||||
|
||||
final proc = await process.start(exeFile.path, []);
|
||||
proc.stdout.listen((log) => logger.info(utf8.decode(log)));
|
||||
proc.stderr.listen((log) => logger.err(utf8.decode(log)));
|
||||
return proc.exitCode;
|
||||
return startAndForwardOutput(exeFile.path);
|
||||
}
|
||||
|
||||
/// Installs and launches the release on macOS.
|
||||
@@ -553,8 +547,12 @@ This is only applicable when previewing Android releases.''',
|
||||
return line.trim().replaceFirst(prefixRegex, '').trim();
|
||||
}
|
||||
|
||||
// TODO(eseidel): Use startAndForwardOutput instead?
|
||||
// This doesn't seem to handle stderr, maybe it should?
|
||||
// Use allowMalformed to handle non-UTF8 bytes in log stream output.
|
||||
const decoder = Utf8Decoder(allowMalformed: true);
|
||||
logs.listen((log) {
|
||||
final logLine = utf8.decode(log);
|
||||
final logLine = decoder.convert(log);
|
||||
if (logFilters.any((filter) => filter.hasMatch(logLine))) {
|
||||
return;
|
||||
}
|
||||
@@ -711,11 +709,14 @@ This is only applicable when previewing Android releases.''',
|
||||
}
|
||||
|
||||
final process = await adb.logcat(filter: 'flutter', deviceId: deviceId);
|
||||
// adb logcat sometimes lets non-utf8 characters through, so we need to
|
||||
// not crash when it does.
|
||||
const decoder = Utf8Decoder(allowMalformed: true);
|
||||
process.stdout.listen((event) {
|
||||
logger.info(utf8.decode(event));
|
||||
logger.info(decoder.convert(event));
|
||||
});
|
||||
process.stderr.listen((event) {
|
||||
logger.err(utf8.decode(event));
|
||||
logger.err(decoder.convert(event));
|
||||
});
|
||||
|
||||
return process.exitCode;
|
||||
@@ -1018,6 +1019,18 @@ This is only applicable when previewing Android releases.''',
|
||||
shorebirdYamlFile.writeAsStringSync(yamlEditor.toString(), flush: true);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Starts a process and forwards its stdout/stderr to the logger.
|
||||
///
|
||||
/// Returns the process exit code.
|
||||
Future<int> startAndForwardOutput(String executable) async {
|
||||
final proc = await process.start(executable, []);
|
||||
// Use allowMalformed to handle non-UTF8 bytes in process output.
|
||||
const decoder = Utf8Decoder(allowMalformed: true);
|
||||
proc.stdout.listen((log) => logger.info(decoder.convert(log)));
|
||||
proc.stderr.listen((log) => logger.err(decoder.convert(log)));
|
||||
return proc.exitCode;
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension on [Release] that exposes the active platforms (e.g. platforms
|
||||
|
||||
@@ -14,7 +14,6 @@ import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
||||
import 'package:shorebird_cli/src/logging/logging.dart';
|
||||
import 'package:shorebird_cli/src/metadata/metadata.dart';
|
||||
import 'package:shorebird_cli/src/platform/apple/apple.dart';
|
||||
import 'package:shorebird_cli/src/platform/apple/plist.dart';
|
||||
import 'package:shorebird_cli/src/release_type.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_env.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_validator.dart';
|
||||
|
||||
@@ -13,7 +13,6 @@ import 'package:shorebird_cli/src/executables/xcodebuild.dart';
|
||||
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
||||
import 'package:shorebird_cli/src/logging/shorebird_logger.dart';
|
||||
import 'package:shorebird_cli/src/metadata/update_release_metadata.dart';
|
||||
import 'package:shorebird_cli/src/platform/apple/plist.dart';
|
||||
import 'package:shorebird_cli/src/platform/platform.dart';
|
||||
import 'package:shorebird_cli/src/release_type.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_env.dart';
|
||||
|
||||
@@ -214,6 +214,7 @@ class AotTools {
|
||||
return version >= Version(0, 0, 1);
|
||||
}
|
||||
|
||||
/// Returns the version of the aot_tools executable.
|
||||
@visibleForTesting
|
||||
Future<Version> getVersion() async {
|
||||
// Use 0.0.0 to allow callers to easily compare w/o checking for null.
|
||||
|
||||
@@ -88,12 +88,14 @@ class IDeviceSysLog {
|
||||
environment: {'DYLD_LIBRARY_PATH': _dyldPathEntry},
|
||||
);
|
||||
|
||||
// Use allowMalformed to handle non-UTF8 bytes in device syslog output.
|
||||
const decoder = Utf8Decoder(allowMalformed: true);
|
||||
loggerProcess.stdout
|
||||
.transform<String>(utf8.decoder)
|
||||
.transform<String>(decoder)
|
||||
.transform<String>(const LineSplitter())
|
||||
.listen(_parseLogLine);
|
||||
loggerProcess.stderr
|
||||
.transform<String>(utf8.decoder)
|
||||
.transform<String>(decoder)
|
||||
.transform<String>(const LineSplitter())
|
||||
.listen(_parseLogLine);
|
||||
return loggerProcess.exitCode;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// cspell:words libinfo networkd
|
||||
// cspell:words libinfo networkd FFFD
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io' hide Platform;
|
||||
@@ -1031,6 +1031,36 @@ channel: ${track.channel}
|
||||
verify(() => logger.err(output)).called(1);
|
||||
});
|
||||
|
||||
test('handles non-UTF8 bytes in output without throwing', () async {
|
||||
when(
|
||||
() => artifactManager.extractZip(
|
||||
zipFile: any(named: 'zipFile'),
|
||||
outputDirectory: any(named: 'outputDirectory'),
|
||||
),
|
||||
).thenAnswer(setupAndroidShorebirdYaml);
|
||||
|
||||
final completer = Completer<int>();
|
||||
when(() => process.exitCode).thenAnswer((_) => completer.future);
|
||||
// Create byte sequences with invalid UTF-8: 0xFF is not valid in UTF-8
|
||||
// "Hello" + 0xFF + "!" for stdout, "Error" + 0xFF + "!" for stderr
|
||||
when(
|
||||
() => process.stdout,
|
||||
).thenAnswer(
|
||||
(_) => Stream.value([0x48, 0x65, 0x6C, 0x6C, 0x6F, 0xFF, 0x21]),
|
||||
);
|
||||
when(
|
||||
() => process.stderr,
|
||||
).thenAnswer(
|
||||
(_) => Stream.value([0x45, 0x72, 0x72, 0x6F, 0x72, 0xFF, 0x21]),
|
||||
);
|
||||
final result = runWithOverrides(command.run);
|
||||
completer.complete(0);
|
||||
await expectLater(await result, equals(ExitCode.success.code));
|
||||
// The invalid bytes should be replaced with the replacement character
|
||||
verify(() => logger.info('Hello\uFFFD!')).called(1);
|
||||
verify(() => logger.err('Error\uFFFD!')).called(1);
|
||||
});
|
||||
|
||||
group('when in a shorebird project without flavors', () {
|
||||
setUp(() {
|
||||
when(
|
||||
@@ -2022,6 +2052,35 @@ channel: ${DeploymentTrack.staging.channel}
|
||||
verify(() => logger.err('hello error')).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('handles non-UTF8 bytes in output without throwing', () async {
|
||||
setupPreviewArtifact(
|
||||
baseDirectory: Directory(
|
||||
p.join(
|
||||
previewDirectory.path,
|
||||
'linux_${releaseVersion}_$releaseArtifactId',
|
||||
),
|
||||
),
|
||||
);
|
||||
// Create byte sequences with invalid UTF-8: 0xFF is not valid in UTF-8
|
||||
// "Hello" + 0xFF + "!" for stdout, "Error" + 0xFF + "!" for stderr
|
||||
when(
|
||||
() => process.stdout,
|
||||
).thenAnswer(
|
||||
(_) => Stream.value([0x48, 0x65, 0x6C, 0x6C, 0x6F, 0xFF, 0x21]),
|
||||
);
|
||||
when(
|
||||
() => process.stderr,
|
||||
).thenAnswer(
|
||||
(_) => Stream.value([0x45, 0x72, 0x72, 0x6F, 0x72, 0xFF, 0x21]),
|
||||
);
|
||||
|
||||
final result = await runWithOverrides(command.run);
|
||||
expect(result, equals(ExitCode.success.code));
|
||||
// The invalid bytes should be replaced with the replacement character
|
||||
verify(() => logger.info('Hello\uFFFD!')).called(1);
|
||||
verify(() => logger.err('Error\uFFFD!')).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('macos', () {
|
||||
@@ -2243,6 +2302,30 @@ channel: ${DeploymentTrack.staging.channel}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('non-UTF8 handling', () {
|
||||
setUp(setupMacosShorebirdYaml);
|
||||
|
||||
test('handles non-UTF8 bytes in log stream without throwing', () async {
|
||||
// Create bytes with invalid UTF-8: 0xFF is not valid in UTF-8
|
||||
// Simulates a macOS log line with invalid bytes
|
||||
final invalidUtf8Bytes = [
|
||||
...utf8.encode(
|
||||
'2025-01-31 09:53:22.889 Df app[22535:1d268] [shorebird] Hello',
|
||||
),
|
||||
0xFF,
|
||||
0x0A, // newline
|
||||
];
|
||||
when(() => open.newApplication(path: any(named: 'path'))).thenAnswer(
|
||||
(_) async => Stream.value(invalidUtf8Bytes),
|
||||
);
|
||||
|
||||
final result = await runWithOverrides(command.run);
|
||||
expect(result, equals(ExitCode.success.code));
|
||||
// The invalid byte should be replaced with the replacement character
|
||||
verify(() => logger.info('[shorebird] Hello\uFFFD')).called(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('windows', () {
|
||||
@@ -2462,6 +2545,31 @@ channel: ${DeploymentTrack.staging.channel}
|
||||
verify(() => logger.err('hello error')).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('handles non-UTF8 bytes in output without throwing', () async {
|
||||
File(
|
||||
p.join(windowsReleaseDirectory.path, 'runner.exe'),
|
||||
).createSync(recursive: true);
|
||||
createShorebirdYaml();
|
||||
// Create byte sequences with invalid UTF-8: 0xFF is not valid in UTF-8
|
||||
// "Hello" + 0xFF + "!" for stdout, "Error" + 0xFF + "!" for stderr
|
||||
when(
|
||||
() => process.stdout,
|
||||
).thenAnswer(
|
||||
(_) => Stream.value([0x48, 0x65, 0x6C, 0x6C, 0x6F, 0xFF, 0x21]),
|
||||
);
|
||||
when(
|
||||
() => process.stderr,
|
||||
).thenAnswer(
|
||||
(_) => Stream.value([0x45, 0x72, 0x72, 0x6F, 0x72, 0xFF, 0x21]),
|
||||
);
|
||||
|
||||
final result = await runWithOverrides(command.run);
|
||||
expect(result, equals(ExitCode.success.code));
|
||||
// The invalid bytes should be replaced with the replacement character
|
||||
verify(() => logger.info('Hello\uFFFD!')).called(1);
|
||||
verify(() => logger.err('Error\uFFFD!')).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('when no platform is specified', () {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// cspell:words FFFD
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
@@ -149,6 +150,40 @@ Nov 10 17:58:47 kernel(Sandbox)[0] <Error>: Sandbox: Runner(52662) deny(1) iokit
|
||||
verify(() => logger.info('flutter: hello from stderr')).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
test('handles non-UTF8 bytes in output without throwing', () async {
|
||||
// Create byte sequences with invalid UTF-8: 0xFF is not valid in UTF-8
|
||||
// "flutter: hello" + 0xFF for stdout, "flutter: error" + 0xFF for
|
||||
// stderr
|
||||
final stdoutBytes = [
|
||||
...utf8.encode(
|
||||
'Nov 10 14:46:57 Runner(Flutter)[1044] <Notice>: flutter: hello',
|
||||
),
|
||||
0xFF,
|
||||
0x0A, // newline
|
||||
];
|
||||
final stderrBytes = [
|
||||
...utf8.encode(
|
||||
'Nov 10 14:46:57 Runner(Flutter)[1044] <Notice>: flutter: error',
|
||||
),
|
||||
0xFF,
|
||||
0x0A, // newline
|
||||
];
|
||||
when(
|
||||
() => loggerProcess.stdout,
|
||||
).thenAnswer((_) => Stream.value(stdoutBytes));
|
||||
when(
|
||||
() => loggerProcess.stderr,
|
||||
).thenAnswer((_) => Stream.value(stderrBytes));
|
||||
|
||||
await runWithOverrides(
|
||||
() => idevicesyslog.startLogger(device: device),
|
||||
);
|
||||
|
||||
// The invalid bytes should be replaced with the replacement character
|
||||
verify(() => logger.info('flutter: hello\uFFFD')).called(1);
|
||||
verify(() => logger.info('flutter: error\uFFFD')).called(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user