5353import os
5454from enum import Enum
5555
56+ if os .name == 'nt' :
57+ import ctypes
58+
5659from xml .dom import minidom
5760
5861class Command_test (Enum ):
@@ -86,17 +89,27 @@ def signal_handler(signum, frame):
8689def send_command (command ):
8790 print ("Executing command: " + str (command ))
8891
89- # this subprocess cannot be executed in shell=True or using bash
92+ creationflags = 0
93+ if os .name == 'nt' :
94+ # Give the child its own console so we can deliver CTRL_C_EVENT to
95+ # it without also affecting the launcher (PowerShell). We cannot use
96+ # Start-Process -WindowStyle Hidden on Windows containers, and
97+ # CREATE_NEW_PROCESS_GROUP + CTRL_BREAK_EVENT does not work either
98+ # because the server only installs a SIGINT handler, not SIGBREAK.
99+ creationflags = subprocess .CREATE_NEW_CONSOLE
100+
101+ # This subprocess cannot be executed in shell=True or using bash
90102 # because a background script will not broadcast the signals
91103 # it receives
92104 proc = subprocess .Popen (command ,
93105 stdout = subprocess .PIPE ,
94106 stderr = subprocess .PIPE ,
95- universal_newlines = True
107+ universal_newlines = True ,
108+ creationflags = creationflags ,
96109 )
97110
98- # sleep to let the server run
99- time .sleep (1 )
111+ # Sleep to let the server run
112+ time .sleep (3 )
100113
101114 # 1. An exit code of 0 means everything was alright
102115 # 2. An exit code of 1 means the tool's process terminated before even
@@ -106,17 +119,44 @@ def send_command(command):
106119 # output was different than expected
107120 exit_code = 0
108121
109- # direct this script to ignore SIGINT
122+ # If the process already exited due to failure (e.g. bad arguments, missing XML),
123+ # skip signalling entirely and collect the output.
124+ if proc .poll () is not None :
125+ output , err = proc .communicate ()
126+ return output , err , exit_code
127+
128+ # Direct this script to ignore SIGINT
110129 signal .signal (signal .SIGINT , signal_handler )
111130
112- # send SIGINT to process and wait for processing
131+ # On Windows, detach from the launcher's console and attach to the child's brand-new
132+ # console. From that attached state, GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0)
133+ # broadcasts CTRL+C to every process in the attached console. This reaches the server's
134+ # SIGINT handler and triggers the clean-shutdown path (which prints "### Server shut down ###")
135+ # without leaking the signal to PowerShell.
136+ kernel32 = None
137+ if os .name == 'nt' :
138+ kernel32 = ctypes .windll .kernel32
139+ ATTACH_PARENT_PROCESS = - 1
140+ CTRL_C_EVENT = 0
141+ kernel32 .FreeConsole ()
142+ if not kernel32 .AttachConsole (proc .pid ):
143+ # Restore our console and bail out hard if the attached failed
144+ kernel32 .AttachConsole (ATTACH_PARENT_PROCESS )
145+ proc .kill ()
146+ print ('Could not attach to child console to send CTRL_C' )
147+ sys .exit (2 )
148+ # Ignore CTRL+C in our own process so the broadcast does not terminate the Python launcher.
149+ kernel32 .SetConsoleCtrlHandler (None , True )
150+
151+ # Send signal to process and wait for processing
113152 lease = 0
114153 while True :
115154
116155 if os .name == 'posix' :
117156 proc .send_signal (signal .SIGINT )
118157 elif os .name == 'nt' :
119- proc .send_signal (signal .CTRL_C_EVENT )
158+ # pid == 0 targets all processes attached to our (the child's) console.
159+ kernel32 .GenerateConsoleCtrlEvent (0 , 0 )
120160
121161 time .sleep (1 )
122162 lease += 1
@@ -127,6 +167,12 @@ def send_command(command):
127167 else :
128168 break
129169
170+ # Restore the launcher's console attachment on Windows before returning.
171+ if os .name == 'nt' :
172+ kernel32 .FreeConsole ()
173+ kernel32 .AttachConsole (- 1 ) # ATTACH_PARENT_PROCESS
174+ kernel32 .SetConsoleCtrlHandler (None , False )
175+
130176 # Check whether SIGINT was able to terminate the process
131177 if proc .poll () is None :
132178 # SIGINT couldn't terminate the process. Kill it and exit with code 2
0 commit comments