async Task <IFileBackedLog> ProcessCrash(string crashFile)
        {
            var name            = Path.GetFileName(crashFile);
            var crashReportFile = logs.Create(name, $"Crash report: {name}", timestamp: false);
            var args            = new MlaunchArguments(
                new DownloadCrashReportArgument(crashFile),
                new DownloadCrashReportToArgument(crashReportFile.FullPath));

            if (!string.IsNullOrEmpty(deviceName))
            {
                args.Add(new DeviceNameArgument(deviceName));
            }

            var result = await processManager.ExecuteCommandAsync(args, log, TimeSpan.FromMinutes(1));

            if (result.Succeeded)
            {
                log.WriteLine("Downloaded crash report {0} to {1}", crashFile, crashReportFile.FullPath);
                return(await GetSymbolicateCrashReportAsync(crashReportFile));
            }
            else
            {
                log.WriteLine("Could not download crash report {0}", crashFile);
                return(null);
            }
        }
Beispiel #2
0
        public async Task KillEverything(ILog log)
        {
            await _processManager.ExecuteCommandAsync("launchctl", new[] { "remove", "com.apple.CoreSimulator.CoreSimulatorService" }, log, TimeSpan.FromSeconds(10));

            var toKill = new string[] { "iPhone Simulator", "iOS Simulator", "Simulator", "Simulator (Watch)", "com.apple.CoreSimulator.CoreSimulatorService", "ibtoold" };

            var args = new List <string>
            {
                "-9"
            };

            args.AddRange(toKill);

            await _processManager.ExecuteCommandAsync("killall", args, log, TimeSpan.FromSeconds(10));

            var dirsToBeDeleted = new[] {
                Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Saved Application State", "com.apple.watchsimulator.savedState"),
                Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Saved Application State", "com.apple.iphonesimulator.savedState"),
            };

            foreach (var dir in dirsToBeDeleted)
            {
                try
                {
                    if (Directory.Exists(dir))
                    {
                        Directory.Delete(dir, true);
                    }
                }
                catch (Exception e)
                {
                    log.WriteLine("Could not delete the directory '{0}': {1}", dir, e.Message);
                }
            }
        }
Beispiel #3
0
        public void Open(string device, ITunnelListener simpleListener, TimeSpan timeout, ILog mainLog)
        {
            if (device == null)
            {
                throw new ArgumentNullException(nameof(device));
            }

            if (simpleListener == null)
            {
                throw new ArgumentNullException(nameof(simpleListener));
            }

            if (mainLog == null)
            {
                throw new ArgumentNullException(nameof(mainLog));
            }

            lock (_processExecutionLock)
            {
                // launch app, but do not await for the result, since we need to create the tunnel
                var tcpArgs = new MlaunchArguments {
                    new TcpTunnelArgument(simpleListener.Port),
                    new VerbosityArgument(),
                    new DeviceNameArgument(device),
                };

                // use a cancelation token, later will be used to kill the tcp tunnel process
                _cancellationToken = new CancellationTokenSource();
                mainLog.WriteLine($"Starting tcp tunnel between mac port: {simpleListener.Port} and devie port {simpleListener.Port}.");
                Port = simpleListener.Port;
                var tunnelbackLog = new CallbackLog((line) =>
                {
                    mainLog.WriteLine($"The tcp tunnel output is {line}");
                    if (line.Contains("Tcp tunnel started on device"))
                    {
                        mainLog.Write($"Tcp tunnel created on port {simpleListener.Port}");
                        startedCompletionSource.TrySetResult(true);
                        simpleListener.TunnelHoleThrough.TrySetResult(true);
                    }
                });
                // do not await since we are going to be running the process in parallel
                _tcpTunnelExecutionTask = _processManager.ExecuteCommandAsync(tcpArgs, tunnelbackLog, timeout, cancellationToken: _cancellationToken.Token);
                _tcpTunnelExecutionTask.ContinueWith(delegate(Task <ProcessExecutionResult> task)
                {
                    // if the task completes, means that we had issues with the creation of the tunnel and the process
                    // exited, if that is the case, we do not want to make the app wait, therefore, set the hole to false
                    // which will throw an exception from the listener.
                    simpleListener.TunnelHoleThrough.TrySetResult(task.Result.Succeeded);
                });
            }
        }
Beispiel #4
0
        public async Task <ProcessExecutionResult> UninstallApp(string deviceName, string appBundleId, CancellationToken cancellationToken = default)
        {
            var args = new MlaunchArguments();

            for (int i = -1; i < _verbosity; i++)
            {
                args.Add(new VerbosityArgument());
            }

            args.Add(new UninstallAppFromDeviceArgument(appBundleId));
            args.Add(new DeviceNameArgument(deviceName));

            return(await _processManager.ExecuteCommandAsync(args, _mainLog, TimeSpan.FromMinutes(1), cancellationToken : cancellationToken));
        }
Beispiel #5
0
        private async Task RunSimulatorTests(
            MlaunchArguments mlaunchArguments,
            AppBundleInformation appInformation,
            ICrashSnapshotReporter crashReporter,
            ITestReporter testReporter,
            ISimulatorDevice simulator,
            ISimulatorDevice?companionSimulator,
            bool ensureCleanSimulatorState,
            TimeSpan timeout,
            CancellationToken cancellationToken)
        {
            var systemLogs = new List <ICaptureLog>();

            try
            {
                _mainLog.WriteLine("System log for the '{1}' simulator is: {0}", simulator.SystemLog, simulator.Name);

                var simulatorLog = _captureLogFactory.Create(
                    path: Path.Combine(_logs.Directory, simulator.Name + ".log"),
                    systemLogPath: simulator.SystemLog,
                    entireFile: true,
                    LogType.SystemLog.ToString());

                simulatorLog.StartCapture();
                _logs.Add(simulatorLog);
                systemLogs.Add(simulatorLog);

                if (companionSimulator != null)
                {
                    _mainLog.WriteLine("System log for the '{1}' companion simulator is: {0}", companionSimulator.SystemLog, companionSimulator.Name);

                    var companionLog = _captureLogFactory.Create(
                        path: Path.Combine(_logs.Directory, companionSimulator.Name + ".log"),
                        systemLogPath: companionSimulator.SystemLog,
                        entireFile: true,
                        LogType.CompanionSystemLog.ToString());

                    companionLog.StartCapture();
                    _logs.Add(companionLog);
                    systemLogs.Add(companionLog);
                }

                if (ensureCleanSimulatorState)
                {
                    await simulator.PrepareSimulator(_mainLog, appInformation.BundleIdentifier);

                    if (companionSimulator != null)
                    {
                        await companionSimulator.PrepareSimulator(_mainLog, appInformation.BundleIdentifier);
                    }
                }

                await crashReporter.StartCaptureAsync();

                _mainLog.WriteLine("Starting test run");

                var result = _processManager.ExecuteCommandAsync(mlaunchArguments, _mainLog, timeout, cancellationToken: cancellationToken);

                await testReporter.CollectSimulatorResult(result);

                // cleanup after us
                if (ensureCleanSimulatorState)
                {
                    await simulator.KillEverything(_mainLog);

                    if (companionSimulator != null)
                    {
                        await companionSimulator.KillEverything(_mainLog);
                    }
                }
            }
            finally
            {
                foreach (ICaptureLog?log in systemLogs)
                {
                    log.StopCapture();
                    log.Dispose();
                }
            }
        }
Beispiel #6
0
        public async Task AgreeToPromptsAsync(string simRuntime, string TCCDb, ILog log, params string[] bundle_identifiers)
        {
            if (bundle_identifiers == null || bundle_identifiers.Length == 0)
            {
                log.WriteLine("No bundle identifiers given when requested permission editing.");
                return;
            }

            var sim_services = new string[] {
                "kTCCServiceAddressBook",
                "kTCCServiceCalendar",
                "kTCCServicePhotos",
                "kTCCServiceMediaLibrary",
                "kTCCServiceMicrophone",
                "kTCCServiceUbiquity",
                "kTCCServiceWillow"
            };

            var failure          = false;
            var tcc_edit_timeout = 30;
            var watch            = new Stopwatch();

            watch.Start();

            do
            {
                if (failure)
                {
                    log.WriteLine("Failed to edit TCC.db, trying again in 1 second... ", (int)(tcc_edit_timeout - watch.Elapsed.TotalSeconds));
                    await Task.Delay(TimeSpan.FromSeconds(1));
                }
                failure = false;
                foreach (var bundle_identifier in bundle_identifiers)
                {
                    var args = new List <string>();
                    var sql  = new System.Text.StringBuilder("\n");
                    args.Add(TCCDb);
                    foreach (var bundle_id in new[] { bundle_identifier, bundle_identifier + ".watchkitapp" })
                    {
                        foreach (var service in sim_services)
                        {
                            switch (GetTCCFormat(simRuntime))
                            {
                            case 1:
                                // CREATE TABLE access (service TEXT NOT NULL, client TEXT NOT NULL, client_type INTEGER NOT NULL, allowed INTEGER NOT NULL, prompt_count INTEGER NOT NULL, csreq BLOB, CONSTRAINT key PRIMARY KEY (service, client, client_type));
                                sql.AppendFormat("DELETE FROM access WHERE service = '{0}' AND client = '{1}';\n", service, bundle_id);
                                sql.AppendFormat("INSERT INTO access VALUES('{0}','{1}',0,1,0,NULL);\n", service, bundle_id);
                                break;

                            case 2:
                                // CREATE TABLE access (service	TEXT NOT NULL, client TEXT NOT NULL, client_type INTEGER NOT NULL, allowed INTEGER NOT NULL, prompt_count INTEGER NOT NULL, csreq BLOB, policy_id INTEGER, PRIMARY KEY (service, client, client_type), FOREIGN KEY (policy_id) REFERENCES policies(id) ON DELETE CASCADE ON UPDATE CASCADE);
                                sql.AppendFormat("DELETE FROM access WHERE service = '{0}' AND client = '{1}';\n", service, bundle_id);
                                sql.AppendFormat("INSERT INTO access VALUES('{0}','{1}',0,1,0,NULL,NULL);\n", service, bundle_id);
                                break;

                            case 3:     // Xcode 10+
                                        // CREATE TABLE access (    service        TEXT        NOT NULL,     client         TEXT        NOT NULL,     client_type    INTEGER     NOT NULL,     allowed        INTEGER     NOT NULL,     prompt_count   INTEGER     NOT NULL,     csreq          BLOB,     policy_id      INTEGER,     indirect_object_identifier_type    INTEGER,     indirect_object_identifier         TEXT,     indirect_object_code_identity      BLOB,     flags          INTEGER,     last_modified  INTEGER     NOT NULL DEFAULT (CAST(strftime('%s','now') AS INTEGER)),     PRIMARY KEY (service, client, client_type, indirect_object_identifier),    FOREIGN KEY (policy_id) REFERENCES policies(id) ON DELETE CASCADE ON UPDATE CASCADE)
                                sql.AppendFormat("INSERT OR REPLACE INTO access VALUES('{0}','{1}',0,1,0,NULL,NULL,NULL,'UNUSED',NULL,NULL,{2});\n", service, bundle_id, DateTimeOffset.Now.ToUnixTimeSeconds());
                                break;

                            default:
                                throw new NotImplementedException();
                            }
                        }
                    }
                    args.Add(sql.ToString());
                    var rv = await processManager.ExecuteCommandAsync("sqlite3", args, log, TimeSpan.FromSeconds(5));

                    if (!rv.Succeeded)
                    {
                        failure = true;
                        break;
                    }
                }
            } while (failure && watch.Elapsed.TotalSeconds <= tcc_edit_timeout);

            if (failure)
            {
                log.WriteLine("Failed to edit TCC.db, the test run might hang due to permission request dialogs");
            }
            else
            {
                log.WriteLine("Successfully edited TCC.db");
            }

            log.WriteLine("Current TCC database contents:");
            await processManager.ExecuteCommandAsync("sqlite3", new[] { TCCDb, ".dump" }, log, TimeSpan.FromSeconds(5));
        }
Beispiel #7
0
        public async Task LoadDevices(ILog log, bool includeLocked = false, bool forceRefresh = false, bool listExtraData = false)
        {
            await _semaphore.WaitAsync();

            if (_loaded)
            {
                if (!forceRefresh)
                {
                    _semaphore.Release();
                    return;
                }

                _supportedRuntimes.Reset();
                _supportedDeviceTypes.Reset();
                _availableDevices.Reset();
                _availableDevicePairs.Reset();
            }

            var tmpfile = Path.GetTempFileName();

            try
            {
                var arguments = new MlaunchArguments(
                    new ListSimulatorsArgument(tmpfile),
                    new XmlOutputFormatArgument());

                var result = await _processManager.ExecuteCommandAsync(arguments, log, timeout : TimeSpan.FromSeconds(30));

                if (!result.Succeeded)
                {
                    // mlaunch can sometimes return 0 but hang and timeout. It still outputs returns valid content to the tmp file
                    log.WriteLine($"mlaunch failed when listing simulators but trying to parse the results anyway");
                }

                var fileInfo = new FileInfo(tmpfile);
                if (!fileInfo.Exists || fileInfo.Length == 0)
                {
                    throw new Exception($"Failed to list simulators - no XML with devices found. " +
                                        $"mlaunch {(result.TimedOut ? "timed out" : "exited")} with {result.ExitCode})");
                }

                var xmlContent = File.ReadAllText(tmpfile);

                log.WriteLine("Simulator listing returned:" + Environment.NewLine + xmlContent);

                var simulatorData = new XmlDocument();
                simulatorData.LoadWithoutNetworkAccess(tmpfile);
                foreach (XmlNode?sim in simulatorData.SelectNodes("/MTouch/Simulator/SupportedRuntimes/SimRuntime"))
                {
                    if (sim == null)
                    {
                        continue;
                    }

                    _supportedRuntimes.Add(new SimRuntime(
                                               name: sim.SelectSingleNode("Name").InnerText,
                                               identifier: sim.SelectSingleNode("Identifier").InnerText,
                                               version: long.Parse(sim.SelectSingleNode("Version").InnerText)));
                }

                foreach (XmlNode?sim in simulatorData.SelectNodes("/MTouch/Simulator/SupportedDeviceTypes/SimDeviceType"))
                {
                    if (sim == null)
                    {
                        continue;
                    }

                    _supportedDeviceTypes.Add(new SimDeviceType(
                                                  name: sim.SelectSingleNode("Name").InnerText,
                                                  identifier: sim.SelectSingleNode("Identifier").InnerText,
                                                  productFamilyId: sim.SelectSingleNode("ProductFamilyId").InnerText,
                                                  minRuntimeVersion: long.Parse(sim.SelectSingleNode("MinRuntimeVersion").InnerText),
                                                  maxRuntimeVersion: long.Parse(sim.SelectSingleNode("MaxRuntimeVersion").InnerText),
                                                  supports64Bits: bool.Parse(sim.SelectSingleNode("Supports64Bits").InnerText)));
                }

                foreach (XmlNode?sim in simulatorData.SelectNodes("/MTouch/Simulator/AvailableDevices/SimDevice"))
                {
                    if (sim == null)
                    {
                        continue;
                    }

                    _availableDevices.Add(new SimulatorDevice(_processManager, new TCCDatabase(_processManager))
                    {
                        Name          = sim.Attributes["Name"].Value,
                        UDID          = sim.Attributes["UDID"].Value,
                        SimRuntime    = sim.SelectSingleNode("SimRuntime").InnerText,
                        SimDeviceType = sim.SelectSingleNode("SimDeviceType").InnerText,
                        DataPath      = sim.SelectSingleNode("DataPath").InnerText,
                        LogPath       = sim.SelectSingleNode("LogPath").InnerText,
                    });
                }

                var sim_device_pairs = simulatorData.
                                       SelectNodes("/MTouch/Simulator/AvailableDevicePairs/SimDevicePair").
                                       Cast <XmlNode>().
                                       // There can be duplicates, so remove those.
                                       Distinct(new SimulatorXmlNodeComparer());

                foreach (XmlNode sim in sim_device_pairs)
                {
                    _availableDevicePairs.Add(new SimDevicePair(
                                                  uDID: sim.Attributes["UDID"].Value,
                                                  companion: sim.SelectSingleNode("Companion").InnerText,
                                                  gizmo: sim.SelectSingleNode("Gizmo").InnerText));
                }
            }
            finally
            {
                _loaded = true;
                _supportedRuntimes.SetCompleted();
                _supportedDeviceTypes.SetCompleted();
                _availableDevices.SetCompleted();
                _availableDevicePairs.SetCompleted();
                File.Delete(tmpfile);
                _semaphore.Release();
            }
        }
Beispiel #8
0
        public async Task <(string deviceName, ProcessExecutionResult result)> InstallApp(AppBundleInformation appBundleInformation, TestTargetOs target, string?deviceName = null, CancellationToken cancellationToken = default)
        {
            if (target.Platform.IsSimulator())
            {
                // We reset the simulator when running, so a separate install step does not make much sense.
                throw new InvalidOperationException("Installing to a simulator is not supported.");
            }

            if (!Directory.Exists(appBundleInformation.LaunchAppPath))
            {
                throw new DirectoryNotFoundException("Failed to find the app bundle directory");
            }

            if (deviceName == null)
            {
                // the _deviceLoader.FindDevice will return the fist device of the type, but we want to make sure that
                // the device we use is if the correct arch, therefore, we will use the LoadDevices and return the
                // correct one
                await _deviceLoader.LoadDevices(_mainLog, false, false);

                IHardwareDevice?device = null;
                if (appBundleInformation.Supports32Bit)
                {
                    // we only support 32b on iOS, therefore we can ignore the target
                    device = _deviceLoader.Connected32BitIOS.FirstOrDefault();
                }
                else
                {
                    device = target.Platform switch
                    {
                        TestTarget.Device_iOS => _deviceLoader.Connected64BitIOS.FirstOrDefault(),
                        TestTarget.Device_tvOS => _deviceLoader.ConnectedTV.FirstOrDefault(),
                        _ => device
                    };
                }

                deviceName = target.Platform.IsWatchOSTarget() ? (await _deviceLoader.FindCompanionDevice(_mainLog, device)).Name : device?.Name;
            }

            if (deviceName == null)
            {
                throw new NoDeviceFoundException();
            }

            var args = new MlaunchArguments();

            for (int i = -1; i < _verbosity; i++)
            {
                args.Add(new VerbosityArgument());
            }

            args.Add(new InstallAppOnDeviceArgument(appBundleInformation.LaunchAppPath));
            args.Add(new DeviceNameArgument(deviceName));

            if (target.Platform.IsWatchOSTarget())
            {
                args.Add(new DeviceArgument("ios,watchos"));
            }

            var totalSize = Directory.GetFiles(appBundleInformation.LaunchAppPath, "*", SearchOption.AllDirectories).Select((v) => new FileInfo(v).Length).Sum();

            _mainLog.WriteLine($"Installing '{appBundleInformation.LaunchAppPath}' to '{deviceName}' ({totalSize / 1024.0 / 1024.0:N2} MB)");

            ProcessExecutionResult result = await _processManager.ExecuteCommandAsync(args, _mainLog, TimeSpan.FromHours(1), cancellationToken : cancellationToken);

            return(deviceName, result);
        }
    }
Beispiel #9
0
        public async Task <(string DeviceName, TestExecutingResult Result, string ResultMessage)> RunApp(
            AppBundleInformation appInformation,
            TestTarget target,
            TimeSpan timeout,
            TimeSpan testLaunchTimeout,
            string?deviceName              = null,
            string?companionDeviceName     = null,
            bool ensureCleanSimulatorState = false,
            int verbosity = 1,
            XmlResultJargon xmlResultJargon     = XmlResultJargon.xUnit,
            CancellationToken cancellationToken = default)
        {
            var args = new MlaunchArguments
            {
                new SetAppArgumentArgument("-connection-mode"),
                new SetAppArgumentArgument("none"), // This will prevent the app from trying to connect to any IDEs
                new SetAppArgumentArgument("-autostart", true),
                new SetEnvVariableArgument(EnviromentVariables.AutoStart, true),
                new SetAppArgumentArgument("-autoexit", true),
                new SetEnvVariableArgument(EnviromentVariables.AutoExit, true),
                new SetAppArgumentArgument("-enablenetwork", true),
                new SetEnvVariableArgument(EnviromentVariables.EnableNetwork, true),

                // On macOS we can't edit the TCC database easily
                // (it requires adding the mac has to be using MDM: https://carlashley.com/2018/09/28/tcc-round-up/)
                // So by default ignore any tests that would pop up permission dialogs in CI.
                new SetEnvVariableArgument(EnviromentVariables.DisableSystemPermissionTests, 1),
            };

            for (int i = -1; i < verbosity; i++)
            {
                args.Add(new VerbosityArgument());
            }

            var isSimulator = target.IsSimulator();

            if (isSimulator)
            {
                args.Add(new SetAppArgumentArgument("-hostname:127.0.0.1", true));
                args.Add(new SetEnvVariableArgument(EnviromentVariables.HostName, "127.0.0.1"));
            }
            else
            {
                var ipAddresses = System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName()).AddressList.Select(ip => ip.ToString());
                var ips         = string.Join(",", ipAddresses);
                args.Add(new SetAppArgumentArgument($"-hostname:{ips}", true));
                args.Add(new SetEnvVariableArgument(EnviromentVariables.HostName, ips));
            }

            var listenerLog = _logs.Create($"test-{target.AsString()}-{_helpers.Timestamp}.log", LogType.TestLog.ToString(), timestamp: true);

            var(transport, listener, listenerTmpFile) = _listenerFactory.Create(target.ToRunMode(),
                                                                                log: _mainLog,
                                                                                testLog: listenerLog,
                                                                                isSimulator: isSimulator,
                                                                                autoExit: true,
                                                                                xmlOutput: true); // cli always uses xml

            // Initialize has to be called before we try to get Port (internal implementation of the listener says so)
            // TODO: Improve this to not get into a broken state - it was really hard to debug when I moved this lower
            listener.Initialize();

            args.Add(new SetAppArgumentArgument($"-transport:{transport}", true));
            args.Add(new SetEnvVariableArgument(EnviromentVariables.Transport, transport.ToString().ToUpper()));

            if (transport == ListenerTransport.File)
            {
                args.Add(new SetEnvVariableArgument(EnviromentVariables.LogFilePath, listenerTmpFile));
            }

            args.Add(new SetAppArgumentArgument($"-hostport:{listener.Port}", true));
            args.Add(new SetEnvVariableArgument(EnviromentVariables.HostPort, listener.Port));

            if (_listenerFactory.UseTunnel && !isSimulator) // simulators do not support tunnels
            {
                args.Add(new SetEnvVariableArgument(EnviromentVariables.UseTcpTunnel, true));
            }

            if (_useXmlOutput)
            {
                // let the runner now via envars that we want to get a xml output, else the runner will default to plain text
                args.Add(new SetEnvVariableArgument(EnviromentVariables.EnableXmlOutput, true));
                args.Add(new SetEnvVariableArgument(EnviromentVariables.XmlMode, "wrapped"));
                args.Add(new SetEnvVariableArgument(EnviromentVariables.XmlVersion, $"{xmlResultJargon}"));
            }

            listener.StartAsync();

            var crashLogs = new Logs(_logs.Directory);

            if (appInformation.Extension.HasValue)
            {
                switch (appInformation.Extension)
                {
                case Extension.TodayExtension:
                    args.Add(isSimulator
                            ? (MlaunchArgument) new LaunchSimulatorExtensionArgument(appInformation.LaunchAppPath, appInformation.BundleIdentifier)
                            : new LaunchDeviceExtensionArgument(appInformation.LaunchAppPath, appInformation.BundleIdentifier));
                    break;

                case Extension.WatchKit2:
                default:
                    throw new NotImplementedException();
                }
            }
            else
            {
                args.Add(isSimulator
                    ? (MlaunchArgument) new LaunchSimulatorArgument(appInformation.LaunchAppPath)
                    : new LaunchDeviceArgument(appInformation.LaunchAppPath));
            }

            var runMode = target.ToRunMode();
            ICrashSnapshotReporter crashReporter;
            ITestReporter          testReporter;

            if (isSimulator)
            {
                crashReporter = _snapshotReporterFactory.Create(_mainLog, crashLogs, isDevice: !isSimulator, deviceName: null !);
                testReporter  = _testReporterFactory.Create(_mainLog,
                                                            _mainLog,
                                                            _logs,
                                                            crashReporter,
                                                            listener,
                                                            new XmlResultParser(),
                                                            appInformation,
                                                            runMode,
                                                            xmlResultJargon,
                                                            device: null,
                                                            timeout,
                                                            null,
                                                            (level, message) => _mainLog.WriteLine(message));

                using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(testReporter.CancellationToken, cancellationToken);

                listener.ConnectedTask
                .TimeoutAfter(testLaunchTimeout)
                .ContinueWith(testReporter.LaunchCallback)
                .DoNotAwait();

                await _simulatorLoader.LoadDevices(_logs.Create($"simulator-list-{_helpers.Timestamp}.log", "Simulator list"), false, false);

                var simulators = await _simulatorLoader.FindSimulators(target, _mainLog);

                if (!(simulators?.Any() ?? false))
                {
                    _mainLog.WriteLine("Didn't find any suitable simulators");
                    throw new NoDeviceFoundException();
                }

                var simulator = string.IsNullOrEmpty(deviceName)
                    ? simulators.FirstOrDefault()
                    : simulators.FirstOrDefault(s => string.Equals(s.Name, deviceName, StringComparison.InvariantCultureIgnoreCase));

                if (simulator == null)
                {
                    throw new NoDeviceFoundException();
                }

                deviceName = simulator.Name;

                string?stdoutLogPath = null, stderrLogPath = null;

                if (!target.IsWatchOSTarget())
                {
                    var stderrTty = _helpers.GetTerminalName(2);
                    if (!string.IsNullOrEmpty(stderrTty))
                    {
                        args.Add(new SetStderrArgument(stderrTty));
                    }
                    else
                    {
                        stdoutLogPath = _logs.CreateFile($"mlaunch-stdout-{_helpers.Timestamp}.log", "Standard output");
                        stderrLogPath = _logs.CreateFile($"mlaunch-stderr-{_helpers.Timestamp}.log", "Standard error");
                        args.Add(new SetStdoutArgument(stdoutLogPath));
                        args.Add(new SetStderrArgument(stderrLogPath));
                    }
                }

                var systemLogs = new List <ICaptureLog>();
                foreach (var sim in simulators)
                {
                    // Upload the system log
                    _mainLog.WriteLine("System log for the '{1}' simulator is: {0}", sim.SystemLog, sim.Name);
                    bool isCompanion = sim != simulator;

                    var logDescription = isCompanion ? LogType.CompanionSystemLog.ToString() : LogType.SystemLog.ToString();
                    var log            = _captureLogFactory.Create(
                        Path.Combine(_logs.Directory, sim.Name + ".log"),
                        sim.SystemLog,
                        true,
                        logDescription);

                    log.StartCapture();
                    _logs.Add(log);
                    systemLogs.Add(log);
                }

                try
                {
                    _mainLog.WriteLine("*** Executing {0}/{1} in the simulator ***", appInformation.AppName, target);

                    if (ensureCleanSimulatorState)
                    {
                        foreach (var sim in simulators)
                        {
                            await sim.PrepareSimulator(_mainLog, appInformation.BundleIdentifier);
                        }
                    }

                    args.Add(new SimulatorUDIDArgument(simulator.UDID));

                    await crashReporter.StartCaptureAsync();

                    _mainLog.WriteLine("Starting test run");

                    var result = _processManager.ExecuteCommandAsync(args, _mainLog, timeout, cancellationToken: linkedCts.Token);

                    await testReporter.CollectSimulatorResult(result);

                    // cleanup after us
                    if (ensureCleanSimulatorState)
                    {
                        await simulator.KillEverything(_mainLog);
                    }

                    foreach (var log in systemLogs)
                    {
                        log.StopCapture();
                    }
                }
                finally
                {
                    foreach (var log in systemLogs)
                    {
                        log.StopCapture();
                        log.Dispose();
                    }

                    // Remove empty files
                    if (stdoutLogPath != null)
                    {
                        var fileInfo = new FileInfo(stdoutLogPath);
                        if (fileInfo.Exists && fileInfo.Length == 0)
                        {
                            fileInfo.Delete();
                        }
                    }

                    if (stderrLogPath != null)
                    {
                        var fileInfo = new FileInfo(stderrLogPath);
                        if (fileInfo.Exists && fileInfo.Length == 0)
                        {
                            fileInfo.Delete();
                        }
                    }
                }
            }
            else
            {
                args.Add(new DisableMemoryLimitsArgument());

                if (deviceName == null)
                {
                    IHardwareDevice?companionDevice = null;
                    IHardwareDevice device          = await _hardwareDeviceLoader.FindDevice(runMode, _mainLog, includeLocked : false, force : false);

                    if (target.IsWatchOSTarget())
                    {
                        companionDevice = await _hardwareDeviceLoader.FindCompanionDevice(_mainLog, device);
                    }

                    deviceName = companionDevice?.Name ?? device.Name;
                }

                if (deviceName == null)
                {
                    throw new NoDeviceFoundException();
                }

                crashReporter = _snapshotReporterFactory.Create(_mainLog, crashLogs, isDevice: !isSimulator, deviceName);
                testReporter  = _testReporterFactory.Create(_mainLog,
                                                            _mainLog,
                                                            _logs,
                                                            crashReporter,
                                                            listener,
                                                            new XmlResultParser(),
                                                            appInformation,
                                                            runMode,
                                                            xmlResultJargon,
                                                            deviceName,
                                                            timeout,
                                                            null,
                                                            (level, message) => _mainLog.WriteLine(message));

                using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(testReporter.CancellationToken, cancellationToken);

                listener.ConnectedTask
                .TimeoutAfter(testLaunchTimeout)
                .ContinueWith(testReporter.LaunchCallback)
                .DoNotAwait();

                _mainLog.WriteLine("*** Executing {0}/{1} on device '{2}' ***", appInformation.AppName, target, deviceName);

                if (target.IsWatchOSTarget())
                {
                    args.Add(new AttachNativeDebuggerArgument()); // this prevents the watch from backgrounding the app.
                }
                else
                {
                    args.Add(new WaitForExitArgument());
                }

                args.Add(new DeviceNameArgument(deviceName));

                var deviceSystemLog   = _logs.Create($"device-{deviceName}-{_helpers.Timestamp}.log", "Device log");
                var deviceLogCapturer = _deviceLogCapturerFactory.Create(_mainLog, deviceSystemLog, deviceName);
                deviceLogCapturer.StartCapture();

                try
                {
                    await crashReporter.StartCaptureAsync();

                    // create a tunnel to communicate with the device
                    if (transport == ListenerTransport.Tcp && _listenerFactory.UseTunnel && listener is SimpleTcpListener tcpListener)
                    {
                        // create a new tunnel using the listener
                        var tunnel = _listenerFactory.TunnelBore.Create(deviceName, _mainLog);
                        tunnel.Open(deviceName, tcpListener, timeout, _mainLog);
                        // wait until we started the tunnel
                        await tunnel.Started;
                    }

                    _mainLog.WriteLine("Starting test run");

                    // We need to check for MT1111 (which means that mlaunch won't wait for the app to exit).
                    var aggregatedLog = Log.CreateReadableAggregatedLog(_mainLog, testReporter.CallbackLog);
                    Task <ProcessExecutionResult> runTestTask = _processManager.ExecuteCommandAsync(
                        args,
                        aggregatedLog,
                        timeout,
                        cancellationToken: linkedCts.Token);

                    await testReporter.CollectDeviceResult(runTestTask);
                }
                finally
                {
                    deviceLogCapturer.StopCapture();
                    deviceSystemLog.Dispose();

                    // close a tunnel if it was created
                    if (!isSimulator && _listenerFactory.UseTunnel)
                    {
                        await _listenerFactory.TunnelBore.Close(deviceName);
                    }
                }

                // Upload the system log
                if (File.Exists(deviceSystemLog.FullPath))
                {
                    _mainLog.WriteLine("A capture of the device log is: {0}", deviceSystemLog.FullPath);
                }
            }

            listener.Cancel();
            listener.Dispose();

            // check the final status, copy all the required data
            var(testResult, resultMessage) = await testReporter.ParseResult();

            return(deviceName, testResult, resultMessage);
        }