5
5
import shutil
6
6
import asyncio
7
7
import tempfile
8
+ import threading
8
9
import subprocess
9
10
from typing import List , Dict , Optional , Callable , Iterator , Tuple
10
11
@@ -42,7 +43,44 @@ async def __aenter__(self) -> "SubprocessManager":
42
43
async def __aexit__ (self , exc_type , exc_value , traceback ):
43
44
self .cleanup ()
44
45
45
- async def run_command (
46
+ def run_command (
47
+ self ,
48
+ command : List [str ],
49
+ env : Optional [Dict [str , str ]] = None ,
50
+ cwd : Optional [str ] = None ,
51
+ show_output : bool = False ,
52
+ ) -> int :
53
+ """
54
+ Run a command synchronously and return its process ID.
55
+
56
+ Parameters
57
+ ----------
58
+ command : List[str]
59
+ The command to run in List form.
60
+ env : Optional[Dict[str, str]], default None
61
+ Environment variables to set for the subprocess; if not specified,
62
+ the current enviornment variables are used.
63
+ cwd : Optional[str], default None
64
+ The directory to run the subprocess in; if not specified, the current
65
+ directory is used.
66
+ show_output : bool, default False
67
+ Suppress the 'stdout' and 'stderr' to the console by default.
68
+ They can be accessed later by reading the files present in the
69
+ CommandManager object:
70
+ - command_obj.log_files["stdout"]
71
+ - command_obj.log_files["stderr"]
72
+ Returns
73
+ -------
74
+ int
75
+ The process ID of the subprocess.
76
+ """
77
+
78
+ command_obj = CommandManager (command , env , cwd )
79
+ pid = command_obj .run (show_output = show_output )
80
+ self .commands [pid ] = command_obj
81
+ return pid
82
+
83
+ async def async_run_command (
46
84
self ,
47
85
command : List [str ],
48
86
env : Optional [Dict [str , str ]] = None ,
@@ -69,7 +107,7 @@ async def run_command(
69
107
"""
70
108
71
109
command_obj = CommandManager (command , env , cwd )
72
- pid = await command_obj .run ()
110
+ pid = await command_obj .async_run ()
73
111
self .commands [pid ] = command_obj
74
112
return pid
75
113
@@ -80,7 +118,7 @@ def get(self, pid: int) -> Optional["CommandManager"]:
80
118
Parameters
81
119
----------
82
120
pid : int
83
- The process ID of the subprocess (returned by run_command).
121
+ The process ID of the subprocess (returned by run_command or async_run_command ).
84
122
85
123
Returns
86
124
-------
@@ -144,7 +182,7 @@ async def wait(
144
182
Wait for the subprocess to finish, optionally with a timeout
145
183
and optionally streaming its output.
146
184
147
- You can only call `wait` if `run ` has already been called.
185
+ You can only call `wait` if `async_run ` has already been called.
148
186
149
187
Parameters
150
188
----------
@@ -178,7 +216,79 @@ async def wait(
178
216
"within %s seconds." % (self .process .pid , command_string , timeout )
179
217
)
180
218
181
- async def run (self ):
219
+ def run (self , show_output : bool = False ):
220
+ """
221
+ Run the subprocess synchronously. This can only be called once.
222
+
223
+ This also waits on the process implicitly.
224
+
225
+ Parameters
226
+ ----------
227
+ show_output : bool, default False
228
+ Suppress the 'stdout' and 'stderr' to the console by default.
229
+ They can be accessed later by reading the files present in:
230
+ - self.log_files["stdout"]
231
+ - self.log_files["stderr"]
232
+ """
233
+
234
+ if not self .run_called :
235
+ self .temp_dir = tempfile .mkdtemp ()
236
+ stdout_logfile = os .path .join (self .temp_dir , "stdout.log" )
237
+ stderr_logfile = os .path .join (self .temp_dir , "stderr.log" )
238
+
239
+ def stream_to_stdout_and_file (pipe , log_file ):
240
+ with open (log_file , "w" ) as file :
241
+ for line in iter (pipe .readline , "" ):
242
+ if show_output :
243
+ sys .stdout .write (line )
244
+ file .write (line )
245
+ pipe .close ()
246
+
247
+ try :
248
+ self .process = subprocess .Popen (
249
+ self .command ,
250
+ cwd = self .cwd ,
251
+ env = self .env ,
252
+ stdout = subprocess .PIPE ,
253
+ stderr = subprocess .PIPE ,
254
+ bufsize = 1 ,
255
+ universal_newlines = True ,
256
+ )
257
+
258
+ self .log_files ["stdout" ] = stdout_logfile
259
+ self .log_files ["stderr" ] = stderr_logfile
260
+
261
+ self .run_called = True
262
+
263
+ stdout_thread = threading .Thread (
264
+ target = stream_to_stdout_and_file ,
265
+ args = (self .process .stdout , stdout_logfile ),
266
+ )
267
+ stderr_thread = threading .Thread (
268
+ target = stream_to_stdout_and_file ,
269
+ args = (self .process .stderr , stderr_logfile ),
270
+ )
271
+
272
+ stdout_thread .start ()
273
+ stderr_thread .start ()
274
+
275
+ self .process .wait ()
276
+
277
+ stdout_thread .join ()
278
+ stderr_thread .join ()
279
+
280
+ return self .process .pid
281
+ except Exception as e :
282
+ print ("Error starting subprocess: %s" % e )
283
+ self .cleanup ()
284
+ else :
285
+ command_string = " " .join (self .command )
286
+ print (
287
+ "Command '%s' has already been called. Please create another "
288
+ "CommandManager object." % command_string
289
+ )
290
+
291
+ async def async_run (self ):
182
292
"""
183
293
Run the subprocess asynchronously. This can only be called once.
184
294
@@ -357,7 +467,7 @@ async def main():
357
467
358
468
async with SubprocessManager () as spm :
359
469
# returns immediately
360
- pid = await spm .run_command (cmd )
470
+ pid = await spm .async_run_command (cmd )
361
471
command_obj = spm .get (pid )
362
472
363
473
print (pid )
0 commit comments