1+ import os
2+ import signal
3+ import fcntl
4+ import time
5+ import subprocess
6+ from typing import List
7+
8+ MAX_BYTES_PER_READ = 1024
9+ SLEEP_BETWEEN_READS = 0.1
10+
11+
12+ class Result :
13+ timeout : int
14+ exit_code : int
15+ stdout : str
16+ stderr : str
17+
18+ def __init__ (self , timeout , exit_code , stdout , stderr ):
19+ self .timeout = timeout
20+ self .exit_code = exit_code
21+ self .stdout = stdout
22+ self .stderr = stderr
23+
24+
25+ def set_nonblocking (reader ):
26+ fd = reader .fileno ()
27+ fl = fcntl .fcntl (fd , fcntl .F_GETFL )
28+ fcntl .fcntl (fd , fcntl .F_SETFL , fl | os .O_NONBLOCK )
29+
30+
31+ def run (
32+ args : List [str ],
33+ timeout_seconds : int = 15 ,
34+ max_output_size : int = 2048 ,
35+ env = None ,
36+ ) -> Result :
37+ """
38+ Runs the given program with arguments. After the timeout elapses, kills the process
39+ and all other processes in the process group. Captures at most max_output_size bytes
40+ of stdout and stderr each, and discards any output beyond that.
41+ """
42+ p = subprocess .Popen (
43+ args ,
44+ env = env ,
45+ stdin = subprocess .DEVNULL ,
46+ stdout = subprocess .PIPE ,
47+ stderr = subprocess .PIPE ,
48+ start_new_session = True ,
49+ bufsize = MAX_BYTES_PER_READ ,
50+ )
51+ set_nonblocking (p .stdout )
52+ set_nonblocking (p .stderr )
53+
54+ process_group_id = os .getpgid (p .pid )
55+
56+ # We sleep for 0.1 seconds in each iteration.
57+ max_iterations = timeout_seconds * 10
58+ stdout_saved_bytes = []
59+ stderr_saved_bytes = []
60+ stdout_bytes_read = 0
61+ stderr_bytes_read = 0
62+
63+ for _ in range (max_iterations ):
64+ this_stdout_read = p .stdout .read (MAX_BYTES_PER_READ )
65+ this_stderr_read = p .stderr .read (MAX_BYTES_PER_READ )
66+ # this_stdout_read and this_stderr_read may be None if stdout or stderr
67+ # are closed. Without these checks, test_close_output fails.
68+ if this_stdout_read is not None and stdout_bytes_read < max_output_size :
69+ stdout_saved_bytes .append (this_stdout_read )
70+ stdout_bytes_read += len (this_stdout_read )
71+ if this_stderr_read is not None and stderr_bytes_read < max_output_size :
72+ stderr_saved_bytes .append (this_stderr_read )
73+ stderr_bytes_read += len (this_stderr_read )
74+
75+ exit_code = p .poll ()
76+ if exit_code is not None :
77+ # finish reading output
78+ this_stdout_read = p .stdout .read (max_output_size - stdout_bytes_read )
79+ this_stderr_read = p .stderr .read (max_output_size - stderr_bytes_read )
80+ if this_stdout_read is not None :
81+ stdout_saved_bytes .append (this_stdout_read )
82+ if this_stderr_read is not None :
83+ stderr_saved_bytes .append (this_stderr_read )
84+ break
85+
86+ time .sleep (SLEEP_BETWEEN_READS )
87+
88+ try :
89+ # Kills the process group. Without this line, test_fork_once fails.
90+ os .killpg (process_group_id , signal .SIGKILL )
91+ except ProcessLookupError :
92+ pass
93+
94+ timeout = exit_code is None
95+ exit_code = exit_code if exit_code is not None else - 1
96+ stdout = b"" .join (stdout_saved_bytes ).decode ("utf-8" , errors = "ignore" )
97+ stderr = b"" .join (stderr_saved_bytes ).decode ("utf-8" , errors = "ignore" )
98+ return Result (timeout = timeout , exit_code = exit_code , stdout = stdout , stderr = stderr )
0 commit comments