diff --git a/packages/flutter_tools/lib/runner.dart b/packages/flutter_tools/lib/runner.dart index 699fe6771a..f1a46f9a9a 100644 --- a/packages/flutter_tools/lib/runner.dart +++ b/packages/flutter_tools/lib/runner.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'package:args/command_runner.dart'; import 'package:intl/intl.dart' as intl; import 'package:intl/intl_standalone.dart' as intl_standalone; +import 'package:process/process.dart'; import 'package:unified_analytics/unified_analytics.dart'; import 'src/base/async_guard.dart'; @@ -185,6 +186,16 @@ Future _handleToolError( } else { return exitWithHooks(error.exitCode, shutdownHooks: shutdownHooks); } + } else if (error is ProcessException && + _isErrorDueToGitMissing(error, globals.processManager, globals.logger)) { + globals.printError('${error.message}\n'); + globals.printError( + 'An error was encountered when trying to run git.\n' + "Please ensure git is installed and available in your system's search path. " + 'See https://docs.flutter.dev/get-started/install for instructions on ' + 'installing git for your platform.', + ); + return exitWithHooks(1, shutdownHooks: shutdownHooks); } else { // We've crashed; emit a log report. globals.stdio.stderrWrite('\n'); @@ -301,3 +312,22 @@ Future _createLocalCrashReport(CrashDetails details) async { return crashFile; } + +bool _isErrorDueToGitMissing( + ProcessException exception, + ProcessManager processManager, + Logger logger, +) { + if (!exception.message.contains('git')) { + return false; + } + + try { + return !processManager.canRun('git'); + } on Object catch (error) { + logger.printTrace( + 'Unable to check whether git is runnable: $error\n' + ); + return true; + } +} diff --git a/packages/flutter_tools/lib/src/web/chrome.dart b/packages/flutter_tools/lib/src/web/chrome.dart index f10f3ac9ce..c9a5fdab81 100644 --- a/packages/flutter_tools/lib/src/web/chrome.dart +++ b/packages/flutter_tools/lib/src/web/chrome.dart @@ -578,7 +578,7 @@ Future getChromeTabGuarded( void Function(Object error, StackTrace stackTrace)? onIoError, }) async { try { - return asyncGuard( + return await asyncGuard( () => chromeConnection.getTab( accept, retryFor: retryFor, diff --git a/packages/flutter_tools/test/general.shard/runner/runner_test.dart b/packages/flutter_tools/test/general.shard/runner/runner_test.dart index 3bb02b8ccf..d67899a7a0 100644 --- a/packages/flutter_tools/test/general.shard/runner/runner_test.dart +++ b/packages/flutter_tools/test/general.shard/runner/runner_test.dart @@ -10,6 +10,7 @@ import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/bot_detector.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart' as io; +import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/net.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/process.dart'; @@ -19,6 +20,7 @@ import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/reporting/crash_reporting.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:flutter_tools/src/runner/flutter_command.dart'; +import 'package:test/fake.dart'; import 'package:unified_analytics/unified_analytics.dart'; import '../../src/common.dart'; @@ -29,12 +31,10 @@ import '../../src/fakes.dart'; const String kCustomBugInstructions = 'These are instructions to report with a custom bug tracker.'; void main() { - int? firstExitCode; - late MemoryFileSystem fileSystem; - - group('runner', () { + group('runner (crash reporting)', () { + int? firstExitCode; + late MemoryFileSystem fileSystem; late FakeAnalytics fakeAnalytics; - late TestUsage testUsage; setUp(() { // Instead of exiting with dart:io exit(), this causes an exception to @@ -59,8 +59,6 @@ void main() { fs: fileSystem, fakeFlutterVersion: FakeFlutterVersion(), ); - - testUsage = TestUsage(); }); tearDown(() { @@ -327,10 +325,84 @@ void main() { HttpClientFactory: () => () => FakeHttpClient.any(), }); }); + }); + + group('runner', () { + late MemoryFileSystem fs; + + setUp(() { + io.setExitFunctionForTests((int exitCode) {}); + + fs = MemoryFileSystem.test(); + + Cache.disableLocking(); + }); + + tearDown(() { + io.restoreExitFunction(); + Cache.enableLocking(); + }); + + testUsingContext("catches ProcessException calling git because it's not available", () async { + final _GitNotFoundFlutterCommand command = _GitNotFoundFlutterCommand(); + + await runner.run( + [command.name], + () => [ + command, + ], + // This flutterVersion disables crash reporting. + flutterVersion: '[user-branch]/', + reportCrashes: false, + shutdownHooks: ShutdownHooks(), + ); + + expect( + (globals.logger as BufferLogger).errorText, + 'Failed to find "git" in the search path.\n' + '\n' + 'An error was encountered when trying to run git.\n' + "Please ensure git is installed and available in your system's search path. " + 'See https://docs.flutter.dev/get-started/install for instructions on installing git for your platform.\n'); + }, + overrides: { + FileSystem: () => fs, + Artifacts: () => Artifacts.test(), + ProcessManager: () => + FakeProcessManager.any()..excludedExecutables.add('git'), + }, + ); + + testUsingContext('handles ProcessException calling git when ProcessManager.canRun fails', () async { + final _GitNotFoundFlutterCommand command = _GitNotFoundFlutterCommand(); + + await runner.run( + [command.name], + () => [ + command, + ], + // This flutterVersion disables crash reporting. + flutterVersion: '[user-branch]/', + reportCrashes: false, + shutdownHooks: ShutdownHooks(), + ); + + expect( + (globals.logger as BufferLogger).errorText, + 'Failed to find "git" in the search path.\n' + '\n' + 'An error was encountered when trying to run git.\n' + "Please ensure git is installed and available in your system's search path. " + 'See https://docs.flutter.dev/get-started/install for instructions on installing git for your platform.\n'); + }, + overrides: { + FileSystem: () => fs, + Artifacts: () => Artifacts.test(), + ProcessManager: () => _ErrorOnCanRunFakeProcessManager(), + }, + ); testUsingContext('do not print welcome on bots', () async { - io.setExitFunctionForTests((int exitCode) {}); - await runner.run( ['--version', '--machine'], () => [], @@ -339,13 +411,13 @@ void main() { shutdownHooks: ShutdownHooks(), ); - expect(testUsage.printedWelcome, false); + expect((globals.flutterUsage as TestUsage).printedWelcome, false); }, overrides: { FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), BotDetector: () => const FakeBotDetector(true), - Usage: () => testUsage, + Usage: () => TestUsage(), }, ); }); @@ -570,6 +642,23 @@ class CrashingFlutterCommand extends FlutterCommand { } } +class _GitNotFoundFlutterCommand extends FlutterCommand { + @override + String get description => ''; + + @override + String get name => 'git-not-found'; + + @override + Future runCommand() { + throw const io.ProcessException( + 'git', + ['log'], + 'Failed to find "git" in the search path.', + ); + } +} + class CrashingUsage implements Usage { CrashingUsage() : _impl = Usage( versionOverride: '[user-branch]', @@ -670,3 +759,14 @@ class WaitingCrashReporter implements CrashReporter { return _future; } } + +class _ErrorOnCanRunFakeProcessManager extends Fake implements FakeProcessManager { + final FakeProcessManager delegate = FakeProcessManager.any(); + @override + bool canRun(dynamic executable, {String? workingDirectory}) { + if (executable == 'git') { + throw Exception("oh no, we couldn't check for git!"); + } + return delegate.canRun(executable, workingDirectory: workingDirectory); + } +}