[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`.
This commit is contained in:
parent
ab1b865e58
commit
3d7cd3594a
@ -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(
|
|||||||
// 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(
|
|||||||
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(
|
|||||||
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(
|
|||||||
// 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(
|
|||||||
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 {
|
|||||||
|
|
||||||
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';
|
|||||||
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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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';
|
|||||||
|
|
||||||
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 {
|
|||||||
@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 {
|
|||||||
/// 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 {
|
|||||||
// 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';
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
|
|
||||||
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 {
|
|||||||
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user