@@ -51,7 +51,7 @@ public static class CommandLineBuilderExtensions
5151 /// </param>
5252 /// <returns>The same instance of <see cref="CommandLineBuilder"/>.</returns>
5353 public static CommandLineBuilder CancelOnProcessTermination (
54- this CommandLineBuilder builder ,
54+ this CommandLineBuilder builder ,
5555 TimeSpan ? timeout = null )
5656 {
5757 // https://tldp.org/LDP/abs/html/exitcodes.html - 130 - script terminated by ctrl-c
@@ -66,70 +66,63 @@ public static CommandLineBuilder CancelOnProcessTermination(
6666 {
6767 ConsoleCancelEventHandler ? consoleHandler = null ;
6868 EventHandler ? processExitHandler = null ;
69- ManualResetEventSlim ? blockProcessExit = null ;
70- CancellationTokenSource ? cts = null ;
69+ ManualResetEventSlim blockProcessExit = new ( initialState : false ) ;
7170
72- context . AddLinkedCancellationToken ( ( ) =>
71+ processExitHandler = ( _ , _ ) =>
7372 {
74- cts = new CancellationTokenSource ( ) ;
75- blockProcessExit = new ManualResetEventSlim ( initialState : false ) ;
76- processExitHandler = ( _ , _ ) =>
73+ // Cancel asynchronously not to block the handler (as then the process might possibly run longer then what was the requested timeout)
74+ Task timeoutTask = Task . Delay ( timeout . Value ) ;
75+ Task cancelTask = Task . Factory . StartNew ( context . Cancel ) ;
76+
77+ // The process exits as soon as the event handler returns.
78+ // We provide a return value using Environment.ExitCode
79+ // because Main will not finish executing.
80+ // Wait for the invocation to finish.
81+ if ( ! blockProcessExit . Wait ( timeout > TimeSpan . Zero
82+ ? timeout . Value
83+ : Timeout . InfiniteTimeSpan ) )
7784 {
78- // Cancel asynchronously not to block the handler (as then the process might possibly run longer then what was the requested timeout)
79- Task timeoutTask = Task . Delay ( timeout . Value ) ;
80- Task cancelTask = Task . Factory . StartNew ( cts . Cancel ) ;
81-
82- // The process exits as soon as the event handler returns.
83- // We provide a return value using Environment.ExitCode
84- // because Main will not finish executing.
85- // Wait for the invocation to finish.
86- if ( ! blockProcessExit . Wait ( timeout > TimeSpan . Zero
87- ? timeout . Value
88- : Timeout . InfiniteTimeSpan ) )
89- {
90- context . ExitCode = SIGINT_EXIT_CODE ;
91- }
92- // Let's block here (to prevent process bailing out) for the rest of the timeout (if any), for cancellation to finish (if it hasn't yet)
93- else if ( Task . WaitAny ( timeoutTask , cancelTask ) == 0 )
94- {
95- // The async cancellation didn't finish in timely manner
96- context . ExitCode = SIGINT_EXIT_CODE ;
97- }
98- ExitCode = context . ExitCode ;
99- } ;
100- // Default limit for ProcesExit handler is 2 seconds
101- // https://docs.microsoft.com/en-us/dotnet/api/system.appdomain.processexit?view=net-6.0
102- consoleHandler = ( _ , args ) =>
85+ context . ExitCode = SIGINT_EXIT_CODE ;
86+ }
87+ // Let's block here (to prevent process bailing out) for the rest of the timeout (if any), for cancellation to finish (if it hasn't yet)
88+ else if ( Task . WaitAny ( timeoutTask , cancelTask ) == 0 )
10389 {
104- // Stop the process from terminating.
105- // Since the context was cancelled, the invocation should
106- // finish and Main will return.
107- args . Cancel = true ;
108-
109- // If timeout was requested - make sure cancellation processing (or any other activity within the current process)
110- // doesn't keep the process running after the timeout
111- if ( timeout ! > TimeSpan . Zero )
112- {
113- Task
114- . Delay ( timeout . Value , default )
115- . ContinueWith ( t =>
116- {
117- // Prevent our ProcessExit from intervene and block the exit
118- AppDomain . CurrentDomain . ProcessExit -= processExitHandler ;
119- Environment . Exit ( SIGINT_EXIT_CODE ) ;
120- } , ( CancellationToken ) default ) ;
121- }
90+ // The async cancellation didn't finish in timely manner
91+ context . ExitCode = SIGINT_EXIT_CODE ;
92+ }
93+ ExitCode = context . ExitCode ;
94+ } ;
95+ // Default limit for ProcesExit handler is 2 seconds
96+ // https://docs.microsoft.com/en-us/dotnet/api/system.appdomain.processexit?view=net-6.0
97+ consoleHandler = ( _ , args ) =>
98+ {
99+ // Stop the process from terminating.
100+ // Since the context was cancelled, the invocation should
101+ // finish and Main will return.
102+ args . Cancel = true ;
103+
104+ // If timeout was requested - make sure cancellation processing (or any other activity within the current process)
105+ // doesn't keep the process running after the timeout
106+ if ( timeout ! > TimeSpan . Zero )
107+ {
108+ Task
109+ . Delay ( timeout . Value , default )
110+ . ContinueWith ( t =>
111+ {
112+ // Prevent our ProcessExit from intervene and block the exit
113+ AppDomain . CurrentDomain . ProcessExit -= processExitHandler ;
114+ Environment . Exit ( SIGINT_EXIT_CODE ) ;
115+ } , ( CancellationToken ) default ) ;
116+ }
122117
123- // Cancel synchronously here - no need to perform it asynchronously as the timeout is already running (and would kill the process if needed),
124- // plus we cannot wait only on the cancellation (e.g. via `Task.Factory.StartNew(cts.Cancel).Wait(cancelationProcessingTimeout.Value)`)
125- // as we need to abort any other possible execution within the process - even outside the context of cancellation processing
126- cts ? . Cancel ( ) ;
127- } ;
128- Console . CancelKeyPress += consoleHandler ;
129- AppDomain . CurrentDomain . ProcessExit += processExitHandler ;
118+ // Cancel synchronously here - no need to perform it asynchronously as the timeout is already running (and would kill the process if needed),
119+ // plus we cannot wait only on the cancellation (e.g. via `Task.Factory.StartNew(cts.Cancel).Wait(cancelationProcessingTimeout.Value)`)
120+ // as we need to abort any other possible execution within the process - even outside the context of cancellation processing
121+ context . Cancel ( ) ;
122+ } ;
130123
131- return cts . Token ;
132- } ) ;
124+ Console . CancelKeyPress += consoleHandler ;
125+ AppDomain . CurrentDomain . ProcessExit += processExitHandler ;
133126
134127 try
135128 {
@@ -139,14 +132,13 @@ public static CommandLineBuilder CancelOnProcessTermination(
139132 {
140133 Console . CancelKeyPress -= consoleHandler ;
141134 AppDomain . CurrentDomain . ProcessExit -= processExitHandler ;
142- Interlocked . Exchange ( ref cts , null ) ? . Dispose ( ) ;
143135 blockProcessExit ? . Set ( ) ;
144136 }
145137 } , MiddlewareOrderInternal . Startup ) ;
146138
147139 return builder ;
148140 }
149-
141+
150142 /// <summary>
151143 /// Enables the parser to recognize command line directives.
152144 /// </summary>
@@ -205,7 +197,7 @@ public static CommandLineBuilder EnablePosixBundling(
205197 builder . EnablePosixBundling = value ;
206198 return builder ;
207199 }
208-
200+
209201 /// <summary>
210202 /// Ensures that the application is registered with the <c>dotnet-suggest</c> tool to enable command line completions.
211203 /// </summary>
@@ -484,7 +476,7 @@ public static CommandLineBuilder AddMiddleware(
484476
485477 return builder ;
486478 }
487-
479+
488480 /// <summary>
489481 /// Adds a middleware delegate to the invocation pipeline called before a command handler is invoked.
490482 /// </summary>
0 commit comments