|
2 | 2 | import os
|
3 | 3 | import subprocess
|
4 | 4 | import traceback
|
| 5 | +from datetime import datetime, timedelta |
5 | 6 | from io import StringIO
|
6 |
| -from multiprocessing import Manager, Process |
7 |
| -from multiprocessing.managers import DictProxy |
8 | 7 | from re import Pattern
|
9 | 8 | from typing import Optional, Tuple, Dict, List, Set, Callable, TypeVar, Collection, Generator
|
10 | 9 |
|
11 | 10 | BAD_USER_ENV_VARS = {'LD_PRELOAD'}
|
12 | 11 | T = TypeVar('T')
|
13 | 12 |
|
14 | 13 |
|
| 14 | +class ProcessTimedOutError(Exception): |
| 15 | + def __init__(self, pid: int): |
| 16 | + super(ProcessTimedOutError, self).__init__() |
| 17 | + self.pid = pid |
| 18 | + |
| 19 | + |
15 | 20 | def syscall(cmd: str, shell: bool = True, cwd: Optional[str] = None, custom_env: Optional[dict] = None, stdin: bool = False, return_output: bool = True) -> Tuple[int, Optional[str]]:
|
16 | 21 | params = {
|
17 | 22 | 'args': cmd.split(' ') if not shell else [cmd],
|
@@ -276,72 +281,70 @@ async def find_children(ppids: Set[int], ppid_map: Optional[Dict[int, Set[int]]]
|
276 | 281 | return children_list
|
277 | 282 |
|
278 | 283 |
|
279 |
| -def run_user_command(cmd: str, user_id: int, wait: bool, timeout: Optional[float] = None, |
280 |
| - env: Optional[dict] = None, response: Optional[DictProxy] = None, forbidden_env_vars: Optional[Set[str]] = BAD_USER_ENV_VARS): |
281 |
| - args = {"args": cmd, "shell": True, "stdin": subprocess.DEVNULL, |
282 |
| - "stdout": subprocess.PIPE if wait else subprocess.DEVNULL, |
283 |
| - "stderr": subprocess.STDOUT if wait else subprocess.DEVNULL} |
284 |
| - |
285 |
| - if env: |
| 284 | +async def run_async_process(cmd: str, user_id: Optional[int] = None, custom_env: Optional[dict] = None, |
| 285 | + forbidden_env_vars: Optional[Set[str]] = BAD_USER_ENV_VARS, |
| 286 | + wait: bool = True, timeout: Optional[float] = None, output: bool = True, |
| 287 | + exception_output: bool = True) \ |
| 288 | + -> Tuple[Optional[int], Optional[int], Optional[str]]: |
| 289 | + """ |
| 290 | + Runs a process using the async API |
| 291 | + Args: |
| 292 | + cmd: |
| 293 | + user_id: if the process should be executed in behalf of a different user |
| 294 | + custom_env: custom environment variables available for the process to be executed |
| 295 | + forbidden_env_vars: environment variables that should not be passed to the process |
| 296 | + wait: if the process should be waited |
| 297 | + timeout: in seconds |
| 298 | + output: if the process output should be read and returned |
| 299 | + exception_output: if the traceback of an unexpected raised exception should be returned as the output |
| 300 | + Returns: a tuple containing the process id, exitcode and output as a String |
| 301 | +
|
| 302 | + """ |
| 303 | + args = {"cmd": cmd, "stdin": subprocess.DEVNULL, |
| 304 | + "stdout": subprocess.PIPE if output else subprocess.DEVNULL, |
| 305 | + "stderr": subprocess.STDOUT if output else subprocess.DEVNULL} |
| 306 | + |
| 307 | + if user_id is not None: |
| 308 | + args["preexec_fn"] = lambda: os.setuid(user_id) |
| 309 | + |
| 310 | + if custom_env: |
286 | 311 | if forbidden_env_vars:
|
287 |
| - args['env'] = {k: v for k, v in env.items() if k not in forbidden_env_vars} |
| 312 | + args['env'] = {k: v for k, v in custom_env.items() if k not in forbidden_env_vars} |
288 | 313 | else:
|
289 |
| - args['env'] = env |
| 314 | + args['env'] = custom_env |
290 | 315 |
|
291 | 316 | try:
|
292 |
| - os.setpriority(os.PRIO_PROCESS, os.getpid(), 0) # always launch a command with nice 0 |
293 |
| - os.setuid(user_id) |
294 |
| - |
295 |
| - p = subprocess.Popen(**args) |
296 |
| - |
297 |
| - if response is not None: |
298 |
| - response['pid'] = p.pid |
299 |
| - |
300 |
| - if timeout is not None and timeout > 0: |
301 |
| - p.wait(timeout) |
302 |
| - elif wait: |
303 |
| - p.wait() |
304 |
| - |
305 |
| - if response is not None: |
306 |
| - response['exitcode'] = p.returncode |
| 317 | + p = await asyncio.create_subprocess_shell(**args) |
| 318 | + except Exception: |
| 319 | + return None, 1, (traceback.format_exc().replace('\n', ' ') if exception_output else None) |
307 | 320 |
|
308 |
| - string = StringIO() |
309 |
| - for output in p.stdout: |
310 |
| - decoded = output.decode() |
311 |
| - string.write(decoded) |
312 |
| - |
313 |
| - string.seek(0) |
314 |
| - response['output'] = string.read() |
315 |
| - |
316 |
| - except Exception as e: |
317 |
| - if response is not None: |
318 |
| - response['exitcode'] = 1 |
319 |
| - response['output'] = traceback.format_exc() |
320 |
| - |
321 |
| - |
322 |
| -async def run_async_user_process(cmd: str, user_id: int, user_env: Optional[dict], forbidden_env_vars: Optional[Set[str]] = BAD_USER_ENV_VARS) -> Tuple[int, Optional[str]]: |
323 |
| - res = new_user_process_response() |
| 321 | + if user_id is not None: # set default niceness in case the process is executed in behalf of another user |
| 322 | + try: |
| 323 | + os.setpriority(os.PRIO_PROCESS, p.pid, 0) # always launch a command with nice 0 |
| 324 | + except Exception: |
| 325 | + pass # do nothing in case the priority could not be changed |
324 | 326 |
|
| 327 | + should_wait = wait or (timeout and timeout > 0) |
325 | 328 | try:
|
326 |
| - p = Process(target=run_user_command, kwargs={'cmd': cmd, 'user_id': user_id, 'wait': True, 'timeout': None, 'env': user_env, |
327 |
| - 'response': res, 'forbidden_env_vars': forbidden_env_vars}) |
328 |
| - p.start() |
| 329 | + if should_wait: |
| 330 | + if timeout is None or timeout < 0: |
| 331 | + return p.pid, await p.wait(), ((await p.stdout.read()).decode() if output else None) |
| 332 | + elif timeout and timeout > 0: |
| 333 | + timeout_at = datetime.now() + timedelta(seconds=timeout) |
329 | 334 |
|
330 |
| - while p.is_alive(): |
331 |
| - await asyncio.sleep(0.001) |
| 335 | + while datetime.now() < timeout_at: |
| 336 | + if p.returncode is not None: |
| 337 | + return p.pid, p.returncode, ((await p.stdout.read()).decode() if output else None) |
332 | 338 |
|
333 |
| - return res['exitcode'], res['output'] |
334 |
| - except: |
335 |
| - error_msg = traceback.format_exc().replace('\n', ' ') |
336 |
| - return 1, error_msg |
| 339 | + await asyncio.sleep(0.0005) |
337 | 340 |
|
| 341 | + raise ProcessTimedOutError(p.pid) |
338 | 342 |
|
339 |
| -def new_user_process_response() -> DictProxy: |
340 |
| - proxy_res = Manager().dict() |
341 |
| - proxy_res['exitcode'] = None |
342 |
| - proxy_res['output'] = None |
343 |
| - proxy_res['pid'] = None |
344 |
| - return proxy_res |
| 343 | + return p.pid, p.returncode, None |
| 344 | + except ProcessTimedOutError: |
| 345 | + raise |
| 346 | + except Exception: |
| 347 | + return p.pid, 1, (traceback.format_exc().replace('\n', ' ') if exception_output else None) |
345 | 348 |
|
346 | 349 |
|
347 | 350 | async def map_processes_by_parent() -> Dict[int, Set[Tuple[int, str]]]:
|
|
0 commit comments