Skip to content

Commit 3d7cd35

Browse files
[flutter_tools] Run ShutdownHooks when handling signals (flutter#134590)
Fixes flutter#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`.
1 parent ab1b865 commit 3d7cd35

File tree

4 files changed

+129
-85
lines changed

4 files changed

+129
-85
lines changed

packages/flutter_tools/lib/runner.dart

Lines changed: 6 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import 'src/context_runner.dart';
2020
import 'src/doctor.dart';
2121
import 'src/globals.dart' as globals;
2222
import 'src/reporting/crash_reporting.dart';
23-
import 'src/reporting/first_run.dart';
2423
import 'src/reporting/reporting.dart';
2524
import 'src/runner/flutter_command.dart';
2625
import 'src/runner/flutter_command_runner.dart';
@@ -115,7 +114,7 @@ Future<int> run(
115114
// Triggering [runZoned]'s error callback does not necessarily mean that
116115
// we stopped executing the body. See https://github.com/dart-lang/sdk/issues/42150.
117116
if (firstError == null) {
118-
return await _exit(0, shutdownHooks: shutdownHooks);
117+
return await exitWithHooks(0, shutdownHooks: shutdownHooks);
119118
}
120119

121120
// We already hit some error, so don't return success. The error path
@@ -151,22 +150,22 @@ Future<int> _handleToolError(
151150
globals.printError('${error.message}\n');
152151
globals.printError("Run 'flutter -h' (or 'flutter <command> -h') for available flutter commands and options.");
153152
// Argument error exit code.
154-
return _exit(64, shutdownHooks: shutdownHooks);
153+
return exitWithHooks(64, shutdownHooks: shutdownHooks);
155154
} else if (error is ToolExit) {
156155
if (error.message != null) {
157156
globals.printError(error.message!);
158157
}
159158
if (verbose) {
160159
globals.printError('\n$stackTrace\n');
161160
}
162-
return _exit(error.exitCode ?? 1, shutdownHooks: shutdownHooks);
161+
return exitWithHooks(error.exitCode ?? 1, shutdownHooks: shutdownHooks);
163162
} else if (error is ProcessExit) {
164163
// We've caught an exit code.
165164
if (error.immediate) {
166165
exit(error.exitCode);
167166
return error.exitCode;
168167
} else {
169-
return _exit(error.exitCode, shutdownHooks: shutdownHooks);
168+
return exitWithHooks(error.exitCode, shutdownHooks: shutdownHooks);
170169
}
171170
} else {
172171
// We've crashed; emit a log report.
@@ -176,7 +175,7 @@ Future<int> _handleToolError(
176175
// Print the stack trace on the bots - don't write a crash report.
177176
globals.stdio.stderrWrite('$error\n');
178177
globals.stdio.stderrWrite('$stackTrace\n');
179-
return _exit(1, shutdownHooks: shutdownHooks);
178+
return exitWithHooks(1, shutdownHooks: shutdownHooks);
180179
}
181180

182181
// Report to both [Usage] and [CrashReportSender].
@@ -217,7 +216,7 @@ Future<int> _handleToolError(
217216
final File file = await _createLocalCrashReport(details);
218217
await globals.crashReporter!.informUser(details, file);
219218

220-
return _exit(1, shutdownHooks: shutdownHooks);
219+
return exitWithHooks(1, shutdownHooks: shutdownHooks);
221220
// This catch catches all exceptions to ensure the message below is printed.
222221
} catch (error, st) { // ignore: avoid_catches_without_on_clauses
223222
globals.stdio.stderrWrite(
@@ -283,76 +282,3 @@ Future<File> _createLocalCrashReport(CrashDetails details) async {
283282

284283
return crashFile;
285284
}
286-
287-
Future<int> _exit(int code, {required ShutdownHooks shutdownHooks}) async {
288-
// Need to get the boolean returned from `messenger.shouldDisplayLicenseTerms()`
289-
// before invoking the print welcome method because the print welcome method
290-
// will set `messenger.shouldDisplayLicenseTerms()` to false
291-
final FirstRunMessenger messenger =
292-
FirstRunMessenger(persistentToolState: globals.persistentToolState!);
293-
final bool legacyAnalyticsMessageShown =
294-
messenger.shouldDisplayLicenseTerms();
295-
296-
// Prints the welcome message if needed for legacy analytics.
297-
globals.flutterUsage.printWelcome();
298-
299-
// Ensure that the consent message has been displayed for unified analytics
300-
if (globals.analytics.shouldShowMessage) {
301-
globals.logger.printStatus(globals.analytics.getConsentMessage);
302-
if (!globals.flutterUsage.enabled) {
303-
globals.printStatus(
304-
'Please note that analytics reporting was already disabled, '
305-
'and will continue to be disabled.\n');
306-
}
307-
308-
// Because the legacy analytics may have also sent a message,
309-
// the conditional below will print additional messaging informing
310-
// users that the two consent messages they are receiving is not a
311-
// bug
312-
if (legacyAnalyticsMessageShown) {
313-
globals.logger
314-
.printStatus('You have received two consent messages because '
315-
'the flutter tool is migrating to a new analytics system. '
316-
'Disabling analytics collection will disable both the legacy '
317-
'and new analytics collection systems. '
318-
'You can disable analytics reporting by running `flutter --disable-analytics`\n');
319-
}
320-
321-
// Invoking this will onboard the flutter tool onto
322-
// the package on the developer's machine and will
323-
// allow for events to be sent to Google Analytics
324-
// on subsequent runs of the flutter tool (ie. no events
325-
// will be sent on the first run to allow developers to
326-
// opt out of collection)
327-
globals.analytics.clientShowedMessage();
328-
}
329-
330-
// Send any last analytics calls that are in progress without overly delaying
331-
// the tool's exit (we wait a maximum of 250ms).
332-
if (globals.flutterUsage.enabled) {
333-
final Stopwatch stopwatch = Stopwatch()..start();
334-
await globals.flutterUsage.ensureAnalyticsSent();
335-
globals.printTrace('ensureAnalyticsSent: ${stopwatch.elapsedMilliseconds}ms');
336-
}
337-
338-
// Run shutdown hooks before flushing logs
339-
await shutdownHooks.runShutdownHooks(globals.logger);
340-
341-
final Completer<void> completer = Completer<void>();
342-
343-
// Give the task / timer queue one cycle through before we hard exit.
344-
Timer.run(() {
345-
try {
346-
globals.printTrace('exiting with code $code');
347-
exit(code);
348-
completer.complete();
349-
// This catches all exceptions because the error is propagated on the
350-
// completer.
351-
} catch (error, stackTrace) { // ignore: avoid_catches_without_on_clauses
352-
completer.completeError(error, stackTrace);
353-
}
354-
});
355-
356-
await completer.future;
357-
return code;
358-
}

packages/flutter_tools/lib/src/base/process.dart

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import 'package:meta/meta.dart';
88
import 'package:process/process.dart';
99

1010
import '../convert.dart';
11+
import '../globals.dart' as globals;
12+
import '../reporting/first_run.dart';
1113
import 'io.dart';
1214
import 'logger.dart';
1315

@@ -564,3 +566,76 @@ class _DefaultProcessUtils implements ProcessUtils {
564566
}
565567
}
566568
}
569+
570+
Future<int> exitWithHooks(int code, {required ShutdownHooks shutdownHooks}) async {
571+
// Need to get the boolean returned from `messenger.shouldDisplayLicenseTerms()`
572+
// before invoking the print welcome method because the print welcome method
573+
// will set `messenger.shouldDisplayLicenseTerms()` to false
574+
final FirstRunMessenger messenger =
575+
FirstRunMessenger(persistentToolState: globals.persistentToolState!);
576+
final bool legacyAnalyticsMessageShown =
577+
messenger.shouldDisplayLicenseTerms();
578+
579+
// Prints the welcome message if needed for legacy analytics.
580+
globals.flutterUsage.printWelcome();
581+
582+
// Ensure that the consent message has been displayed for unified analytics
583+
if (globals.analytics.shouldShowMessage) {
584+
globals.logger.printStatus(globals.analytics.getConsentMessage);
585+
if (!globals.flutterUsage.enabled) {
586+
globals.printStatus(
587+
'Please note that analytics reporting was already disabled, '
588+
'and will continue to be disabled.\n');
589+
}
590+
591+
// Because the legacy analytics may have also sent a message,
592+
// the conditional below will print additional messaging informing
593+
// users that the two consent messages they are receiving is not a
594+
// bug
595+
if (legacyAnalyticsMessageShown) {
596+
globals.logger
597+
.printStatus('You have received two consent messages because '
598+
'the flutter tool is migrating to a new analytics system. '
599+
'Disabling analytics collection will disable both the legacy '
600+
'and new analytics collection systems. '
601+
'You can disable analytics reporting by running `flutter --disable-analytics`\n');
602+
}
603+
604+
// Invoking this will onboard the flutter tool onto
605+
// the package on the developer's machine and will
606+
// allow for events to be sent to Google Analytics
607+
// on subsequent runs of the flutter tool (ie. no events
608+
// will be sent on the first run to allow developers to
609+
// opt out of collection)
610+
globals.analytics.clientShowedMessage();
611+
}
612+
613+
// Send any last analytics calls that are in progress without overly delaying
614+
// the tool's exit (we wait a maximum of 250ms).
615+
if (globals.flutterUsage.enabled) {
616+
final Stopwatch stopwatch = Stopwatch()..start();
617+
await globals.flutterUsage.ensureAnalyticsSent();
618+
globals.printTrace('ensureAnalyticsSent: ${stopwatch.elapsedMilliseconds}ms');
619+
}
620+
621+
// Run shutdown hooks before flushing logs
622+
await shutdownHooks.runShutdownHooks(globals.logger);
623+
624+
final Completer<void> completer = Completer<void>();
625+
626+
// Give the task / timer queue one cycle through before we hard exit.
627+
Timer.run(() {
628+
try {
629+
globals.printTrace('exiting with code $code');
630+
exit(code);
631+
completer.complete();
632+
// This catches all exceptions because the error is propagated on the
633+
// completer.
634+
} catch (error, stackTrace) { // ignore: avoid_catches_without_on_clauses
635+
completer.completeError(error, stackTrace);
636+
}
637+
});
638+
639+
await completer.future;
640+
return code;
641+
}

packages/flutter_tools/lib/src/base/signals.dart

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import 'dart:async';
66

77
import 'package:meta/meta.dart';
88

9+
import '../base/process.dart';
10+
import '../globals.dart' as globals;
911
import 'async_guard.dart';
1012
import 'io.dart';
1113

@@ -18,7 +20,8 @@ abstract class Signals {
1820
@visibleForTesting
1921
factory Signals.test({
2022
List<ProcessSignal> exitSignals = defaultExitSignals,
21-
}) => LocalSignals._(exitSignals);
23+
ShutdownHooks? shutdownHooks,
24+
}) => LocalSignals._(exitSignals, shutdownHooks: shutdownHooks);
2225

2326
// The default list of signals that should cause the process to exit.
2427
static const List<ProcessSignal> defaultExitSignals = <ProcessSignal>[
@@ -50,13 +53,17 @@ abstract class Signals {
5053
/// We use a singleton instance of this class to ensure that all handlers for
5154
/// fatal signals run before this class calls exit().
5255
class LocalSignals implements Signals {
53-
LocalSignals._(this.exitSignals);
56+
LocalSignals._(
57+
this.exitSignals, {
58+
ShutdownHooks? shutdownHooks,
59+
}) : _shutdownHooks = shutdownHooks ?? globals.shutdownHooks;
5460

5561
static LocalSignals instance = LocalSignals._(
5662
Signals.defaultExitSignals,
5763
);
5864

5965
final List<ProcessSignal> exitSignals;
66+
final ShutdownHooks _shutdownHooks;
6067

6168
// A table mapping (signal, token) -> signal handler.
6269
final Map<ProcessSignal, Map<Object, SignalHandler>> _handlersTable =
@@ -144,7 +151,7 @@ class LocalSignals implements Signals {
144151
// If this was a signal that should cause the process to go down, then
145152
// call exit();
146153
if (_shouldExitFor(s)) {
147-
exit(0);
154+
await exitWithHooks(0, shutdownHooks: _shutdownHooks);
148155
}
149156
}
150157

packages/flutter_tools/test/general.shard/base/signals_test.dart

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,24 @@ import 'dart:async';
66
import 'dart:io' as io;
77

88
import 'package:flutter_tools/src/base/io.dart';
9+
import 'package:flutter_tools/src/base/logger.dart';
10+
import 'package:flutter_tools/src/base/process.dart';
911
import 'package:flutter_tools/src/base/signals.dart';
1012
import 'package:test/fake.dart';
1113

1214
import '../../src/common.dart';
15+
import '../../src/context.dart';
1316

1417
void main() {
1518
group('Signals', () {
1619
late Signals signals;
1720
late FakeProcessSignal fakeSignal;
1821
late ProcessSignal signalUnderTest;
22+
late FakeShutdownHooks shutdownHooks;
1923

2024
setUp(() {
21-
signals = Signals.test();
25+
shutdownHooks = FakeShutdownHooks();
26+
signals = Signals.test(shutdownHooks: shutdownHooks);
2227
fakeSignal = FakeProcessSignal();
2328
signalUnderTest = ProcessSignal(fakeSignal);
2429
});
@@ -168,9 +173,10 @@ void main() {
168173
expect(errList, isEmpty);
169174
});
170175

171-
testWithoutContext('all handlers for exiting signals are run before exit', () async {
176+
testUsingContext('all handlers for exiting signals are run before exit', () async {
172177
final Signals signals = Signals.test(
173178
exitSignals: <ProcessSignal>[signalUnderTest],
179+
shutdownHooks: shutdownHooks,
174180
);
175181
final Completer<void> completer = Completer<void>();
176182
bool first = false;
@@ -201,6 +207,27 @@ void main() {
201207

202208
fakeSignal.controller.add(fakeSignal);
203209
await completer.future;
210+
expect(shutdownHooks.ranShutdownHooks, isTrue);
211+
});
212+
213+
testUsingContext('ShutdownHooks run before exiting', () async {
214+
final Signals signals = Signals.test(
215+
exitSignals: <ProcessSignal>[signalUnderTest],
216+
shutdownHooks: shutdownHooks,
217+
);
218+
final Completer<void> completer = Completer<void>();
219+
220+
setExitFunctionForTests((int exitCode) {
221+
expect(exitCode, 0);
222+
restoreExitFunction();
223+
completer.complete();
224+
});
225+
226+
signals.addHandler(signalUnderTest, (ProcessSignal s) {});
227+
228+
fakeSignal.controller.add(fakeSignal);
229+
await completer.future;
230+
expect(shutdownHooks.ranShutdownHooks, isTrue);
204231
});
205232
});
206233
}
@@ -211,3 +238,12 @@ class FakeProcessSignal extends Fake implements io.ProcessSignal {
211238
@override
212239
Stream<io.ProcessSignal> watch() => controller.stream;
213240
}
241+
242+
class FakeShutdownHooks extends Fake implements ShutdownHooks {
243+
bool ranShutdownHooks = false;
244+
245+
@override
246+
Future<void> runShutdownHooks(Logger logger) async {
247+
ranShutdownHooks = true;
248+
}
249+
}

0 commit comments

Comments
 (0)