| // Copyright 2017 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:io'; |
| |
| import 'package:meta/meta.dart'; |
| import 'package:path/path.dart' as path; |
| import 'package:file/local.dart' as local; |
| import 'package:file/file.dart' as file; |
| import 'package:platform/platform.dart' as platform; |
| |
| import 'adb.dart'; |
| import 'agent.dart'; |
| import 'utils.dart'; |
| |
| final RegExp _kLinuxIpAddrExp = RegExp(r'inet +(\d+\.\d+\.\d+.\d+)/\d+'); |
| final RegExp _kWindowsIpAddrExp = |
| RegExp(r'IPv4 Address.*: +(\d+\.\d+\.\d+.\d+)\(Preferred\)'); |
| |
| Future<AgentHealth> performHealthChecks(Agent agent) async { |
| AgentHealth results = AgentHealth(); |
| |
| results['able-to-perform-health-check'] = await _captureErrors(() async { |
| results['ssh-connectivity'] = await _captureErrors(_scrapeRemoteAccessInfo); |
| |
| Map<String, HealthCheckResult> deviceChecks = await devices.checkDevices(); |
| results.addAll(deviceChecks); |
| |
| bool hasHealthyDevices = deviceChecks.values |
| .where((HealthCheckResult r) => r.succeeded) |
| .isNotEmpty; |
| |
| results['has-healthy-devices'] = hasHealthyDevices |
| ? HealthCheckResult.success( |
| 'Found ${deviceChecks.length} healthy devices') |
| : HealthCheckResult.failure('No attached devices were found.'); |
| |
| results['cocoon-connection'] = await _captureErrors(() async { |
| String authStatus = await agent.getAuthenticationStatus(); |
| |
| if (authStatus != 'OK') { |
| results['cocoon-authentication'] = HealthCheckResult.failure( |
| 'Failed to authenticate to Cocoon. Check config.yaml.'); |
| } else { |
| results['cocoon-authentication'] = HealthCheckResult.success(); |
| } |
| }); |
| results['remove-xcode-derived-data'] = |
| await _captureErrors(removeXcodeDerivedData); |
| results['remove-cached-data'] = await _captureErrors(removeCachedData); |
| }); |
| |
| return results; |
| } |
| |
| /// Catches all exceptions and turns them into [HealthCheckResult] error. |
| /// |
| /// Null callback results are turned into [HealthCheckResult] success. |
| Future<HealthCheckResult> _captureErrors( |
| Future<dynamic> healthCheckCallback()) async { |
| Completer<HealthCheckResult> completer = Completer<HealthCheckResult>(); |
| |
| // We intentionally ignore the future returned by the Chain because we're |
| // reporting the results via the completer. Instead of reporting a Future |
| // error, we report a successful Future carrying a HealthCheckResult error. |
| // One way to think about this is that "we _successfully_ discovered health |
| // check error, and will report it to the Cocoon back-end". |
| // ignore: unawaited_futures |
| try { |
| dynamic result = await healthCheckCallback(); |
| completer.complete( |
| result is HealthCheckResult ? result : HealthCheckResult.success()); |
| } catch (error, stackTrace) { |
| completer.complete(HealthCheckResult.error(error, stackTrace)); |
| } |
| return completer.future; |
| } |
| |
| /// Returns the IP address for remote (SSH) access to this agent. |
| /// |
| /// Uses `ipconfig getifaddr en0`. |
| /// |
| /// Always returns [HealthCheckResult] success regardless of whether an IP |
| /// is available or not. Having remote access to an agent is not a prerequisite |
| /// for being able to perform Cocoon tasks. It's only there to make maintenance |
| /// convenient. The goal is only to report available IPs as part of the health |
| /// check. |
| Future<HealthCheckResult> _scrapeRemoteAccessInfo() async { |
| String ip; |
| if (Platform.isMacOS) { |
| ip = (await eval('ipconfig', ['getifaddr', 'en0'], canFail: true)).trim(); |
| } else if (Platform.isLinux) { |
| if (config.hostType == HostType.vm) { |
| // Use hostname for VMs |
| ip = (await eval('hostname', <String>[], canFail: true)).trim(); |
| } else { |
| // Expect: 3: eno1 inet 123.45.67.89/26 brd ... |
| final String out = (await eval('ip', ['-o', '-4', 'addr', 'show', 'eno1'], |
| canFail: true)) |
| .trim(); |
| final Match match = _kLinuxIpAddrExp.firstMatch(out); |
| ip = match?.group(1) ?? ''; |
| } |
| } else if (Platform.isWindows) { |
| final String out = (await eval('ipconfig', ['/all'], canFail: true)).trim(); |
| final Match match = _kWindowsIpAddrExp.firstMatch(out); |
| ip = match?.group(1) ?? ''; |
| } |
| |
| return HealthCheckResult.success(ip.isEmpty |
| ? 'Unable to determine IP address. Is wired ethernet connected?' |
| : 'Last known IP address: $ip'); |
| } |
| |
| /// Completely removes Xcode DerivedData directory. |
| /// |
| /// There're two purposes. First, it's a well known trick to fix Xcode when |
| /// Xcode behaves strangely for no obvious reason. Second, it avoids eating |
| /// all of the remaining disk space over time. |
| @visibleForTesting |
| Future<HealthCheckResult> removeXcodeDerivedData( |
| {platform.Platform pf = const platform.LocalPlatform(), |
| file.FileSystem fs = const local.LocalFileSystem()}) async { |
| if (!pf.isMacOS) { |
| return HealthCheckResult.success(); |
| } |
| String home = pf.environment['HOME']; |
| if (home == null) { |
| return HealthCheckResult.failure('Missing \$HOME environment variable.'); |
| } |
| String p = path.join(home, 'Library/Developer/Xcode/DerivedData'); |
| rrm(fs.directory(p)); |
| return HealthCheckResult.success(); |
| } |
| |
| /// Completely removes Cache directories. |
| /// |
| /// This is needed for VMs with limited resources where the |
| /// cache directories grow very fast. |
| @visibleForTesting |
| Future<HealthCheckResult> removeCachedData( |
| {platform.Platform pf = const platform.LocalPlatform(), |
| file.FileSystem fs = const local.LocalFileSystem()}) async { |
| String home = pf.environment['HOME']; |
| if (home == null) { |
| return HealthCheckResult.failure('Missing \$HOME environment variable.'); |
| } |
| List<String> cacheFolders = ['.graddle', '.dartServer']; |
| for (String folder in cacheFolders) { |
| String folderPath = path.join(home, folder); |
| rrm(fs.directory(folderPath)); |
| } |
| return HealthCheckResult.success(); |
| } |