blob: 168220193c4838e05aaf1aab1b5e205886ef5a8c [file] [log] [blame]
// Copyright 2016 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' show json, utf8, JsonEncoder, LineSplitter;
import 'dart:io';
import 'package:args/args.dart';
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import 'package:path/path.dart' as path;
import 'package:yaml/yaml.dart';
import 'adb.dart';
import 'list_processes.dart';
/// Virtual current working directory, which affect functions, such as [exec].
String cwd = Directory.current.path;
Config _config;
Config get config => _config;
List<ProcessInfo> _runningProcesses = <ProcessInfo>[];
ProcessManager _processManager = LocalProcessManager();
final Logger logger = PrintLogger(out: stdout, level: LogLevel.info);
class LogLevel {
const LogLevel._(this._level, this.name);
final int _level;
final String name;
static const LogLevel debug = const LogLevel._(0, 'DEBUG');
static const LogLevel info = const LogLevel._(1, 'INFO');
static const LogLevel warning = const LogLevel._(2, 'WARN');
static const LogLevel error = const LogLevel._(3, 'ERROR');
}
abstract class Logger {
void debug(Object message);
void info(Object message);
void warning(Object message);
void error(Object message);
}
class PrintLogger implements Logger {
PrintLogger({
IOSink out,
this.level = LogLevel.info,
}) : out = out ?? stdout;
final IOSink out;
final LogLevel level;
@override
void debug(Object message) => _log(LogLevel.debug, message);
@override
void info(Object message) => _log(LogLevel.info, message);
@override
void warning(Object message) => _log(LogLevel.warning, message);
@override
void error(Object message) => _log(LogLevel.error, message);
void _log(LogLevel level, Object message) {
if (level._level >= this.level._level)
out.writeln(toLogString('$message', level: level));
}
}
String toLogString(String message, {LogLevel level}) {
final StringBuffer buffer = StringBuffer();
buffer.write(DateTime.now().toIso8601String());
buffer.write(': ');
if (level != null) {
buffer.write(level.name);
buffer.write(' ');
}
buffer.write(message);
return buffer.toString();
}
class ProcessInfo {
ProcessInfo(this.command, this.process);
final DateTime startTime = 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() {
StringBuffer buf = 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 BuildFailedError(message);
}
void rm(FileSystemEntity entity) {
if (entity.existsSync()) entity.deleteSync();
}
/// Remove recursively.
void rrm(FileSystemEntity entity) {
if (entity.existsSync()) entity.deleteSync(recursive: true);
}
List<FileSystemEntity> ls(Directory directory) => directory.listSync();
/// Creates a directory from the given path, or multiple path parts by joining
/// them using OS-specific file path separator.
Directory dir(String thePath,
[String part2,
String part3,
String part4,
String part5,
String part6,
String part7,
String part8]) {
return Directory(
path.join(thePath, part2, part3, part4, part5, part6, part7, part8));
}
/// Creates a file from the given path, or multiple path parts by joining
/// them using OS-specific file path separator.
File file(String thePath,
[String part2,
String part3,
String part4,
String part5,
String part6,
String part7,
String part8]) {
return File(
path.join(thePath, part2, part3, part4, part5, part6, part7, part8));
}
void copy(File sourceFile, Directory targetDirectory, {String name}) {
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) {
logger.info('');
logger.info('••• $title •••');
}
Future<String> getDartVersion() async {
// The Dart VM returns the version text to stderr.
ProcessResult result =
_processManager.runSync(<String>[dartBin, '--version']);
String version = result.stderr.trim() as String;
// 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.indexOf('(') != -1)
version = version.substring(0, version.indexOf('(')).trim();
if (version.indexOf(':') != -1)
version = version.substring(version.indexOf(':') + 1).trim();
return version.replaceAll('"', "'");
}
Future<String> getCurrentFlutterRepoCommit() async {
if (!dir(config.flutterDirectory.path, '.git').existsSync()) {
return null;
}
final String result = await inDirectory(config.flutterDirectory, () {
return eval('git', ['rev-parse', 'HEAD']);
}) as String;
return result;
}
Future<DateTime> getFlutterRepoCommitTimestamp(String commit) async {
// git show -s --format=%at 4b546df7f0b3858aaaa56c4079e5be1ba91fbb65
final DateTime result = await inDirectory(config.flutterDirectory, () async {
String unixTimestamp =
await eval('git', ['show', '-s', '--format=%at', commit]);
int secondsSinceEpoch = int.parse(unixTimestamp);
return DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch * 1000);
}) as DateTime;
return result;
}
/// When exists, this file indicates an installation is in progress or failed
/// to complete.
File get _installationLock =>
file(config.flutterDirectory.parent.path, '.installation-lock');
/// Flutter repository revision that's currently installed.
///
/// Returns `null` if nothing is installed or installation failed to complete.
Future<String> _getCurrentInstallationRevision() async {
if (exists(_installationLock)) {
return null;
}
return getCurrentFlutterRepoCommit();
}
Future<Null> getFlutterAt(String revision) async {
String currentRevision = await _getCurrentInstallationRevision();
// This agent will likely run multiple tasks in the same checklist and
// therefore the same revision. It would be too costly to have to reinstall
// Flutter every time.
if (currentRevision == revision) {
logger.info('Reusing previously checked out Flutter revision: $revision');
return;
}
await getFlutter(revision);
}
Future<Process> startProcess(String executable, List<String> arguments,
{Map<String, String> env, bool silent: false}) async {
String command = '$executable ${arguments?.join(" ") ?? ""}';
if (!silent) logger.info('Executing: $command');
Process proc = await _processManager.start(
<String>[executable]..addAll(arguments),
environment: env,
workingDirectory: cwd);
ProcessInfo procInfo = ProcessInfo(command, proc);
_runningProcesses.add(procInfo);
// ignore: unawaited_futures
proc.exitCode.then((_) {
_runningProcesses.remove(procInfo);
});
return proc;
}
Future<Null> forceQuitRunningProcesses() async {
// Give normally quitting processes a chance to report their exit code.
await Future<void>.delayed(const Duration(seconds: 1));
// Whatever's left, kill it.
for (ProcessInfo p in _runningProcesses) {
logger.info('Force quitting process:\n$p');
if (!p.process.kill()) {
logger.warning('Failed to force quit process');
}
}
_runningProcesses.clear();
// Also kill sub-processes launched by top-level processes. We may not be
// able to find all of them, but finding those whose CWD is the Flutter
// repository are good candidates.
List<int> pids = await listFlutterProcessIds(config.flutterDirectory);
for (int pid in pids) {
Process.killPid(pid, ProcessSignal.sigkill);
}
}
/// Executes a command and returns its exit code.
Future<int> exec(String executable, List<String> arguments,
{Map<String, String> env, bool canFail: false, bool silent: false}) async {
Process proc =
await startProcess(executable, arguments, env: env, silent: silent);
final StreamSubscription<String> stdoutSubscription = proc.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen(logger.info);
final StreamSubscription<String> stderrSubscription = proc.stderr
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen(logger.warning);
// Wait for stdout and stderr to be fully processed because proc.exitCode
// may complete first.
await Future.wait<void>(<Future<void>>[
stdoutSubscription.asFuture<void>(),
stderrSubscription.asFuture<void>(),
]);
// The streams as futures have already completed, so waiting for the
// potentially async stream cancellation to complete likely has no benefit.
unawaited(stdoutSubscription.cancel());
unawaited(stderrSubscription.cancel());
int exitCode = await proc.exitCode;
if (exitCode != 0 && !canFail) {
final List<String> command = [executable]..addAll(arguments);
fail('Command "${command.join(' ')}" 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> env, bool canFail: false, bool silent: false}) async {
Process proc =
await startProcess(executable, arguments, env: env, silent: silent);
proc.stderr.listen((List<int> data) {
stderr.add(data);
});
String output = await utf8.decodeStream(proc.stdout);
int exitCode = await proc.exitCode;
if (exitCode != 0 && !canFail)
fail('Executable $executable failed with exit code $exitCode.');
return output.trimRight();
}
Future<int> flutter(String command,
{List<String> options: const <String>[], bool canFail: false}) {
List<String> args = [command]..addAll(options);
return exec(path.join(config.flutterDirectory.path, 'bin', 'flutter'), args,
canFail: canFail);
}
String get dartBin => path.join(
config.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 {
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';
}
class Config {
Config({
@required this.baseCocoonUrl,
@required this.agentId,
@required this.authToken,
@required this.flutterDirectory,
@required this.deviceOperatingSystem,
@required this.hostType,
});
static void initialize(ArgResults args) {
File agentConfigFile = file(args['config-file'] as String);
if (!agentConfigFile.existsSync()) {
throw ('Agent config file not found: ${agentConfigFile.path}.');
}
YamlMap agentConfig =
loadYaml(agentConfigFile.readAsStringSync()) as YamlMap;
String baseCocoonUrl = agentConfig['base_cocoon_url'] as String ??
'https://flutter-dashboard.appspot.com';
String agentId = requireConfigProperty<String>(agentConfig, 'agent_id');
String authToken = requireConfigProperty<String>(agentConfig, 'auth_token');
String home =
Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'];
if (home == null) throw "Unable to find \$HOME or \$USERPROFILE.";
Directory flutterDirectory = dir(home, '.cocoon', 'flutter');
mkdirs(flutterDirectory);
DeviceOperatingSystem deviceOperatingSystem;
switch (agentConfig['device_os'] as String) {
case 'android':
deviceOperatingSystem = DeviceOperatingSystem.android;
break;
case 'ios':
deviceOperatingSystem = DeviceOperatingSystem.ios;
break;
case 'none':
deviceOperatingSystem = DeviceOperatingSystem.none;
break;
default:
throw BuildFailedError(
'Unrecognized device_os value: ${agentConfig['device_os']}');
}
HostType hostType;
switch (agentConfig['host_type'] as String) {
case 'physical':
hostType = HostType.physical;
break;
case 'vm':
hostType = HostType.vm;
break;
default:
hostType = HostType.physical;
}
_config = Config(
baseCocoonUrl: baseCocoonUrl,
agentId: agentId,
authToken: authToken,
flutterDirectory: flutterDirectory,
deviceOperatingSystem: deviceOperatingSystem,
hostType: hostType,
);
}
final String baseCocoonUrl;
final String agentId;
final String authToken;
final Directory flutterDirectory;
final DeviceOperatingSystem deviceOperatingSystem;
final HostType hostType;
String get adbPath {
String androidHome = Platform.environment['ANDROID_HOME'];
if (androidHome == null)
throw 'ANDROID_HOME environment variable missing. This variable must '
'point to the Android SDK directory containing platform-tools.';
String adbPath = path.join(androidHome, 'platform-tools', 'adb');
if (!_processManager.canRun(adbPath)) throw 'adb not found at: $adbPath';
return path.absolute(adbPath);
}
@override
String toString() => '''
baseCocoonUrl: $baseCocoonUrl
agentId: $agentId
flutterDirectory: $flutterDirectory
adbPath: ${deviceOperatingSystem == DeviceOperatingSystem.android ? adbPath : 'N/A'}
deviceOperatingSystem: $deviceOperatingSystem
hostType: $hostType
'''
.trim();
}
String requireEnvVar(String name) {
String value = Platform.environment[name];
if (value == null) fail('${name} environment variable is missing. Quitting.');
return value;
}
T requireConfigProperty<T>(YamlMap map, String propertyName) {
if (!map.containsKey(propertyName))
fail('Configuration property not found: $propertyName');
return map[propertyName] as T;
}
String jsonEncode(dynamic data) {
return JsonEncoder.withIndent(' ').convert(data) + '\n';
}
Future<Null> getFlutter(String revision) async {
section('Get Flutter!');
_installationLock.createSync(recursive: true);
if (exists(config.flutterDirectory)) {
rrm(config.flutterDirectory);
}
await inDirectory(config.flutterDirectory.parent, () async {
await exec('git', ['clone', 'https://github.com/flutter/flutter.git']);
});
await inDirectory(config.flutterDirectory, () async {
await exec('git', ['checkout', revision]);
});
await flutter('config', options: ['--no-analytics']);
section('flutter doctor');
await flutter('doctor');
section('flutter update-packages');
await flutter('update-packages');
rm(_installationLock);
}
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> jsonData;
if (jsonFile.existsSync()) {
jsonData = json.decode(jsonFile.readAsStringSync()) as Map<String, dynamic>;
} else {
jsonData = <String, dynamic>{};
}
if (expected != null) jsonData['expected'] = expected;
if (sdk != null) jsonData['sdk'] = sdk;
if (commit != null) jsonData['commit'] = commit;
if (timestamp != null)
jsonData['timestamp'] = timestamp.millisecondsSinceEpoch;
jsonFile.writeAsStringSync(jsonEncode(jsonData));
// Return the elapsed time of the benchmark (if any).
return jsonData['time'] as num;
}
/// 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);
});
}
bool canRun(String path) => _processManager.canRun(path);
final RegExp _whitespace = RegExp(r'\s+');
List<String> runningProcessesOnWindows(String processName) {
final ProcessResult result = _processManager
.runSync(<String>['powershell', 'Get-CimInstance', 'Win32_Process']);
List<String> pids = <String>[];
if (result.exitCode == 0) {
final String stdoutResult = result.stdout as String;
for (String rawProcess in stdoutResult.split('\n')) {
final String process = rawProcess.trim();
if (!process.contains(processName)) {
continue;
}
final List<String> parts = process.split(_whitespace);
final String processPid = parts[0];
final String currentRunningProcessPid = pid.toString();
// Don't kill current process
if (processPid == currentRunningProcessPid) {
continue;
}
pids.add(processPid);
}
}
return pids;
}
Future<void> killAllRunningProcessesOnWindows(String processName) async {
while (true) {
final pids = runningProcessesOnWindows(processName);
if (pids.isEmpty) {
return;
}
for (String pid in pids) {
_processManager.runSync(<String>['taskkill', '/pid', pid, '/f']);
}
// Killed processes don't release resources instantenously.
await Future<void>.delayed(Duration(seconds: 1));
}
}
/// Indicates to the linter that the given future is intentionally not `await`-ed.
///
/// Has the same functionality as `unawaited` from `package:pedantic`.
///
/// In an async context, it is normally expected than all Futures are awaited,
/// and that is the basis of the lint unawaited_futures which is turned on for
/// the flutter_tools package. However, there are times where one or more
/// futures are intentionally not awaited. This function may be used to ignore a
/// particular future. It silences the unawaited_futures lint.
void unawaited(Future<void> future) {}