Unverified Commit 8474f41e authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

Migrate xcdevice and ios devices to null safety (#92056)

parent 921cfebb
......@@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'package:file/memory.dart';
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import 'base/common.dart';
......@@ -282,7 +281,6 @@ abstract class Artifacts {
/// If a [fileSystem] is not provided, creates a new [MemoryFileSystem] instance.
///
/// Creates a [LocalEngineArtifacts] if `localEngine` is non-null
@visibleForTesting
factory Artifacts.test({String? localEngine, FileSystem? fileSystem}) {
fileSystem ??= MemoryFileSystem.test();
if (localEngine != null) {
......
......@@ -127,7 +127,6 @@ class Cache {
/// Defaults to a memory file system, fake platform,
/// buffer logger, and no accessible artifacts.
/// By default, the root cache directory path is "cache".
@visibleForTesting
factory Cache.test({
Directory? rootOverride,
List<ArtifactSet>? artifacts,
......
......@@ -7,11 +7,8 @@
import 'base/context.dart';
import 'doctor.dart';
import 'ios/simulators.dart';
import 'macos/xcdevice.dart';
export 'globals_null_migrated.dart';
Doctor get doctor => context.get<Doctor>();
IOSSimulatorUtils get iosSimulatorUtils => context.get<IOSSimulatorUtils>();
XCDevice get xcdevice => context.get<XCDevice>();
......@@ -34,6 +34,7 @@ import 'ios/plist_parser.dart';
import 'ios/xcodeproj.dart';
import 'macos/cocoapods.dart';
import 'macos/cocoapods_validator.dart';
import 'macos/xcdevice.dart';
import 'macos/xcode.dart';
import 'persistent_tool_state.dart';
import 'project.dart';
......@@ -62,6 +63,7 @@ FlutterVersion get flutterVersion => context.get<FlutterVersion>()!;
FuchsiaArtifacts? get fuchsiaArtifacts => context.get<FuchsiaArtifacts>();
Usage get flutterUsage => context.get<Usage>()!;
XcodeProjectInterpreter? get xcodeProjectInterpreter => context.get<XcodeProjectInterpreter>();
XCDevice? get xcdevice => context.get<XCDevice>();
Xcode? get xcode => context.get<Xcode>();
IOSWorkflow? get iosWorkflow => context.get<IOSWorkflow>();
LocalEngineLocator? get localEngineLocator => context.get<LocalEngineLocator>();
......
......@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.8
import 'dart:async';
import 'package:meta/meta.dart';
......@@ -34,10 +32,10 @@ import 'mac.dart';
class IOSDevices extends PollingDeviceDiscovery {
IOSDevices({
@required Platform platform,
@required XCDevice xcdevice,
@required IOSWorkflow iosWorkflow,
@required Logger logger,
required Platform platform,
required XCDevice xcdevice,
required IOSWorkflow iosWorkflow,
required Logger logger,
}) : _platform = platform,
_xcdevice = xcdevice,
_iosWorkflow = iosWorkflow,
......@@ -55,7 +53,7 @@ class IOSDevices extends PollingDeviceDiscovery {
@override
bool get canListAnything => _iosWorkflow.canListDevices;
StreamSubscription<Map<XCDeviceEvent, String>> _observedDeviceEventsSubscription;
StreamSubscription<Map<XCDeviceEvent, String>>? _observedDeviceEventsSubscription;
@override
Future<void> startPolling() async {
......@@ -71,13 +69,13 @@ class IOSDevices extends PollingDeviceDiscovery {
deviceNotifier ??= ItemListNotifier<Device>();
// Start by populating all currently attached devices.
deviceNotifier.updateWithNewList(await pollingGetDevices());
deviceNotifier!.updateWithNewList(await pollingGetDevices());
// cancel any outstanding subscriptions.
await _observedDeviceEventsSubscription?.cancel();
_observedDeviceEventsSubscription = _xcdevice.observedDeviceEvents()?.listen(
_onDeviceEvent,
onError: (dynamic error, StackTrace stack) {
onError: (Object error, StackTrace stack) {
_logger.printTrace('Process exception running xcdevice observe:\n$error\n$stack');
}, onDone: () {
// If xcdevice is killed or otherwise dies, polling will be stopped.
......@@ -92,18 +90,26 @@ class IOSDevices extends PollingDeviceDiscovery {
Future<void> _onDeviceEvent(Map<XCDeviceEvent, String> event) async {
final XCDeviceEvent eventType = event.containsKey(XCDeviceEvent.attach) ? XCDeviceEvent.attach : XCDeviceEvent.detach;
final String deviceIdentifier = event[eventType];
final Device knownDevice = deviceNotifier.items
.firstWhere((Device device) => device.id == deviceIdentifier, orElse: () => null);
final String? deviceIdentifier = event[eventType];
final ItemListNotifier<Device>? notifier = deviceNotifier;
if (notifier == null) {
return;
}
Device? knownDevice;
for (final Device device in notifier.items) {
if (device.id == deviceIdentifier) {
knownDevice = device;
}
}
// Ignore already discovered devices (maybe populated at the beginning).
if (eventType == XCDeviceEvent.attach && knownDevice == null) {
// There's no way to get details for an individual attached device,
// so repopulate them all.
final List<Device> devices = await pollingGetDevices();
deviceNotifier.updateWithNewList(devices);
notifier.updateWithNewList(devices);
} else if (eventType == XCDeviceEvent.detach && knownDevice != null) {
deviceNotifier.removeItem(knownDevice);
notifier.removeItem(knownDevice);
}
}
......@@ -113,7 +119,7 @@ class IOSDevices extends PollingDeviceDiscovery {
}
@override
Future<List<Device>> pollingGetDevices({ Duration timeout }) async {
Future<List<Device>> pollingGetDevices({ Duration? timeout }) async {
if (!_platform.isMacOS) {
throw UnsupportedError(
'Control of iOS devices or simulators only supported on macOS.'
......@@ -140,16 +146,16 @@ class IOSDevices extends PollingDeviceDiscovery {
class IOSDevice extends Device {
IOSDevice(String id, {
@required FileSystem fileSystem,
@required this.name,
@required this.cpuArchitecture,
@required this.interfaceType,
@required String sdkVersion,
@required Platform platform,
@required IOSDeploy iosDeploy,
@required IMobileDevice iMobileDevice,
@required IProxy iProxy,
@required Logger logger,
required FileSystem fileSystem,
required this.name,
required this.cpuArchitecture,
required this.interfaceType,
String? sdkVersion,
required Platform platform,
required IOSDeploy iosDeploy,
required IMobileDevice iMobileDevice,
required IProxy iProxy,
required Logger logger,
})
: _sdkVersion = sdkVersion,
_iosDeploy = iosDeploy,
......@@ -170,7 +176,7 @@ class IOSDevice extends Device {
}
}
final String _sdkVersion;
final String? _sdkVersion;
final IOSDeploy _iosDeploy;
final FileSystem _fileSystem;
final Logger _logger;
......@@ -180,7 +186,7 @@ class IOSDevice extends Device {
/// May be 0 if version cannot be parsed.
int get majorSdkVersion {
final String majorVersionString = _sdkVersion?.split('.')?.first?.trim();
final String? majorVersionString = _sdkVersion?.split('.').first.trim();
return majorVersionString != null ? int.tryParse(majorVersionString) ?? 0 : 0;
}
......@@ -203,18 +209,18 @@ class IOSDevice extends Device {
final IOSDeviceConnectionInterface interfaceType;
Map<IOSApp, DeviceLogReader> _logReaders;
final Map<IOSApp?, DeviceLogReader> _logReaders = <IOSApp?, DeviceLogReader>{};
DevicePortForwarder _portForwarder;
DevicePortForwarder? _portForwarder;
@visibleForTesting
IOSDeployDebugger iosDeployDebugger;
IOSDeployDebugger? iosDeployDebugger;
@override
Future<bool> get isLocalEmulator async => false;
@override
Future<String> get emulatorId async => null;
Future<String?> get emulatorId async => null;
@override
bool get supportsStartPaused => false;
......@@ -222,7 +228,7 @@ class IOSDevice extends Device {
@override
Future<bool> isAppInstalled(
IOSApp app, {
String userIdentifier,
String? userIdentifier,
}) async {
bool result;
try {
......@@ -243,7 +249,7 @@ class IOSDevice extends Device {
@override
Future<bool> installApp(
IOSApp app, {
String userIdentifier,
String? userIdentifier,
}) async {
final Directory bundle = _fileSystem.directory(app.deviceBundlePath);
if (!bundle.existsSync()) {
......@@ -277,7 +283,7 @@ class IOSDevice extends Device {
@override
Future<bool> uninstallApp(
IOSApp app, {
String userIdentifier,
String? userIdentifier,
}) async {
int uninstallationResult;
try {
......@@ -302,16 +308,16 @@ class IOSDevice extends Device {
@override
Future<LaunchResult> startApp(
IOSApp package, {
String mainPath,
String route,
DebuggingOptions debuggingOptions,
Map<String, dynamic> platformArgs,
String? mainPath,
String? route,
required DebuggingOptions debuggingOptions,
Map<String, Object?> platformArgs = const <String, Object?>{},
bool prebuiltApplication = false,
bool ipv6 = false,
String userIdentifier,
@visibleForTesting Duration discoveryTimeout,
String? userIdentifier,
@visibleForTesting Duration? discoveryTimeout,
}) async {
String packageId;
String? packageId;
if (!prebuiltApplication) {
_logger.printTrace('Building ${package.name} for $id');
......@@ -321,7 +327,6 @@ class IOSDevice extends Device {
app: package as BuildableIOSApp,
buildInfo: debuggingOptions.buildInfo,
targetOverride: mainPath,
environmentType: EnvironmentType.physical,
activeArch: cpuArchitecture,
deviceID: id,
);
......@@ -366,14 +371,14 @@ class IOSDevice extends Device {
if (debuggingOptions.verboseSystemLogs) '--verbose-logging',
if (debuggingOptions.cacheSkSL) '--cache-sksl',
if (debuggingOptions.purgePersistentCache) '--purge-persistent-cache',
if (platformArgs['trace-startup'] as bool ?? false) '--trace-startup',
if (platformArgs['trace-startup'] as bool? ?? false) '--trace-startup',
];
final Status installStatus = _logger.startProgress(
'Installing and launching...',
);
try {
ProtocolDiscovery observatoryDiscovery;
ProtocolDiscovery? observatoryDiscovery;
int installationResult = 1;
if (debuggingOptions.debuggingEnabled) {
_logger.printTrace('Debugging is enabled, connecting to observatory');
......@@ -411,7 +416,7 @@ class IOSDevice extends Device {
interfaceType: interfaceType,
);
} else {
installationResult = await iosDeployDebugger.launchAndAttach() ? 0 : 1;
installationResult = await iosDeployDebugger!.launchAndAttach() ? 0 : 1;
}
if (installationResult != 0) {
_logger.printError('Could not run ${bundle.path} on $id.');
......@@ -429,7 +434,7 @@ class IOSDevice extends Device {
final Timer timer = Timer(discoveryTimeout ?? const Duration(seconds: 30), () {
_logger.printError('iOS Observatory not discovered after 30 seconds. This is taking much longer than expected...');
});
final Uri localUri = await observatoryDiscovery.uri;
final Uri? localUri = await observatoryDiscovery?.uri;
timer.cancel();
if (localUri == null) {
iosDeployDebugger?.detach();
......@@ -448,12 +453,12 @@ class IOSDevice extends Device {
@override
Future<bool> stopApp(
IOSApp app, {
String userIdentifier,
String? userIdentifier,
}) async {
// If the debugger is not attached, killing the ios-deploy process won't stop the app.
if (iosDeployDebugger!= null && iosDeployDebugger.debuggerAttached) {
// Avoid null.
return iosDeployDebugger?.exit() == true;
final IOSDeployDebugger? deployDebugger = iosDeployDebugger;
if (deployDebugger != null && deployDebugger.debuggerAttached) {
return deployDebugger.exit() == true;
}
return false;
}
......@@ -466,11 +471,10 @@ class IOSDevice extends Device {
@override
DeviceLogReader getLogReader({
IOSApp app,
IOSApp? app,
bool includePastLogs = false,
}) {
assert(!includePastLogs, 'Past log reading not supported on iOS devices.');
_logReaders ??= <IOSApp, DeviceLogReader>{};
return _logReaders.putIfAbsent(app, () => IOSDeviceLogReader.create(
device: this,
app: app,
......@@ -480,7 +484,6 @@ class IOSDevice extends Device {
@visibleForTesting
void setLogReader(IOSApp app, DeviceLogReader logReader) {
_logReaders ??= <IOSApp, DeviceLogReader>{};
_logReaders[app] = logReader;
}
......@@ -515,9 +518,10 @@ class IOSDevice extends Device {
@override
Future<void> dispose() async {
_logReaders?.forEach((IOSApp application, DeviceLogReader logReader) {
for (final DeviceLogReader logReader in _logReaders.values) {
logReader.dispose();
});
}
_logReaders.clear();
await _portForwarder?.dispose();
}
}
......@@ -590,31 +594,19 @@ class IOSDeviceLogReader extends DeviceLogReader {
this._deviceId,
this.name,
String appName,
) {
_linesController = StreamController<String>.broadcast(
onListen: _listenToSysLog,
onCancel: dispose,
);
// Match for lines for the runner in syslog.
) : // Match for lines for the runner in syslog.
//
// iOS 9 format: Runner[297] <Notice>:
// iOS 10 format: Runner(Flutter)[297] <Notice>:
_runnerLineRegex = RegExp(appName + r'(\(Flutter\))?\[[\d]+\] <[A-Za-z]+>: ');
// Similar to above, but allows ~arbitrary components instead of "Runner"
// and "Flutter". The regex tries to strike a balance between not producing
// false positives and not producing false negatives.
_anyLineRegex = RegExp(r'\w+(\([^)]*\))?\[\d+\] <[A-Za-z]+>: ');
_loggingSubscriptions = <StreamSubscription<void>>[];
}
/// Create a new [IOSDeviceLogReader].
factory IOSDeviceLogReader.create({
@required IOSDevice device,
@required IOSApp app,
@required IMobileDevice iMobileDevice,
required IOSDevice device,
IOSApp? app,
required IMobileDevice iMobileDevice,
}) {
final String appName = app == null ? '' : app.name.replaceAll('.app', '');
final String appName = app?.name?.replaceAll('.app', '') ?? '';
return IOSDeviceLogReader._(
iMobileDevice,
device.majorSdkVersion,
......@@ -626,7 +618,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
/// Create an [IOSDeviceLogReader] for testing.
factory IOSDeviceLogReader.test({
@required IMobileDevice iMobileDevice,
required IMobileDevice iMobileDevice,
bool useSyslog = true,
}) {
return IOSDeviceLogReader._(
......@@ -641,8 +633,11 @@ class IOSDeviceLogReader extends DeviceLogReader {
// Matches a syslog line from the runner.
RegExp _runnerLineRegex;
// Matches a syslog line from any app.
RegExp _anyLineRegex;
// Similar to above, but allows ~arbitrary components instead of "Runner"
// and "Flutter". The regex tries to strike a balance between not producing
// false positives and not producing false negatives.
final RegExp _anyLineRegex = RegExp(r'\w+(\([^)]*\))?\[\d+\] <[A-Za-z]+>: ');
// Logging from native code/Flutter engine is prefixed by timestamp and process metadata:
// 2020-09-15 19:15:10.931434-0700 Runner[541:226276] Did finish launching.
......@@ -651,19 +646,24 @@ class IOSDeviceLogReader extends DeviceLogReader {
// Logging from the dart code has no prefixing metadata.
final RegExp _debuggerLoggingRegex = RegExp(r'^\S* \S* \S*\[[0-9:]*] (.*)');
StreamController<String> _linesController;
List<StreamSubscription<void>> _loggingSubscriptions;
late final StreamController<String> _linesController = StreamController<String>.broadcast(
onListen: _listenToSysLog,
onCancel: dispose,
);
final List<StreamSubscription<void>> _loggingSubscriptions = <StreamSubscription<void>>[];
@override
Stream<String> get logLines => _linesController.stream;
@override
FlutterVmService get connectedVMService => _connectedVMService;
FlutterVmService _connectedVMService;
FlutterVmService? get connectedVMService => _connectedVMService;
FlutterVmService? _connectedVMService;
@override
set connectedVMService(FlutterVmService connectedVmService) {
set connectedVMService(FlutterVmService? connectedVmService) {
if (connectedVmService != null) {
_listenToUnifiedLoggingEvents(connectedVmService);
}
_connectedVMService = connectedVmService;
}
......@@ -687,7 +687,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
}
void logMessage(vm_service.Event event) {
if (_iosDeployDebugger != null && _iosDeployDebugger.debuggerAttached) {
if (_iosDeployDebugger != null && _iosDeployDebugger!.debuggerAttached) {
// Prefer the more complete logs from the attached debugger.
return;
}
......@@ -704,13 +704,16 @@ class IOSDeviceLogReader extends DeviceLogReader {
}
/// Log reader will listen to [debugger.logLines] and will detach debugger on dispose.
IOSDeployDebugger get debuggerStream => _iosDeployDebugger;
set debuggerStream(IOSDeployDebugger debugger) {
IOSDeployDebugger? get debuggerStream => _iosDeployDebugger;
set debuggerStream(IOSDeployDebugger? debugger) {
// Logging is gathered from syslog on iOS 13 and earlier.
if (_majorSdkVersion < minimumUniversalLoggingSdkVersion) {
return;
}
_iosDeployDebugger = debugger;
if (debugger == null) {
return;
}
// Add the debugger logs to the controller created on initialization.
_loggingSubscriptions.add(debugger.logLines.listen(
(String line) => _linesController.add(_debuggerLineHandler(line)),
......@@ -719,10 +722,10 @@ class IOSDeviceLogReader extends DeviceLogReader {
cancelOnError: true,
));
}
IOSDeployDebugger _iosDeployDebugger;
IOSDeployDebugger? _iosDeployDebugger;
// Strip off the logging metadata (leave the category), or just echo the line.
String _debuggerLineHandler(String line) => _debuggerLoggingRegex?.firstMatch(line)?.group(1) ?? line;
String _debuggerLineHandler(String line) => _debuggerLoggingRegex.firstMatch(line)?.group(1) ?? line;
void _listenToSysLog() {
// syslog is not written on iOS 13+.
......@@ -743,7 +746,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
}
@visibleForTesting
Process idevicesyslogProcess;
Process? idevicesyslogProcess;
// Returns a stateful line handler to properly capture multiline output.
//
......@@ -764,7 +767,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
printing = false;
}
final Match match = _runnerLineRegex.firstMatch(line);
final Match? match = _runnerLineRegex.firstMatch(line);
if (match != null) {
final String logLine = line.substring(match.end);
......@@ -791,10 +794,10 @@ class IOSDevicePortForwarder extends DevicePortForwarder {
/// Create a new [IOSDevicePortForwarder].
IOSDevicePortForwarder({
@required Logger logger,
@required String id,
@required IProxy iproxy,
@required OperatingSystemUtils operatingSystemUtils,
required Logger logger,
required String id,
required IProxy iproxy,
required OperatingSystemUtils operatingSystemUtils,
}) : _logger = logger,
_id = id,
_iproxy = iproxy,
......@@ -807,10 +810,10 @@ class IOSDevicePortForwarder extends DevicePortForwarder {
///
/// The device id may be provided, but otherwise defaults to '1234'.
factory IOSDevicePortForwarder.test({
@required ProcessManager processManager,
@required Logger logger,
String id,
OperatingSystemUtils operatingSystemUtils,
required ProcessManager processManager,
required Logger logger,
String? id,
required OperatingSystemUtils operatingSystemUtils,
}) {
return IOSDevicePortForwarder(
logger: logger,
......@@ -839,20 +842,20 @@ class IOSDevicePortForwarder extends DevicePortForwarder {
static const Duration _kiProxyPortForwardTimeout = Duration(seconds: 1);
@override
Future<int> forward(int devicePort, { int hostPort }) async {
Future<int> forward(int devicePort, { int? hostPort }) async {
final bool autoselect = hostPort == null || hostPort == 0;
if (autoselect) {
final int freePort = await _operatingSystemUtils?.findFreePort();
final int freePort = await _operatingSystemUtils.findFreePort();
// Dynamic port range 49152 - 65535.
hostPort = freePort == null || freePort == 0 ? 49152 : freePort;
hostPort = freePort == 0 ? 49152 : freePort;
}
Process process;
Process? process;
bool connected = false;
while (!connected) {
_logger.printTrace('Attempting to forward device port $devicePort to host port $hostPort');
process = await _iproxy.forward(devicePort, hostPort, _id);
process = await _iproxy.forward(devicePort, hostPort!, _id);
// TODO(ianh): This is a flaky race condition, https://github.com/libimobiledevice/libimobiledevice/issues/674
connected = !await process.stdout.isEmpty.timeout(_kiProxyPortForwardTimeout, onTimeout: () => false);
if (!connected) {
......@@ -871,7 +874,7 @@ class IOSDevicePortForwarder extends DevicePortForwarder {
assert(process != null);
final ForwardedPort forwardedPort = ForwardedPort.withContext(
hostPort, devicePort, process,
hostPort!, devicePort, process,
);
_logger.printTrace('Forwarded port $forwardedPort');
forwardedPorts.add(forwardedPort);
......
......@@ -45,6 +45,16 @@ class IMobileDevice {
_processUtils = ProcessUtils(logger: logger, processManager: processManager),
_processManager = processManager;
/// Create an [IMobileDevice] for testing.
factory IMobileDevice.test({ required ProcessManager processManager }) {
return IMobileDevice(
artifacts: Artifacts.test(),
cache: Cache.test(processManager: processManager),
processManager: processManager,
logger: BufferLogger.test(),
);
}
final String _idevicesyslogPath;
final String _idevicescreenshotPath;
final MapEntry<String, String> _dyLdLibEntry;
......@@ -93,7 +103,7 @@ class IMobileDevice {
Future<XcodeBuildResult> buildXcodeProject({
required BuildableIOSApp app,
required BuildInfo buildInfo,
required String targetOverride,
String? targetOverride,
EnvironmentType environmentType = EnvironmentType.physical,
DarwinArch? activeArch,
bool codesign = true,
......
......@@ -2,11 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.8
import 'dart:async';
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import '../artifacts.dart';
......@@ -34,13 +31,13 @@ enum XCDeviceEvent {
/// A utility class for interacting with Xcode xcdevice command line tools.
class XCDevice {
XCDevice({
@required Artifacts artifacts,
@required Cache cache,
@required ProcessManager processManager,
@required Logger logger,
@required Xcode xcode,
@required Platform platform,
@required IProxy iproxy,
required Artifacts artifacts,
required Cache cache,
required ProcessManager processManager,
required Logger logger,
required Xcode xcode,
required Platform platform,
required IProxy iproxy,
}) : _processUtils = ProcessUtils(logger: logger, processManager: processManager),
_logger = logger,
_iMobileDevice = IMobileDevice(
......@@ -73,9 +70,9 @@ class XCDevice {
final Xcode _xcode;
final IProxy _iProxy;
List<dynamic> _cachedListResults;
Process _deviceObservationProcess;
StreamController<Map<XCDeviceEvent, String>> _deviceIdentifierByEvent;
List<Object>? _cachedListResults;
Process? _deviceObservationProcess;
StreamController<Map<XCDeviceEvent, String>>? _deviceIdentifierByEvent;
void _setupDeviceIdentifierByEventStream() {
// _deviceIdentifierByEvent Should always be available for listeners
......@@ -88,9 +85,9 @@ class XCDevice {
bool get isInstalled => _xcode.isInstalledAndMeetsVersionCheck;
Future<List<dynamic>> _getAllDevices({
Future<List<Object>?> _getAllDevices({
bool useCache = false,
@required Duration timeout
required Duration timeout
}) async {
if (!isInstalled) {
_logger.printTrace("Xcode not found. Run 'flutter doctor' for more information.");
......@@ -114,7 +111,7 @@ class XCDevice {
if (result.exitCode == 0) {
final String listOutput = result.stdout;
try {
final List<dynamic> listResults = json.decode(listOutput) as List<dynamic>;
final List<Object> listResults = (json.decode(result.stdout) as List<Object?>).whereType<Object>().toList();
_cachedListResults = listResults;
return listResults;
} on FormatException {
......@@ -137,12 +134,12 @@ class XCDevice {
///
/// Each attach and detach event is a tuple of one event type
/// and identifier.
Stream<Map<XCDeviceEvent, String>> observedDeviceEvents() {
Stream<Map<XCDeviceEvent, String>>? observedDeviceEvents() {
if (!isInstalled) {
_logger.printTrace("Xcode not found. Run 'flutter doctor' for more information.");
return null;
}
return _deviceIdentifierByEvent.stream;
return _deviceIdentifierByEvent?.stream;
}
// Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
......@@ -171,7 +168,7 @@ class XCDevice {
],
);
final StreamSubscription<String> stdoutSubscription = _deviceObservationProcess.stdout
final StreamSubscription<String> stdoutSubscription = _deviceObservationProcess!.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
......@@ -183,35 +180,35 @@ class XCDevice {
// Attach: 00008027-00192736010F802E
// Detach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
// Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
final RegExpMatch match = _observationIdentifierPattern.firstMatch(line);
final RegExpMatch? match = _observationIdentifierPattern.firstMatch(line);
if (match != null && match.groupCount == 2) {
final String verb = match.group(1).toLowerCase();
final String identifier = match.group(2);
final String verb = match.group(1)!.toLowerCase();
final String identifier = match.group(2)!;
if (verb.startsWith('attach')) {
_deviceIdentifierByEvent.add(<XCDeviceEvent, String>{
_deviceIdentifierByEvent?.add(<XCDeviceEvent, String>{
XCDeviceEvent.attach: identifier
});
} else if (verb.startsWith('detach')) {
_deviceIdentifierByEvent.add(<XCDeviceEvent, String>{
_deviceIdentifierByEvent?.add(<XCDeviceEvent, String>{
XCDeviceEvent.detach: identifier
});
}
}
});
final StreamSubscription<String> stderrSubscription = _deviceObservationProcess.stderr
final StreamSubscription<String> stderrSubscription = _deviceObservationProcess!.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
_logger.printTrace('xcdevice observe error: $line');
});
unawaited(_deviceObservationProcess.exitCode.then((int status) {
unawaited(_deviceObservationProcess?.exitCode.then((int status) {
_logger.printTrace('xcdevice exited with code $exitCode');
unawaited(stdoutSubscription.cancel());
unawaited(stderrSubscription.cancel());
}).whenComplete(() async {
if (_deviceIdentifierByEvent.hasListener) {
if (_deviceIdentifierByEvent?.hasListener == true) {
// Tell listeners the process died.
await _deviceIdentifierByEvent.close();
await _deviceIdentifierByEvent?.close();
}
_deviceObservationProcess = null;
......@@ -219,9 +216,9 @@ class XCDevice {
_setupDeviceIdentifierByEventStream();
}));
} on ProcessException catch (exception, stackTrace) {
_deviceIdentifierByEvent.addError(exception, stackTrace);
_deviceIdentifierByEvent?.addError(exception, stackTrace);
} on ArgumentError catch (exception, stackTrace) {
_deviceIdentifierByEvent.addError(exception, stackTrace);
_deviceIdentifierByEvent?.addError(exception, stackTrace);
}
}
......@@ -230,8 +227,8 @@ class XCDevice {
}
/// [timeout] defaults to 2 seconds.
Future<List<IOSDevice>> getAvailableIOSDevices({ Duration timeout }) async {
final List<dynamic> allAvailableDevices = await _getAllDevices(timeout: timeout ?? const Duration(seconds: 2));
Future<List<IOSDevice>> getAvailableIOSDevices({ Duration? timeout }) async {
final List<Object>? allAvailableDevices = await _getAllDevices(timeout: timeout ?? const Duration(seconds: 2));
if (allAvailableDevices == null) {
return const <IOSDevice>[];
......@@ -275,22 +272,29 @@ class XCDevice {
// ...
final List<IOSDevice> devices = <IOSDevice>[];
for (final dynamic device in allAvailableDevices) {
if (device is Map<String, dynamic>) {
for (final Object device in allAvailableDevices) {
if (device is Map<String, Object?>) {
// Only include iPhone, iPad, iPod, or other iOS devices.
if (!_isIPhoneOSDevice(device)) {
continue;
}
final String? identifier = device['identifier'] as String?;
final String? name = device['name'] as String?;
if (identifier == null || name == null) {
continue;
}
final Map<String, dynamic> errorProperties = _errorProperties(device);
final Map<String, Object?>? errorProperties = _errorProperties(device);
if (errorProperties != null) {
final String errorMessage = _parseErrorMessage(errorProperties);
final String? errorMessage = _parseErrorMessage(errorProperties);
if (errorMessage != null) {
if (errorMessage.contains('not paired')) {
UsageEvent('device', 'ios-trust-failure', flutterUsage: globals.flutterUsage).send();
}
_logger.printTrace(errorMessage);
}
final int code = _errorCode(errorProperties);
final int? code = _errorCode(errorProperties);
// Temporary error -10: iPhone is busy: Preparing debugger support for iPhone.
// Sometimes the app launch will fail on these devices until Xcode is done setting up the device.
......@@ -308,18 +312,18 @@ class XCDevice {
continue;
}
String sdkVersion = _sdkVersion(device);
String? sdkVersion = _sdkVersion(device);
if (sdkVersion != null) {
final String buildVersion = _buildVersion(device);
final String? buildVersion = _buildVersion(device);
if (buildVersion != null) {
sdkVersion = '$sdkVersion $buildVersion';
}
}
devices.add(IOSDevice(
device['identifier'] as String,
name: device['name'] as String,
identifier,
name: name,
cpuArchitecture: _cpuArchitecture(device),
interfaceType: interface,
sdkVersion: sdkVersion,
......@@ -338,33 +342,30 @@ class XCDevice {
/// Despite the name, com.apple.platform.iphoneos includes iPhone, iPads, and all iOS devices.
/// Excludes simulators.
static bool _isIPhoneOSDevice(Map<String, dynamic> deviceProperties) {
if (deviceProperties.containsKey('platform')) {
final String platform = deviceProperties['platform'] as String;
static bool _isIPhoneOSDevice(Map<String, Object?> deviceProperties) {
final Object? platform = deviceProperties['platform'];
if (platform is String) {
return platform == 'com.apple.platform.iphoneos';
}
return false;
}
static Map<String, dynamic> _errorProperties(Map<String, dynamic> deviceProperties) {
if (deviceProperties.containsKey('error')) {
return deviceProperties['error'] as Map<String, dynamic>;
}
return null;
static Map<String, Object?>? _errorProperties(Map<String, Object?> deviceProperties) {
final Object? error = deviceProperties['error'];
return error is Map<String, Object?> ? error : null;
}
static int _errorCode(Map<String, dynamic> errorProperties) {
if (errorProperties.containsKey('code') && errorProperties['code'] is int) {
return errorProperties['code'] as int;
}
return null;
static int? _errorCode(Map<String, Object?> errorProperties) {
final Object? code = errorProperties['code'];
return code is int ? code : null;
}
static IOSDeviceConnectionInterface _interfaceType(Map<String, dynamic> deviceProperties) {
static IOSDeviceConnectionInterface _interfaceType(Map<String, Object?> deviceProperties) {
// Interface can be "usb", "network", or "none" for simulators
// and unknown future interfaces.
if (deviceProperties.containsKey('interface')) {
if ((deviceProperties['interface'] as String).toLowerCase() == 'network') {
final Object? interface = deviceProperties['interface'];
if (interface is String) {
if (interface.toLowerCase() == 'network') {
return IOSDeviceConnectionInterface.network;
} else {
return IOSDeviceConnectionInterface.usb;
......@@ -374,13 +375,13 @@ class XCDevice {
return IOSDeviceConnectionInterface.none;
}
static String _sdkVersion(Map<String, dynamic> deviceProperties) {
if (deviceProperties.containsKey('operatingSystemVersion')) {
static String? _sdkVersion(Map<String, Object?> deviceProperties) {
final Object? operatingSystemVersion = deviceProperties['operatingSystemVersion'];
if (operatingSystemVersion is String) {
// Parse out the OS version, ignore the build number in parentheses.
// "13.3 (17C54)"
final RegExp operatingSystemRegex = RegExp(r'(.*) \(.*\)$');
final String operatingSystemVersion = deviceProperties['operatingSystemVersion'] as String;
if(operatingSystemRegex.hasMatch(operatingSystemVersion.trim())) {
if (operatingSystemRegex.hasMatch(operatingSystemVersion.trim())) {
return operatingSystemRegex.firstMatch(operatingSystemVersion.trim())?.group(1);
}
return operatingSystemVersion;
......@@ -388,20 +389,20 @@ class XCDevice {
return null;
}
static String _buildVersion(Map<String, dynamic> deviceProperties) {
if (deviceProperties.containsKey('operatingSystemVersion')) {
static String? _buildVersion(Map<String, Object?> deviceProperties) {
final Object? operatingSystemVersion = deviceProperties['operatingSystemVersion'];
if (operatingSystemVersion is String) {
// Parse out the build version, for example 17C54 from "13.3 (17C54)".
final RegExp buildVersionRegex = RegExp(r'\(.*\)$');
final String operatingSystemVersion = deviceProperties['operatingSystemVersion'] as String;
return buildVersionRegex.firstMatch(operatingSystemVersion)?.group(0)?.replaceAll(RegExp('[()]'), '');
}
return null;
}
DarwinArch _cpuArchitecture(Map<String, dynamic> deviceProperties) {
DarwinArch cpuArchitecture;
if (deviceProperties.containsKey('architecture')) {
final String architecture = deviceProperties['architecture'] as String;
DarwinArch _cpuArchitecture(Map<String, Object?> deviceProperties) {
DarwinArch? cpuArchitecture;
final Object? architecture = deviceProperties['architecture'];
if (architecture is String) {
try {
cpuArchitecture = getIOSArchForName(architecture);
} on Exception {
......@@ -420,11 +421,11 @@ class XCDevice {
);
}
}
return cpuArchitecture;
return cpuArchitecture ?? DarwinArch.arm64;
}
/// Error message parsed from xcdevice. null if no error.
static String _parseErrorMessage(Map<String, dynamic> errorProperties) {
static String? _parseErrorMessage(Map<String, Object?>? errorProperties) {
// {
// "simulator" : false,
// "operatingSystemVersion" : "13.3 (17C54)",
......@@ -479,8 +480,8 @@ class XCDevice {
final StringBuffer errorMessage = StringBuffer('Error: ');
if (errorProperties.containsKey('description')) {
final String description = errorProperties['description'] as String;
final Object? description = errorProperties['description'];
if (description is String) {
errorMessage.write(description);
if (!description.endsWith('.')) {
errorMessage.write('.');
......@@ -489,12 +490,12 @@ class XCDevice {
errorMessage.write('Xcode pairing error.');
}
if (errorProperties.containsKey('recoverySuggestion')) {
final String recoverySuggestion = errorProperties['recoverySuggestion'] as String;
final Object? recoverySuggestion = errorProperties['recoverySuggestion'];
if (recoverySuggestion is String) {
errorMessage.write(' $recoverySuggestion');
}
final int code = _errorCode(errorProperties);
final int? code = _errorCode(errorProperties);
if (code != null) {
errorMessage.write(' (code $code)');
}
......@@ -504,7 +505,7 @@ class XCDevice {
/// List of all devices reporting errors.
Future<List<String>> getDiagnostics() async {
final List<dynamic> allAvailableDevices = await _getAllDevices(
final List<Object>? allAvailableDevices = await _getAllDevices(
useCache: true,
timeout: const Duration(seconds: 2)
);
......@@ -514,13 +515,12 @@ class XCDevice {
}
final List<String> diagnostics = <String>[];
for (final dynamic device in allAvailableDevices) {
if (device is! Map) {
for (final Object deviceProperties in allAvailableDevices) {
if (deviceProperties is! Map<String, Object?>) {
continue;
}
final Map<String, dynamic> deviceProperties = device as Map<String, dynamic>;
final Map<String, dynamic> errorProperties = _errorProperties(deviceProperties);
final String errorMessage = _parseErrorMessage(errorProperties);
final Map<String, Object?>? errorProperties = _errorProperties(deviceProperties);
final String? errorMessage = _parseErrorMessage(errorProperties);
if (errorMessage != null) {
diagnostics.add(errorMessage);
}
......
......@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.8
import 'dart:async';
import 'dart:io' as io;
......@@ -39,16 +37,17 @@ void main() {
group('IOSDevice', () {
final List<Platform> unsupportedPlatforms = <Platform>[linuxPlatform, windowsPlatform];
Cache cache;
Logger logger;
IOSDeploy iosDeploy;
IMobileDevice iMobileDevice;
FileSystem nullFileSystem;
late Cache cache;
late Logger logger;
late IOSDeploy iosDeploy;
late IMobileDevice iMobileDevice;
late FileSystem fileSystem;
setUp(() {
final Artifacts artifacts = Artifacts.test();
cache = Cache.test(processManager: FakeProcessManager.any());
logger = BufferLogger.test();
fileSystem = MemoryFileSystem.test();
iosDeploy = IOSDeploy(
artifacts: artifacts,
cache: cache,
......@@ -68,7 +67,7 @@ void main() {
IOSDevice(
'device-123',
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
fileSystem: nullFileSystem,
fileSystem: fileSystem,
logger: logger,
platform: macPlatform,
iosDeploy: iosDeploy,
......@@ -84,7 +83,7 @@ void main() {
expect(IOSDevice(
'device-123',
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
fileSystem: nullFileSystem,
fileSystem: fileSystem,
logger: logger,
platform: macPlatform,
iosDeploy: iosDeploy,
......@@ -97,7 +96,7 @@ void main() {
expect(IOSDevice(
'device-123',
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
fileSystem: nullFileSystem,
fileSystem: fileSystem,
logger: logger,
platform: macPlatform,
iosDeploy: iosDeploy,
......@@ -110,7 +109,7 @@ void main() {
expect(IOSDevice(
'device-123',
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
fileSystem: nullFileSystem,
fileSystem: fileSystem,
logger: logger,
platform: macPlatform,
iosDeploy: iosDeploy,
......@@ -123,7 +122,7 @@ void main() {
expect(IOSDevice(
'device-123',
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
fileSystem: nullFileSystem,
fileSystem: fileSystem,
logger: logger,
platform: macPlatform,
iosDeploy: iosDeploy,
......@@ -136,7 +135,7 @@ void main() {
expect(IOSDevice(
'device-123',
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
fileSystem: nullFileSystem,
fileSystem: fileSystem,
logger: logger,
platform: macPlatform,
iosDeploy: iosDeploy,
......@@ -152,7 +151,7 @@ void main() {
final IOSDevice device = IOSDevice(
'device-123',
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
fileSystem: nullFileSystem,
fileSystem: fileSystem,
logger: logger,
platform: macPlatform,
iosDeploy: iosDeploy,
......@@ -170,7 +169,7 @@ void main() {
final IOSDevice device = IOSDevice(
'device-123',
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
fileSystem: nullFileSystem,
fileSystem: fileSystem,
logger: logger,
platform: macPlatform,
iosDeploy: iosDeploy,
......@@ -194,7 +193,7 @@ void main() {
IOSDevice(
'device-123',
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
fileSystem: nullFileSystem,
fileSystem: fileSystem,
logger: logger,
platform: platform,
iosDeploy: iosDeploy,
......@@ -211,21 +210,21 @@ void main() {
}
group('.dispose()', () {
IOSDevice device;
FakeIOSApp appPackage1;
FakeIOSApp appPackage2;
IOSDeviceLogReader logReader1;
IOSDeviceLogReader logReader2;
FakeProcess process1;
FakeProcess process2;
FakeProcess process3;
IOSDevicePortForwarder portForwarder;
ForwardedPort forwardedPort;
Cache cache;
Logger logger;
IOSDeploy iosDeploy;
FileSystem nullFileSystem;
IProxy iproxy;
late IOSDevice device;
late FakeIOSApp appPackage1;
late FakeIOSApp appPackage2;
late IOSDeviceLogReader logReader1;
late IOSDeviceLogReader logReader2;
late FakeProcess process1;
late FakeProcess process2;
late FakeProcess process3;
late IOSDevicePortForwarder portForwarder;
late ForwardedPort forwardedPort;
late Cache cache;
late Logger logger;
late IOSDeploy iosDeploy;
late FileSystem fileSystem;
late IProxy iproxy;
IOSDevicePortForwarder createPortForwarder(
ForwardedPort forwardedPort,
......@@ -235,7 +234,7 @@ void main() {
id: device.id,
logger: logger,
operatingSystemUtils: OperatingSystemUtils(
fileSystem: nullFileSystem,
fileSystem: fileSystem,
logger: logger,
platform: FakePlatform(operatingSystem: 'macos'),
processManager: FakeProcessManager.any(),
......@@ -253,7 +252,7 @@ void main() {
final IOSDeviceLogReader logReader = IOSDeviceLogReader.create(
device: device,
app: appPackage,
iMobileDevice: null, // not used by this test.
iMobileDevice: IMobileDevice.test(processManager: FakeProcessManager.any()),
);
logReader.idevicesyslogProcess = process;
return logReader;
......@@ -269,6 +268,8 @@ void main() {
cache = Cache.test(
processManager: FakeProcessManager.any(),
);
fileSystem = MemoryFileSystem.test();
logger = BufferLogger.test();
iosDeploy = IOSDeploy(
artifacts: Artifacts.test(),
cache: cache,
......@@ -282,7 +283,7 @@ void main() {
device = IOSDevice(
'123',
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
fileSystem: nullFileSystem,
fileSystem: fileSystem,
logger: logger,
platform: macPlatform,
iosDeploy: iosDeploy,
......@@ -309,15 +310,15 @@ void main() {
});
group('polling', () {
FakeXcdevice xcdevice;
Cache cache;
FakeProcessManager fakeProcessManager;
BufferLogger logger;
IOSDeploy iosDeploy;
IMobileDevice iMobileDevice;
IOSWorkflow iosWorkflow;
IOSDevice device1;
IOSDevice device2;
late FakeXcdevice xcdevice;
late Cache cache;
late FakeProcessManager fakeProcessManager;
late BufferLogger logger;
late IOSDeploy iosDeploy;
late IMobileDevice iMobileDevice;
late IOSWorkflow iosWorkflow;
late IOSDevice device1;
late IOSDevice device2;
setUp(() {
xcdevice = FakeXcdevice();
......@@ -414,22 +415,22 @@ void main() {
await iosDevices.startPolling();
expect(xcdevice.getAvailableIOSDevicesCount, 1);
expect(iosDevices.deviceNotifier.items, isEmpty);
expect(iosDevices.deviceNotifier!.items, isEmpty);
expect(xcdevice.deviceEventController.hasListener, isTrue);
xcdevice.deviceEventController.add(<XCDeviceEvent, String>{
XCDeviceEvent.attach: 'd83d5bc53967baa0ee18626ba87b6254b2ab5418'
});
await added.future;
expect(iosDevices.deviceNotifier.items.length, 2);
expect(iosDevices.deviceNotifier.items, contains(device1));
expect(iosDevices.deviceNotifier.items, contains(device2));
expect(iosDevices.deviceNotifier!.items.length, 2);
expect(iosDevices.deviceNotifier!.items, contains(device1));
expect(iosDevices.deviceNotifier!.items, contains(device2));
xcdevice.deviceEventController.add(<XCDeviceEvent, String>{
XCDeviceEvent.detach: 'd83d5bc53967baa0ee18626ba87b6254b2ab5418'
});
await removed.future;
expect(iosDevices.deviceNotifier.items, <Device>[device2]);
expect(iosDevices.deviceNotifier!.items, <Device>[device2]);
// Remove stream will throw over-completion if called more than once
// which proves this is ignored.
......@@ -489,7 +490,7 @@ void main() {
xcdevice.devices.add(<IOSDevice>[]);
await iosDevices.startPolling();
expect(iosDevices.deviceNotifier.items, isEmpty);
expect(iosDevices.deviceNotifier!.items, isEmpty);
expect(xcdevice.deviceEventController.hasListener, isTrue);
iosDevices.dispose();
......@@ -531,9 +532,9 @@ void main() {
});
group('getDiagnostics', () {
FakeXcdevice xcdevice;
IOSWorkflow iosWorkflow;
Logger logger;
late FakeXcdevice xcdevice;
late IOSWorkflow iosWorkflow;
late Logger logger;
setUp(() {
xcdevice = FakeXcdevice();
......@@ -601,7 +602,7 @@ class FakeXcdevice extends Fake implements XCDevice {
}
@override
Future<List<IOSDevice>> getAvailableIOSDevices({Duration timeout}) async {
Future<List<IOSDevice>> getAvailableIOSDevices({Duration? timeout}) async {
return devices[getAvailableIOSDevicesCount++];
}
}
......
......@@ -5,12 +5,16 @@
// @dart = 2.8
import 'package:file/memory.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/ios/devices.dart';
import 'package:flutter_tools/src/ios/ios_deploy.dart';
import 'package:flutter_tools/src/ios/iproxy.dart';
import 'package:flutter_tools/src/ios/mac.dart';
import 'package:flutter_tools/src/project.dart';
import '../../src/common.dart';
......@@ -76,17 +80,26 @@ flutter:
}
IOSDevice setUpIOSDevice(FileSystem fileSystem) {
final Platform platform = FakePlatform(operatingSystem: 'macos');
final Logger logger = BufferLogger.test();
final ProcessManager processManager = FakeProcessManager.any();
return IOSDevice(
'test',
fileSystem: fileSystem,
logger: BufferLogger.test(),
iosDeploy: null, // not used in this test
iMobileDevice: null, // not used in this test
platform: FakePlatform(operatingSystem: 'macos'),
logger: logger,
iosDeploy: IOSDeploy(
platform: platform,
logger: logger,
processManager: processManager,
artifacts: Artifacts.test(),
cache: Cache.test(processManager: processManager),
),
iMobileDevice: IMobileDevice.test(processManager: processManager),
platform: platform,
name: 'iPhone 1',
sdkVersion: '13.3',
cpuArchitecture: DarwinArch.arm64,
iProxy: IProxy.test(logger: BufferLogger.test(), processManager: FakeProcessManager.any()),
iProxy: IProxy.test(logger: logger, processManager: processManager),
interfaceType: IOSDeviceConnectionInterface.usb,
);
}
......@@ -326,7 +326,7 @@ void main() {
processManager: fakeProcessManager,
logger: logger,
xcode: xcode,
platform: null,
platform: FakePlatform(operatingSystem: 'macos'),
artifacts: Artifacts.test(),
cache: Cache.test(processManager: FakeProcessManager.any()),
iproxy: IProxy.test(logger: logger, processManager: fakeProcessManager),
......@@ -354,7 +354,7 @@ void main() {
processManager: fakeProcessManager,
logger: logger,
xcode: xcode,
platform: null,
platform: FakePlatform(operatingSystem: 'macos'),
artifacts: Artifacts.test(),
cache: Cache.test(processManager: FakeProcessManager.any()),
iproxy: IProxy.test(logger: logger, processManager: fakeProcessManager),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment