-
Notifications
You must be signed in to change notification settings - Fork 325
/
Copy pathVsTestConsoleProcessManager.cs
274 lines (237 loc) · 9.4 KB
/
VsTestConsoleProcessManager.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
// 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.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 Process? _process;
private bool _vstestConsoleStarted;
private bool _vstestConsoleExited;
private bool _isDisposed;
internal IFileHelper FileHelper { get; set; }
/// <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);
}
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);
}
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
if (!_processExitedEvent.WaitOne(Endsessiontimeout) && IsProcessInitialized())
{
EqtTrace.Info($"VsTestConsoleProcessManager.ShutDownProcess : Terminating vstest.console process 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;
}
}
}
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;
ProcessExited?.Invoke(sender, e);
}
}
private void Process_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
if (e.Data != null)
{
EqtTrace.Error(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;
}
}
}