mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
542 lines
19 KiB
Dart
542 lines
19 KiB
Dart
// Copyright 2014 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 'package:meta/meta.dart';
|
|
import 'package:process/process.dart';
|
|
|
|
import '../artifacts.dart';
|
|
import '../base/file_system.dart';
|
|
import '../base/io.dart';
|
|
import '../base/logger.dart';
|
|
import '../base/platform.dart';
|
|
import '../base/process.dart';
|
|
import '../cache.dart';
|
|
import '../convert.dart';
|
|
import 'code_signing.dart';
|
|
import 'iproxy.dart';
|
|
|
|
// Error message patterns from ios-deploy output
|
|
const String noProvisioningProfileErrorOne = 'Error 0xe8008015';
|
|
const String noProvisioningProfileErrorTwo = 'Error 0xe8000067';
|
|
const String deviceLockedError = 'e80000e2';
|
|
const String unknownAppLaunchError = 'Error 0xe8000022';
|
|
|
|
class IOSDeploy {
|
|
IOSDeploy({
|
|
required Artifacts artifacts,
|
|
required Cache cache,
|
|
required Logger logger,
|
|
required Platform platform,
|
|
required ProcessManager processManager,
|
|
}) : _platform = platform,
|
|
_cache = cache,
|
|
_processUtils = ProcessUtils(processManager: processManager, logger: logger),
|
|
_logger = logger,
|
|
_binaryPath = artifacts.getHostArtifact(HostArtifact.iosDeploy).path;
|
|
|
|
final Cache _cache;
|
|
final String _binaryPath;
|
|
final Logger _logger;
|
|
final Platform _platform;
|
|
final ProcessUtils _processUtils;
|
|
|
|
Map<String, String> get iosDeployEnv {
|
|
// Push /usr/bin to the front of PATH to pick up default system python, package 'six'.
|
|
//
|
|
// ios-deploy transitively depends on LLDB.framework, which invokes a
|
|
// Python script that uses package 'six'. LLDB.framework relies on the
|
|
// python at the front of the path, which may not include package 'six'.
|
|
// Ensure that we pick up the system install of python, which includes it.
|
|
final Map<String, String> environment = Map<String, String>.of(_platform.environment);
|
|
environment['PATH'] = '/usr/bin:${environment['PATH']}';
|
|
environment.addEntries(<MapEntry<String, String>>[_cache.dyLdLibEntry]);
|
|
return environment;
|
|
}
|
|
|
|
/// Uninstalls the specified app bundle.
|
|
///
|
|
/// Uses ios-deploy and returns the exit code.
|
|
Future<int> uninstallApp({
|
|
required String deviceId,
|
|
required String bundleId,
|
|
}) async {
|
|
final List<String> launchCommand = <String>[
|
|
_binaryPath,
|
|
'--id',
|
|
deviceId,
|
|
'--uninstall_only',
|
|
'--bundle_id',
|
|
bundleId,
|
|
];
|
|
|
|
return _processUtils.stream(
|
|
launchCommand,
|
|
mapFunction: _monitorFailure,
|
|
trace: true,
|
|
environment: iosDeployEnv,
|
|
);
|
|
}
|
|
|
|
/// Installs the specified app bundle.
|
|
///
|
|
/// Uses ios-deploy and returns the exit code.
|
|
Future<int> installApp({
|
|
required String deviceId,
|
|
required String bundlePath,
|
|
required List<String>launchArguments,
|
|
required IOSDeviceConnectionInterface interfaceType,
|
|
Directory? appDeltaDirectory,
|
|
}) async {
|
|
appDeltaDirectory?.createSync(recursive: true);
|
|
final List<String> launchCommand = <String>[
|
|
_binaryPath,
|
|
'--id',
|
|
deviceId,
|
|
'--bundle',
|
|
bundlePath,
|
|
if (appDeltaDirectory != null) ...<String>[
|
|
'--app_deltas',
|
|
appDeltaDirectory.path,
|
|
],
|
|
if (interfaceType != IOSDeviceConnectionInterface.network)
|
|
'--no-wifi',
|
|
if (launchArguments.isNotEmpty) ...<String>[
|
|
'--args',
|
|
launchArguments.join(' '),
|
|
],
|
|
];
|
|
|
|
return _processUtils.stream(
|
|
launchCommand,
|
|
mapFunction: _monitorFailure,
|
|
trace: true,
|
|
environment: iosDeployEnv,
|
|
);
|
|
}
|
|
|
|
/// Returns [IOSDeployDebugger] wrapping attached debugger logic.
|
|
///
|
|
/// This method does not install the app. Call [IOSDeployDebugger.launchAndAttach()]
|
|
/// to install and attach the debugger to the specified app bundle.
|
|
IOSDeployDebugger prepareDebuggerForLaunch({
|
|
required String deviceId,
|
|
required String bundlePath,
|
|
required List<String> launchArguments,
|
|
required IOSDeviceConnectionInterface interfaceType,
|
|
Directory? appDeltaDirectory,
|
|
required bool uninstallFirst,
|
|
}) {
|
|
appDeltaDirectory?.createSync(recursive: true);
|
|
// Interactive debug session to support sending the lldb detach command.
|
|
final List<String> launchCommand = <String>[
|
|
'script',
|
|
'-t',
|
|
'0',
|
|
'/dev/null',
|
|
_binaryPath,
|
|
'--id',
|
|
deviceId,
|
|
'--bundle',
|
|
bundlePath,
|
|
if (appDeltaDirectory != null) ...<String>[
|
|
'--app_deltas',
|
|
appDeltaDirectory.path,
|
|
],
|
|
if (uninstallFirst)
|
|
'--uninstall',
|
|
'--debug',
|
|
if (interfaceType != IOSDeviceConnectionInterface.network)
|
|
'--no-wifi',
|
|
if (launchArguments.isNotEmpty) ...<String>[
|
|
'--args',
|
|
launchArguments.join(' '),
|
|
],
|
|
];
|
|
return IOSDeployDebugger(
|
|
launchCommand: launchCommand,
|
|
logger: _logger,
|
|
processUtils: _processUtils,
|
|
iosDeployEnv: iosDeployEnv,
|
|
);
|
|
}
|
|
|
|
/// Installs and then runs the specified app bundle.
|
|
///
|
|
/// Uses ios-deploy and returns the exit code.
|
|
Future<int> launchApp({
|
|
required String deviceId,
|
|
required String bundlePath,
|
|
required List<String> launchArguments,
|
|
required IOSDeviceConnectionInterface interfaceType,
|
|
required bool uninstallFirst,
|
|
Directory? appDeltaDirectory,
|
|
}) async {
|
|
appDeltaDirectory?.createSync(recursive: true);
|
|
final List<String> launchCommand = <String>[
|
|
_binaryPath,
|
|
'--id',
|
|
deviceId,
|
|
'--bundle',
|
|
bundlePath,
|
|
if (appDeltaDirectory != null) ...<String>[
|
|
'--app_deltas',
|
|
appDeltaDirectory.path,
|
|
],
|
|
if (interfaceType != IOSDeviceConnectionInterface.network)
|
|
'--no-wifi',
|
|
if (uninstallFirst)
|
|
'--uninstall',
|
|
'--justlaunch',
|
|
if (launchArguments.isNotEmpty) ...<String>[
|
|
'--args',
|
|
launchArguments.join(' '),
|
|
],
|
|
];
|
|
|
|
return _processUtils.stream(
|
|
launchCommand,
|
|
mapFunction: _monitorFailure,
|
|
trace: true,
|
|
environment: iosDeployEnv,
|
|
);
|
|
}
|
|
|
|
Future<bool> isAppInstalled({
|
|
required String bundleId,
|
|
required String deviceId,
|
|
}) async {
|
|
final List<String> launchCommand = <String>[
|
|
_binaryPath,
|
|
'--id',
|
|
deviceId,
|
|
'--exists',
|
|
'--timeout', // If the device is not connected, ios-deploy will wait forever.
|
|
'10',
|
|
'--bundle_id',
|
|
bundleId,
|
|
];
|
|
final RunResult result = await _processUtils.run(
|
|
launchCommand,
|
|
environment: iosDeployEnv,
|
|
);
|
|
// Device successfully connected, but app not installed.
|
|
if (result.exitCode == 255) {
|
|
_logger.printTrace('$bundleId not installed on $deviceId');
|
|
return false;
|
|
}
|
|
if (result.exitCode != 0) {
|
|
_logger.printTrace('App install check failed: ${result.stderr}');
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
String _monitorFailure(String stdout) => _monitorIOSDeployFailure(stdout, _logger);
|
|
}
|
|
|
|
/// lldb attach state flow.
|
|
enum _IOSDeployDebuggerState {
|
|
detached,
|
|
launching,
|
|
attached,
|
|
}
|
|
|
|
/// Wrapper to launch app and attach the debugger with ios-deploy.
|
|
class IOSDeployDebugger {
|
|
IOSDeployDebugger({
|
|
required Logger logger,
|
|
required ProcessUtils processUtils,
|
|
required List<String> launchCommand,
|
|
required Map<String, String> iosDeployEnv,
|
|
}) : _processUtils = processUtils,
|
|
_logger = logger,
|
|
_launchCommand = launchCommand,
|
|
_iosDeployEnv = iosDeployEnv,
|
|
_debuggerState = _IOSDeployDebuggerState.detached;
|
|
|
|
/// Create a [IOSDeployDebugger] for testing.
|
|
///
|
|
/// Sets the command to "ios-deploy" and environment to an empty map.
|
|
@visibleForTesting
|
|
factory IOSDeployDebugger.test({
|
|
required ProcessManager processManager,
|
|
Logger? logger,
|
|
}) {
|
|
final Logger debugLogger = logger ?? BufferLogger.test();
|
|
return IOSDeployDebugger(
|
|
logger: debugLogger,
|
|
processUtils: ProcessUtils(logger: debugLogger, processManager: processManager),
|
|
launchCommand: <String>['ios-deploy'],
|
|
iosDeployEnv: <String, String>{},
|
|
);
|
|
}
|
|
|
|
final Logger _logger;
|
|
final ProcessUtils _processUtils;
|
|
final List<String> _launchCommand;
|
|
final Map<String, String> _iosDeployEnv;
|
|
|
|
Process? _iosDeployProcess;
|
|
|
|
Stream<String> get logLines => _debuggerOutput.stream;
|
|
final StreamController<String> _debuggerOutput = StreamController<String>.broadcast();
|
|
|
|
bool get debuggerAttached => _debuggerState == _IOSDeployDebuggerState.attached;
|
|
_IOSDeployDebuggerState _debuggerState;
|
|
|
|
// (lldb) run
|
|
// https://github.com/ios-control/ios-deploy/blob/1.11.2-beta.1/src/ios-deploy/ios-deploy.m#L51
|
|
static final RegExp _lldbRun = RegExp(r'\(lldb\)\s*run');
|
|
|
|
// (lldb) run
|
|
// https://github.com/ios-control/ios-deploy/blob/1.11.2-beta.1/src/ios-deploy/ios-deploy.m#L51
|
|
static final RegExp _lldbProcessExit = RegExp(r'Process \d* exited with status =');
|
|
|
|
// (lldb) Process 6152 stopped
|
|
static final RegExp _lldbProcessStopped = RegExp(r'Process \d* stopped');
|
|
|
|
// (lldb) Process 6152 detached
|
|
static final RegExp _lldbProcessDetached = RegExp(r'Process \d* detached');
|
|
|
|
// (lldb) Process 6152 resuming
|
|
static final RegExp _lldbProcessResuming = RegExp(r'Process \d+ resuming');
|
|
|
|
// Send signal to stop (pause) the app. Used before a backtrace dump.
|
|
static const String _signalStop = 'process signal SIGSTOP';
|
|
|
|
static const String _processResume = 'process continue';
|
|
static const String _processInterrupt = 'process interrupt';
|
|
|
|
// Print backtrace for all threads while app is stopped.
|
|
static const String _backTraceAll = 'thread backtrace all';
|
|
|
|
/// If this is non-null, then the app process is paused and awaiting backtrace logging.
|
|
///
|
|
/// The future should be completed once the backtraces are logged.
|
|
Completer<void>? _processResumeCompleter;
|
|
|
|
/// Launch the app on the device, and attach the debugger.
|
|
///
|
|
/// Returns whether or not the debugger successfully attached.
|
|
Future<bool> launchAndAttach() async {
|
|
// Return when the debugger attaches, or the ios-deploy process exits.
|
|
final Completer<bool> debuggerCompleter = Completer<bool>();
|
|
try {
|
|
_iosDeployProcess = await _processUtils.start(
|
|
_launchCommand,
|
|
environment: _iosDeployEnv,
|
|
);
|
|
String? lastLineFromDebugger;
|
|
final StreamSubscription<String> stdoutSubscription = _iosDeployProcess!.stdout
|
|
.transform<String>(utf8.decoder)
|
|
.transform<String>(const LineSplitter())
|
|
.listen((String line) {
|
|
_monitorIOSDeployFailure(line, _logger);
|
|
// (lldb) run
|
|
// success
|
|
// 2020-09-15 13:42:25.185474-0700 Runner[477:181141] flutter: The Dart VM service is listening on http://127.0.0.1:57782/
|
|
if (_lldbRun.hasMatch(line)) {
|
|
_logger.printTrace(line);
|
|
_debuggerState = _IOSDeployDebuggerState.launching;
|
|
return;
|
|
}
|
|
// Next line after "run" must be "success", or the attach failed.
|
|
// Example: "error: process launch failed"
|
|
if (_debuggerState == _IOSDeployDebuggerState.launching) {
|
|
_logger.printTrace(line);
|
|
final bool attachSuccess = line == 'success';
|
|
_debuggerState = attachSuccess ? _IOSDeployDebuggerState.attached : _IOSDeployDebuggerState.detached;
|
|
if (!debuggerCompleter.isCompleted) {
|
|
debuggerCompleter.complete(attachSuccess);
|
|
}
|
|
return;
|
|
}
|
|
if (line == _signalStop) {
|
|
// The app is about to be stopped. Only show in verbose mode.
|
|
_logger.printTrace(line);
|
|
return;
|
|
}
|
|
if (line == _backTraceAll) {
|
|
// The app is stopped and the backtrace for all threads will be printed.
|
|
_logger.printTrace(line);
|
|
// Even though we're not "detached", just stopped, mark as detached so the backtrace
|
|
// is only show in verbose.
|
|
_debuggerState = _IOSDeployDebuggerState.detached;
|
|
|
|
// If we paused the app and are waiting to resume it, complete the completer
|
|
final Completer<void>? processResumeCompleter = _processResumeCompleter;
|
|
if (processResumeCompleter != null) {
|
|
_processResumeCompleter = null;
|
|
processResumeCompleter.complete();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (line.contains('PROCESS_STOPPED') || _lldbProcessStopped.hasMatch(line)) {
|
|
// The app has been stopped. Dump the backtrace, and detach.
|
|
_logger.printTrace(line);
|
|
_iosDeployProcess?.stdin.writeln(_backTraceAll);
|
|
if (_processResumeCompleter == null) {
|
|
detach();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (line.contains('PROCESS_EXITED') || _lldbProcessExit.hasMatch(line)) {
|
|
// The app exited or crashed, so exit. Continue passing debugging
|
|
// messages to the log reader until it exits to capture crash dumps.
|
|
_logger.printTrace(line);
|
|
exit();
|
|
return;
|
|
}
|
|
if (_lldbProcessDetached.hasMatch(line)) {
|
|
// The debugger has detached from the app, and there will be no more debugging messages.
|
|
// Kill the ios-deploy process.
|
|
exit();
|
|
return;
|
|
}
|
|
|
|
if (_lldbProcessResuming.hasMatch(line)) {
|
|
_logger.printTrace(line);
|
|
// we marked this detached when we received [_backTraceAll]
|
|
_debuggerState = _IOSDeployDebuggerState.attached;
|
|
return;
|
|
}
|
|
|
|
if (_debuggerState != _IOSDeployDebuggerState.attached) {
|
|
_logger.printTrace(line);
|
|
return;
|
|
}
|
|
if (lastLineFromDebugger != null && lastLineFromDebugger!.isNotEmpty && line.isEmpty) {
|
|
// The lldb console stream from ios-deploy is separated lines by an extra \r\n.
|
|
// To avoid all lines being double spaced, if the last line from the
|
|
// debugger was not an empty line, skip this empty line.
|
|
// This will still cause "legit" logged newlines to be doubled...
|
|
} else if (!_debuggerOutput.isClosed) {
|
|
_debuggerOutput.add(line);
|
|
}
|
|
lastLineFromDebugger = line;
|
|
});
|
|
final StreamSubscription<String> stderrSubscription = _iosDeployProcess!.stderr
|
|
.transform<String>(utf8.decoder)
|
|
.transform<String>(const LineSplitter())
|
|
.listen((String line) {
|
|
_monitorIOSDeployFailure(line, _logger);
|
|
_logger.printTrace(line);
|
|
});
|
|
unawaited(_iosDeployProcess!.exitCode.then((int status) async {
|
|
_logger.printTrace('ios-deploy exited with code $exitCode');
|
|
_debuggerState = _IOSDeployDebuggerState.detached;
|
|
await stdoutSubscription.cancel();
|
|
await stderrSubscription.cancel();
|
|
}).whenComplete(() async {
|
|
if (_debuggerOutput.hasListener) {
|
|
// Tell listeners the process died.
|
|
await _debuggerOutput.close();
|
|
}
|
|
if (!debuggerCompleter.isCompleted) {
|
|
debuggerCompleter.complete(false);
|
|
}
|
|
_iosDeployProcess = null;
|
|
}));
|
|
} on ProcessException catch (exception, stackTrace) {
|
|
_logger.printTrace('ios-deploy failed: $exception');
|
|
_debuggerState = _IOSDeployDebuggerState.detached;
|
|
if (!_debuggerOutput.isClosed) {
|
|
_debuggerOutput.addError(exception, stackTrace);
|
|
}
|
|
} on ArgumentError catch (exception, stackTrace) {
|
|
_logger.printTrace('ios-deploy failed: $exception');
|
|
_debuggerState = _IOSDeployDebuggerState.detached;
|
|
if (!_debuggerOutput.isClosed) {
|
|
_debuggerOutput.addError(exception, stackTrace);
|
|
}
|
|
}
|
|
// Wait until the debugger attaches, or the attempt fails.
|
|
return debuggerCompleter.future;
|
|
}
|
|
|
|
bool exit() {
|
|
final bool success = (_iosDeployProcess == null) || _iosDeployProcess!.kill();
|
|
_iosDeployProcess = null;
|
|
return success;
|
|
}
|
|
|
|
/// Pause app, dump backtrace for debugging, and resume.
|
|
Future<void> pauseDumpBacktraceResume() async {
|
|
if (!debuggerAttached) {
|
|
return;
|
|
}
|
|
final Completer<void> completer = Completer<void>();
|
|
_processResumeCompleter = completer;
|
|
try {
|
|
// Stop the app, which will prompt the backtrace to be printed for all threads in the stdoutSubscription handler.
|
|
_iosDeployProcess?.stdin.writeln(_processInterrupt);
|
|
} on SocketException catch (error) {
|
|
_logger.printTrace('Could not stop app from debugger: $error');
|
|
}
|
|
// wait for backtrace to be dumped
|
|
await completer.future;
|
|
_iosDeployProcess?.stdin.writeln(_processResume);
|
|
}
|
|
|
|
Future<void> stopAndDumpBacktrace() async {
|
|
if (!debuggerAttached) {
|
|
return;
|
|
}
|
|
try {
|
|
// Stop the app, which will prompt the backtrace to be printed for all threads in the stdoutSubscription handler.
|
|
_iosDeployProcess?.stdin.writeln(_signalStop);
|
|
} on SocketException catch (error) {
|
|
// Best effort, try to detach, but maybe the app already exited or already detached.
|
|
_logger.printTrace('Could not stop app from debugger: $error');
|
|
}
|
|
// Wait for logging to finish on process exit.
|
|
return logLines.drain();
|
|
}
|
|
|
|
void detach() {
|
|
if (!debuggerAttached) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Detach lldb from the app process.
|
|
_iosDeployProcess?.stdin.writeln('process detach');
|
|
} on SocketException catch (error) {
|
|
// Best effort, try to detach, but maybe the app already exited or already detached.
|
|
_logger.printTrace('Could not detach from debugger: $error');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Maps stdout line stream. Must return original line.
|
|
String _monitorIOSDeployFailure(String stdout, Logger logger) {
|
|
// Installation issues.
|
|
if (stdout.contains(noProvisioningProfileErrorOne) || stdout.contains(noProvisioningProfileErrorTwo)) {
|
|
logger.printError(noProvisioningProfileInstruction, emphasis: true);
|
|
|
|
// Launch issues.
|
|
} else if (stdout.contains(deviceLockedError)) {
|
|
logger.printError('''
|
|
═══════════════════════════════════════════════════════════════════════════════════
|
|
Your device is locked. Unlock your device first before running.
|
|
═══════════════════════════════════════════════════════════════════════════════════''',
|
|
emphasis: true);
|
|
} else if (stdout.contains(unknownAppLaunchError)) {
|
|
logger.printError('''
|
|
═══════════════════════════════════════════════════════════════════════════════════
|
|
Error launching app. Try launching from within Xcode via:
|
|
open ios/Runner.xcworkspace
|
|
|
|
Your Xcode version may be too old for your iOS version.
|
|
═══════════════════════════════════════════════════════════════════════════════════''',
|
|
emphasis: true);
|
|
}
|
|
|
|
return stdout;
|
|
}
|