| // Copyright 2013 The Flutter Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import 'dart:async'; | |
| import 'dart:convert'; | |
| import 'dart:io'; | |
| import 'dart:math'; | |
| import 'package:path/path.dart' as p; | |
| import 'package:process_runner/process_runner.dart'; | |
| import 'environment.dart'; | |
| import 'json_utils.dart'; | |
| import 'logger.dart'; | |
| import 'worker_pool.dart'; | |
| /// Artifacts from an exited sub-process. | |
| final class ProcessArtifacts { | |
| /// Constructs an instance of ProcessArtifacts from raw values. | |
| ProcessArtifacts( | |
| this.cwd, this.commandLine, this.exitCode, this.stdout, this.stderr, | |
| {this.pid}); | |
| /// Constructs an instance of ProcessArtifacts from a ProcessRunnerResult | |
| /// and the spawning context. | |
| factory ProcessArtifacts.fromResult( | |
| Directory cwd, List<String> commandLine, ProcessRunnerResult result) { | |
| return ProcessArtifacts( | |
| cwd, commandLine, result.exitCode, result.stdout, result.stderr, | |
| pid: result.pid); | |
| } | |
| /// Constructs an instance of ProcessArtifacts from serialized JSON text. | |
| factory ProcessArtifacts.fromJson(String serialized) { | |
| final Map<String, dynamic> artifact = | |
| jsonDecode(serialized) as Map<String, dynamic>; | |
| final List<String> errors = <String>[]; | |
| final Directory cwd = Directory(stringOfJson(artifact, 'cwd', errors)!); | |
| final List<String> commandLine = | |
| stringListOfJson(artifact, 'commandLine', errors)!; | |
| final int exitCode = intOfJson(artifact, 'exitCode', errors)!; | |
| final String stdout = stringOfJson(artifact, 'stdout', errors)!; | |
| final String stderr = stringOfJson(artifact, 'stderr', errors)!; | |
| final int? pid = intOfJson(artifact, 'pid', errors); | |
| return ProcessArtifacts(cwd, commandLine, exitCode, stdout, stderr, | |
| pid: pid); | |
| } | |
| /// Constructs an instance of ProcessArtifacts from a file containing JSON. | |
| factory ProcessArtifacts.fromFile(File file) { | |
| return ProcessArtifacts.fromJson(file.readAsStringSync()); | |
| } | |
| /// Saves ProcessArtifacts into file. | |
| void save(File file) { | |
| final Map<String, Object> data = <String, Object>{}; | |
| if (pid != null) { | |
| data['pid'] = pid!; | |
| } | |
| data['exitCode'] = exitCode; | |
| data['stdout'] = stdout; | |
| data['stderr'] = stderr; | |
| data['cwd'] = cwd.absolute.path; | |
| data['commandLine'] = commandLine; | |
| file.writeAsStringSync(jsonEncodePretty(data)); | |
| } | |
| /// Creates a temporary file and saves the artifacts into it. | |
| /// Returns the File. | |
| File saveTemp() { | |
| final Directory systemTemp = Directory.systemTemp; | |
| final String prefix = pid != null ? 'et$pid' : 'et'; | |
| final Directory artifacts = systemTemp.createTempSync(prefix); | |
| final File resultFile = | |
| File(p.join(artifacts.path, 'process_artifacts.json')); | |
| save(resultFile); | |
| return resultFile; | |
| } | |
| /// Current working directory of process when it was spawned. | |
| final Directory cwd; | |
| /// Full command line of process. | |
| final List<String> commandLine; | |
| /// Exit code. | |
| final int exitCode; | |
| /// Stdout (may be empty). | |
| final String stdout; | |
| /// Stdout (may be empty). | |
| final String stderr; | |
| /// Pid (when available). | |
| final int? pid; | |
| } | |
| /// A WorkerTask that runs a process | |
| class ProcessTask extends WorkerTask { | |
| /// Construct a new process task with name, cwd, and command line. | |
| ProcessTask(super.name, this._environment, this._cwd, this._commandLine); | |
| final Environment _environment; | |
| final Directory _cwd; | |
| final List<String> _commandLine; | |
| late ProcessArtifacts? _processArtifacts; | |
| late String? _processArtifactsPath; | |
| @override | |
| Future<bool> run() async { | |
| final ProcessRunnerResult result = await _environment.processRunner | |
| .runProcess(_commandLine, failOk: true, workingDirectory: _cwd); | |
| _processArtifacts = ProcessArtifacts( | |
| _cwd, _commandLine, result.exitCode, result.stdout, result.stderr, | |
| pid: result.pid); | |
| _processArtifactsPath = _processArtifacts!.saveTemp().path; | |
| return result.exitCode == 0; | |
| } | |
| /// Returns the ProcessArtifacts after run completes. | |
| ProcessArtifacts get processArtifacts { | |
| return _processArtifacts!; | |
| } | |
| /// Returns the path that the process artifacts were saved in. | |
| String get processArtifactsPath { | |
| return _processArtifactsPath!; | |
| } | |
| } | |
| /// A WorkerPoolProgressReporter designed to work with ProcessTasks. | |
| class ProcessTaskProgressReporter implements WorkerPoolProgressReporter { | |
| /// Construct a new reporter. | |
| ProcessTaskProgressReporter(this._environment); | |
| final Environment _environment; | |
| Spinner? _spinner; | |
| bool _finished = false; | |
| int _longestName = 0; | |
| int _doneCount = 0; | |
| int _totalCount = 0; | |
| @override | |
| void onRun(Set<WorkerTask> tasks) { | |
| _totalCount = tasks.length; | |
| for (final WorkerTask task in tasks) { | |
| assert(task is ProcessTask); | |
| _longestName = max(_longestName, task.name.length); | |
| } | |
| } | |
| @override | |
| void onFinish() { | |
| _finished = true; | |
| _updateSpinner(<ProcessTask>{}); | |
| } | |
| @override | |
| void onTaskStart(WorkerPool pool, WorkerTask task) { | |
| _updateSpinner(pool.running); | |
| } | |
| @override | |
| void onTaskDone(WorkerPool pool, WorkerTask task, [Object? err]) { | |
| _doneCount++; | |
| task as ProcessTask; | |
| final ProcessArtifacts pa = task.processArtifacts; | |
| final String dt = _formatDurationShort(task.runTime); | |
| if (pa.exitCode != 0) { | |
| final String paPath = task.processArtifactsPath; | |
| _environment.logger.clearLine(); | |
| _environment.logger.status('FAIL: $dt ${task.name} [details in $paPath]'); | |
| } else { | |
| _environment.logger.clearLine(); | |
| _environment.logger.status('OKAY: $dt ${task.name}'); | |
| } | |
| _updateSpinner(pool.running); | |
| } | |
| void _updateSpinner(Set<WorkerTask> tasks) { | |
| if (_spinner != null) { | |
| _spinner!.finish(); | |
| _spinner = null; | |
| } | |
| if (_finished) { | |
| return; | |
| } | |
| _environment.logger.clearLine(); | |
| final String taskName = tasks.isEmpty ? '' : tasks.first.name; | |
| final String etc = tasks.length > 1 ? '... [${tasks.length}]' : ''; | |
| _environment.logger.status( | |
| 'Running $_doneCount/$_totalCount $taskName$etc ', | |
| newline: false); | |
| _spinner = _environment.logger.startSpinner(); | |
| } | |
| String _formatDurationShort(Duration dur) { | |
| int micros = dur.inMicroseconds; | |
| String r = ''; | |
| if (micros >= Duration.microsecondsPerMinute) { | |
| final int minutes = micros ~/ Duration.microsecondsPerMinute; | |
| micros -= minutes * Duration.microsecondsPerMinute; | |
| r += '${minutes}m'; | |
| } | |
| if (micros >= Duration.microsecondsPerSecond) { | |
| final int seconds = micros ~/ Duration.microsecondsPerSecond; | |
| micros -= seconds * Duration.microsecondsPerSecond; | |
| if (r.isNotEmpty) { | |
| r += '.'; | |
| } | |
| r += '${seconds}s'; | |
| } | |
| if (micros >= Duration.microsecondsPerMillisecond) { | |
| final int millis = micros ~/ Duration.microsecondsPerMillisecond; | |
| micros -= millis * Duration.microsecondsPerMillisecond; | |
| if (r.isNotEmpty) { | |
| r += '.'; | |
| } | |
| r += '${millis}ms'; | |
| } | |
| return r.padLeft(15); | |
| } | |
| } | |
| /// If result.exitCode != 0, will call logger.fatal with relevant information | |
| /// and terminate the program. | |
| void fatalIfFailed(Environment environment, List<String> commandLine, | |
| ProcessRunnerResult result) { | |
| if (result.exitCode == 0) { | |
| return; | |
| } | |
| environment.logger.fatal( | |
| 'Process "${commandLine.join(' ')}" failed exitCode=${result.exitCode}\n' | |
| 'STDOUT:\n${result.stdout}' | |
| 'STDERR:\n${result.stderr}'); | |
| } | |
| /// Ensures that pathToBinary includes a '.exe' suffix on relevant platforms. | |
| String exePath(Environment environment, String pathToBinary) { | |
| String suffix = ''; | |
| if (environment.platform.isWindows) { | |
| suffix = '.exe'; | |
| } | |
| return '$pathToBinary$suffix'; | |
| } | |
| /// Returns the path to the gn binary. | |
| String gnBinPath(Environment environment) { | |
| return exePath( | |
| environment, | |
| p.join(environment.engine.srcDir.path, 'flutter', 'third_party', 'gn', | |
| 'gn')); | |
| } |