blob: 06de108b5f9b052255a36f5dd29e9cdac01cb70d [file] [log] [blame]
// Copyright (c) 2016 The Chromium 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 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'package:process/process.dart';
import 'package:stack_trace/stack_trace.dart';
/// Virtual current working directory, which affect functions, such as [exec].
String cwd = Directory.current.path;
List<ProcessInfo> _runningProcesses = <ProcessInfo>[];
ProcessManager _processManager = const LocalProcessManager();
class ProcessInfo {
ProcessInfo(this.command, this.process);
final DateTime startTime = new DateTime.now();
final String command;
final Process process;
@override
String toString() {
return '''
command : $command
started : $startTime
pid : ${process.pid}
'''
.trim();
}
}
/// Result of a health check for a specific parameter.
class HealthCheckResult {
HealthCheckResult.success([this.details]) : succeeded = true;
HealthCheckResult.failure(this.details) : succeeded = false;
HealthCheckResult.error(dynamic error, dynamic stackTrace)
: succeeded = false,
details = 'ERROR: $error${'\n$stackTrace' ?? ''}';
final bool succeeded;
final String details;
@override
String toString() {
final StringBuffer buf = new StringBuffer(succeeded ? 'succeeded' : 'failed');
if (details != null && details.trim().isNotEmpty) {
buf.writeln();
// Indent details by 4 spaces
for (String line in details.trim().split('\n')) {
buf.writeln(' $line');
}
}
return '$buf';
}
}
class BuildFailedError extends Error {
BuildFailedError(this.message);
final String message;
@override
String toString() => message;
}
void fail(String message) {
throw new BuildFailedError(message);
}
void rm(FileSystemEntity entity) {
if (entity.existsSync())
entity.deleteSync();
}
/// Remove recursively.
void rmTree(FileSystemEntity entity) {
if (entity.existsSync())
entity.deleteSync(recursive: true);
}
List<FileSystemEntity> ls(Directory directory) => directory.listSync();
Directory dir(String path) => new Directory(path);
File file(String path) => new File(path);
void copy(File sourceFile, Directory targetDirectory, {String name}) {
final File target = file(
path.join(targetDirectory.path, name ?? path.basename(sourceFile.path)));
target.writeAsBytesSync(sourceFile.readAsBytesSync());
}
FileSystemEntity move(FileSystemEntity whatToMove,
{Directory to, String name}) {
return whatToMove
.renameSync(path.join(to.path, name ?? path.basename(whatToMove.path)));
}
/// Equivalent of `mkdir directory`.
void mkdir(Directory directory) {
directory.createSync();
}
/// Equivalent of `mkdir -p directory`.
void mkdirs(Directory directory) {
directory.createSync(recursive: true);
}
bool exists(FileSystemEntity entity) => entity.existsSync();
void section(String title) {
print('\n••• $title •••');
}
Future<String> getDartVersion() async {
// The Dart VM returns the version text to stderr.
final ProcessResult result = _processManager.runSync(<String>[dartBin, '--version']);
String version = result.stderr.trim();
// Convert:
// Dart VM version: 1.17.0-dev.2.0 (Tue May 3 12:14:52 2016) on "macos_x64"
// to:
// 1.17.0-dev.2.0
if (version.contains('('))
version = version.substring(0, version.indexOf('(')).trim();
if (version.contains(':'))
version = version.substring(version.indexOf(':') + 1).trim();
return version.replaceAll('"', "'");
}
Future<String> getCurrentFlutterRepoCommit() {
if (!dir('${flutterDirectory.path}/.git').existsSync()) {
return null;
}
return inDirectory(flutterDirectory, () {
return eval('git', <String>['rev-parse', 'HEAD']);
});
}
Future<DateTime> getFlutterRepoCommitTimestamp(String commit) {
// git show -s --format=%at 4b546df7f0b3858aaaa56c4079e5be1ba91fbb65
return inDirectory(flutterDirectory, () async {
final String unixTimestamp = await eval('git', <String>[
'show',
'-s',
'--format=%at',
commit,
]);
final int secondsSinceEpoch = int.parse(unixTimestamp);
return new DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch * 1000);
});
}
Future<Process> startProcess(
String executable,
List<String> arguments, {
Map<String, String> environment,
String workingDirectory,
}) async {
final String command = '$executable ${arguments?.join(" ") ?? ""}';
print('Executing: $command');
environment ??= <String, String>{};
environment['BOT'] = 'true';
final Process process = await _processManager.start(
<String>[executable]..addAll(arguments),
environment: environment,
workingDirectory: workingDirectory ?? cwd,
);
final ProcessInfo processInfo = new ProcessInfo(command, process);
_runningProcesses.add(processInfo);
process.exitCode.whenComplete(() {
_runningProcesses.remove(processInfo);
});
return process;
}
Future<Null> forceQuitRunningProcesses() async {
if (_runningProcesses.isEmpty)
return;
// Give normally quitting processes a chance to report their exit code.
await new Future<Null>.delayed(const Duration(seconds: 1));
// Whatever's left, kill it.
for (ProcessInfo p in _runningProcesses) {
print('Force quitting process:\n$p');
if (!p.process.kill()) {
print('Failed to force quit process');
}
}
_runningProcesses.clear();
}
/// Executes a command and returns its exit code.
Future<int> exec(
String executable,
List<String> arguments, {
Map<String, String> environment,
bool canFail: false,
}) async {
final Process process = await startProcess(executable, arguments, environment: environment);
process.stdout
.transform(UTF8.decoder)
.transform(const LineSplitter())
.listen(print);
process.stderr
.transform(UTF8.decoder)
.transform(const LineSplitter())
.listen(stderr.writeln);
final int exitCode = await process.exitCode;
if (exitCode != 0 && !canFail)
fail('Executable failed with exit code $exitCode.');
return exitCode;
}
/// Executes a command and returns its standard output as a String.
///
/// Standard error is redirected to the current process' standard error stream.
Future<String> eval(
String executable,
List<String> arguments, {
Map<String, String> environment,
bool canFail: false,
}) async {
final Process process = await startProcess(executable, arguments, environment: environment);
process.stderr.listen((List<int> data) {
stderr.add(data);
});
final String output = await UTF8.decodeStream(process.stdout);
final int exitCode = await process.exitCode;
if (exitCode != 0 && !canFail)
fail('Executable failed with exit code $exitCode.');
return output.trimRight();
}
Future<int> flutter(String command, {
List<String> options: const <String>[],
bool canFail: false,
Map<String, String> environment,
}) {
final List<String> args = <String>[command]..addAll(options);
return exec(path.join(flutterDirectory.path, 'bin', 'flutter'), args,
canFail: canFail, environment: environment);
}
/// Runs a `flutter` command and returns the standard output as a string.
Future<String> evalFlutter(String command, {
List<String> options: const <String>[],
bool canFail: false,
Map<String, String> environment,
}) {
final List<String> args = <String>[command]..addAll(options);
return eval(path.join(flutterDirectory.path, 'bin', 'flutter'), args,
canFail: canFail, environment: environment);
}
String get dartBin =>
path.join(flutterDirectory.path, 'bin', 'cache', 'dart-sdk', 'bin', 'dart');
Future<int> dart(List<String> args) => exec(dartBin, args);
Future<dynamic> inDirectory(dynamic directory, Future<dynamic> action()) async {
final String previousCwd = cwd;
try {
cd(directory);
return await action();
} finally {
cd(previousCwd);
}
}
void cd(dynamic directory) {
Directory d;
if (directory is String) {
cwd = directory;
d = dir(directory);
} else if (directory is Directory) {
cwd = directory.path;
d = directory;
} else {
throw 'Unsupported type ${directory.runtimeType} of $directory';
}
if (!d.existsSync())
throw 'Cannot cd into directory that does not exist: $directory';
}
Directory get flutterDirectory => dir('../..').absolute;
String requireEnvVar(String name) {
final String value = Platform.environment[name];
if (value == null)
fail('$name environment variable is missing. Quitting.');
return value;
}
T requireConfigProperty<T>(Map<String, dynamic> map, String propertyName) {
if (!map.containsKey(propertyName))
fail('Configuration property not found: $propertyName');
final T result = map[propertyName];
return result;
}
String jsonEncode(dynamic data) {
return const JsonEncoder.withIndent(' ').convert(data) + '\n';
}
Future<Null> getFlutter(String revision) async {
section('Get Flutter!');
if (exists(flutterDirectory)) {
rmTree(flutterDirectory);
}
await inDirectory(flutterDirectory.parent, () async {
await exec('git', <String>['clone', 'https://github.com/flutter/flutter.git']);
});
await inDirectory(flutterDirectory, () async {
await exec('git', <String>['checkout', revision]);
});
await flutter('config', options: <String>['--no-analytics']);
section('flutter doctor');
await flutter('doctor');
section('flutter update-packages');
await flutter('update-packages');
}
void checkNotNull(Object o1,
[Object o2 = 1,
Object o3 = 1,
Object o4 = 1,
Object o5 = 1,
Object o6 = 1,
Object o7 = 1,
Object o8 = 1,
Object o9 = 1,
Object o10 = 1]) {
if (o1 == null)
throw 'o1 is null';
if (o2 == null)
throw 'o2 is null';
if (o3 == null)
throw 'o3 is null';
if (o4 == null)
throw 'o4 is null';
if (o5 == null)
throw 'o5 is null';
if (o6 == null)
throw 'o6 is null';
if (o7 == null)
throw 'o7 is null';
if (o8 == null)
throw 'o8 is null';
if (o9 == null)
throw 'o9 is null';
if (o10 == null)
throw 'o10 is null';
}
/// Add benchmark values to a JSON results file.
///
/// If the file contains information about how long the benchmark took to run
/// (a `time` field), then return that info.
// TODO(yjbanov): move this data to __metadata__
num addBuildInfo(File jsonFile,
{num expected, String sdk, String commit, DateTime timestamp}) {
Map<String, dynamic> json;
if (jsonFile.existsSync())
json = JSON.decode(jsonFile.readAsStringSync());
else
json = <String, dynamic>{};
if (expected != null)
json['expected'] = expected;
if (sdk != null)
json['sdk'] = sdk;
if (commit != null)
json['commit'] = commit;
if (timestamp != null)
json['timestamp'] = timestamp.millisecondsSinceEpoch;
jsonFile.writeAsStringSync(jsonEncode(json));
// Return the elapsed time of the benchmark (if any).
return json['time'];
}
/// Splits [from] into lines and selects those that contain [pattern].
Iterable<String> grep(Pattern pattern, {@required String from}) {
return from.split('\n').where((String line) {
return line.contains(pattern);
});
}
/// Captures asynchronous stack traces thrown by [callback].
///
/// This is a convenience wrapper around [Chain] optimized for use with
/// `async`/`await`.
///
/// Example:
///
/// try {
/// await captureAsyncStacks(() { /* async things */ });
/// } catch (error, chain) {
///
/// }
Future<Null> runAndCaptureAsyncStacks(Future<Null> callback()) {
final Completer<Null> completer = new Completer<Null>();
Chain.capture(() async {
await callback();
completer.complete();
}, onError: completer.completeError);
return completer.future;
}
/// Return an unused TCP port number.
Future<int> findAvailablePort() async {
int port = 20000;
while (true) {
try {
final ServerSocket socket =
await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, port);
await socket.close();
return port;
} catch (_) {
port++;
}
}
}
bool canRun(String path) => _processManager.canRun(path);