@@ -13,17 +13,20 @@ public class BoundedCliProcessProbeTests
1313 private const string SentinelValue = "explicit-environment-value" ;
1414 private const string OverflowMessage = "CLI metadata process exceeded the configured output limit." ;
1515 private const string TimeoutMessage = "CLI metadata process did not exit within the configured time limit." ;
16- private const string OutputTimeoutMessage = "CLI metadata process output did not close within the configured time limit." ;
16+ private const string OutputCleanupFailedMessage = "CLI metadata process output readers did not stop within the configured time limit." ;
17+ private const string ProbeBlockedMessage = "CLI metadata process probe is blocked by an earlier incomplete cleanup." ;
18+ private const string ProbeBusyMessage = "CLI metadata process probe could not acquire its bounded process lease." ;
1719 private const string LinuxFixtureSkipReason = "The detached inherited-pipe fixture requires Linux setsid." ;
1820 private const string DetachedChildCommandPrefix = "/usr/bin/setsid /bin/sleep 30 >&2 & echo $! > '" ;
1921 private const string DetachedChildCommandSuffix = "'; exit 0" ;
2022 private const string FixtureDirectoryPrefix = "BoundedCliProcessProbe-" ;
21- private const string ChildProcessIdFileName = "child.pid" ;
23+ private const string ChildProcessIdFilePattern = "child-* .pid" ;
2224 private const string StandardOutputPressureMarker = "stdout-pressure-1999" ;
2325 private const string StandardErrorPressureMarker = "stderr-pressure-1999" ;
2426 private const string StandardInputEofCommandUnix = "cat >/dev/null" ;
2527 private const string StandardInputEofCommandWindows = "more >NUL" ;
2628 private const int DetachedPipeProbeCount = 3 ;
29+ private static readonly TimeSpan ConcurrentProbeTimeout = TimeSpan . FromSeconds ( 3 ) ;
2730 private const string SentinelCommandUnix = "printf '%s' \" $SDK_METADATA_PROBE_SENTINEL\" " ;
2831 private const string SentinelCommandWindows = "echo %SDK_METADATA_PROBE_SENTINEL%" ;
2932 private const string PressureCommandUnix =
@@ -110,6 +113,55 @@ public async Task Run_StopsProcessWhenProbeTimesOut()
110113 await Assert . That ( stopwatch . Elapsed ) . IsLessThan ( ProcessTimeout ) ;
111114 }
112115
116+ [ Test ]
117+ public async Task Run_RejectsConcurrentProcessBeforeStartingIt ( )
118+ {
119+ var firstReadersStarted = new TaskCompletionSource ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
120+ var firstReaderCount = 0 ;
121+ Action < int > firstReaderCountChanged = delta =>
122+ {
123+ if ( Interlocked . Add ( ref firstReaderCount , delta ) == 2 )
124+ {
125+ firstReadersStarted . TrySetResult ( ) ;
126+ }
127+ } ;
128+ var firstProbe = Task . Run ( ( ) => Run ( LongRunningCommandUnix , LongRunningCommandWindows ,
129+ environment : null , inheritEnvironmentVariables : true , timeout : ConcurrentProbeTimeout ,
130+ readerCountChanged : firstReaderCountChanged ) ) ;
131+
132+ await firstReadersStarted . Task . WaitAsync ( ProcessTimeout ) ;
133+
134+ var secondReaderCount = 0 ;
135+ Action < int > secondReaderCountChanged = delta => Interlocked . Add ( ref secondReaderCount , delta ) ;
136+ var secondException = await Assert . That ( ( ) => BoundedCliProcessProbe . Run (
137+ OperatingSystem . IsWindows ( ) ? Path . Combine ( Environment . SystemDirectory , ShellExecutableWindows ) : ShellExecutableUnix ,
138+ [ OperatingSystem . IsWindows ( ) ? ShellArgumentWindows : ShellArgumentUnix ,
139+ OperatingSystem . IsWindows ( ) ? StandardInputEofCommandWindows : StandardInputEofCommandUnix ] ,
140+ environment : null ,
141+ inheritEnvironmentVariables : true ,
142+ timeout : ProbeTimeout ,
143+ maximumOutputCharacters : 64 ,
144+ readerCountChanged : secondReaderCountChanged ,
145+ leaseAcquisitionTimeout : ProbeTimeout ) ) . ThrowsException ( ) ;
146+
147+ await Assert . That ( secondException ) . IsTypeOf < InvalidOperationException > ( ) ;
148+ await Assert . That ( secondException ! . Message ) . IsEqualTo ( ProbeBusyMessage ) ;
149+ await Assert . That ( secondReaderCount ) . IsEqualTo ( 0 ) ;
150+
151+ Exception ? firstException = null ;
152+ try
153+ {
154+ await firstProbe ;
155+ }
156+ catch ( Exception exception )
157+ {
158+ firstException = exception ;
159+ }
160+
161+ await Assert . That ( firstException ) . IsTypeOf < TimeoutException > ( ) ;
162+ await Assert . That ( BoundedCliProcessProbe . PendingReaderCleanupCount ) . IsEqualTo ( 0 ) ;
163+ }
164+
113165 [ Test ]
114166 public async Task Run_FailsWithinBoundWhenExitedRootHasDescendantHoldingPipes ( )
115167 {
@@ -122,14 +174,22 @@ public async Task Run_FailsWithinBoundWhenExitedRootHasDescendantHoldingPipes()
122174 var sandbox = Path . Combine ( Environment . CurrentDirectory , "tests" , ".sandbox" ,
123175 $ "{ FixtureDirectoryPrefix } { Guid . NewGuid ( ) : N} ") ;
124176 Directory . CreateDirectory ( sandbox ) ;
125- var childProcessIdPath = Path . Combine ( sandbox , ChildProcessIdFileName ) ;
126- var detachedCommand = string . Concat ( DetachedChildCommandPrefix , childProcessIdPath ,
127- DetachedChildCommandSuffix ) ;
128177 try
129178 {
130179 for ( var attempt = 0 ; attempt < DetachedPipeProbeCount ; attempt ++ )
131180 {
181+ var childProcessIdPath = Path . Combine ( sandbox , $ "child-{ attempt } .pid") ;
182+ var detachedCommand = string . Concat ( DetachedChildCommandPrefix , childProcessIdPath ,
183+ DetachedChildCommandSuffix ) ;
132184 var activeReaders = 0 ;
185+ var readersDrained = new TaskCompletionSource ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
186+ Action < int > readerCountChanged = delta =>
187+ {
188+ if ( Interlocked . Add ( ref activeReaders , delta ) == 0 )
189+ {
190+ readersDrained . TrySetResult ( ) ;
191+ }
192+ } ;
133193 var stopwatch = Stopwatch . StartNew ( ) ;
134194 var exception = await Assert . That ( ( ) => BoundedCliProcessProbe . Run (
135195 ShellExecutableUnix ,
@@ -138,43 +198,77 @@ public async Task Run_FailsWithinBoundWhenExitedRootHasDescendantHoldingPipes()
138198 inheritEnvironmentVariables : true ,
139199 timeout : ProbeTimeout ,
140200 maximumOutputCharacters : 64 ,
141- readerCountChanged : delta => Interlocked . Add ( ref activeReaders , delta ) ) ) . ThrowsException ( ) ;
201+ readerCountChanged : readerCountChanged ,
202+ leaseAcquisitionTimeout : ProcessTimeout ) ) . ThrowsException ( ) ;
142203 stopwatch . Stop ( ) ;
143204
144- await Assert . That ( exception ) . IsTypeOf < TimeoutException > ( ) ;
145- await Assert . That ( exception ! . Message ) . IsEqualTo ( OutputTimeoutMessage ) ;
205+ await Assert . That ( exception ) . IsTypeOf < InvalidOperationException > ( ) ;
206+ await Assert . That ( exception ! . Message ) . IsEqualTo ( OutputCleanupFailedMessage ) ;
146207 await Assert . That ( stopwatch . Elapsed ) . IsLessThan ( ProcessTimeout ) ;
208+ await Assert . That ( activeReaders ) . IsEqualTo ( 2 ) ;
209+ await Assert . That ( BoundedCliProcessProbe . PendingReaderCleanupCount ) . IsEqualTo ( 1 ) ;
210+
211+ var blockedChildPath = Path . Combine ( sandbox , $ "blocked-{ attempt } .pid") ;
212+ var blockedCommand = string . Concat ( DetachedChildCommandPrefix , blockedChildPath ,
213+ DetachedChildCommandSuffix ) ;
214+ var blockedException = await Assert . That ( ( ) => BoundedCliProcessProbe . Run (
215+ ShellExecutableUnix ,
216+ [ ShellArgumentUnix , blockedCommand ] ,
217+ environment : null ,
218+ inheritEnvironmentVariables : true ,
219+ timeout : ProbeTimeout ,
220+ maximumOutputCharacters : 64 ) ) . ThrowsException ( ) ;
221+
222+ await Assert . That ( blockedException ) . IsTypeOf < InvalidOperationException > ( ) ;
223+ await Assert . That ( blockedException ! . Message ) . IsEqualTo ( ProbeBlockedMessage ) ;
224+ await Assert . That ( File . Exists ( blockedChildPath ) ) . IsFalse ( ) ;
225+ StopChildProcess ( childProcessIdPath ) ;
226+ await readersDrained . Task . WaitAsync ( ProcessTimeout ) ;
147227 await Assert . That ( activeReaders ) . IsEqualTo ( 0 ) ;
228+ await Assert . That ( BoundedCliProcessProbe . PendingReaderCleanupCount ) . IsEqualTo ( 0 ) ;
148229 }
149230 }
150231 finally
151232 {
152- if ( File . Exists ( childProcessIdPath ) )
233+ foreach ( var childProcessIdPath in Directory . EnumerateFiles ( sandbox , ChildProcessIdFilePattern ) )
153234 {
154- var processIdText = File . ReadAllText ( childProcessIdPath ) ;
155- if ( int . TryParse ( processIdText , out var processId ) )
156- {
157- using var child = Process . GetProcessById ( processId ) ;
158- if ( ! child . HasExited )
159- {
160- child . Kill ( entireProcessTree : true ) ;
161- }
162-
163- child . WaitForExit ( 1000 ) ;
164- }
235+ StopChildProcess ( childProcessIdPath ) ;
165236 }
166237
167238 Directory . Delete ( sandbox , recursive : true ) ;
168239 }
169240 }
170241
242+ private static void StopChildProcess ( string childProcessIdPath )
243+ {
244+ if ( ! File . Exists ( childProcessIdPath ) )
245+ {
246+ return ;
247+ }
248+
249+ var processIdText = File . ReadAllText ( childProcessIdPath ) ;
250+ if ( ! int . TryParse ( processIdText , out var processId ) )
251+ {
252+ return ;
253+ }
254+
255+ using var child = Process . GetProcessById ( processId ) ;
256+ if ( ! child . HasExited )
257+ {
258+ child . Kill ( entireProcessTree : true ) ;
259+ }
260+
261+ child . WaitForExit ( 1000 ) ;
262+ }
263+
171264 private static CliProcessProbeResult Run (
172265 string unixCommand ,
173266 string windowsCommand ,
174267 IReadOnlyDictionary < string , string > ? environment ,
175268 bool inheritEnvironmentVariables ,
176269 TimeSpan ? timeout = null ,
177- int maximumOutputCharacters = 200000 )
270+ int maximumOutputCharacters = 200000 ,
271+ Action < int > ? readerCountChanged = null )
178272 {
179273 var executablePath = OperatingSystem . IsWindows ( )
180274 ? Path . Combine ( Environment . SystemDirectory , ShellExecutableWindows )
@@ -185,6 +279,7 @@ private static CliProcessProbeResult Run(
185279 OperatingSystem . IsWindows ( ) ? windowsCommand : unixCommand ,
186280 } ;
187281 return BoundedCliProcessProbe . Run ( executablePath , arguments , environment , inheritEnvironmentVariables ,
188- timeout ?? ProcessTimeout , maximumOutputCharacters ) ;
282+ timeout ?? ProcessTimeout , maximumOutputCharacters , readerCountChanged ,
283+ leaseAcquisitionTimeout : ProcessTimeout ) ;
189284 }
190285}
0 commit comments