|
27 | 27 | import posixpath |
28 | 28 | import random |
29 | 29 | import re |
| 30 | +import signal |
30 | 31 | import socket |
31 | 32 | import string |
32 | 33 | import subprocess |
@@ -2476,22 +2477,58 @@ def getConsoleWidth(default=80): |
2476 | 2477 |
|
2477 | 2478 | return width or default |
2478 | 2479 |
|
2479 | | -def shellExec(cmd): |
| 2480 | +def shellExec(cmd, timeout=None): |
2480 | 2481 | """ |
2481 | | - Executes arbitrary shell command |
| 2482 | + Executes arbitrary shell command, optionally bounded by 'timeout' seconds - killing (and |
| 2483 | + flagging) a hung child instead of blocking forever, as callers otherwise have no other |
| 2484 | + watchdog around this call (e.g. --vuln-test runs one such call per entry, unattended) |
2482 | 2485 |
|
2483 | 2486 | >>> shellExec('echo 1').strip() == '1' |
2484 | 2487 | True |
2485 | 2488 | """ |
2486 | 2489 |
|
2487 | 2490 | retVal = "" |
| 2491 | + timedOut = [] |
2488 | 2492 |
|
2489 | 2493 | try: |
2490 | | - retVal = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate()[0] or "" |
| 2494 | + popenKwargs = {"shell": True, "stdout": subprocess.PIPE, "stderr": subprocess.STDOUT} |
| 2495 | + if timeout: |
| 2496 | + # shell=True's Popen.pid is the shell, not the (possibly grandchild) command it runs - |
| 2497 | + # killing just that pid leaves the real child holding the stdout pipe open, so |
| 2498 | + # communicate() keeps blocking past the deadline; run it in its own group/session instead |
| 2499 | + # so the whole tree can be killed at once |
| 2500 | + if IS_WIN: |
| 2501 | + popenKwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP |
| 2502 | + else: |
| 2503 | + popenKwargs["preexec_fn"] = os.setsid |
| 2504 | + |
| 2505 | + process = subprocess.Popen(cmd, **popenKwargs) |
| 2506 | + |
| 2507 | + def _kill(): |
| 2508 | + timedOut.append(True) |
| 2509 | + try: |
| 2510 | + if IS_WIN: |
| 2511 | + subprocess.call(["taskkill", "/F", "/T", "/PID", str(process.pid)]) |
| 2512 | + else: |
| 2513 | + os.killpg(os.getpgid(process.pid), signal.SIGKILL) |
| 2514 | + except Exception: |
| 2515 | + pass |
| 2516 | + |
| 2517 | + timer = threading.Timer(timeout, _kill) if timeout else None |
| 2518 | + if timer: |
| 2519 | + timer.daemon = True |
| 2520 | + timer.start() |
| 2521 | + |
| 2522 | + retVal = process.communicate()[0] or "" |
| 2523 | + |
| 2524 | + if timer: |
| 2525 | + timer.cancel() |
2491 | 2526 | except Exception as ex: |
2492 | 2527 | retVal = getSafeExString(ex) |
2493 | 2528 | finally: |
2494 | 2529 | retVal = getText(retVal) |
| 2530 | + if timedOut: |
| 2531 | + retVal += "\n[shellExec] child process tree killed after exceeding %d-second timeout" % timeout |
2495 | 2532 |
|
2496 | 2533 | return retVal |
2497 | 2534 |
|
|
0 commit comments