2
2
// The .NET Foundation licenses this file to you under the MIT license.
3
3
4
4
using System ;
5
- using System . Collections . Generic ;
6
5
using System . Diagnostics ;
7
6
using System . Globalization ;
8
7
using Microsoft . Build . Execution ;
@@ -46,7 +45,6 @@ public void TaskNodesDieAfterBuild(bool taskHostFactorySpecified, bool envVariab
46
45
{
47
46
using ( TestEnvironment env = TestEnvironment . Create ( ) )
48
47
{
49
- using ProcessTracker processTracker = new ( ) ;
50
48
string taskFactory = taskHostFactorySpecified ? "TaskHostFactory" : "AssemblyTaskFactory" ;
51
49
string pidTaskProject = $@ "
52
50
<Project>
@@ -64,9 +62,16 @@ public void TaskNodesDieAfterBuild(bool taskHostFactorySpecified, bool envVariab
64
62
env . SetEnvironmentVariable ( "MSBUILDFORCEALLTASKSOUTOFPROC" , "1" ) ;
65
63
}
66
64
65
+ // To execute the task in sidecar mode, both node reuse and the environment variable must be set.
66
+ BuildParameters buildParameters = new ( ) { EnableNodeReuse = envVariableSpecified && true /* node reuse enabled */ } ;
67
+
67
68
ProjectInstance projectInstance = new ( project . Path ) ;
68
69
69
- projectInstance . Build ( ) . ShouldBeTrue ( ) ;
70
+ BuildManager buildManager = BuildManager . DefaultBuildManager ;
71
+ BuildResult buildResult = buildManager . Build ( buildParameters , new BuildRequestData ( projectInstance , targetsToBuild : [ "AccessPID" ] ) ) ;
72
+
73
+ buildResult . OverallResult . ShouldBe ( BuildResultCode . Success ) ;
74
+
70
75
string processId = projectInstance . GetPropertyValue ( "PID" ) ;
71
76
string . IsNullOrEmpty ( processId ) . ShouldBeFalse ( ) ;
72
77
Int32 . TryParse ( processId , out int pid ) . ShouldBeTrue ( ) ;
@@ -88,23 +93,18 @@ public void TaskNodesDieAfterBuild(bool taskHostFactorySpecified, bool envVariab
88
93
}
89
94
else
90
95
{
96
+ // This is the sidecar TaskHost case - it should persist after build is done. So we need to clean up and kill it ourselves.
97
+ Process taskHostNode = Process . GetProcessById ( pid ) ;
98
+ bool processExited = taskHostNode . WaitForExit ( 3000 ) ;
99
+
100
+ processExited . ShouldBeFalse ( ) ;
91
101
try
92
102
{
93
- // This is the sidecar TaskHost case - it should persist after build is done. So we need to clean up and kill it ourselves.
94
- Process taskHostNode = Process . GetProcessById ( pid ) ;
95
- using var taskHostNodeTracker = processTracker . AttachToProcess ( pid , "Sidecar" , _output ) ;
96
- bool processExited = taskHostNode . WaitForExit ( 3000 ) ;
97
- if ( processExited )
98
- {
99
- processTracker . PrintSummary ( _output ) ;
100
- }
101
-
102
- processExited . ShouldBeFalse ( ) ;
103
103
taskHostNode . Kill ( ) ;
104
104
}
105
105
catch
106
106
{
107
- processTracker . PrintSummary ( _output ) ;
107
+ // Ignore exceptions from Kill - the process may have exited between the WaitForExit and Kill calls.
108
108
}
109
109
}
110
110
}
@@ -119,9 +119,7 @@ public void TransientAndSidecarNodeCanCoexist()
119
119
{
120
120
using ( TestEnvironment env = TestEnvironment . Create ( _output ) )
121
121
{
122
- using ProcessTracker processTracker = new ( ) ;
123
- {
124
- string pidTaskProject = $@ "
122
+ string pidTaskProject = $@ "
125
123
<Project>
126
124
<UsingTask TaskName=""ProcessIdTask"" AssemblyName=""Microsoft.Build.Engine.UnitTests"" TaskFactory=""TaskHostFactory"" />
127
125
<UsingTask TaskName=""ProcessIdTaskSidecar"" AssemblyName=""Microsoft.Build.Engine.UnitTests"" TaskFactory=""AssemblyTaskFactory"" />
@@ -136,48 +134,44 @@ public void TransientAndSidecarNodeCanCoexist()
136
134
</Target>
137
135
</Project>" ;
138
136
139
- TransientTestFile project = env . CreateFile ( "testProject.csproj" , pidTaskProject ) ;
140
-
141
- env . SetEnvironmentVariable ( "MSBUILDFORCEALLTASKSOUTOFPROC" , "1" ) ;
142
- ProjectInstance projectInstance = new ( project . Path ) ;
137
+ TransientTestFile project = env . CreateFile ( "testProject.csproj" , pidTaskProject ) ;
143
138
144
- projectInstance . Build ( ) . ShouldBeTrue ( ) ;
139
+ env . SetEnvironmentVariable ( "MSBUILDFORCEALLTASKSOUTOFPROC" , "1" ) ;
140
+ ProjectInstance projectInstance = new ( project . Path ) ;
145
141
146
- string transientPid = projectInstance . GetPropertyValue ( "PID" ) ;
147
- string sidecarPid = projectInstance . GetPropertyValue ( "PID2" ) ;
148
- sidecarPid . ShouldNotBe ( transientPid , "Each task should have it's own TaskHost node." ) ;
142
+ projectInstance . Build ( ) . ShouldBeTrue ( ) ;
149
143
150
- using var sidecarTracker = processTracker . AttachToProcess ( int . Parse ( sidecarPid ) , "Sidecar" , _output ) ;
144
+ string transientPid = projectInstance . GetPropertyValue ( "PID" ) ;
145
+ string sidecarPid = projectInstance . GetPropertyValue ( "PID2" ) ;
146
+ sidecarPid . ShouldNotBe ( transientPid , "Each task should have it's own TaskHost node." ) ;
151
147
152
- string . IsNullOrEmpty ( transientPid ) . ShouldBeFalse ( ) ;
153
- Int32 . TryParse ( transientPid , out int pid ) . ShouldBeTrue ( ) ;
154
- Int32 . TryParse ( sidecarPid , out int pidSidecar ) . ShouldBeTrue ( ) ;
148
+ string . IsNullOrEmpty ( transientPid ) . ShouldBeFalse ( ) ;
149
+ Int32 . TryParse ( transientPid , out int pid ) . ShouldBeTrue ( ) ;
150
+ Int32 . TryParse ( sidecarPid , out int pidSidecar ) . ShouldBeTrue ( ) ;
155
151
156
- Process . GetCurrentProcess ( ) . Id . ShouldNotBe ( pid ) ;
152
+ Process . GetCurrentProcess ( ) . Id . ShouldNotBe ( pid ) ;
157
153
158
- try
159
- {
160
- Process transientTaskHostNode = Process . GetProcessById ( pid ) ;
161
- transientTaskHostNode . WaitForExit ( 3000 ) . ShouldBeTrue ( "The node should be dead since this is the transient case." ) ;
162
- }
163
- catch ( ArgumentException e )
164
- {
165
- // We expect the TaskHostNode to exit quickly. If it exits before Process.GetProcessById, it will throw an ArgumentException.
166
- e . Message . ShouldBe ( $ "Process with an Id of { pid } is not running.") ;
167
- }
154
+ try
155
+ {
156
+ Process transientTaskHostNode = Process . GetProcessById ( pid ) ;
157
+ transientTaskHostNode . WaitForExit ( 3000 ) . ShouldBeTrue ( "The node should be dead since this is the transient case." ) ;
158
+ }
159
+ catch ( ArgumentException e )
160
+ {
161
+ // We expect the TaskHostNode to exit quickly. If it exits before Process.GetProcessById, it will throw an ArgumentException.
162
+ e . Message . ShouldBe ( $ "Process with an Id of { pid } is not running.") ;
163
+ }
168
164
169
- try
170
- {
171
- // This is the sidecar TaskHost case - it should persist after build is done. So we need to clean up and kill it ourselves.
172
- Process sidecarTaskHostNode = Process . GetProcessById ( pidSidecar ) ;
173
- sidecarTaskHostNode . WaitForExit ( 3000 ) . ShouldBeFalse ( $ "The node should be alive since it is the sidecar node.") ;
174
- sidecarTaskHostNode . Kill ( ) ;
175
- }
176
- catch ( Exception e )
177
- {
178
- processTracker . PrintSummary ( _output ) ;
179
- e . Message . ShouldNotBe ( $ "Process with an Id of { pidSidecar } is not running") ;
180
- }
165
+ try
166
+ {
167
+ // This is the sidecar TaskHost case - it should persist after build is done. So we need to clean up and kill it ourselves.
168
+ Process sidecarTaskHostNode = Process . GetProcessById ( pidSidecar ) ;
169
+ sidecarTaskHostNode . WaitForExit ( 3000 ) . ShouldBeFalse ( $ "The node should be alive since it is the sidecar node.") ;
170
+ sidecarTaskHostNode . Kill ( ) ;
171
+ }
172
+ catch ( Exception e )
173
+ {
174
+ e . Message . ShouldNotBe ( $ "Process with an Id of { pidSidecar } is not running") ;
181
175
}
182
176
}
183
177
}
@@ -332,177 +326,5 @@ public void VariousParameterTypesCanBeTransmittedToAndReceivedFromTaskHost()
332
326
projectInstance . GetPropertyValue ( "CustomStructOutput" ) . ShouldBe ( TaskBuilderTestTask . s_customStruct . ToString ( CultureInfo . InvariantCulture ) ) ;
333
327
projectInstance . GetPropertyValue ( "EnumOutput" ) . ShouldBe ( TargetBuiltReason . BeforeTargets . ToString ( ) ) ;
334
328
}
335
-
336
- /// <summary>
337
- /// Helper class for tracking external processes during tests.
338
- /// Monitors process lifecycle and provides diagnostic information for debugging.
339
- /// </summary>
340
- internal sealed class ProcessTracker : IDisposable
341
- {
342
- private readonly List < TrackedProcess > _trackedProcesses = new ( ) ;
343
-
344
- /// <summary>
345
- /// Attaches to an existing process for monitoring.
346
- /// </summary>
347
- /// <param name="pid">Process ID to attach to</param>
348
- /// <param name="name">Friendly name for the process</param>
349
- /// <param name="output">Test output helper for logging</param>
350
- /// <returns>TrackedProcess instance for the attached process</returns>
351
- public TrackedProcess AttachToProcess ( int pid , string name , ITestOutputHelper output )
352
- {
353
- try
354
- {
355
- var process = Process . GetProcessById ( pid ) ;
356
- var tracked = new TrackedProcess ( process , name ) ;
357
-
358
- // Enable event notifications
359
- process . EnableRaisingEvents = true ;
360
-
361
- // Subscribe to exit event
362
- process . Exited += ( sender , e ) =>
363
- {
364
- var proc = sender as Process ;
365
- tracked . ExitTime = DateTime . Now ;
366
- tracked . ExitCode = proc ? . ExitCode ?? - 999 ;
367
- tracked . HasExited = true ;
368
-
369
- output . WriteLine ( $ "[{ DateTime . Now : HH:mm:ss.fff} ] { tracked . Name } (PID { tracked . ProcessId } ) EXITED with code { tracked . ExitCode } ") ;
370
- } ;
371
-
372
- _trackedProcesses . Add ( tracked ) ;
373
- output . WriteLine ( $ "[{ DateTime . Now : HH:mm:ss.fff} ] Attached to { name } (PID { pid } )") ;
374
-
375
- return tracked ;
376
- }
377
- catch ( ArgumentException )
378
- {
379
- output . WriteLine ( $ "[{ DateTime . Now : HH:mm:ss.fff} ] Could not attach to { name } (PID { pid } ) - process not found") ;
380
- return new TrackedProcess ( null , name ) { ProcessId = pid , NotFound = true } ;
381
- }
382
- }
383
-
384
- /// <summary>
385
- /// Prints a summary of all tracked processes for diagnostic purposes.
386
- /// </summary>
387
- /// <param name="output">Test output helper for logging</param>
388
- public void PrintSummary ( ITestOutputHelper output )
389
- {
390
- output . WriteLine ( "\n === PROCESS TRACKING SUMMARY ===" ) ;
391
- foreach ( var tracked in _trackedProcesses )
392
- {
393
- tracked . PrintStatus ( output ) ;
394
- }
395
- }
396
-
397
- public void Dispose ( )
398
- {
399
- foreach ( var tracked in _trackedProcesses )
400
- {
401
- tracked . Dispose ( ) ;
402
- }
403
- }
404
- }
405
-
406
- /// <summary>
407
- /// Represents a tracked process with lifecycle monitoring capabilities.
408
- /// </summary>
409
- internal sealed class TrackedProcess : IDisposable
410
- {
411
- /// <summary>
412
- /// The underlying Process object being tracked.
413
- /// </summary>
414
- public Process Process { get ; }
415
-
416
- /// <summary>
417
- /// Friendly name for the tracked process.
418
- /// </summary>
419
- public string Name { get ; }
420
-
421
- /// <summary>
422
- /// Process ID of the tracked process.
423
- /// </summary>
424
- public int ProcessId { get ; set ; }
425
-
426
- /// <summary>
427
- /// Time when tracking began for this process.
428
- /// </summary>
429
- public DateTime AttachTime { get ; }
430
-
431
- /// <summary>
432
- /// Time when the process exited, if applicable.
433
- /// </summary>
434
- public DateTime ? ExitTime { get ; set ; }
435
-
436
- /// <summary>
437
- /// Exit code of the process, if it has exited.
438
- /// </summary>
439
- public int ? ExitCode { get ; set ; }
440
-
441
- /// <summary>
442
- /// Whether the process has exited.
443
- /// </summary>
444
- public bool HasExited { get ; set ; }
445
-
446
- /// <summary>
447
- /// Whether the process was not found when attempting to attach.
448
- /// </summary>
449
- public bool NotFound { get ; set ; }
450
-
451
- public TrackedProcess ( Process process , string name )
452
- {
453
- Process = process ;
454
- Name = name ;
455
- ProcessId = process ? . Id ?? - 1 ;
456
- AttachTime = DateTime . Now ;
457
- }
458
-
459
- /// <summary>
460
- /// Prints detailed status information about the tracked process.
461
- /// </summary>
462
- /// <param name="output">Test output helper for logging</param>
463
- public void PrintStatus ( ITestOutputHelper output )
464
- {
465
- output . WriteLine ( $ "\n { Name } (PID { ProcessId } ):") ;
466
- output . WriteLine ( $ " Attached at: { AttachTime : HH:mm:ss.fff} ") ;
467
-
468
- if ( NotFound )
469
- {
470
- output . WriteLine ( " Status: Not found when trying to attach" ) ;
471
- }
472
- else if ( HasExited )
473
- {
474
- var duration = ( ExitTime . Value - AttachTime ) . TotalMilliseconds ;
475
- output . WriteLine ( $ " Status: Exited with code { ExitCode } ") ;
476
- output . WriteLine ( $ " Exit time: { ExitTime : HH:mm:ss.fff} ") ;
477
- output . WriteLine ( $ " Duration: { duration : F0} ms") ;
478
- }
479
- else
480
- {
481
- try
482
- {
483
- if ( Process != null && ! Process . HasExited )
484
- {
485
- output . WriteLine ( " Status: Still running" ) ;
486
- output . WriteLine ( $ " Start time: { Process . StartTime : HH:mm:ss.fff} ") ;
487
- output . WriteLine ( $ " CPU time: { Process . TotalProcessorTime . TotalMilliseconds : F0} ms") ;
488
- }
489
- else
490
- {
491
- output . WriteLine ( " Status: Exited (detected during status check)" ) ;
492
- if ( Process != null )
493
- {
494
- output . WriteLine ( $ " Exit code: { Process . ExitCode } ") ;
495
- }
496
- }
497
- }
498
- catch ( Exception ex )
499
- {
500
- output . WriteLine ( $ " Status: Error checking process - { ex . Message } ") ;
501
- }
502
- }
503
- }
504
-
505
- public void Dispose ( ) => Process ? . Dispose ( ) ;
506
- }
507
329
}
508
330
}
0 commit comments