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

extern alias Abstraction;

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;

using Microsoft.VisualStudio.TestPlatform.CommandLine.Internal;
using Microsoft.VisualStudio.TestPlatform.CommandLine.Processors.Utilities;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Abstraction::Microsoft.VisualStudio.TestPlatform.PlatformAbstractions;
using Abstraction::Microsoft.VisualStudio.TestPlatform.PlatformAbstractions.Interfaces;
using Abstraction::Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.Utilities;
using Microsoft.VisualStudio.TestPlatform.Utilities.Helpers;
using Microsoft.VisualStudio.TestPlatform.Utilities.Helpers.Interfaces;

using CommandLineResources = Microsoft.VisualStudio.TestPlatform.CommandLine.Resources.Resources;

namespace Microsoft.VisualStudio.TestPlatform.CommandLine.Processors;

internal class EnableDiagArgumentProcessor : IArgumentProcessor
{
    /// <summary>
    /// The name of the command line argument that the ListTestsArgumentExecutor handles.
    /// </summary>
    public const string CommandName = "/Diag";

    private readonly IFileHelper _fileHelper;

    private Lazy<IArgumentProcessorCapabilities>? _metadata;
    private Lazy<IArgumentExecutor>? _executor;

    /// <summary>
    /// Initializes a new instance of the <see cref="EnableDiagArgumentProcessor"/> class.
    /// </summary>
    public EnableDiagArgumentProcessor() : this(new FileHelper())
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="EnableDiagArgumentProcessor"/> class.
    /// </summary>
    /// <param name="fileHelper">A file helper instance.</param>
    protected EnableDiagArgumentProcessor(IFileHelper fileHelper)
    {
        _fileHelper = fileHelper;
    }

    public Lazy<IArgumentProcessorCapabilities> Metadata
        => _metadata ??= new Lazy<IArgumentProcessorCapabilities>(() =>
            new EnableDiagArgumentProcessorCapabilities());

    /// <summary>
    /// Gets or sets the executor.
    /// </summary>
    public Lazy<IArgumentExecutor>? Executor
    {
        get => _executor ??= new Lazy<IArgumentExecutor>(() => new EnableDiagArgumentExecutor(_fileHelper, new ProcessHelper()));

        set => _executor = value;
    }
}

/// <summary>
/// The argument capabilities.
/// </summary>
internal class EnableDiagArgumentProcessorCapabilities : BaseArgumentProcessorCapabilities
{
    public override string CommandName => EnableDiagArgumentProcessor.CommandName;

    public override bool AllowMultiple => false;

    public override bool IsAction => false;

    public override ArgumentProcessorPriority Priority => ArgumentProcessorPriority.Diag;

    public override string HelpContentResourceName => CommandLineResources.EnableDiagUsage;

    public override HelpContentPriority HelpPriority => HelpContentPriority.EnableDiagArgumentProcessorHelpPriority;
}

/// <summary>
/// The argument executor.
/// </summary>
internal class EnableDiagArgumentExecutor : IArgumentExecutor
{
    private readonly IFileHelper _fileHelper;
    private readonly IProcessHelper _processHelper;

    /// <summary>
    /// Parameter for trace level
    /// </summary>
    public const string TraceLevelParam = "tracelevel";

    /// <summary>
    /// Default constructor.
    /// </summary>
    /// <param name="fileHelper">The file helper.</param>
    /// <param name="processHelper">Process helper</param>
    public EnableDiagArgumentExecutor(IFileHelper fileHelper, IProcessHelper processHelper)
    {
        _fileHelper = fileHelper;
        _processHelper = processHelper;
    }

    #region IArgumentExecutor

    /// <summary>
    /// Initializes with the argument that was provided with the command.
    /// </summary>
    /// <param name="argument">Argument that was provided with the command.</param>
    public void Initialize(string? argument)
    {
        string exceptionMessage = string.Format(CultureInfo.CurrentCulture, CommandLineResources.InvalidDiagArgument, argument);

        // Throw error if argument is null or empty.
        if (argument.IsNullOrWhiteSpace())
        {
            throw new CommandLineException(exceptionMessage);
        }

        // Get diag argument list.
        var diagArgumentList = ArgumentProcessorUtilities.GetArgumentList(argument, ArgumentProcessorUtilities.SemiColonArgumentSeparator, exceptionMessage);

        // Get diag file path.
        // Note: Even though semi colon is valid file path, we are not respecting the file name having semi-colon [As we are separating arguments based on semi colon].
        var diagFilePathArg = diagArgumentList[0];
        var diagFilePath = GetDiagFilePath(diagFilePathArg);

        // Get diag parameters.
        var diagParameterArgs = diagArgumentList.Skip(1);
        var diagParameters = ArgumentProcessorUtilities.GetArgumentParameters(diagParameterArgs, ArgumentProcessorUtilities.EqualNameValueSeparator, exceptionMessage);

        // Initialize diag logging.
        InitializeDiagLogging(diagFilePath, diagParameters);

        // Write version to the log here, because that is the
        // first place where we know if we log or not.
        EqtTrace.Verbose($"Version: {Product.Version} Current process architecture: {_processHelper.GetCurrentProcessArchitecture()}");
        // https://docs.microsoft.com/en-us/dotnet/api/system.reflection.assembly.location?view=net-6.0#remarks
        // In .NET 5 and later versions, for bundled assemblies, the value returned is an empty string.
        string objectTypeLocation = typeof(object).Assembly.Location;
        if (!objectTypeLocation.IsNullOrEmpty())
        {
            EqtTrace.Verbose($"Runtime location: {Path.GetDirectoryName(objectTypeLocation)}");
        }
    }

    /// <summary>
    /// Executes the argument processor.
    /// </summary>
    /// <returns>The <see cref="ArgumentProcessorResult"/>.</returns>
    public ArgumentProcessorResult Execute()
    {
        // Nothing to do since we updated the parameter during initialize parameter
        return ArgumentProcessorResult.Success;
    }

    /// <summary>
    /// Initialize diag logging.
    /// </summary>
    /// <param name="diagFilePath">Diag file path.</param>
    /// <param name="diagParameters">Diag parameters</param>
    private static void InitializeDiagLogging(string diagFilePath, Dictionary<string, string> diagParameters)
    {
        // Get trace level from diag parameters.
        var traceLevel = GetDiagTraceLevel(diagParameters);

        // Initialize trace.
        // Trace initialized is false in case of any exception at time of initialization like Catch exception(UnauthorizedAccessException, PathTooLongException...)
        var traceInitialized = EqtTrace.InitializeTrace(diagFilePath, traceLevel);

        // Show console warning in case trace is not initialized.
        if (!traceInitialized && !StringUtils.IsNullOrEmpty(EqtTrace.ErrorOnInitialization))
        {
            ConsoleOutput.Instance.Warning(false, EqtTrace.ErrorOnInitialization);
        }
    }

    /// <summary>
    /// Gets diag trace level.
    /// </summary>
    /// <param name="diagParameters">Diag parameters.</param>
    /// <returns>Diag trace level.</returns>
    private static PlatformTraceLevel GetDiagTraceLevel(Dictionary<string, string> diagParameters)
    {
        // If diag parameters is null, set value of trace level as verbose.
        if (diagParameters == null)
        {
            return PlatformTraceLevel.Verbose;
        }

        // Get trace level from diag parameters.
        var traceLevelExists = diagParameters.TryGetValue(TraceLevelParam, out var traceLevelStr);
        if (traceLevelExists && Enum.TryParse(traceLevelStr, true, out PlatformTraceLevel traceLevel))
        {
            return traceLevel;
        }

        // Default value of diag trace level is verbose.
        return PlatformTraceLevel.Verbose;
    }

    /// <summary>
    /// Gets diag file path.
    /// </summary>
    /// <param name="diagFilePathArgument">Diag file path argument.</param>
    /// <returns>Diag file path.</returns>
    private string GetDiagFilePath(string diagFilePathArgument)
    {
        // Remove double quotes if present.
        diagFilePathArgument = diagFilePathArgument.Replace("\"", "");

        // If we provide a directory we don't need to create the base directory.
        if (!diagFilePathArgument.EndsWith(@"\") && !diagFilePathArgument.EndsWith("/"))
        {
            // Create base directory for diag file path (if doesn't exist)
            CreateDirectoryIfNotExists(diagFilePathArgument);
        }

        // return full diag file path. (This is done so that vstest and testhost create logs at same location.)
        return Path.GetFullPath(diagFilePathArgument);
    }

    /// <summary>
    /// Create directory if not exists.
    /// </summary>
    /// <param name="filePath">File path.</param>
    private void CreateDirectoryIfNotExists(string filePath)
    {
        // Create the base directory of file path if doesn't exist.
        // Directory could be empty if just a filename is provided. E.g. log.txt
        var directory = Path.GetDirectoryName(filePath);
        if (!StringUtils.IsNullOrEmpty(directory) && !_fileHelper.DirectoryExists(directory))
        {
            _fileHelper.CreateDirectory(directory);
        }
    }

    #endregion
}