blob: fa43c80c4c30d7c6676796a490531af6fb8734a9 [file] [log] [blame]
// 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'));
}