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