File: Hosting\TestHostManagerCallbacks.cs
Web Access
Project: src\src\vstest\src\Microsoft.TestPlatform.TestHostProvider\Microsoft.TestPlatform.TestHostProvider.csproj (Microsoft.TestPlatform.TestHostRuntimeProvider)
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;

using Microsoft.VisualStudio.TestPlatform.CoreUtilities.Extensions;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Host;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;
using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions.Interfaces;

namespace Microsoft.TestPlatform.TestHostProvider.Hosting;

internal class TestHostManagerCallbacks
{
    private const int E_HANDLE = unchecked((int)0x80070006);
    private readonly bool _forwardOutput;
    private readonly IMessageLogger? _messageLogger;

    public TestHostManagerCallbacks(bool forwardOutput, IMessageLogger? logger)
    {
        if (forwardOutput)
        {
            string? but = null;
            if (logger == null)
            {
                but = " But logger is null, so it won't forward any output.";
            }
            EqtTrace.Verbose($"TestHostManagerCallbacks.ctor: Forwarding output is enabled.{but}");
        }
        else
        {
            EqtTrace.Verbose($"TestHostManagerCallbacks.ctor: Forwarding output is disabled.");
        }
        _forwardOutput = forwardOutput;
        _messageLogger = logger;
    }

    public void StandardOutputReceivedCallback(StringBuilder testHostProcessStdOut, string? data)
    {
        EqtTrace.Verbose("TestHostManagerCallbacks.StandardOutputReceivedCallback Test host standard output line: {0}", data);
        testHostProcessStdOut.AppendSafeWithNewLine(data);
        if (_forwardOutput && _messageLogger != null && !StringUtils.IsNullOrWhiteSpace(data))
        {
            _messageLogger.SendMessage(TestMessageLevel.Informational, data);
        }
    }

    public void ErrorReceivedCallback(StringBuilder testHostProcessStdError, string? data)
    {
        // Log all standard error message because on too much data we ignore starting part.
        // This is helpful in abnormal failure of testhost.
        EqtTrace.Warning("TestHostManagerCallbacks.ErrorReceivedCallback Test host standard error line: {0}", data);

        testHostProcessStdError.AppendSafeWithNewLine(data);
        if (_forwardOutput && _messageLogger != null && !StringUtils.IsNullOrWhiteSpace(data))
        {
            // Forward the error output, but DO NOT forward it as error. Until now it was only written into logs,
            // and applications love to write Debug messages into error stream. Which we do not want to fail the test run.
            _messageLogger.SendMessage(TestMessageLevel.Informational, data);
        }
    }

    public static void ExitCallBack(
        IProcessHelper processHelper,
        object? process,
        StringBuilder testHostProcessStdError,
        Action<HostProviderEventArgs> onHostExited)
    {
        EqtTrace.Verbose("TestHostProvider.ExitCallBack: Host exited starting callback.");
        var testHostProcessStdErrorStr = testHostProcessStdError.ToString();

        int exitCode;
        try
        {
            processHelper.TryGetExitCode(process, out exitCode);
        }
        catch (COMException ex) when (ex.HResult == E_HANDLE)
        {
            exitCode = -1;
            EqtTrace.Error("TestHostProvider.ExitCallBack: Invalid process handle we cannot get the error code, error {0}.", ex);
        }

        int procId = -1;
        try
        {
            if (process is Process p)
            {
                procId = p.Id;
            }
        }
        catch (InvalidOperationException ex)
        {
            EqtTrace.Error("TestHostProvider.ExitCallBack: could not get proccess id from process, error: {0}.", ex);
        }

        if (exitCode != 0)
        {
            EqtTrace.Error("TestHostManagerCallbacks.ExitCallBack: Testhost processId: {0} exited with exitcode: {1} error: '{2}'", procId, exitCode, testHostProcessStdErrorStr);
        }
        else
        {
            EqtTrace.Info("TestHostManagerCallbacks.ExitCallBack: Testhost processId: {0} exited with exitcode: 0 error: '{1}'", procId, testHostProcessStdErrorStr);
        }

        onHostExited(new HostProviderEventArgs(testHostProcessStdErrorStr, exitCode, procId));
    }
}