1// 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 '../convert.dart'; 8import '../globals.dart'; 9import 'common.dart'; 10import 'file_system.dart'; 11import 'io.dart'; 12import 'process_manager.dart'; 13import 'utils.dart'; 14 15typedef StringConverter = String Function(String string); 16 17/// A function that will be run before the VM exits. 18typedef ShutdownHook = Future<dynamic> Function(); 19 20// TODO(ianh): We have way too many ways to run subprocesses in this project. 21// Convert most of these into one or more lightweight wrappers around the 22// [ProcessManager] API using named parameters for the various options. 23// See [here](https://github.com/flutter/flutter/pull/14535#discussion_r167041161) 24// for more details. 25 26/// The stage in which a [ShutdownHook] will be run. All shutdown hooks within 27/// a given stage will be started in parallel and will be guaranteed to run to 28/// completion before shutdown hooks in the next stage are started. 29class ShutdownStage implements Comparable<ShutdownStage> { 30 const ShutdownStage._(this.priority); 31 32 /// The stage priority. Smaller values will be run before larger values. 33 final int priority; 34 35 /// The stage before the invocation recording (if one exists) is serialized 36 /// to disk. Tasks performed during this stage *will* be recorded. 37 static const ShutdownStage STILL_RECORDING = ShutdownStage._(1); 38 39 /// The stage during which the invocation recording (if one exists) will be 40 /// serialized to disk. Invocations performed after this stage will not be 41 /// recorded. 42 static const ShutdownStage SERIALIZE_RECORDING = ShutdownStage._(2); 43 44 /// The stage during which a serialized recording will be refined (e.g. 45 /// cleansed for tests, zipped up for bug reporting purposes, etc.). 46 static const ShutdownStage POST_PROCESS_RECORDING = ShutdownStage._(3); 47 48 /// The stage during which temporary files and directories will be deleted. 49 static const ShutdownStage CLEANUP = ShutdownStage._(4); 50 51 @override 52 int compareTo(ShutdownStage other) => priority.compareTo(other.priority); 53} 54 55Map<ShutdownStage, List<ShutdownHook>> _shutdownHooks = <ShutdownStage, List<ShutdownHook>>{}; 56bool _shutdownHooksRunning = false; 57 58/// Registers a [ShutdownHook] to be executed before the VM exits. 59/// 60/// If [stage] is specified, the shutdown hook will be run during the specified 61/// stage. By default, the shutdown hook will be run during the 62/// [ShutdownStage.CLEANUP] stage. 63void addShutdownHook( 64 ShutdownHook shutdownHook, [ 65 ShutdownStage stage = ShutdownStage.CLEANUP, 66]) { 67 assert(!_shutdownHooksRunning); 68 _shutdownHooks.putIfAbsent(stage, () => <ShutdownHook>[]).add(shutdownHook); 69} 70 71/// Runs all registered shutdown hooks and returns a future that completes when 72/// all such hooks have finished. 73/// 74/// Shutdown hooks will be run in groups by their [ShutdownStage]. All shutdown 75/// hooks within a given stage will be started in parallel and will be 76/// guaranteed to run to completion before shutdown hooks in the next stage are 77/// started. 78Future<void> runShutdownHooks() async { 79 printTrace('Running shutdown hooks'); 80 _shutdownHooksRunning = true; 81 try { 82 for (ShutdownStage stage in _shutdownHooks.keys.toList()..sort()) { 83 printTrace('Shutdown hook priority ${stage.priority}'); 84 final List<ShutdownHook> hooks = _shutdownHooks.remove(stage); 85 final List<Future<dynamic>> futures = <Future<dynamic>>[]; 86 for (ShutdownHook shutdownHook in hooks) 87 futures.add(shutdownHook()); 88 await Future.wait<dynamic>(futures); 89 } 90 } finally { 91 _shutdownHooksRunning = false; 92 } 93 assert(_shutdownHooks.isEmpty); 94 printTrace('Shutdown hooks complete'); 95} 96 97Map<String, String> _environment(bool allowReentrantFlutter, [ Map<String, String> environment ]) { 98 if (allowReentrantFlutter) { 99 if (environment == null) 100 environment = <String, String>{'FLUTTER_ALREADY_LOCKED': 'true'}; 101 else 102 environment['FLUTTER_ALREADY_LOCKED'] = 'true'; 103 } 104 105 return environment; 106} 107 108/// This runs the command in the background from the specified working 109/// directory. Completes when the process has been started. 110Future<Process> runCommand( 111 List<String> cmd, { 112 String workingDirectory, 113 bool allowReentrantFlutter = false, 114 Map<String, String> environment, 115}) { 116 _traceCommand(cmd, workingDirectory: workingDirectory); 117 return processManager.start( 118 cmd, 119 workingDirectory: workingDirectory, 120 environment: _environment(allowReentrantFlutter, environment), 121 ); 122} 123 124/// This runs the command and streams stdout/stderr from the child process to 125/// this process' stdout/stderr. Completes with the process's exit code. 126/// 127/// If [filter] is null, no lines are removed. 128/// 129/// If [filter] is non-null, all lines that do not match it are removed. If 130/// [mapFunction] is present, all lines that match [filter] are also forwarded 131/// to [mapFunction] for further processing. 132Future<int> runCommandAndStreamOutput( 133 List<String> cmd, { 134 String workingDirectory, 135 bool allowReentrantFlutter = false, 136 String prefix = '', 137 bool trace = false, 138 RegExp filter, 139 StringConverter mapFunction, 140 Map<String, String> environment, 141}) async { 142 final Process process = await runCommand( 143 cmd, 144 workingDirectory: workingDirectory, 145 allowReentrantFlutter: allowReentrantFlutter, 146 environment: environment, 147 ); 148 final StreamSubscription<String> stdoutSubscription = process.stdout 149 .transform<String>(utf8.decoder) 150 .transform<String>(const LineSplitter()) 151 .where((String line) => filter == null || filter.hasMatch(line)) 152 .listen((String line) { 153 if (mapFunction != null) 154 line = mapFunction(line); 155 if (line != null) { 156 final String message = '$prefix$line'; 157 if (trace) 158 printTrace(message); 159 else 160 printStatus(message, wrap: false); 161 } 162 }); 163 final StreamSubscription<String> stderrSubscription = process.stderr 164 .transform<String>(utf8.decoder) 165 .transform<String>(const LineSplitter()) 166 .where((String line) => filter == null || filter.hasMatch(line)) 167 .listen((String line) { 168 if (mapFunction != null) 169 line = mapFunction(line); 170 if (line != null) 171 printError('$prefix$line', wrap: false); 172 }); 173 174 // Wait for stdout to be fully processed 175 // because process.exitCode may complete first causing flaky tests. 176 await waitGroup<void>(<Future<void>>[ 177 stdoutSubscription.asFuture<void>(), 178 stderrSubscription.asFuture<void>(), 179 ]); 180 181 await waitGroup<void>(<Future<void>>[ 182 stdoutSubscription.cancel(), 183 stderrSubscription.cancel(), 184 ]); 185 186 return await process.exitCode; 187} 188 189/// Runs the [command] interactively, connecting the stdin/stdout/stderr 190/// streams of this process to those of the child process. Completes with 191/// the exit code of the child process. 192Future<int> runInteractively( 193 List<String> command, { 194 String workingDirectory, 195 bool allowReentrantFlutter = false, 196 Map<String, String> environment, 197}) async { 198 final Process process = await runCommand( 199 command, 200 workingDirectory: workingDirectory, 201 allowReentrantFlutter: allowReentrantFlutter, 202 environment: environment, 203 ); 204 // The real stdin will never finish streaming. Pipe until the child process 205 // finishes. 206 unawaited(process.stdin.addStream(stdin)); 207 // Wait for stdout and stderr to be fully processed, because process.exitCode 208 // may complete first. 209 await Future.wait<dynamic>(<Future<dynamic>>[ 210 stdout.addStream(process.stdout), 211 stderr.addStream(process.stderr), 212 ]); 213 return await process.exitCode; 214} 215 216Future<Process> runDetached(List<String> cmd) { 217 _traceCommand(cmd); 218 final Future<Process> proc = processManager.start( 219 cmd, 220 mode: ProcessStartMode.detached, 221 ); 222 return proc; 223} 224 225Future<RunResult> runAsync( 226 List<String> cmd, { 227 String workingDirectory, 228 bool allowReentrantFlutter = false, 229 Map<String, String> environment, 230}) async { 231 _traceCommand(cmd, workingDirectory: workingDirectory); 232 final ProcessResult results = await processManager.run( 233 cmd, 234 workingDirectory: workingDirectory, 235 environment: _environment(allowReentrantFlutter, environment), 236 ); 237 final RunResult runResults = RunResult(results, cmd); 238 printTrace(runResults.toString()); 239 return runResults; 240} 241 242typedef RunResultChecker = bool Function(int); 243 244Future<RunResult> runCheckedAsync( 245 List<String> cmd, { 246 String workingDirectory, 247 bool allowReentrantFlutter = false, 248 Map<String, String> environment, 249 RunResultChecker whiteListFailures, 250}) async { 251 final RunResult result = await runAsync( 252 cmd, 253 workingDirectory: workingDirectory, 254 allowReentrantFlutter: allowReentrantFlutter, 255 environment: environment, 256 ); 257 if (result.exitCode != 0) { 258 if (whiteListFailures == null || !whiteListFailures(result.exitCode)) { 259 throw ProcessException(cmd[0], cmd.sublist(1), 260 'Process "${cmd[0]}" exited abnormally:\n$result', result.exitCode); 261 } 262 } 263 return result; 264} 265 266bool exitsHappy( 267 List<String> cli, { 268 Map<String, String> environment, 269}) { 270 _traceCommand(cli); 271 try { 272 return processManager.runSync(cli, environment: environment).exitCode == 0; 273 } catch (error) { 274 printTrace('$cli failed with $error'); 275 return false; 276 } 277} 278 279Future<bool> exitsHappyAsync( 280 List<String> cli, { 281 Map<String, String> environment, 282}) async { 283 _traceCommand(cli); 284 try { 285 return (await processManager.run(cli, environment: environment)).exitCode == 0; 286 } catch (error) { 287 printTrace('$cli failed with $error'); 288 return false; 289 } 290} 291 292/// Run cmd and return stdout. 293/// 294/// Throws an error if cmd exits with a non-zero value. 295String runCheckedSync( 296 List<String> cmd, { 297 String workingDirectory, 298 bool allowReentrantFlutter = false, 299 bool hideStdout = false, 300 Map<String, String> environment, 301 RunResultChecker whiteListFailures, 302}) { 303 return _runWithLoggingSync( 304 cmd, 305 workingDirectory: workingDirectory, 306 allowReentrantFlutter: allowReentrantFlutter, 307 hideStdout: hideStdout, 308 checked: true, 309 noisyErrors: true, 310 environment: environment, 311 whiteListFailures: whiteListFailures 312 ); 313} 314 315/// Run cmd and return stdout. 316String runSync( 317 List<String> cmd, { 318 String workingDirectory, 319 bool allowReentrantFlutter = false, 320}) { 321 return _runWithLoggingSync( 322 cmd, 323 workingDirectory: workingDirectory, 324 allowReentrantFlutter: allowReentrantFlutter, 325 ); 326} 327 328void _traceCommand(List<String> args, { String workingDirectory }) { 329 final String argsText = args.join(' '); 330 if (workingDirectory == null) { 331 printTrace('executing: $argsText'); 332 } else { 333 printTrace('executing: [$workingDirectory${fs.path.separator}] $argsText'); 334 } 335} 336 337String _runWithLoggingSync( 338 List<String> cmd, { 339 bool checked = false, 340 bool noisyErrors = false, 341 bool throwStandardErrorOnError = false, 342 String workingDirectory, 343 bool allowReentrantFlutter = false, 344 bool hideStdout = false, 345 Map<String, String> environment, 346 RunResultChecker whiteListFailures, 347}) { 348 _traceCommand(cmd, workingDirectory: workingDirectory); 349 final ProcessResult results = processManager.runSync( 350 cmd, 351 workingDirectory: workingDirectory, 352 environment: _environment(allowReentrantFlutter, environment), 353 ); 354 355 printTrace('Exit code ${results.exitCode} from: ${cmd.join(' ')}'); 356 357 bool failedExitCode = results.exitCode != 0; 358 if (whiteListFailures != null && failedExitCode) { 359 failedExitCode = !whiteListFailures(results.exitCode); 360 } 361 362 if (results.stdout.isNotEmpty && !hideStdout) { 363 if (failedExitCode && noisyErrors) 364 printStatus(results.stdout.trim()); 365 else 366 printTrace(results.stdout.trim()); 367 } 368 369 if (failedExitCode) { 370 if (results.stderr.isNotEmpty) { 371 if (noisyErrors) 372 printError(results.stderr.trim()); 373 else 374 printTrace(results.stderr.trim()); 375 } 376 377 if (throwStandardErrorOnError) 378 throw results.stderr.trim(); 379 380 if (checked) 381 throw 'Exit code ${results.exitCode} from: ${cmd.join(' ')}'; 382 } 383 384 return results.stdout.trim(); 385} 386 387class ProcessExit implements Exception { 388 ProcessExit(this.exitCode, {this.immediate = false}); 389 390 final bool immediate; 391 final int exitCode; 392 393 String get message => 'ProcessExit: $exitCode'; 394 395 @override 396 String toString() => message; 397} 398 399class RunResult { 400 RunResult(this.processResult, this._command) 401 : assert(_command != null), 402 assert(_command.isNotEmpty); 403 404 final ProcessResult processResult; 405 406 final List<String> _command; 407 408 int get exitCode => processResult.exitCode; 409 String get stdout => processResult.stdout; 410 String get stderr => processResult.stderr; 411 412 @override 413 String toString() { 414 final StringBuffer out = StringBuffer(); 415 if (processResult.stdout.isNotEmpty) 416 out.writeln(processResult.stdout); 417 if (processResult.stderr.isNotEmpty) 418 out.writeln(processResult.stderr); 419 return out.toString().trimRight(); 420 } 421 422 /// Throws a [ProcessException] with the given `message`. 423 void throwException(String message) { 424 throw ProcessException( 425 _command.first, 426 _command.skip(1).toList(), 427 message, 428 exitCode, 429 ); 430 } 431} 432