Unverified Commit 3d7cd359 authored by Christopher Fujino's avatar Christopher Fujino Committed by GitHub

[flutter_tools] Run ShutdownHooks when handling signals (#134590)

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

Prior to this fix, `ShutdownHooks` were run in the private helper
function `_exit()` defined in the `package:flutter_tools/runner.dart`
library. Independent of this, the tool had signal handling logic that
traps SIGINT and SIGTERM. However, these handlers called `exit()` from
`dart:io`, and didn't run these hooks.

This PR moves the `_exit()` private helper to
`package:flutter_tools/src/base/process.dart` and renames it to
`exitWithHooks()`, so that it can be called by the signal handlers in
`package:flutter_tools/src/base/signals.dart`.
parent ab1b865e
...@@ -20,7 +20,6 @@ import 'src/context_runner.dart'; ...@@ -20,7 +20,6 @@ import 'src/context_runner.dart';
import 'src/doctor.dart'; import 'src/doctor.dart';
import 'src/globals.dart' as globals; import 'src/globals.dart' as globals;
import 'src/reporting/crash_reporting.dart'; import 'src/reporting/crash_reporting.dart';
import 'src/reporting/first_run.dart';
import 'src/reporting/reporting.dart'; import 'src/reporting/reporting.dart';
import 'src/runner/flutter_command.dart'; import 'src/runner/flutter_command.dart';
import 'src/runner/flutter_command_runner.dart'; import 'src/runner/flutter_command_runner.dart';
...@@ -115,7 +114,7 @@ Future<int> run( ...@@ -115,7 +114,7 @@ Future<int> run(
// Triggering [runZoned]'s error callback does not necessarily mean that // Triggering [runZoned]'s error callback does not necessarily mean that
// we stopped executing the body. See https://github.com/dart-lang/sdk/issues/42150. // we stopped executing the body. See https://github.com/dart-lang/sdk/issues/42150.
if (firstError == null) { if (firstError == null) {
return await _exit(0, shutdownHooks: shutdownHooks); return await exitWithHooks(0, shutdownHooks: shutdownHooks);
} }
// We already hit some error, so don't return success. The error path // We already hit some error, so don't return success. The error path
...@@ -151,7 +150,7 @@ Future<int> _handleToolError( ...@@ -151,7 +150,7 @@ Future<int> _handleToolError(
globals.printError('${error.message}\n'); globals.printError('${error.message}\n');
globals.printError("Run 'flutter -h' (or 'flutter <command> -h') for available flutter commands and options."); globals.printError("Run 'flutter -h' (or 'flutter <command> -h') for available flutter commands and options.");
// Argument error exit code. // Argument error exit code.
return _exit(64, shutdownHooks: shutdownHooks); return exitWithHooks(64, shutdownHooks: shutdownHooks);
} else if (error is ToolExit) { } else if (error is ToolExit) {
if (error.message != null) { if (error.message != null) {
globals.printError(error.message!); globals.printError(error.message!);
...@@ -159,14 +158,14 @@ Future<int> _handleToolError( ...@@ -159,14 +158,14 @@ Future<int> _handleToolError(
if (verbose) { if (verbose) {
globals.printError('\n$stackTrace\n'); globals.printError('\n$stackTrace\n');
} }
return _exit(error.exitCode ?? 1, shutdownHooks: shutdownHooks); return exitWithHooks(error.exitCode ?? 1, shutdownHooks: shutdownHooks);
} else if (error is ProcessExit) { } else if (error is ProcessExit) {
// We've caught an exit code. // We've caught an exit code.
if (error.immediate) { if (error.immediate) {
exit(error.exitCode); exit(error.exitCode);
return error.exitCode; return error.exitCode;
} else { } else {
return _exit(error.exitCode, shutdownHooks: shutdownHooks); return exitWithHooks(error.exitCode, shutdownHooks: shutdownHooks);
} }
} else { } else {
// We've crashed; emit a log report. // We've crashed; emit a log report.
...@@ -176,7 +175,7 @@ Future<int> _handleToolError( ...@@ -176,7 +175,7 @@ Future<int> _handleToolError(
// Print the stack trace on the bots - don't write a crash report. // Print the stack trace on the bots - don't write a crash report.
globals.stdio.stderrWrite('$error\n'); globals.stdio.stderrWrite('$error\n');
globals.stdio.stderrWrite('$stackTrace\n'); globals.stdio.stderrWrite('$stackTrace\n');
return _exit(1, shutdownHooks: shutdownHooks); return exitWithHooks(1, shutdownHooks: shutdownHooks);
} }
// Report to both [Usage] and [CrashReportSender]. // Report to both [Usage] and [CrashReportSender].
...@@ -217,7 +216,7 @@ Future<int> _handleToolError( ...@@ -217,7 +216,7 @@ Future<int> _handleToolError(
final File file = await _createLocalCrashReport(details); final File file = await _createLocalCrashReport(details);
await globals.crashReporter!.informUser(details, file); await globals.crashReporter!.informUser(details, file);
return _exit(1, shutdownHooks: shutdownHooks); return exitWithHooks(1, shutdownHooks: shutdownHooks);
// This catch catches all exceptions to ensure the message below is printed. // This catch catches all exceptions to ensure the message below is printed.
} catch (error, st) { // ignore: avoid_catches_without_on_clauses } catch (error, st) { // ignore: avoid_catches_without_on_clauses
globals.stdio.stderrWrite( globals.stdio.stderrWrite(
...@@ -283,76 +282,3 @@ Future<File> _createLocalCrashReport(CrashDetails details) async { ...@@ -283,76 +282,3 @@ Future<File> _createLocalCrashReport(CrashDetails details) async {
return crashFile; return crashFile;
} }
Future<int> _exit(int code, {required ShutdownHooks shutdownHooks}) async {
// Need to get the boolean returned from `messenger.shouldDisplayLicenseTerms()`
// before invoking the print welcome method because the print welcome method
// will set `messenger.shouldDisplayLicenseTerms()` to false
final FirstRunMessenger messenger =
FirstRunMessenger(persistentToolState: globals.persistentToolState!);
final bool legacyAnalyticsMessageShown =
messenger.shouldDisplayLicenseTerms();
// Prints the welcome message if needed for legacy analytics.
globals.flutterUsage.printWelcome();
// Ensure that the consent message has been displayed for unified analytics
if (globals.analytics.shouldShowMessage) {
globals.logger.printStatus(globals.analytics.getConsentMessage);
if (!globals.flutterUsage.enabled) {
globals.printStatus(
'Please note that analytics reporting was already disabled, '
'and will continue to be disabled.\n');
}
// Because the legacy analytics may have also sent a message,
// the conditional below will print additional messaging informing
// users that the two consent messages they are receiving is not a
// bug
if (legacyAnalyticsMessageShown) {
globals.logger
.printStatus('You have received two consent messages because '
'the flutter tool is migrating to a new analytics system. '
'Disabling analytics collection will disable both the legacy '
'and new analytics collection systems. '
'You can disable analytics reporting by running `flutter --disable-analytics`\n');
}
// Invoking this will onboard the flutter tool onto
// the package on the developer's machine and will
// allow for events to be sent to Google Analytics
// on subsequent runs of the flutter tool (ie. no events
// will be sent on the first run to allow developers to
// opt out of collection)
globals.analytics.clientShowedMessage();
}
// Send any last analytics calls that are in progress without overly delaying
// the tool's exit (we wait a maximum of 250ms).
if (globals.flutterUsage.enabled) {
final Stopwatch stopwatch = Stopwatch()..start();
await globals.flutterUsage.ensureAnalyticsSent();
globals.printTrace('ensureAnalyticsSent: ${stopwatch.elapsedMilliseconds}ms');
}
// Run shutdown hooks before flushing logs
await shutdownHooks.runShutdownHooks(globals.logger);
final Completer<void> completer = Completer<void>();
// Give the task / timer queue one cycle through before we hard exit.
Timer.run(() {
try {
globals.printTrace('exiting with code $code');
exit(code);
completer.complete();
// This catches all exceptions because the error is propagated on the
// completer.
} catch (error, stackTrace) { // ignore: avoid_catches_without_on_clauses
completer.completeError(error, stackTrace);
}
});
await completer.future;
return code;
}
...@@ -8,6 +8,8 @@ import 'package:meta/meta.dart'; ...@@ -8,6 +8,8 @@ import 'package:meta/meta.dart';
import 'package:process/process.dart'; import 'package:process/process.dart';
import '../convert.dart'; import '../convert.dart';
import '../globals.dart' as globals;
import '../reporting/first_run.dart';
import 'io.dart'; import 'io.dart';
import 'logger.dart'; import 'logger.dart';
...@@ -564,3 +566,76 @@ class _DefaultProcessUtils implements ProcessUtils { ...@@ -564,3 +566,76 @@ class _DefaultProcessUtils implements ProcessUtils {
} }
} }
} }
Future<int> exitWithHooks(int code, {required ShutdownHooks shutdownHooks}) async {
// Need to get the boolean returned from `messenger.shouldDisplayLicenseTerms()`
// before invoking the print welcome method because the print welcome method
// will set `messenger.shouldDisplayLicenseTerms()` to false
final FirstRunMessenger messenger =
FirstRunMessenger(persistentToolState: globals.persistentToolState!);
final bool legacyAnalyticsMessageShown =
messenger.shouldDisplayLicenseTerms();
// Prints the welcome message if needed for legacy analytics.
globals.flutterUsage.printWelcome();
// Ensure that the consent message has been displayed for unified analytics
if (globals.analytics.shouldShowMessage) {
globals.logger.printStatus(globals.analytics.getConsentMessage);
if (!globals.flutterUsage.enabled) {
globals.printStatus(
'Please note that analytics reporting was already disabled, '
'and will continue to be disabled.\n');
}
// Because the legacy analytics may have also sent a message,
// the conditional below will print additional messaging informing
// users that the two consent messages they are receiving is not a
// bug
if (legacyAnalyticsMessageShown) {
globals.logger
.printStatus('You have received two consent messages because '
'the flutter tool is migrating to a new analytics system. '
'Disabling analytics collection will disable both the legacy '
'and new analytics collection systems. '
'You can disable analytics reporting by running `flutter --disable-analytics`\n');
}
// Invoking this will onboard the flutter tool onto
// the package on the developer's machine and will
// allow for events to be sent to Google Analytics
// on subsequent runs of the flutter tool (ie. no events
// will be sent on the first run to allow developers to
// opt out of collection)
globals.analytics.clientShowedMessage();
}
// Send any last analytics calls that are in progress without overly delaying
// the tool's exit (we wait a maximum of 250ms).
if (globals.flutterUsage.enabled) {
final Stopwatch stopwatch = Stopwatch()..start();
await globals.flutterUsage.ensureAnalyticsSent();
globals.printTrace('ensureAnalyticsSent: ${stopwatch.elapsedMilliseconds}ms');
}
// Run shutdown hooks before flushing logs
await shutdownHooks.runShutdownHooks(globals.logger);
final Completer<void> completer = Completer<void>();
// Give the task / timer queue one cycle through before we hard exit.
Timer.run(() {
try {
globals.printTrace('exiting with code $code');
exit(code);
completer.complete();
// This catches all exceptions because the error is propagated on the
// completer.
} catch (error, stackTrace) { // ignore: avoid_catches_without_on_clauses
completer.completeError(error, stackTrace);
}
});
await completer.future;
return code;
}
...@@ -6,6 +6,8 @@ import 'dart:async'; ...@@ -6,6 +6,8 @@ import 'dart:async';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import '../base/process.dart';
import '../globals.dart' as globals;
import 'async_guard.dart'; import 'async_guard.dart';
import 'io.dart'; import 'io.dart';
...@@ -18,7 +20,8 @@ abstract class Signals { ...@@ -18,7 +20,8 @@ abstract class Signals {
@visibleForTesting @visibleForTesting
factory Signals.test({ factory Signals.test({
List<ProcessSignal> exitSignals = defaultExitSignals, List<ProcessSignal> exitSignals = defaultExitSignals,
}) => LocalSignals._(exitSignals); ShutdownHooks? shutdownHooks,
}) => LocalSignals._(exitSignals, shutdownHooks: shutdownHooks);
// The default list of signals that should cause the process to exit. // The default list of signals that should cause the process to exit.
static const List<ProcessSignal> defaultExitSignals = <ProcessSignal>[ static const List<ProcessSignal> defaultExitSignals = <ProcessSignal>[
...@@ -50,13 +53,17 @@ abstract class Signals { ...@@ -50,13 +53,17 @@ abstract class Signals {
/// We use a singleton instance of this class to ensure that all handlers for /// We use a singleton instance of this class to ensure that all handlers for
/// fatal signals run before this class calls exit(). /// fatal signals run before this class calls exit().
class LocalSignals implements Signals { class LocalSignals implements Signals {
LocalSignals._(this.exitSignals); LocalSignals._(
this.exitSignals, {
ShutdownHooks? shutdownHooks,
}) : _shutdownHooks = shutdownHooks ?? globals.shutdownHooks;
static LocalSignals instance = LocalSignals._( static LocalSignals instance = LocalSignals._(
Signals.defaultExitSignals, Signals.defaultExitSignals,
); );
final List<ProcessSignal> exitSignals; final List<ProcessSignal> exitSignals;
final ShutdownHooks _shutdownHooks;
// A table mapping (signal, token) -> signal handler. // A table mapping (signal, token) -> signal handler.
final Map<ProcessSignal, Map<Object, SignalHandler>> _handlersTable = final Map<ProcessSignal, Map<Object, SignalHandler>> _handlersTable =
...@@ -144,7 +151,7 @@ class LocalSignals implements Signals { ...@@ -144,7 +151,7 @@ class LocalSignals implements Signals {
// If this was a signal that should cause the process to go down, then // If this was a signal that should cause the process to go down, then
// call exit(); // call exit();
if (_shouldExitFor(s)) { if (_shouldExitFor(s)) {
exit(0); await exitWithHooks(0, shutdownHooks: _shutdownHooks);
} }
} }
......
...@@ -6,19 +6,24 @@ import 'dart:async'; ...@@ -6,19 +6,24 @@ import 'dart:async';
import 'dart:io' as io; import 'dart:io' as io;
import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/base/signals.dart'; import 'package:flutter_tools/src/base/signals.dart';
import 'package:test/fake.dart'; import 'package:test/fake.dart';
import '../../src/common.dart'; import '../../src/common.dart';
import '../../src/context.dart';
void main() { void main() {
group('Signals', () { group('Signals', () {
late Signals signals; late Signals signals;
late FakeProcessSignal fakeSignal; late FakeProcessSignal fakeSignal;
late ProcessSignal signalUnderTest; late ProcessSignal signalUnderTest;
late FakeShutdownHooks shutdownHooks;
setUp(() { setUp(() {
signals = Signals.test(); shutdownHooks = FakeShutdownHooks();
signals = Signals.test(shutdownHooks: shutdownHooks);
fakeSignal = FakeProcessSignal(); fakeSignal = FakeProcessSignal();
signalUnderTest = ProcessSignal(fakeSignal); signalUnderTest = ProcessSignal(fakeSignal);
}); });
...@@ -168,9 +173,10 @@ void main() { ...@@ -168,9 +173,10 @@ void main() {
expect(errList, isEmpty); expect(errList, isEmpty);
}); });
testWithoutContext('all handlers for exiting signals are run before exit', () async { testUsingContext('all handlers for exiting signals are run before exit', () async {
final Signals signals = Signals.test( final Signals signals = Signals.test(
exitSignals: <ProcessSignal>[signalUnderTest], exitSignals: <ProcessSignal>[signalUnderTest],
shutdownHooks: shutdownHooks,
); );
final Completer<void> completer = Completer<void>(); final Completer<void> completer = Completer<void>();
bool first = false; bool first = false;
...@@ -201,6 +207,27 @@ void main() { ...@@ -201,6 +207,27 @@ void main() {
fakeSignal.controller.add(fakeSignal); fakeSignal.controller.add(fakeSignal);
await completer.future; await completer.future;
expect(shutdownHooks.ranShutdownHooks, isTrue);
});
testUsingContext('ShutdownHooks run before exiting', () async {
final Signals signals = Signals.test(
exitSignals: <ProcessSignal>[signalUnderTest],
shutdownHooks: shutdownHooks,
);
final Completer<void> completer = Completer<void>();
setExitFunctionForTests((int exitCode) {
expect(exitCode, 0);
restoreExitFunction();
completer.complete();
});
signals.addHandler(signalUnderTest, (ProcessSignal s) {});
fakeSignal.controller.add(fakeSignal);
await completer.future;
expect(shutdownHooks.ranShutdownHooks, isTrue);
}); });
}); });
} }
...@@ -211,3 +238,12 @@ class FakeProcessSignal extends Fake implements io.ProcessSignal { ...@@ -211,3 +238,12 @@ class FakeProcessSignal extends Fake implements io.ProcessSignal {
@override @override
Stream<io.ProcessSignal> watch() => controller.stream; Stream<io.ProcessSignal> watch() => controller.stream;
} }
class FakeShutdownHooks extends Fake implements ShutdownHooks {
bool ranShutdownHooks = false;
@override
Future<void> runShutdownHooks(Logger logger) async {
ranShutdownHooks = 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