| // 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')); |
| } |