@@ -5,6 +5,7 @@ namespace VirtualClient.Common
55{
66 using System ;
77 using System . Collections . Generic ;
8+ using System . IO ;
89 using System . Text . RegularExpressions ;
910 using System . Threading ;
1011 using System . Threading . Tasks ;
@@ -36,13 +37,69 @@ public static IProcessProxy Interactive(this IProcessProxy process)
3637 /// An absolute timeout to apply for the case that the process does not finish in the amount of time expected. If the
3738 /// timeout is reached a <see cref="TimeoutException"/> exception will be thrown.
3839 /// </param>
39- public static async Task StartAndWaitAsync ( this IProcessProxy process , CancellationToken cancellationToken , TimeSpan ? timeout = null )
40+ /// <param name="withExitConfirmation">True to confirm an exit code before returning. Default = false.</param>
41+ public static async Task StartAndWaitAsync ( this IProcessProxy process , CancellationToken cancellationToken , TimeSpan ? timeout = null , bool withExitConfirmation = false )
4042 {
4143 process . ThrowIfNull ( nameof ( process ) ) ;
4244
4345 if ( process . Start ( ) )
4446 {
4547 await process . WaitForExitAsync ( cancellationToken , timeout ) ;
48+
49+ if ( withExitConfirmation )
50+ {
51+ // There is a race condition-style flaw in the .NET implementation of the
52+ // WaitForExit() method. The race condition allows for the process to exit after
53+ // completion but for a period of time to pass before the kernel completes all finalization
54+ // and cleanup steps (e.g. setting an exit code). To help prevent downstream issues that
55+ // happen when attempting to access properties on the process during this race condition period
56+ // of time, we are adding in an extra check on the process HasExited.
57+ //
58+ // Example of error hit during race condition period of time:
59+ // Process must exit before requested information can be determined.
60+ DateTime exitTime = DateTime . UtcNow . AddMinutes ( 2 ) ;
61+ int exitCode = - 1 ;
62+ bool confirmed = false ;
63+
64+ while ( DateTime . UtcNow < exitTime )
65+ {
66+ try
67+ {
68+ // If the exit code is not available, this line will throw an exception.
69+ exitCode = process . ExitCode ;
70+ confirmed = true ;
71+ break ;
72+ }
73+ catch
74+ {
75+ // Wait, but don't throttle the CPU.
76+ await Task . Delay ( 1000 ) ;
77+ }
78+ }
79+
80+ if ( ! confirmed )
81+ {
82+ try
83+ {
84+ string processName = null ;
85+ ProcessExtensions . TryGetValue < string > (
86+ ( ) =>
87+ {
88+ return $ "{ Path . GetFileName ( process ? . StartInfo . FileName ) } { process ? . StartInfo . Arguments } "? . Trim ( ) ;
89+ } ,
90+ out processName ) ;
91+
92+ int processId = - 1 ;
93+ ProcessExtensions . TryGetValue < int > ( ( ) => process . Id , out processId ) ;
94+
95+ Console . Error . WriteLine ( $ "Process exit confirmation failed for process '{ processName } (id={ processId } )'.") ;
96+ }
97+ catch
98+ {
99+ // Do not allow any exceptions to surface from here.
100+ }
101+ }
102+ }
46103 }
47104 }
48105
@@ -59,11 +116,33 @@ public static ProcessDetails ToProcessDetails(this IProcessProxy process, string
59116 {
60117 process . ThrowIfNull ( nameof ( process ) ) ;
61118
119+ int processId = - 1 ;
120+ int exitCode = - 1 ;
121+
122+ try
123+ {
124+ if ( ProcessExtensions . TryGetValue < int > ( ( ) => process . Id , out int id ) )
125+ {
126+ processId = id ;
127+ }
128+
129+ if ( ProcessExtensions . TryGetValue < int > ( ( ) => process . ExitCode , out int code ) )
130+ {
131+ exitCode = code ;
132+ }
133+ }
134+ catch
135+ {
136+ // Avoid exceptions caused by kernel-layer race conditions on process finalization.
137+ // e.g.
138+ // Process must exit before requested information can be determined.
139+ }
140+
62141 return new ProcessDetails
63142 {
64- Id = process . Id ,
143+ Id = processId ,
65144 CommandLine = SensitiveData . ObscureSecrets ( $ "{ process . StartInfo ? . FileName } { process . StartInfo ? . Arguments } ". Trim ( ) ) ,
66- ExitCode = process . ExitCode ,
145+ ExitCode = exitCode ,
67146 Results = results ,
68147 StandardError = process . StandardError ? . Length > 0 ? process . StandardError . ToString ( ) : string . Empty ,
69148 StandardOutput = process . StandardOutput ? . Length > 0 ? process . StandardOutput . ToString ( ) : string . Empty ,
@@ -142,5 +221,30 @@ public static Task<IProcessProxy> WaitForResponseAsync(
142221
143222 return process . WaitForResponseAsync ( new Regex ( response , comparisonOptions ) , cancellationToken , timeout ) ;
144223 }
224+
225+ /// <summary>
226+ /// Returns true/false whether the value can be derived.
227+ /// </summary>
228+ /// <typeparam name="T">The data type of the value.</typeparam>
229+ /// <param name="reader">A function/delegate used to retrieve the value.</param>
230+ /// <param name="value">The value if existing.</param>
231+ /// <returns>True if the value can be confirmed to exist and is non-null.</returns>
232+ private static bool TryGetValue < T > ( Func < T > reader , out T value )
233+ where T : IConvertible
234+ {
235+ bool confirmed = false ;
236+ value = default ( T ) ;
237+
238+ try
239+ {
240+ value = reader . Invoke ( ) ;
241+ confirmed = true ;
242+ }
243+ catch
244+ {
245+ }
246+
247+ return confirmed ;
248+ }
145249 }
146250}
0 commit comments