Unverified Commit 944a4c97 authored by Lau Ching Jun's avatar Lau Ching Jun Committed by GitHub

Write crash report in temp directory if writing to CWD failed. (#113316)

parent 91d88336
......@@ -13,6 +13,7 @@ import 'package:intl/intl_standalone.dart' as intl_standalone;
import 'src/base/async_guard.dart';
import 'src/base/common.dart';
import 'src/base/context.dart';
import 'src/base/error_handling_io.dart';
import 'src/base/file_system.dart';
import 'src/base/io.dart';
import 'src/base/logger.dart';
......@@ -189,12 +190,6 @@ String _crashException(dynamic error) => '${error.runtimeType}: $error';
/// Saves the crash report to a local file.
Future<File> _createLocalCrashReport(CrashDetails details) async {
File crashFile = globals.fsUtils.getUniqueFile(
globals.fs.currentDirectory,
'flutter',
'log',
);
final StringBuffer buffer = StringBuffer();
buffer.writeln('Flutter crash report.');
......@@ -210,22 +205,32 @@ Future<File> _createLocalCrashReport(CrashDetails details) async {
buffer.writeln('## flutter doctor\n');
buffer.writeln('```\n${await details.doctorText.text}```');
try {
crashFile.writeAsStringSync(buffer.toString());
} on FileSystemException catch (_) {
// Fallback to the system temporary directory.
crashFile = globals.fsUtils.getUniqueFile(
globals.fs.systemTempDirectory,
'flutter',
'log',
);
late File crashFile;
ErrorHandlingFileSystem.noExitOnFailure(() {
try {
crashFile = globals.fsUtils.getUniqueFile(
globals.fs.currentDirectory,
'flutter',
'log',
);
crashFile.writeAsStringSync(buffer.toString());
} on FileSystemException catch (e) {
globals.printError('Could not write crash report to disk: $e');
globals.printError(buffer.toString());
} on FileSystemException catch (_) {
// Fallback to the system temporary directory.
try {
crashFile = globals.fsUtils.getUniqueFile(
globals.fs.systemTempDirectory,
'flutter',
'log',
);
crashFile.writeAsStringSync(buffer.toString());
} on FileSystemException catch (e) {
globals.printError('Could not write crash report to disk: $e');
globals.printError(buffer.toString());
rethrow;
}
}
}
});
return crashFile;
}
......
......@@ -816,7 +816,7 @@ void _throwFileSystemException(String? errorMessage) {
return;
}
if (ErrorHandlingFileSystem._noExitOnFailure) {
throw Exception(errorMessage);
throw FileSystemException(errorMessage);
}
throwToolExit(errorMessage);
}
......@@ -27,6 +27,7 @@ const String kCustomBugInstructions = 'These are instructions to report with a c
void main() {
int? firstExitCode;
late MemoryFileSystem fileSystem;
group('runner', () {
setUp(() {
......@@ -46,6 +47,7 @@ void main() {
});
Cache.disableLocking();
fileSystem = MemoryFileSystem.test();
});
tearDown(() {
......@@ -93,7 +95,7 @@ void main() {
'FLUTTER_ANALYTICS_LOG_FILE': 'test',
'FLUTTER_ROOT': '/',
}),
FileSystem: () => MemoryFileSystem.test(),
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Usage: () => CrashingUsage(),
Artifacts: () => Artifacts.test(),
......@@ -137,7 +139,7 @@ void main() {
'FLUTTER_ANALYTICS_LOG_FILE': 'test',
'FLUTTER_ROOT': '/',
}),
FileSystem: () => MemoryFileSystem.test(),
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
CrashReporter: () => WaitingCrashReporter(commandCompleter.future),
Artifacts: () => Artifacts.test(),
......@@ -207,13 +209,109 @@ void main() {
'FLUTTER_ROOT': '/',
}
),
FileSystem: () => MemoryFileSystem.test(),
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
UserMessages: () => CustomBugInstructions(),
Artifacts: () => Artifacts.test(),
CrashReporter: () => WaitingCrashReporter(Future<void>.value()),
HttpClientFactory: () => () => FakeHttpClient.any(),
});
group('in directory without permission', () {
setUp(() {
bool inTestSetup = true;
fileSystem = MemoryFileSystem(opHandle: (String context, FileSystemOp operation) {
if (inTestSetup) {
// Allow all operations during test setup.
return;
}
const Set<FileSystemOp> disallowedOperations = <FileSystemOp>{
FileSystemOp.create,
FileSystemOp.delete,
FileSystemOp.copy,
FileSystemOp.write,
};
// Make current_directory not writable.
if (context.startsWith('/current_directory') && disallowedOperations.contains(operation)) {
throw FileSystemException('No permission, context = $context, operation = $operation');
}
});
final Directory currentDirectory = fileSystem.directory('/current_directory');
currentDirectory.createSync();
fileSystem.currentDirectory = currentDirectory;
inTestSetup = false;
});
testUsingContext('create local report in temporary directory', () async {
// Since crash reporting calls the doctor, which checks for the devtools
// version file in the cache, write a version file to the memory fs.
Cache.flutterRoot = '/path/to/flutter';
final Directory devtoolsDir = globals.fs.directory(
'${Cache.flutterRoot}/bin/cache/dart-sdk/bin/resources/devtools',
)..createSync(recursive: true);
devtoolsDir.childFile('version.json').writeAsStringSync(
'{"version": "1.2.3"}',
);
final Completer<void> completer = Completer<void>();
// runner.run() asynchronously calls the exit function set above, so we
// catch it in a zone.
unawaited(runZoned<Future<void>?>(
() {
unawaited(runner.run(
<String>['crash'],
() => <FlutterCommand>[
CrashingFlutterCommand(),
],
// This flutterVersion disables crash reporting.
flutterVersion: '[user-branch]/',
reportCrashes: true,
shutdownHooks: ShutdownHooks(),
));
return null;
},
onError: (Object error, StackTrace stack) { // ignore: deprecated_member_use
expect(firstExitCode, isNotNull);
expect(firstExitCode, isNot(0));
expect(error.toString(), 'Exception: test exit');
completer.complete();
},
));
await completer.future;
final String errorText = testLogger.errorText;
expect(
errorText,
containsIgnoringWhitespace('Oops; flutter has exited unexpectedly: "Exception: an exception % --".\n'),
);
final File log = globals.fs.systemTempDirectory.childFile('flutter_01.log');
final String logContents = log.readAsStringSync();
expect(logContents, contains(kCustomBugInstructions));
expect(logContents, contains('flutter crash'));
expect(logContents, contains('Exception: an exception % --'));
expect(logContents, contains('CrashingFlutterCommand.runCommand'));
expect(logContents, contains('[!] Flutter'));
final CrashDetails sentDetails = (globals.crashReporter! as WaitingCrashReporter)._details;
expect(sentDetails.command, 'flutter crash');
expect(sentDetails.error.toString(), 'Exception: an exception % --');
expect(sentDetails.stackTrace.toString(), contains('CrashingFlutterCommand.runCommand'));
expect(await sentDetails.doctorText.text, contains('[!] Flutter'));
}, overrides: <Type, Generator>{
Platform: () => FakePlatform(
environment: <String, String>{
'FLUTTER_ANALYTICS_LOG_FILE': 'test',
'FLUTTER_ROOT': '/',
}
),
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
UserMessages: () => CustomBugInstructions(),
Artifacts: () => Artifacts.test(),
CrashReporter: () => WaitingCrashReporter(Future<void>.value()),
HttpClientFactory: () => () => FakeHttpClient.any(),
});
});
});
}
......
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