| File: VsTestConsoleProcessManager.cs | Web Access |
| Project: src\vstest\src\Microsoft.TestPlatform.VsTestConsole.TranslationLayer\Microsoft.TestPlatform.VsTestConsole.TranslationLayer.csproj (Microsoft.TestPlatform.VsTestConsole.TranslationLayer) |
// 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.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.Text; using System.Threading; using Microsoft.TestPlatform.VsTestConsole.TranslationLayer.Interfaces; using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities; using Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Helpers; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.Utilities.Helpers; using Microsoft.VisualStudio.TestPlatform.Utilities.Helpers.Interfaces; using Microsoft.VisualStudio.TestPlatform.VsTestConsole.TranslationLayer.Resources; using Resources = Microsoft.VisualStudio.TestPlatform.VsTestConsole.TranslationLayer.Resources.Resources; namespace Microsoft.TestPlatform.VsTestConsole.TranslationLayer; /// <summary> /// Vstest.console process manager /// </summary> internal sealed class VsTestConsoleProcessManager : IProcessManager, IDisposable { /// <summary> /// Port number for communicating with Vstest CLI /// </summary> private const string PortArgument = "/port:{0}"; /// <summary> /// Process Id of the Current Process which is launching Vstest CLI /// Helps Vstest CLI in auto-exit if current process dies without notifying it /// </summary> private const string ParentProcessidArgument = "/parentprocessid:{0}"; /// <summary> /// Diagnostics argument for Vstest CLI /// Enables Diagnostic logging for Vstest CLI and TestHost - Optional /// </summary> private const string DiagArgument = "/diag:{0};tracelevel={1}"; /// <summary> /// EndSession timeout /// </summary> private const int Endsessiontimeout = 1000; private readonly string _vstestConsolePath; private readonly object _syncObject = new(); private readonly bool _isNetCoreRunner; private readonly string? _dotnetExePath; private readonly ManualResetEvent _processExitedEvent = new(false); private readonly StringBuilder _vstestConsoleError = new(); private Process? _process; private bool _vstestConsoleStarted; private bool _vstestConsoleExited; private bool _isDisposed; internal IFileHelper FileHelper { get; set; } /// <inheritdoc/> public string? ProcessName { get; set; } /// <inheritdoc/> public int? ProcessId { get; set; } /// <inheritdoc/> public string? ErrorOutput => _vstestConsoleError.ToString(); /// <inheritdoc/> public int? ExitCode { get; private set; } = -1; /// <inheritdoc/> public event EventHandler? ProcessExited; /// <summary> /// Creates an instance of VsTestConsoleProcessManager class. /// </summary> /// <param name="vstestConsolePath">The full path to vstest.console</param> public VsTestConsoleProcessManager(string vstestConsolePath) { FileHelper = new FileHelper(); if (!FileHelper.Exists(vstestConsolePath)) { EqtTrace.Error("Invalid File Path: {0}", vstestConsolePath); throw new Exception(string.Format(CultureInfo.CurrentCulture, Resources.InvalidFilePath, vstestConsolePath)); } _vstestConsolePath = vstestConsolePath; _isNetCoreRunner = vstestConsolePath.EndsWith(".dll"); } public VsTestConsoleProcessManager(string vstestConsolePath, string dotnetExePath) : this(vstestConsolePath) { _dotnetExePath = dotnetExePath; } /// <summary> /// Checks if the process has been initialized. /// </summary> /// <returns>True if process is successfully initialized</returns> public bool IsProcessInitialized() { lock (_syncObject) { return _vstestConsoleStarted && !_vstestConsoleExited && _process != null; } } /// <summary> /// Call vstest.console with the parameters previously specified /// </summary> public void StartProcess(ConsoleParameters consoleParameters) { var consoleRunnerPath = GetConsoleRunner(); // The console runner path we retrieve might have been escaped so we need to remove the // extra double quotes before testing whether the file exists. if (!File.Exists(consoleRunnerPath.Trim('"'))) { throw new FileNotFoundException(string.Format(CultureInfo.CurrentCulture, InternalResources.CannotFindConsoleRunner, consoleRunnerPath), consoleRunnerPath); } ProcessName = null; ProcessId = null; ExitCode = null; _vstestConsoleError.Clear(); var arguments = string.Join(" ", BuildArguments(consoleParameters)); var info = new ProcessStartInfo(consoleRunnerPath, arguments) { UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, }; EqtTrace.Verbose("VsTestCommandLineWrapper.StartProcess: Process Start Info {0} {1}", info.FileName, info.Arguments); if (!consoleParameters.InheritEnvironmentVariables) { EqtTrace.Verbose("VsTestCommandLineWrapper.StartProcess: Clearing all environment variables."); info.EnvironmentVariables.Clear(); } if (consoleParameters.EnvironmentVariables != null) { foreach (var envVariable in consoleParameters.EnvironmentVariables) { if (envVariable.Key != null) { // Not printing the value on purpose, env variables can contain secrets and we don't need to know the values // most of the time. // EqtTrace.Verbose("VsTestCommandLineWrapper.StartProcess: Setting environment variable: {0}", envVariable.Key); info.EnvironmentVariables[envVariable.Key] = envVariable.Value?.ToString(); } } } try { _process = Process.Start(info); ProcessId = _process!.Id; ProcessName = _process.ProcessName; EqtTrace.Info($"VsTestConsoleProcessManager.StartProcess: Started process id:{_process.Id}"); // Not normally needed, but if you run multiple instances of wrapper it helps to also add {Environment.StackTrace} } catch (Win32Exception ex) { throw new Exception(string.Format(CultureInfo.CurrentCulture, InternalResources.ProcessStartWin32Failure, consoleRunnerPath, arguments), ex); } lock (_syncObject) { _vstestConsoleExited = false; _vstestConsoleStarted = true; } _process!.EnableRaisingEvents = true; _process.Exited += Process_Exited; _process.OutputDataReceived += Process_OutputDataReceived; _process.ErrorDataReceived += Process_ErrorDataReceived; _process.BeginOutputReadLine(); _process.BeginErrorReadLine(); _processExitedEvent.Reset(); } /// <summary> /// Shutdown the vstest.console process /// </summary> public void ShutdownProcess() { // Ideally process should die by itself EqtTrace.Info($"VsTestConsoleProcessManager.ShutDownProcess : Will terminate vstest.console process: {ProcessId}-{ProcessName}, waiting {Endsessiontimeout} milliseconds for it to exit."); if (!_processExitedEvent.WaitOne(Endsessiontimeout) && IsProcessInitialized()) { EqtTrace.Info($"VsTestConsoleProcessManager.ShutDownProcess : Terminating vstest.console process: {ProcessId}-{ProcessName} after waiting for {Endsessiontimeout} milliseconds."); _vstestConsoleExited = true; if (_process is not null) { _process.OutputDataReceived -= Process_OutputDataReceived; _process.ErrorDataReceived -= Process_ErrorDataReceived; SafelyTerminateProcess(); _process.Dispose(); _process = null; } EqtTrace.Info($"VsTestConsoleProcessManager.ShutDownProcess : Terminated vstest.console process: {ProcessId}-{ProcessName}."); } else { EqtTrace.Verbose($"VsTestConsoleProcessManager.ShutDownProcess : Process: {ProcessId}-{ProcessName} already exited, doing nothing."); } } private void SafelyTerminateProcess() { try { if (_process != null && !_process.HasExited) { _process.Kill(); } } catch (InvalidOperationException ex) { EqtTrace.Info("VsTestCommandLineWrapper: Error While Terminating Process {0} ", ex.Message); } } private void Process_Exited(object? sender, EventArgs e) { lock (_syncObject) { _processExitedEvent.Set(); _vstestConsoleExited = true; ExitCode = ((Process)sender!).ExitCode; ProcessExited?.Invoke(sender, e); } } private void Process_ErrorDataReceived(object sender, DataReceivedEventArgs e) { if (e.Data != null) { EqtTrace.Error(e.Data); _vstestConsoleError.AppendLine(e.Data); } } private void Process_OutputDataReceived(object sender, DataReceivedEventArgs e) { if (e.Data != null) { EqtTrace.Verbose(e.Data); } } internal string[] BuildArguments(ConsoleParameters parameters) { var args = new List<string> { // Start Vstest.console with args: --parentProcessId|/parentprocessid:<ppid> --port|/port:<port> string.Format(CultureInfo.InvariantCulture, ParentProcessidArgument, parameters.ParentProcessId), string.Format(CultureInfo.InvariantCulture, PortArgument, parameters.PortNumber) }; if (!parameters.LogFilePath.IsNullOrEmpty()) { // Extra args: --diag|/diag:<PathToLogFile>;tracelevel=<tracelevel> args.Add(string.Format(CultureInfo.InvariantCulture, DiagArgument, parameters.LogFilePath, parameters.TraceLevel)); } if (_isNetCoreRunner) { args.Insert(0, GetEscapeSequencedPath(_vstestConsolePath)); } return args.ToArray(); } private string GetConsoleRunner() => _isNetCoreRunner ? _dotnetExePath.IsNullOrEmpty() ? new DotnetHostHelper().GetDotnetPath() : _dotnetExePath : GetEscapeSequencedPath(_vstestConsolePath); private static string GetEscapeSequencedPath(string path) => path.IsNullOrEmpty() ? path : $"\"{path.Trim('"')}\""; public void Dispose() { if (!_isDisposed) { _processExitedEvent.Dispose(); _process?.Dispose(); _isDisposed = true; } } }