4
4
import os
5
5
import os .path
6
6
import posixpath
7
+ import signal
7
8
import subprocess
8
9
import sys
9
10
import time
@@ -97,6 +98,47 @@ def noop(_: str):
97
98
pass
98
99
99
100
101
+ class TerminationSignal (RuntimeError ): # noqa: N818
102
+ def __init__ (self , signal ):
103
+ self .signal = signal
104
+ super ().__init__ ("Received termination signal" , signal )
105
+
106
+ def __repr__ (self ):
107
+ return f"{ self .__class__ .__name__ } ({ self .signal } )"
108
+
109
+
110
+ if sys .platform == "win32" :
111
+ SIGINT = signal .CTRL_C_EVENT
112
+ else :
113
+ SIGINT = signal .SIGINT
114
+
115
+
116
+ def shutdown_process (
117
+ proc : subprocess .Popen ,
118
+ interrupt_timeout : Optional [int ] = None ,
119
+ terminate_timeout : Optional [int ] = None ,
120
+ ) -> int :
121
+ """Shut down the process gracefully with SIGINT -> SIGTERM -> SIGKILL."""
122
+
123
+ logger .info ("sending interrupt signal to the process %s" , proc .pid )
124
+ proc .send_signal (SIGINT )
125
+
126
+ logger .info ("waiting for the process %s to finish" , proc .pid )
127
+ try :
128
+ return proc .wait (interrupt_timeout )
129
+ except subprocess .TimeoutExpired :
130
+ logger .info (
131
+ "timed out waiting, sending terminate signal to the process %s" , proc .pid
132
+ )
133
+ proc .terminate ()
134
+ try :
135
+ return proc .wait (terminate_timeout )
136
+ except subprocess .TimeoutExpired :
137
+ logger .info ("timed out waiting, killing the process %s" , proc .pid )
138
+ proc .kill ()
139
+ return proc .wait ()
140
+
141
+
100
142
def _process_stream (stream : "IO[bytes]" , callback : Callable [[str ], None ]) -> None :
101
143
buffer = b""
102
144
while byt := stream .read (1 ): # Read one byte at a time
@@ -1493,6 +1535,8 @@ def query(
1493
1535
output_hook : Callable [[str ], None ] = noop ,
1494
1536
params : Optional [dict [str , str ]] = None ,
1495
1537
job_id : Optional [str ] = None ,
1538
+ interrupt_timeout : Optional [int ] = None ,
1539
+ terminate_timeout : Optional [int ] = None ,
1496
1540
) -> None :
1497
1541
cmd = [python_executable , "-c" , query_script ]
1498
1542
env = dict (env or os .environ )
@@ -1506,13 +1550,48 @@ def query(
1506
1550
if capture_output :
1507
1551
popen_kwargs = {"stdout" : subprocess .PIPE , "stderr" : subprocess .STDOUT }
1508
1552
1553
+ def raise_termination_signal (sig : int , _ : Any ) -> NoReturn :
1554
+ raise TerminationSignal (sig )
1555
+
1556
+ thread : Optional [Thread ] = None
1509
1557
with subprocess .Popen (cmd , env = env , ** popen_kwargs ) as proc : # noqa: S603
1510
- if capture_output :
1511
- args = (proc .stdout , output_hook )
1512
- thread = Thread (target = _process_stream , args = args , daemon = True )
1513
- thread .start ()
1514
- thread .join () # wait for the reader thread
1558
+ logger .info ("Starting process %s" , proc .pid )
1559
+
1560
+ orig_sigint_handler = signal .getsignal (signal .SIGINT )
1561
+ # ignore SIGINT in the main process.
1562
+ # In the terminal, SIGINTs are received by all the processes in
1563
+ # the foreground process group, so the script will receive the signal too.
1564
+ # (If we forward the signal to the child, it will receive it twice.)
1565
+ signal .signal (signal .SIGINT , signal .SIG_IGN )
1515
1566
1567
+ orig_sigterm_handler = signal .getsignal (signal .SIGTERM )
1568
+ signal .signal (signal .SIGTERM , raise_termination_signal )
1569
+ try :
1570
+ if capture_output :
1571
+ args = (proc .stdout , output_hook )
1572
+ thread = Thread (target = _process_stream , args = args , daemon = True )
1573
+ thread .start ()
1574
+
1575
+ proc .wait ()
1576
+ except TerminationSignal as exc :
1577
+ signal .signal (signal .SIGTERM , orig_sigterm_handler )
1578
+ signal .signal (signal .SIGINT , orig_sigint_handler )
1579
+ logging .info ("Shutting down process %s, received %r" , proc .pid , exc )
1580
+ # Rather than forwarding the signal to the child, we try to shut it down
1581
+ # gracefully. This is because we consider the script to be interactive
1582
+ # and special, so we give it time to cleanup before exiting.
1583
+ shutdown_process (proc , interrupt_timeout , terminate_timeout )
1584
+ if proc .returncode :
1585
+ raise QueryScriptCancelError (
1586
+ "Query script was canceled by user" , return_code = proc .returncode
1587
+ ) from exc
1588
+ finally :
1589
+ signal .signal (signal .SIGTERM , orig_sigterm_handler )
1590
+ signal .signal (signal .SIGINT , orig_sigint_handler )
1591
+ if thread :
1592
+ thread .join () # wait for the reader thread
1593
+
1594
+ logging .info ("Process %s exited with return code %s" , proc .pid , proc .returncode )
1516
1595
if proc .returncode == QUERY_SCRIPT_CANCELED_EXIT_CODE :
1517
1596
raise QueryScriptCancelError (
1518
1597
"Query script was canceled by user" ,
0 commit comments