ProcessStartData CreateTailLogsProcessStartData(SshTarget target, uint remotePid)
        {
            // If the game process exits, give the tail process a chance to shut down gracefully.
            ProcessManager.ProcessStopHandler stopHandler = (process, reason) =>
            {
                // Did the game process, i.e. the process with pid |remotePid|, exit?
                if (reason != ExitReason.ProcessExited &&
                    reason != ExitReason.DebuggerTerminated)
                {
                    Trace.WriteLine("Game process did not exit, won't wait for tail process exit");
                    return;
                }

                // Give it a second to finish.
                bool exited = process.WaitForExit(TimeSpan.FromSeconds(1));

                // Emit a log message to help tracking down issues, just in case.
                Trace.WriteLine($"Tail process {(exited ? "exited" : "did not exit")} gracefully");
            };

            var startInfo = ProcessStartInfoBuilder.BuildForSsh(
                $"tail --pid={remotePid} -n +0 -F -q /var/game/stdout /var/game/stderr",
                new List <string>(), target);

            return(new ProcessStartData("output tail", startInfo, monitorExit: false,
                                        outputToConsole: true, stopHandler: stopHandler));
        }
        /// <summary>
        /// Check that the specified remote process's binary has a valid build id. Log messages and
        /// record metrics to indicate the result of the checks.
        /// </summary>
        /// <param name="pid">Process ID of the remote process that we will check</param>
        /// <param name="target">The machine that should have a valid remote binary</param>
        public async Task CheckRemoteBinaryOnAttachAsync(uint pid, SshTarget target,
                                                         IAction action)
        {
            var remoteTargetPath = string.Format(PID_EXE_PATH_TEMPLATE, pid);

            try
            {
                var dataRecorder =
                    new DataRecorder(action, DebugPreflightCheckData.Types.CheckType.AttachOnly);

                BuildId remoteBuildId;
                try
                {
                    remoteBuildId = await binaryFileUtil.ReadBuildIdAsync(remoteTargetPath,
                                                                          target);
                }
                catch (BinaryFileUtilException e) when(dataRecorder.RemoteBuildIdError(e))
                {
                    Debug.Fail("Exception should never be caught");
                    throw;
                }

                // Log the remote Build ID for debugging purposes.
                dataRecorder.ValidRemoveBuildId();
                Trace.WriteLine("Remote build ID: " + remoteBuildId.ToString());
            }
            catch (BinaryFileUtilException e)
            {
                Trace.WriteLine($"Failed to read build ID for '{remoteTargetPath}' " +
                                $"on '{target.GetString()}': " + e.ToString());
                throw new PreflightBinaryCheckerException(
                          ErrorStrings.FailedToCheckRemoteBuildIdWithExplanation(e.Message), e);
            }
        }
        public void AttachToCoreWithEmptyTargetSucceeds()
        {
            int                   calls                = 0;
            SshTarget             expectedTarget       = null;
            var                   attachReason         = enum_ATTACH_REASON.ATTACH_REASON_LAUNCH;
            IDebugSessionLauncher debugSessionLauncher =
                CreateConfiguredDebugSessionLauncher(expectedTarget, x => {
                calls++;
                var attachedProgram = Substitute.For <ILldbAttachedProgram>();
                return(Task.FromResult(attachedProgram));
            });
            IDebugSessionLauncherFactory debugSessionLauncherFactory =
                CreateDebugSessionLauncherFactory(debugSessionLauncher);
            IGgpDebugEngine debugEngine = CreateGgpDebugEngine(debugSessionLauncherFactory);
            var             debugPort   = Substitute.For <IDebugPort2>();

            debugEngine.LaunchSuspended("", debugPort, _exePath, null, null, null, null,
                                        enum_LAUNCH_FLAGS.LAUNCH_DEBUG, 0, 0, 0, null,
                                        out IDebugProcess2 _);
            var rgpPrograms     = new[] { Substitute.For <IDebugProgram2>() };
            var rgpProgramNodes = new[] { Substitute.For <IDebugProgramNode2>() };

            int result =
                debugEngine.Attach(rgpPrograms, rgpProgramNodes, _celtPrograms, null, attachReason);

            Assert.Multiple(() => {
                debugPort.Received().GetProcess(Arg.Any <AD_PROCESS_ID>(), out _);
                Assert.That(result, Is.EqualTo(VSConstants.S_OK));
                Assert.That(calls, Is.EqualTo(1));
            });
        }
        public void StartPreGame(LaunchOption launchOption, bool rgpEnabled, bool renderdocEnabled,
                                 SshTarget target, out GrpcConnection grpcConnection,
                                 out ITransportSession transportSession)
        {
            lock (thisLock)
            {
                grpcConnection        = null;
                this.transportSession = transportSession = transportSessionFactory.Create();
                if (transportSession == null)
                {
                    Trace.WriteLine("Unable to start the debug transport, invalid session.");
                    throw new YetiDebugTransportException(ErrorStrings.FailedToStartTransport);
                }

                if (!LaunchPreGameProcesses(launchOption, rgpEnabled, renderdocEnabled, target))
                {
                    Stop(ExitReason.Unknown);
                    throw new YetiDebugTransportException(
                              "Failed to launch all needed pre-game processes");
                }

                Trace.WriteLine("Started debug transport.  Session ID: " +
                                transportSession.GetSessionId());

                // The grpcConnection is created during the launch of one of the processes.
                grpcConnection = this.grpcConnection;
            }
        }
Esempio n. 5
0
        async Task DeployToTargetAsync(DataRecorder record, ICancelable task, SshTarget target,
                                       string localPath, string remotePath, bool force = false)
        {
            Stopwatch stopwatch = Stopwatch.StartNew();

            try
            {
                BinarySignatureCheck.Types.Result signatureCheck = force
                    ? BinarySignatureCheck.Types.Result.AlwaysCopy
                    : BinarySignatureCheck.Types.Result.YesCopy;

                record.SetCopyAttempted(true);
                record.BinarySize(FileUtil.GetFileSize(localPath, _fileSystem));
                record.SignatureCheckResult(signatureCheck);
                record.DeploymentMode();

                await _remoteFile.SyncAsync(target, localPath, remotePath, task, force);

                record.CopyBinary(stopwatch.ElapsedMilliseconds, DataRecorder.NoError);
            }
            catch (ProcessException exception)
            {
                record.CopyBinary(stopwatch.ElapsedMilliseconds, exception);
                throw new DeployException(
                          ErrorStrings.FailedToDeployExecutable(exception.Message),
                          exception);
            }
        }
        public void StartPreGameLaunchAborted()
        {
            SshTarget sshTarget = new SshTarget(_targetString);

            var mockProcess            = Substitute.For <IProcess>();
            ProcessStartInfo startInfo = null;

            mockManagedProcessFactory
            .Create(Arg.Is <ProcessStartInfo>(x => Path.GetFileName(x.FileName) ==
                                              YetiConstants.DebuggerGrpcServerExecutable))
            .Returns(mockProcess).AndDoes(x => { startInfo = x.Arg <ProcessStartInfo>(); });

            yetiDebugTransport.StartPreGame(LaunchOption.LaunchGame, false, false, sshTarget, out _,
                                            out _);

            int exitCode = 123;

            mockProcess.StartInfo.Returns(startInfo);
            mockProcess.ExitCode.Returns(exitCode);
            mockProcess.OnExit += Raise.EventWith(mockProcess, new EventArgs());
            Assert.IsInstanceOf(typeof(ProcessExecutionException), abortError);

            var processError = abortError as ProcessExecutionException;

            Assert.AreEqual(exitCode, ((ProcessExecutionException)abortError).ExitCode);
        }
        /// <summary>
        /// Launches all needed processes that can be launched before the game.
        /// </summary>
        /// <param name="launchOption">How the game will be launched</param>
        /// <param name="rgpEnabled">Whether RPG is enabled</param>
        /// <param name="renderdocEnabled">Whether Renderdoc is enabled</param>
        /// <param name="target">Remote instance</param>
        /// <returns>
        /// True if all processes launched successfully and false otherwise and we should abort.
        /// </returns>
        bool LaunchPreGameProcesses(LaunchOption launchOption, bool rgpEnabled,
                                    bool renderdocEnabled, SshTarget target)
        {
            var processes = new List <ProcessStartData>();

            if (launchOption == LaunchOption.LaunchGame ||
                launchOption == LaunchOption.AttachToGame)
            {
                processes.Add(CreatePortForwardingProcessStartData(target));
                processes.Add(CreateLldbServerProcessStartData(target));
            }

            if (launchOption == LaunchOption.LaunchGame)
            {
                if (renderdocEnabled)
                {
                    processes.Add(CreateRenderDocPortForwardingProcessStartData(target));
                }

                if (rgpEnabled)
                {
                    processes.Add(CreateRgpPortForwardingProcessStartData(target));
                }
            }

            processes.Add(CreateDebuggerGrpcServerProcessStartData());

            return(LaunchProcesses(processes, "pre-game"));
        }
        ProcessStartData CreateLldbServerProcessStartData(SshTarget target)
        {
            string lldbServerCommand = string.Format(
                "{0} platform --listen 127.0.0.1:{1} --min-gdbserver-port={2} " +
                "--max-gdbserver-port={3}",
                Path.Combine(YetiConstants.LldbServerLinuxPath,
                             YetiConstants.LldbServerLinuxExecutable),
                transportSession.GetRemoteDebuggerPort(),
                transportSession.GetReservedLocalAndRemotePort(),
                transportSession.GetReservedLocalAndRemotePort() + 1);
            List <string> lldbServerEnvironment = new List <string>();

            if (yetiVSIService.DebuggerOptions[DebuggerOption.SERVER_LOGGING] ==
                DebuggerOptionState.ENABLED)
            {
                string channels = "lldb default:posix default:gdb-remote default";
                // gdb-server.log
                lldbServerEnvironment.Add(
                    "LLDB_DEBUGSERVER_LOG_FILE=/usr/local/cloudcast/log/gdb-server.log");
                lldbServerEnvironment.Add("LLDB_SERVER_LOG_CHANNELS=\\\"" + channels + "\\\"");
                // lldb-server.log
                lldbServerCommand += " --log-file=/usr/local/cloudcast/log/lldb-server.log " +
                                     "--log-channels=\\\"" + channels + "\\\"";
            }

            var startInfo = ProcessStartInfoBuilder.BuildForSsh(
                lldbServerCommand, lldbServerEnvironment, target);

            return(new ProcessStartData("lldb server", startInfo));
        }
Esempio n. 9
0
        // Queries the provided port (gamelet) for a list of running cores.  On an error, a
        // TransportException will be thrown with the error code.
        public async Task <List <CoreListEntry> > GetCoreListAsync(SshTarget sshTarget)
        {
            // TODO: Find a more robust method of listing files on the gamelet.
            ProcessStartInfo processStartInfo = ProcessStartInfoBuilder.BuildForSsh(
                COMMAND, new List <string>(), sshTarget);

            return(await GetCoreListFromProcessStartInfoAsync(processStartInfo));
        }
        public void StartPostGameLaunchNoCaptureOutput()
        {
            SshTarget sshTarget = new SshTarget(_targetString);

            optionPageGrid.CaptureGameOutput.Returns(false);
            yetiDebugTransport.StartPostGame(LaunchOption.AttachToGame, sshTarget, _remotePid);
            Assert.IsNull(abortError);
        }
Esempio n. 11
0
 public async Task <List <ProcessListEntry> > GetBySshAsync(SshTarget target)
 {
     using (var process = remoteProcessFactory.Create(
                ProcessStartInfoBuilder.BuildForSsh(COMMAND, new List <string>(), target)))
     {
         return(await GetByProcessAsync(process));
     }
 }
        public void StartPreGameAttachToCoreLocal()
        {
            SshTarget sshTargetNull = null;

            yetiDebugTransport.StartPreGame(LaunchOption.AttachToCore, false, false, sshTargetNull,
                                            out _, out _);
            ExpectLocalProcessWithName(YetiConstants.DebuggerGrpcServerExecutable);
            Assert.IsNull(abortError);
        }
        public void StartPostGameLaunchCaptureOutput()
        {
            SshTarget sshTarget = new SshTarget(_targetString);

            optionPageGrid.CaptureGameOutput.Returns(true);
            yetiDebugTransport.StartPostGame(LaunchOption.AttachToGame, sshTarget, _remotePid);
            ExpectRemoteProcessWithArg($"tail --pid={_remotePid}", 1);
            Assert.IsNull(abortError);
        }
Esempio n. 14
0
        /// <summary>
        /// Parses an elf binary or symbol file and returns the build ID encoded
        /// in the .note.gnu.build-id section of the file.
        /// </summary>
        /// <param name="filepath">The local or remote absolute file path.</param>
        /// <param name="target">Optional parameter specifying the remote gamelet.</param>
        /// <returns>A non-empty build id.</returns>
        /// <exception cref="BinaryFileUtilException">
        /// Thrown when an error is encountered reading or parsing the build id.
        /// InnerException contains more details.
        /// </exception>
        public async Task <BuildId> ReadBuildIdAsync(string filepath, SshTarget target = null)
        {
            try
            {
                var outputLines = await ReadSectionFromFileAsync(".note.gnu.build-id", filepath,
                                                                 target);

                var hexString = ParseHexDump(outputLines);
                var result    = ParseBuildIdOutput(hexString);
                if (result == BuildId.Empty)
                {
                    throw new InvalidBuildIdException(
                              ErrorStrings.FailedToReadBuildId(filepath,
                                                               ErrorStrings.EmptyBuildId));
                }
                return(result);
            }
            catch (ProcessExecutionException e)
            {
                LogObjdumpOutput(e);

                // objdump returned an error code, possibly because the file being parsed is not
                // actually an elf file. With an SSH target, exit code 255 means SSH failed before
                // it had a chance to execute the remote command.
                if (target != null && e.ExitCode < 255)
                {
                    // The remote command failed, so we need to fix the exception message.
                    // TODO: ManagedProcess should report the remote filename.
                    throw new BinaryFileUtilException(
                              ErrorStrings.FailedToReadBuildId(
                                  filepath, ErrorStrings.ProcessExitedWithErrorCode(
                                      YetiConstants.ObjDumpLinuxExecutable, e.ExitCode)),
                              e);
                }
                else
                {
                    throw new BinaryFileUtilException(
                              ErrorStrings.FailedToReadBuildId(filepath, e.Message), e);
                }
            }
            catch (ProcessException e)
            {
                // objdump failed to launch, possibly because the SDK was not found. With an SSH
                // target, this indicates that SSH failed to launch. In either case, the specific
                // filepath was never accessed, so it is not part of the error.
                throw new BinaryFileUtilException(
                          ErrorStrings.FailedToReadBuildId(e.Message), e);
            }
            catch (FormatException e)
            {
                // Indicates the build ID section is malformed.
                throw new InvalidBuildIdException(
                          ErrorStrings.FailedToReadBuildId(
                              filepath, ErrorStrings.MalformedBuildID),
                          e);
            }
        }
Esempio n. 15
0
        public async Task GetAsync(SshTarget target, string file, string destination,
                                   ICancelable task)
        {
            await ScpAsync(ProcessStartInfoBuilder.BuildForScpGet(file, target, destination),
                           ProcessManager.CreateForCancelableTask(task));

            // Notify client if operation was cancelled.
            task.ThrowIfCancellationRequested();
        }
Esempio n. 16
0
        public async Task RunWithSuccessAsync(SshTarget target, string command)
        {
            var startInfo =
                ProcessStartInfoBuilder.BuildForSsh(command, new List <string>(), target);

            using (var process = remoteProcessFactory.Create(startInfo))
            {
                await process.RunToExitWithSuccessAsync();
            }
        }
Esempio n. 17
0
        public void SetUp()
        {
            var testGamelet = new Gamelet {
                Id = TEST_GAMELET_ID, IpAddr = TEST_IP
            };

            sshTarget              = new SshTarget(testGamelet);
            managedProcessFactory  = Substitute.For <ManagedProcess.Factory>();
            coreListRequestFactory = new CoreListRequest.Factory(managedProcessFactory);
        }
        public void StartPreGameLaunchRenderDoc()
        {
            SshTarget sshTarget = new SshTarget(_targetString);

            yetiDebugTransport.StartPreGame(LaunchOption.LaunchGame, false, true, sshTarget, out _,
                                            out _);
            ExpectRemoteProcessWithArg("lldb-server", 1);
            ExpectRemoteProcessWithArg("-L", 2);
            ExpectLocalProcessWithName(YetiConstants.DebuggerGrpcServerExecutable);
            Assert.IsNull(abortError);
        }
Esempio n. 19
0
        public async Task ReadBuildId_RemoteAsync(string buildIdStr, string[] outputLines)
        {
            mockRemoteProcess.When(x => x.RunToExitAsync()).Do(x =>
            {
                OutputTestData(mockRemoteProcess, outputLines);
            });
            SshTarget target  = new SshTarget(fakeGameletIp + ":" + fakeGameletPort);
            var       buildId = await elfFileUtil.ReadBuildIdAsync(fakeRemoteFilename, target);

            Assert.AreEqual(new BuildId(buildIdStr), buildId);
        }
Esempio n. 20
0
 public void StartPostGame(LaunchOption launchOption, SshTarget target, uint remotePid)
 {
     lock (thisLock)
     {
         if (!LaunchPostGameProcesses(launchOption, target, remotePid))
         {
             Stop(ExitReason.Unknown);
             throw new YetiDebugTransportException(
                       "Failed to launch all needed post-game processes");
         }
     }
 }
Esempio n. 21
0
        public void ReadBuildId_Remote_ProcessException()
        {
            mockRemoteProcess.RunToExitAsync().Returns(
                Task.FromException <int>(new ProcessException("test")));
            SshTarget target = new SshTarget(fakeGameletIp + ":" + fakeGameletPort);

            var ex = Assert.ThrowsAsync <BinaryFileUtilException>(
                () => elfFileUtil.ReadBuildIdAsync(fakeRemoteFilename, target));

            Assert.IsInstanceOf <ProcessException>(ex.InnerException);
            // Note: message doesn't need to include remote filename.
        }
        public void StartPreGameNoSession()
        {
            SshTarget sshTargetNull = null;

            mockMemoryMappedFileFactory.CreateNew(Arg.Any <string>(), Arg.Any <long>())
            .Returns((IMemoryMappedFile)null);
            Assert.Throws <YetiDebugTransportException>(
                () => yetiDebugTransport.StartPreGame(LaunchOption.AttachToCore, false, false,
                                                      sshTargetNull, out _, out _));
            // Early errors don't cause aborts.
            Assert.IsNull(abortError);
        }
Esempio n. 23
0
        /// <summary>
        /// Launches all needed processes that can be launched after the game.
        /// </summary>
        /// <param name="launchOption">How the game will be launched</param>
        /// <param name="target">Remote instance</param>
        /// <param name="remotePid">Id of the remote process</param>
        /// <returns>
        /// True if all processes launched successfully and false otherwise and we should abort.
        /// </returns>
        bool LaunchPostGameProcesses(LaunchOption launchOption, SshTarget target, uint remotePid)
        {
            var processes = new List <ProcessStartData>();

            if ((launchOption == LaunchOption.LaunchGame ||
                 launchOption == LaunchOption.AttachToGame) &&
                yetiVSIService.Options.CaptureGameOutput)
            {
                processes.Add(CreateTailLogsProcessStartData(target, remotePid));
            }

            return(LaunchProcesses(processes, "post-game"));
        }
Esempio n. 24
0
        ProcessStartData CreateRgpPortForwardingProcessStartData(SshTarget target)
        {
            var ports = new List <ProcessStartInfoBuilder.PortForwardEntry>()
            {
                new ProcessStartInfoBuilder.PortForwardEntry
                {
                    LocalPort  = WorkstationPorts.RGP_LOCAL,
                    RemotePort = WorkstationPorts.RGP_REMOTE,
                }
            };
            var startInfo = ProcessStartInfoBuilder.BuildForSshPortForward(ports, target);

            return(new ProcessStartData("rgp port forwarding", startInfo));
        }
        public void TailKilledIfOtherExitReason()
        {
            IProcess  tailProcess = Substitute.For <IProcess>();
            SshTarget sshTarget   = new SshTarget(_targetString);

            optionPageGrid.CaptureGameOutput.Returns(true);
            mockManagedProcessFactory.Create(Arg.Any <ProcessStartInfo>(), Arg.Any <int>())
            .Returns(tailProcess);
            yetiDebugTransport.StartPostGame(LaunchOption.AttachToGame, sshTarget, _remotePid);

            yetiDebugTransport.Stop(ExitReason.Unknown);
            tailProcess.DidNotReceiveWithAnyArgs().WaitForExit(Arg.Any <TimeSpan>());
            tailProcess.Received().Kill();
        }
Esempio n. 26
0
        public async Task DeployLldbServerAsync(SshTarget target, Metrics.IAction action)
        {
            DataRecorder record = new DataRecorder(action, DataRecorder.File.LLDB_SERVER);

            record.SetCopyAttempted(true);

            string localLldbServerPath = GetLldbServerPath();
            string remotePath          = Path.Combine(YetiConstants.LldbServerLinuxPath,
                                                      YetiConstants.LldbServerLinuxExecutable);

            await DeployToTargetAsync(record, new NothingToCancel(), target, localLldbServerPath,
                                      YetiConstants.LldbServerLinuxPath);
            await SetRemoteExecutableBitAsync(target, remotePath, record);
        }
        public void StartPreGameAttachNoGrpcPipeLeak()
        {
            // Verify that things happen in this order:
            // 1) Other processes are started (if any)
            // 2) PipeCallInvoker is created (and the pipes long with it)
            // 3) DebuggerGrpcServer is created
            // 4) Client pipe handles are released
            // 5) Other processes are started (if any)
            // Otherwise, other processes might get a copy of the pipes and cause the VSI to
            // freeze, see (internal).
            GrpcState state = GrpcState.Initial;

            mockGrpcCallInvokerFactory.When(x => x.Create()).Do(x =>
            {
                // 2)
                Assert.That(state, Is.EqualTo(GrpcState.Initial));
                state = GrpcState.PipeCallInvokerCreated;
            });

            mockGrpcCallInvoker.When(x => x.DisposeLocalCopyOfClientPipeHandles()).Do(x =>
            {
                // 4)
                Assert.That(state, Is.EqualTo(GrpcState.DebuggerGrpcServerCreated));
                state = GrpcState.ClientPipeHandlesReleased;
            });

            mockManagedProcessFactory.Create(Arg.Do <ProcessStartInfo>(x =>
            {
                if (Path.GetFileName(x.FileName) == YetiConstants.DebuggerGrpcServerExecutable)
                {
                    // 3)
                    Assert.That(state, Is.EqualTo(GrpcState.PipeCallInvokerCreated));
                    state = GrpcState.DebuggerGrpcServerCreated;
                }
                else
                {
                    // 1) or 5)
                    Assert.IsTrue(state == GrpcState.Initial ||
                                  state == GrpcState.ClientPipeHandlesReleased);
                }
            }));

            SshTarget sshTarget = new SshTarget(_targetString);

            yetiDebugTransport.StartPreGame(LaunchOption.AttachToGame, true, true, sshTarget, out _,
                                            out _);

            Assert.That(state, Is.EqualTo(GrpcState.ClientPipeHandlesReleased));
        }
        public void TailKilledIfGracefulExitFailsAndGameExits()
        {
            IProcess  tailProcess = Substitute.For <IProcess>();
            SshTarget sshTarget   = new SshTarget(_targetString);

            optionPageGrid.CaptureGameOutput.Returns(true);
            mockManagedProcessFactory.Create(Arg.Any <ProcessStartInfo>(), Arg.Any <int>())
            .Returns(tailProcess);
            yetiDebugTransport.StartPostGame(LaunchOption.AttachToGame, sshTarget, _remotePid);

            tailProcess.WaitForExit(_tailExitTimeout).Returns(false);
            yetiDebugTransport.Stop(ExitReason.ProcessExited);
            tailProcess.Received().WaitForExit(_tailExitTimeout);
            tailProcess.Received().Kill();
        }
Esempio n. 29
0
        ProcessStartInfo BuildForGgpSync(SshTarget target, string localPath, string remotePath,
                                         bool force)
        {
            string tunnelSetup      = $"--port {target.Port} --ip {target.IpAddress} --compress";
            string quotedLocalPath  = ProcessUtil.QuoteArgument(localPath);
            string quotedRemotePath = ProcessUtil.QuoteArgument(remotePath);
            string copyWholeFiles   = force ? "--whole-file --checksum" : "";

            return(new ProcessStartInfo
            {
                FileName = Path.Combine(SDKUtil.GetSDKToolsPath(), "ggp_rsync.exe"),
                Arguments =
                    $"{tunnelSetup} {copyWholeFiles} {quotedLocalPath} {quotedRemotePath}",
            });
        }
        /// <summary>
        /// Get IDebugSessionLauncher instance that will execute `func` on LaunchAsync call
        /// (with specified targetIpAddress and targetIpPort (all other arguments ignored)).
        /// </summary>
        /// <param name="expectedTarget">Gamelet target used to check that LaunchAsync was called
        /// with expected values for IpAddress and Port of the Gamelet.</param>
        /// <param name="func">Function being called when LaunchAsync with specified arguments
        /// is called from the code.</param>
        IDebugSessionLauncher CreateConfiguredDebugSessionLauncher(
            SshTarget expectedTarget, Func <CallInfo, Task <ILldbAttachedProgram> > func = null)
        {
            var debugSessionLauncher = Substitute.For <IDebugSessionLauncher>();

            debugSessionLauncher.LaunchAsync(Arg.Any <ICancelableTask>(), Arg.Any <IDebugProcess2>(),
                                             Arg.Any <Guid>(), Arg.Any <uint?>(),
                                             Arg.Any <YetiVSI.DebuggerOptions.DebuggerOptions>(),
                                             Arg.Any <HashSet <string> >(), Arg.Any <GrpcConnection>(),
                                             Arg.Any <int>(), Arg.Is(expectedTarget?.IpAddress),
                                             Arg.Is(expectedTarget?.Port ?? 0),
                                             Arg.Any <IDebugEventCallback2>()).Returns(func);

            return(debugSessionLauncher);
        }