5252import signal
5353import os
5454
55+ if os .name == 'nt' :
56+ import ctypes
57+
5558from xml .dom import minidom
5659from xml .etree .ElementTree import XML
5760
@@ -64,16 +67,27 @@ def signal_handler(signum, frame):
6467def send_command (command ):
6568 print ("Executing command: " + str (command ))
6669
67- # this subprocess cannot be executed in shell=True or using bash
70+ creationflags = 0
71+ if os .name == 'nt' :
72+ # Give the child its own console so we can deliver CTRL_C_EVENT to
73+ # it without also affecting the launcher (PowerShell). We cannot use
74+ # Start-Process -WindowStyle Hidden on Windows containers, and
75+ # CREATE_NEW_PROCESS_GROUP + CTRL_BREAK_EVENT does not work either
76+ # because the server only installs a SIGINT handler, not SIGBREAK.
77+ creationflags = subprocess .CREATE_NEW_CONSOLE
78+
79+ # This subprocess cannot be executed in shell=True or using bash
6880 # because a background script will not broadcast the signals
6981 # it receives
7082 proc = subprocess .Popen (command ,
7183 stdout = subprocess .PIPE ,
72- universal_newlines = True
84+ stderr = subprocess .PIPE ,
85+ universal_newlines = True ,
86+ creationflags = creationflags ,
7387 )
7488
75- # sleep to let the server run
76- time .sleep (1 )
89+ # Sleep to let the server run
90+ time .sleep (3 )
7791
7892 # 1. An exit code of 0 means everything was alright
7993 # 2. An exit code of 1 means the tool's process terminated before even
@@ -83,17 +97,44 @@ def send_command(command):
8397 # output was different than expected
8498 exit_code = 0
8599
86- # direct this script to ignore SIGINT
100+ # If the process already exited due to failure (e.g. bad arguments, missing XML),
101+ # skip signalling entirely and collect the output.
102+ if proc .poll () is not None :
103+ output , err = proc .communicate ()
104+ return output , err , exit_code
105+
106+ # Direct this script to ignore SIGINT
87107 signal .signal (signal .SIGINT , signal_handler )
88108
89- # send SIGINT to process and wait for processing
109+ # On Windows, detach from the launcher's console and attach to the child's brand-new
110+ # console. From that attached state, GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0)
111+ # broadcasts CTRL+C to every process in the attached console. This reaches the server's
112+ # SIGINT handler and triggers the clean-shutdown path (which prints "### Server shut down ###")
113+ # without leaking the signal to PowerShell.
114+ kernel32 = None
115+ if os .name == 'nt' :
116+ kernel32 = ctypes .windll .kernel32
117+ ATTACH_PARENT_PROCESS = - 1
118+ CTRL_C_EVENT = 0
119+ kernel32 .FreeConsole ()
120+ if not kernel32 .AttachConsole (proc .pid ):
121+ # Restore our console and bail out hard if the attached failed
122+ kernel32 .AttachConsole (ATTACH_PARENT_PROCESS )
123+ proc .kill ()
124+ print ('Could not attach to child console to send CTRL_C' )
125+ sys .exit (2 )
126+ # Ignore CTRL+C in our own process so the broadcast does not terminate the Python launcher.
127+ kernel32 .SetConsoleCtrlHandler (None , True )
128+
129+ # Send signal to process and wait for processing
90130 lease = 0
91131 while True :
92132
93133 if os .name == 'posix' :
94134 proc .send_signal (signal .SIGINT )
95135 elif os .name == 'nt' :
96- proc .send_signal (signal .CTRL_C_EVENT )
136+ # pid == 0 targets all processes attached to our (the child's) console.
137+ kernel32 .GenerateConsoleCtrlEvent (0 , 0 )
97138
98139 time .sleep (1 )
99140 lease += 1
@@ -104,6 +145,12 @@ def send_command(command):
104145 else :
105146 break
106147
148+ # Restore the launcher's console attachment on Windows before returning.
149+ if os .name == 'nt' :
150+ kernel32 .FreeConsole ()
151+ kernel32 .AttachConsole (- 1 ) # ATTACH_PARENT_PROCESS
152+ kernel32 .SetConsoleCtrlHandler (None , False )
153+
107154 # Check whether SIGINT was able to terminate the process
108155 if proc .poll () is None :
109156 # SIGINT couldn't terminate the process. Kill it and exit with code 2
0 commit comments