blob: 9479beaee42ecd322f28ed5323f199faa3e37f98 [file] [log] [blame]
Todd Volkert454db9d2017-11-09 21:45:31 -08001// Copyright 2015 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6
7import 'package:args/command_runner.dart';
8import 'package:intl/intl_standalone.dart' as intl;
9import 'package:meta/meta.dart';
10import 'package:process/process.dart';
11
12import 'src/artifacts.dart';
13import 'src/base/common.dart';
14import 'src/base/config.dart';
15import 'src/base/context.dart';
16import 'src/base/file_system.dart';
17import 'src/base/io.dart';
18import 'src/base/logger.dart';
19import 'src/base/platform.dart';
20import 'src/base/process.dart';
21import 'src/base/terminal.dart';
22import 'src/base/utils.dart';
23import 'src/cache.dart';
24import 'src/crash_reporting.dart';
25import 'src/devfs.dart';
26import 'src/device.dart';
27import 'src/doctor.dart';
28import 'src/globals.dart';
29import 'src/ios/simulators.dart';
30import 'src/run_hot.dart';
31import 'src/runner/flutter_command.dart';
32import 'src/runner/flutter_command_runner.dart';
33import 'src/usage.dart';
34import 'src/version.dart';
35
36/// Runs the Flutter tool with support for the specified list of [commands].
37Future<int> run(
38 List<String> args,
39 List<FlutterCommand> commands, {
40 bool verbose: false,
41 bool verboseHelp: false,
42 bool reportCrashes,
43 String flutterVersion,
44}) async {
45 reportCrashes ??= !isRunningOnBot;
46
47 if (verboseHelp) {
48 // Remove the verbose option; for help, users don't need to see verbose logs.
49 args = new List<String>.from(args);
50 args.removeWhere((String option) => option == '-v' || option == '--verbose');
51 }
52
53 final FlutterCommandRunner runner = new FlutterCommandRunner(verboseHelp: verboseHelp);
54 commands.forEach(runner.addCommand);
55
56 // Construct a context.
57 final AppContext _executableContext = new AppContext();
58
59 // Make the context current.
60 return await _executableContext.runInZone(() async {
61 // Initialize the context with some defaults.
62 // NOTE: Similar lists also exist in `bin/fuchsia_builder.dart` and
63 // `test/src/context.dart`. If you update this list of defaults, look
64 // in those locations as well to see if you need a similar update there.
65
66 // Seed these context entries first since others depend on them
67 context.putIfAbsent(Stdio, () => const Stdio());
68 context.putIfAbsent(Platform, () => const LocalPlatform());
69 context.putIfAbsent(FileSystem, () => const LocalFileSystem());
70 context.putIfAbsent(ProcessManager, () => const LocalProcessManager());
71 context.putIfAbsent(AnsiTerminal, () => new AnsiTerminal());
72 context.putIfAbsent(Logger, () => platform.isWindows ? new WindowsStdoutLogger() : new StdoutLogger());
73 context.putIfAbsent(Config, () => new Config());
74
75 // Order-independent context entries
76 context.putIfAbsent(DeviceManager, () => new DeviceManager());
77 context.putIfAbsent(DevFSConfig, () => new DevFSConfig());
78 context.putIfAbsent(Doctor, () => new Doctor());
79 context.putIfAbsent(HotRunnerConfig, () => new HotRunnerConfig());
80 context.putIfAbsent(Cache, () => new Cache());
81 context.putIfAbsent(Artifacts, () => new CachedArtifacts());
82 context.putIfAbsent(IOSSimulatorUtils, () => new IOSSimulatorUtils());
83 context.putIfAbsent(SimControl, () => new SimControl());
84
85 // Initialize the system locale.
86 await intl.findSystemLocale();
87
88 try {
89 await runner.run(args);
90 await _exit(0);
91 } catch (error, stackTrace) {
92 String getVersion() => flutterVersion ?? FlutterVersion.getVersionString();
93 return await _handleToolError(error, stackTrace, verbose, args, reportCrashes, getVersion);
94 }
95 return 0;
96 });
97}
98
99/// Writes the [string] to one of the standard output streams.
100@visibleForTesting
101typedef void WriteCallback([String string]);
102
103/// Writes a line to STDERR.
104///
105/// Overwrite this in tests to avoid spurious test output.
106@visibleForTesting
107WriteCallback writelnStderr = stderr.writeln;
108
109Future<int> _handleToolError(
110 dynamic error,
111 StackTrace stackTrace,
112 bool verbose,
113 List<String> args,
114 bool reportCrashes,
115 String getFlutterVersion(),
116 ) async {
117 if (error is UsageException) {
118 writelnStderr(error.message);
119 writelnStderr();
120 writelnStderr(
121 "Run 'flutter -h' (or 'flutter <command> -h') for available "
122 'flutter commands and options.'
123 );
124 // Argument error exit code.
125 return _exit(64);
126 } else if (error is ToolExit) {
127 if (error.message != null)
128 writelnStderr(error.message);
129 if (verbose) {
130 writelnStderr();
131 writelnStderr(stackTrace.toString());
132 writelnStderr();
133 }
134 return _exit(error.exitCode ?? 1);
135 } else if (error is ProcessExit) {
136 // We've caught an exit code.
137 if (error.immediate) {
138 exit(error.exitCode);
139 return error.exitCode;
140 } else {
141 return _exit(error.exitCode);
142 }
143 } else {
144 // We've crashed; emit a log report.
145 writelnStderr();
146
147 if (!reportCrashes) {
148 // Print the stack trace on the bots - don't write a crash report.
149 writelnStderr('$error');
150 writelnStderr(stackTrace.toString());
151 return _exit(1);
152 } else {
153 flutterUsage.sendException(error, stackTrace);
154
155 if (error is String)
156 writelnStderr('Oops; flutter has exited unexpectedly: "$error".');
157 else
158 writelnStderr('Oops; flutter has exited unexpectedly.');
159
160 await CrashReportSender.instance.sendReport(
161 error: error,
162 stackTrace: stackTrace,
163 getFlutterVersion: getFlutterVersion,
164 );
165 try {
166 final File file = await _createLocalCrashReport(args, error, stackTrace);
167 writelnStderr(
168 'Crash report written to ${file.path};\n'
169 'please let us know at https://github.com/flutter/flutter/issues.',
170 );
171 return _exit(1);
172 } catch (error) {
173 writelnStderr(
174 'Unable to generate crash report due to secondary error: $error\n'
175 'please let us know at https://github.com/flutter/flutter/issues.',
176 );
177 // Any exception throw here (including one thrown by `_exit()`) will
178 // get caught by our zone's `onError` handler. In order to avoid an
179 // infinite error loop, we throw an error that is recognized above
180 // and will trigger an immediate exit.
181 throw new ProcessExit(1, immediate: true);
182 }
183 }
184 }
185}
186
187/// File system used by the crash reporting logic.
188///
189/// We do not want to use the file system stored in the context because it may
190/// be recording. Additionally, in the case of a crash we do not trust the
191/// integrity of the [AppContext].
192@visibleForTesting
193FileSystem crashFileSystem = const LocalFileSystem();
194
195/// Saves the crash report to a local file.
196Future<File> _createLocalCrashReport(List<String> args, dynamic error, StackTrace stackTrace) async {
197 File crashFile = getUniqueFile(crashFileSystem.currentDirectory, 'flutter', 'log');
198
199 final StringBuffer buffer = new StringBuffer();
200
201 buffer.writeln('Flutter crash report; please file at https://github.com/flutter/flutter/issues.\n');
202
203 buffer.writeln('## command\n');
204 buffer.writeln('flutter ${args.join(' ')}\n');
205
206 buffer.writeln('## exception\n');
207 buffer.writeln('${error.runtimeType}: $error\n');
208 buffer.writeln('```\n$stackTrace```\n');
209
210 buffer.writeln('## flutter doctor\n');
211 buffer.writeln('```\n${await _doctorText()}```');
212
213 try {
214 await crashFile.writeAsString(buffer.toString());
215 } on FileSystemException catch (_) {
216 // Fallback to the system temporary directory.
217 crashFile = getUniqueFile(crashFileSystem.systemTempDirectory, 'flutter', 'log');
218 try {
219 await crashFile.writeAsString(buffer.toString());
220 } on FileSystemException catch (e) {
221 printError('Could not write crash report to disk: $e');
222 printError(buffer.toString());
223 }
224 }
225
226 return crashFile;
227}
228
229Future<String> _doctorText() async {
230 try {
231 final BufferLogger logger = new BufferLogger();
232 final AppContext appContext = new AppContext();
233
234 appContext.setVariable(Logger, logger);
235
236 await appContext.runInZone(() => doctor.diagnose());
237
238 return logger.statusText;
239 } catch (error, trace) {
240 return 'encountered exception: $error\n\n${trace.toString().trim()}\n';
241 }
242}
243
244Future<int> _exit(int code) async {
245 if (flutterUsage.isFirstRun)
246 flutterUsage.printWelcome();
247
248 // Send any last analytics calls that are in progress without overly delaying
249 // the tool's exit (we wait a maximum of 250ms).
250 if (flutterUsage.enabled) {
251 final Stopwatch stopwatch = new Stopwatch()..start();
252 await flutterUsage.ensureAnalyticsSent();
253 printTrace('ensureAnalyticsSent: ${stopwatch.elapsedMilliseconds}ms');
254 }
255
256 // Run shutdown hooks before flushing logs
257 await runShutdownHooks();
258
259 final Completer<Null> completer = new Completer<Null>();
260
261 // Give the task / timer queue one cycle through before we hard exit.
262 Timer.run(() {
263 try {
264 printTrace('exiting with code $code');
265 exit(code);
266 completer.complete();
267 } catch (error, stackTrace) {
268 completer.completeError(error, stackTrace);
269 }
270 });
271
272 await completer.future;
273 return code;
274}