mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
348 lines
9.3 KiB
Dart
348 lines
9.3 KiB
Dart
// Copyright 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:path/path.dart' as path;
|
|
|
|
import '../application_package.dart';
|
|
import '../base/common.dart';
|
|
import '../base/os.dart';
|
|
import '../base/process.dart';
|
|
import '../build_configuration.dart';
|
|
import '../device.dart';
|
|
import '../globals.dart';
|
|
import '../toolchain.dart';
|
|
import 'mac.dart';
|
|
|
|
const String _ideviceinstallerInstructions =
|
|
'To work with iOS devices, please install ideviceinstaller.\n'
|
|
'If you use homebrew, you can install it with "\$ brew install ideviceinstaller".';
|
|
|
|
class IOSDevices extends PollingDeviceDiscovery {
|
|
IOSDevices() : super('IOSDevices');
|
|
|
|
bool get supportsPlatform => Platform.isMacOS;
|
|
List<Device> pollingGetDevices() => IOSDevice.getAttachedDevices();
|
|
}
|
|
|
|
class IOSDevice extends Device {
|
|
IOSDevice(String id, { this.name }) : super(id) {
|
|
_installerPath = _checkForCommand('ideviceinstaller');
|
|
_listerPath = _checkForCommand('idevice_id');
|
|
_informerPath = _checkForCommand('ideviceinfo');
|
|
_debuggerPath = _checkForCommand('idevicedebug');
|
|
_loggerPath = _checkForCommand('idevicesyslog');
|
|
_pusherPath = _checkForCommand(
|
|
'ios-deploy',
|
|
'To copy files to iOS devices, please install ios-deploy. '
|
|
'You can do this using homebrew as follows:\n'
|
|
'\$ brew tap flutter/flutter\n'
|
|
'\$ brew install ios-deploy');
|
|
}
|
|
|
|
String _installerPath;
|
|
String get installerPath => _installerPath;
|
|
|
|
String _listerPath;
|
|
String get listerPath => _listerPath;
|
|
|
|
String _informerPath;
|
|
String get informerPath => _informerPath;
|
|
|
|
String _debuggerPath;
|
|
String get debuggerPath => _debuggerPath;
|
|
|
|
String _loggerPath;
|
|
String get loggerPath => _loggerPath;
|
|
|
|
String _pusherPath;
|
|
String get pusherPath => _pusherPath;
|
|
|
|
final String name;
|
|
|
|
_IOSDeviceLogReader _logReader;
|
|
|
|
_IOSDevicePortForwarder _portForwarder;
|
|
|
|
bool get isLocalEmulator => false;
|
|
|
|
bool get supportsStartPaused => false;
|
|
|
|
static List<IOSDevice> getAttachedDevices([IOSDevice mockIOS]) {
|
|
if (!doctor.iosWorkflow.hasIdeviceId)
|
|
return <IOSDevice>[];
|
|
|
|
List<IOSDevice> devices = [];
|
|
for (String id in _getAttachedDeviceIDs(mockIOS)) {
|
|
String name = _getDeviceName(id, mockIOS);
|
|
devices.add(new IOSDevice(id, name: name));
|
|
}
|
|
return devices;
|
|
}
|
|
|
|
static Iterable<String> _getAttachedDeviceIDs([IOSDevice mockIOS]) {
|
|
String listerPath = (mockIOS != null) ? mockIOS.listerPath : _checkForCommand('idevice_id');
|
|
try {
|
|
String output = runSync([listerPath, '-l']);
|
|
return output.trim().split('\n').where((String s) => s != null && s.isNotEmpty);
|
|
} catch (e) {
|
|
return <String>[];
|
|
}
|
|
}
|
|
|
|
static String _getDeviceName(String deviceID, [IOSDevice mockIOS]) {
|
|
String informerPath = (mockIOS != null)
|
|
? mockIOS.informerPath
|
|
: _checkForCommand('ideviceinfo');
|
|
return runSync([informerPath, '-k', 'DeviceName', '-u', deviceID]).trim();
|
|
}
|
|
|
|
static final Map<String, String> _commandMap = {};
|
|
static String _checkForCommand(
|
|
String command, [
|
|
String macInstructions = _ideviceinstallerInstructions
|
|
]) {
|
|
return _commandMap.putIfAbsent(command, () {
|
|
try {
|
|
command = runCheckedSync(['which', command]).trim();
|
|
} catch (e) {
|
|
if (Platform.isMacOS) {
|
|
printError('$command not found. $macInstructions');
|
|
} else {
|
|
printError('Cannot control iOS devices or simulators. $command is not available on your platform.');
|
|
}
|
|
}
|
|
return command;
|
|
});
|
|
}
|
|
|
|
@override
|
|
bool installApp(ApplicationPackage app) {
|
|
try {
|
|
runCheckedSync([installerPath, '-i', app.localPath]);
|
|
return true;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@override
|
|
bool isSupported() => true;
|
|
|
|
@override
|
|
bool isAppInstalled(ApplicationPackage app) {
|
|
try {
|
|
String apps = runCheckedSync([installerPath, '--list-apps']);
|
|
if (new RegExp(app.id, multiLine: true).hasMatch(apps)) {
|
|
return true;
|
|
}
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@override
|
|
Future<bool> startApp(
|
|
ApplicationPackage app,
|
|
Toolchain toolchain, {
|
|
String mainPath,
|
|
String route,
|
|
bool checked: true,
|
|
bool clearLogs: false,
|
|
bool startPaused: false,
|
|
int debugPort: observatoryDefaultPort,
|
|
Map<String, dynamic> platformArgs
|
|
}) async {
|
|
// TODO(chinmaygarde): Use checked, mainPath, route, clearLogs.
|
|
// TODO(devoncarew): Handle startPaused, debugPort.
|
|
printTrace('Building ${app.name} for $id');
|
|
|
|
// Step 1: Install the precompiled application if necessary.
|
|
bool buildResult = await buildIOSXcodeProject(app, buildForDevice: true);
|
|
if (!buildResult) {
|
|
printError('Could not build the precompiled application for the device.');
|
|
return false;
|
|
}
|
|
|
|
// Step 2: Check that the application exists at the specified path.
|
|
Directory bundle = new Directory(path.join(app.localPath, 'build', 'Release-iphoneos', 'Runner.app'));
|
|
bool bundleExists = bundle.existsSync();
|
|
if (!bundleExists) {
|
|
printError('Could not find the built application bundle at ${bundle.path}.');
|
|
return false;
|
|
}
|
|
|
|
// Step 3: Attempt to install the application on the device.
|
|
int installationResult = await runCommandAndStreamOutput([
|
|
'/usr/bin/env',
|
|
'ios-deploy',
|
|
'--id',
|
|
id,
|
|
'--bundle',
|
|
bundle.path,
|
|
]);
|
|
|
|
if (installationResult != 0) {
|
|
printError('Could not install ${bundle.path} on $id.');
|
|
return false;
|
|
}
|
|
|
|
printTrace('Installation successful.');
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
Future<bool> stopApp(ApplicationPackage app) async {
|
|
// Currently we don't have a way to stop an app running on iOS.
|
|
return false;
|
|
}
|
|
|
|
Future<bool> pushFile(ApplicationPackage app, String localFile, String targetFile) async {
|
|
if (Platform.isMacOS) {
|
|
runSync(<String>[
|
|
pusherPath,
|
|
'-t',
|
|
'1',
|
|
'--bundle_id',
|
|
app.id,
|
|
'--upload',
|
|
localFile,
|
|
'--to',
|
|
targetFile
|
|
]);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@override
|
|
TargetPlatform get platform => TargetPlatform.iOS;
|
|
|
|
DeviceLogReader get logReader {
|
|
if (_logReader == null)
|
|
_logReader = new _IOSDeviceLogReader(this);
|
|
|
|
return _logReader;
|
|
}
|
|
|
|
DevicePortForwarder get portForwarder {
|
|
if (_portForwarder == null)
|
|
_portForwarder = new _IOSDevicePortForwarder(this);
|
|
|
|
return _portForwarder;
|
|
}
|
|
|
|
void clearLogs() {
|
|
}
|
|
}
|
|
|
|
class _IOSDeviceLogReader extends DeviceLogReader {
|
|
_IOSDeviceLogReader(this.device);
|
|
|
|
final IOSDevice device;
|
|
|
|
final StreamController<String> _linesStreamController =
|
|
new StreamController<String>.broadcast();
|
|
|
|
Process _process;
|
|
StreamSubscription _stdoutSubscription;
|
|
StreamSubscription _stderrSubscription;
|
|
|
|
Stream<String> get lines => _linesStreamController.stream;
|
|
|
|
String get name => device.name;
|
|
|
|
bool get isReading => _process != null;
|
|
|
|
Future get finished =>
|
|
_process != null ? _process.exitCode : new Future.value(0);
|
|
|
|
Future start() async {
|
|
if (_process != null) {
|
|
throw new StateError(
|
|
'_IOSDeviceLogReader must be stopped before it can be started.');
|
|
}
|
|
_process = await runCommand(<String>[device.loggerPath]);
|
|
_stdoutSubscription =
|
|
_process.stdout.transform(UTF8.decoder)
|
|
.transform(const LineSplitter()).listen(_onLine);
|
|
_stderrSubscription =
|
|
_process.stderr.transform(UTF8.decoder)
|
|
.transform(const LineSplitter()).listen(_onLine);
|
|
_process.exitCode.then(_onExit);
|
|
}
|
|
|
|
Future stop() async {
|
|
if (_process == null) {
|
|
throw new StateError(
|
|
'_IOSDeviceLogReader must be started before it can be stopped.');
|
|
}
|
|
_stdoutSubscription?.cancel();
|
|
_stdoutSubscription = null;
|
|
_stderrSubscription?.cancel();
|
|
_stderrSubscription = null;
|
|
await _process.kill();
|
|
_process = null;
|
|
}
|
|
|
|
void _onExit(int exitCode) {
|
|
_stdoutSubscription?.cancel();
|
|
_stdoutSubscription = null;
|
|
_stderrSubscription?.cancel();
|
|
_stderrSubscription = null;
|
|
_process = null;
|
|
}
|
|
|
|
RegExp _runnerRegex = new RegExp(r'Runner');
|
|
|
|
void _onLine(String line) {
|
|
if (!_runnerRegex.hasMatch(line))
|
|
return;
|
|
|
|
_linesStreamController.add(line);
|
|
}
|
|
|
|
int get hashCode => name.hashCode;
|
|
|
|
bool operator ==(dynamic other) {
|
|
if (identical(this, other))
|
|
return true;
|
|
if (other is! _IOSDeviceLogReader)
|
|
return false;
|
|
return other.name == name;
|
|
}
|
|
}
|
|
|
|
class _IOSDevicePortForwarder extends DevicePortForwarder {
|
|
_IOSDevicePortForwarder(this.device);
|
|
|
|
final IOSDevice device;
|
|
|
|
List<ForwardedPort> get forwardedPorts {
|
|
final List<ForwardedPort> ports = <ForwardedPort>[];
|
|
// TODO(chinmaygarde): Implement.
|
|
return ports;
|
|
}
|
|
|
|
Future<int> forward(int devicePort, {int hostPort: null}) async {
|
|
if ((hostPort == null) || (hostPort == 0)) {
|
|
// Auto select host port.
|
|
hostPort = await findAvailablePort();
|
|
}
|
|
// TODO(chinmaygarde): Implement.
|
|
return hostPort;
|
|
}
|
|
|
|
Future unforward(ForwardedPort forwardedPort) async {
|
|
// TODO(chinmaygarde): Implement.
|
|
}
|
|
}
|