@@ -15,22 +15,31 @@ DEFAULT_TIMEOUT = 30
15
15
# TODO: print unmatched patterns
16
16
17
17
18
- async def terminate (p ):
19
- # Terminate the process group (shell, crowdsec plugins)
18
+ async def terminate_group (p : asyncio .subprocess .Process ):
19
+ """
20
+ Terminate the process group (shell, crowdsec plugins)
21
+ """
20
22
try :
21
23
os .killpg (os .getpgid (p .pid ), signal .SIGTERM )
22
24
except ProcessLookupError :
23
25
pass
24
26
25
27
26
- async def monitor (cmd , args , want_out , want_err , timeout ):
27
- """Monitor a process and terminate it if a pattern is matched in stdout or stderr.
28
+ async def monitor (
29
+ cmd : str ,
30
+ args : list [str ],
31
+ out_regex : re .Pattern [str ] | None ,
32
+ err_regex : re .Pattern [str ] | None ,
33
+ timeout : float
34
+ ) -> int :
35
+ """
36
+ Run a subprocess, monitor its stdout/stderr for matches, and handle timeouts or pattern hits.
28
37
29
38
Args:
30
39
cmd: The command to run.
31
40
args: A list of arguments to pass to the command.
32
- stdout : A regular expression pattern to search for in stdout.
33
- stderr : A regular expression pattern to search for in stderr.
41
+ out_regex : A compiled regular expression to search for in stdout.
42
+ err_regex : A compiled regular expression to search for in stderr.
34
43
timeout: The maximum number of seconds to wait for the process to terminate.
35
44
36
45
Returns:
@@ -39,17 +48,18 @@ async def monitor(cmd, args, want_out, want_err, timeout):
39
48
40
49
status = None
41
50
42
- async def read_stream (stream , outstream , pattern ):
51
+ async def read_stream (stream : asyncio . StreamReader | None , out , pattern : re . Pattern [ str ] | None ):
43
52
nonlocal status
44
53
if stream is None :
45
54
return
55
+
46
56
while True :
47
57
line = await stream .readline ()
48
58
if line :
49
59
line = line .decode ('utf-8' )
50
- outstream .write (line )
60
+ out .write (line )
51
61
if pattern and pattern .search (line ):
52
- await terminate (process )
62
+ await terminate_group (process )
53
63
# this is nasty.
54
64
# if we timeout, we want to return a different exit code
55
65
# in case of a match, so that the caller can tell
@@ -76,9 +86,6 @@ async def monitor(cmd, args, want_out, want_err, timeout):
76
86
# (required to kill child processes when cmd is a shell)
77
87
preexec_fn = os .setsid )
78
88
79
- out_regex = re .compile (want_out ) if want_out else None
80
- err_regex = re .compile (want_err ) if want_err else None
81
-
82
89
# Apply a timeout
83
90
try :
84
91
await asyncio .wait_for (
@@ -90,27 +97,38 @@ async def monitor(cmd, args, want_out, want_err, timeout):
90
97
if status is None :
91
98
status = process .returncode
92
99
except asyncio .TimeoutError :
93
- await terminate (process )
100
+ await terminate_group (process )
94
101
status = 241
95
102
96
103
# Return the same exit code, stdout and stderr as the spawned process
97
- return status
104
+ return status or 0
105
+
106
+
107
+ class Args (argparse .Namespace ):
108
+ cmd : str = ''
109
+ args : list [str ] = []
110
+ out : str = ''
111
+ err : str = ''
112
+ timeout : float = DEFAULT_TIMEOUT
98
113
99
114
100
115
async def main ():
101
116
parser = argparse .ArgumentParser (
102
117
description = 'Monitor a process and terminate it if a pattern is matched in stdout or stderr.' )
103
- parser .add_argument ('cmd' , help = 'The command to run.' )
104
- parser .add_argument ('args' , nargs = argparse .REMAINDER , help = 'A list of arguments to pass to the command.' )
105
- parser .add_argument ('--out' , default = '' , help = 'A regular expression pattern to search for in stdout.' )
106
- parser .add_argument ('--err' , default = '' , help = 'A regular expression pattern to search for in stderr.' )
107
- parser .add_argument ('--timeout' , type = float , default = DEFAULT_TIMEOUT )
108
- args = parser .parse_args ()
118
+ _ = parser .add_argument ('cmd' , help = 'The command to run.' )
119
+ _ = parser .add_argument ('args' , nargs = argparse .REMAINDER , help = 'A list of arguments to pass to the command.' )
120
+ _ = parser .add_argument ('--out' , help = 'A regular expression pattern to search for in stdout.' )
121
+ _ = parser .add_argument ('--err' , help = 'A regular expression pattern to search for in stderr.' )
122
+ _ = parser .add_argument ('--timeout' , type = float , default = DEFAULT_TIMEOUT )
123
+ args : Args = parser .parse_args (namespace = Args ())
124
+
125
+ out_regex = re .compile (args .out ) if args .out else None
126
+ err_regex = re .compile (args .err ) if args .err else None
109
127
110
- exit_code = await monitor (args .cmd , args .args , args . out , args . err , args .timeout )
128
+ exit_code = await monitor (args .cmd , args .args , out_regex , err_regex , args .timeout )
111
129
112
- sys . exit ( exit_code )
130
+ return exit_code
113
131
114
132
115
133
if __name__ == '__main__' :
116
- asyncio .run (main ())
134
+ sys . exit ( asyncio .run (main () ))
0 commit comments