22// The .NET Foundation licenses this file to you under the MIT license.
33
44using System ;
5+ using System . IO ;
6+ using System . IO . Pipes ;
7+ using System . Reflection ;
58using System . Runtime . InteropServices ;
69using System . Threading ;
710using System . Threading . Tasks ;
@@ -18,14 +21,22 @@ public class ConsoleLifetimeExitTests
1821 /// and the rest of "main" gets executed.
1922 /// </summary>
2023 [ ConditionalTheory ( typeof ( RemoteExecutor ) , nameof ( RemoteExecutor . IsSupported ) ) ]
21- [ PlatformSpecific ( TestPlatforms . AnyUnix ) ]
2224 [ InlineData ( SIGTERM ) ]
2325 [ InlineData ( SIGINT ) ]
2426 [ InlineData ( SIGQUIT ) ]
2527 public async Task EnsureSignalContinuesMainMethod ( int signal )
2628 {
27- using var remoteHandle = RemoteExecutor . Invoke ( async ( ) =>
29+ // simulate signals on Windows by using a pipe to communicate with the remote process
30+ using var messagePipe = new AnonymousPipeServerStream ( PipeDirection . Out , HandleInheritability . Inheritable ) ;
31+
32+ using var remoteHandle = RemoteExecutor . Invoke ( async ( pipeHandleAsString ) =>
2833 {
34+ if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
35+ {
36+ // kick off a thread to simulate the signal on Windows
37+ _ = Task . Run ( ( ) => SimulatePosixSignalWindows ( pipeHandleAsString ) ) ;
38+ }
39+
2940 await Host . CreateDefaultBuilder ( )
3041 . ConfigureServices ( ( hostContext , services ) =>
3142 {
@@ -40,7 +51,7 @@ await Host.CreateDefaultBuilder()
4051
4152 Console . WriteLine ( "Run has completed" ) ;
4253 return 123 ;
43- } , new RemoteInvokeOptions ( ) { Start = false , ExpectedExitCode = 123 } ) ;
54+ } , messagePipe . GetClientHandleAsString ( ) , new RemoteInvokeOptions ( ) { Start = false , ExpectedExitCode = 123 } ) ;
4455
4556 remoteHandle . Process . StartInfo . RedirectStandardOutput = true ;
4657 remoteHandle . Process . Start ( ) ;
@@ -53,7 +64,16 @@ await Host.CreateDefaultBuilder()
5364 }
5465
5566 // send the signal to the process
56- kill ( remoteHandle . Process . Id , signal ) ;
67+ if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
68+ {
69+ // on Windows, we use the pipe to signal the process
70+ messagePipe . WriteByte ( ( byte ) signal ) ;
71+ }
72+ else
73+ {
74+ // on Unix, we send the signal directly
75+ kill ( remoteHandle . Process . Id , signal ) ;
76+ }
5777
5878 remoteHandle . Process . WaitForExit ( ) ;
5979
@@ -69,6 +89,69 @@ await Host.CreateDefaultBuilder()
6989 [ DllImport ( "libc" , SetLastError = true ) ]
7090 private static extern int kill ( int pid , int sig ) ;
7191
92+
93+ private const int CTRL_C_EVENT = 0 ;
94+ private const int CTRL_BREAK_EVENT = 1 ;
95+ private const int CTRL_CLOSE_EVENT = 2 ;
96+ private const int CTRL_LOGOFF_EVENT = 5 ;
97+ private const int CTRL_SHUTDOWN_EVENT = 6 ;
98+
99+ private static unsafe void SimulatePosixSignalWindows ( string pipeHandleAsString )
100+ {
101+ try
102+ {
103+ using var readPipe = new AnonymousPipeClientStream ( PipeDirection . In , pipeHandleAsString ) ;
104+
105+ int signal = ( int ) readPipe . ReadByte ( ) ;
106+
107+ int ctrlType = ( int ) signal switch
108+ {
109+ SIGINT => CTRL_C_EVENT ,
110+ SIGQUIT => CTRL_BREAK_EVENT ,
111+ SIGTERM => CTRL_SHUTDOWN_EVENT ,
112+ _ => throw new ArgumentOutOfRangeException ( nameof ( signal ) , "Unsupported signal" )
113+ } ;
114+
115+ #if NETFRAMEWORK
116+ if ( ctrlType == CTRL_C_EVENT || ctrlType == CTRL_BREAK_EVENT )
117+ {
118+ var handlerMethod = Type . GetType ( "System.Console, mscorlib" ) ? . GetMethod ( "BreakEvent" , BindingFlags . NonPublic | BindingFlags . Static ) ;
119+ Assert . NotNull ( handlerMethod ) ;
120+ handlerMethod . Invoke ( null , [ ctrlType ] ) ;
121+ }
122+ else // CTRL_SHUTDOWN_EVENT
123+ {
124+ var handlerField = typeof ( AppDomain ) . GetField ( "_processExit" , BindingFlags . NonPublic | BindingFlags . Instance ) ;
125+ Assert . NotNull ( handlerField ) ;
126+ EventHandler handler = ( EventHandler ) handlerField . GetValue ( AppDomain . CurrentDomain ) ;
127+ Assert . NotNull ( handler ) ;
128+ handler . Invoke ( AppDomain . CurrentDomain , null ) ;
129+ }
130+ #else
131+ // get the System.Runtime.InteropServices.PosixSignalRegistration.HandlerRoutine private method
132+ var handlerMethod = typeof ( PosixSignalRegistration ) . GetMethod ( "HandlerRoutine" , BindingFlags . NonPublic | BindingFlags . Static ) ;
133+ Assert . NotNull ( handlerMethod ) ;
134+
135+ var handlerPtr = handlerMethod . MethodHandle . GetFunctionPointer ( ) ;
136+ delegate * unmanaged< int , int > handler = ( delegate * unmanaged< int , int > ) handlerPtr ;
137+
138+ handler ( ctrlType ) ;
139+
140+ if ( signal == SIGTERM )
141+ {
142+ // on Windows the OS will kill the process immediately after this
143+ Environment . FailFast ( "Simulating shutdown" ) ;
144+ }
145+ #endif
146+ }
147+ catch ( Exception ex )
148+ {
149+ // Exceptions on this thread will not be observed, nor will they cause the process to exit.
150+ // Use failfast to ensure the process will exit without running any handlers.
151+ Environment . FailFast ( ex . ToString ( ) ) ;
152+ }
153+ }
154+
72155 private class EnsureSignalContinuesMainMethodWorker : BackgroundService
73156 {
74157 protected override async Task ExecuteAsync ( CancellationToken stoppingToken )
0 commit comments