1""" 2 :codeauthor: Pedro Algarvio (pedro@algarvio.me) 3 4 5 salt.utils.vt 6 ~~~~~~~~~~~~~ 7 8 Virtual Terminal 9 10 This code has been heavily inspired by Python's subprocess code, the `non 11 blocking version of it`__, some minor online snippets about TTY handling 12 with python including `Python's own ``pty`` source code`__ and `Pexpect`__ 13 which has already surpassed some of the pitfalls that some systems would 14 get us into. 15 16 .. __: http://code.activestate.com/recipes/440554/ 17 .. __: https://github.com/python-mirror/python/blob/3.3/Lib/pty.py 18 .. __: https://github.com/pexpect/pexpect 19 20""" 21 22import errno 23import functools 24import logging 25import os 26import select 27import signal 28import subprocess 29import sys 30import time 31 32import salt.utils.crypt 33import salt.utils.data 34import salt.utils.stringutils 35from salt.log.setup import LOG_LEVELS 36 37mswindows = sys.platform == "win32" 38 39try: 40 # pylint: disable=F0401,W0611 41 from win32file import ReadFile, WriteFile 42 from win32pipe import PeekNamedPipe 43 import msvcrt 44 import win32api 45 import win32con 46 import win32process 47 48 # pylint: enable=F0401,W0611 49except ImportError: 50 import pty 51 import fcntl 52 import struct 53 import termios 54 55 56log = logging.getLogger(__name__) 57 58 59class TerminalException(Exception): 60 """ 61 Terminal specific exception 62 """ 63 64 65def setwinsize(child, rows=80, cols=80): 66 """ 67 This sets the terminal window size of the child tty. This will 68 cause a SIGWINCH signal to be sent to the child. This does not 69 change the physical window size. It changes the size reported to 70 TTY-aware applications like vi or curses -- applications that 71 respond to the SIGWINCH signal. 72 73 Thank you for the shortcut PEXPECT 74 """ 75 TIOCSWINSZ = getattr(termios, "TIOCSWINSZ", -2146929561) 76 if TIOCSWINSZ == 2148037735: 77 # Same bits, but with sign. 78 TIOCSWINSZ = -2146929561 79 # Note, assume ws_xpixel and ws_ypixel are zero. 80 packed = struct.pack(b"HHHH", rows, cols, 0, 0) 81 fcntl.ioctl(child, TIOCSWINSZ, packed) 82 83 84def getwinsize(child): 85 """ 86 This returns the terminal window size of the child tty. The return 87 value is a tuple of (rows, cols). 88 89 Thank you for the shortcut PEXPECT 90 """ 91 TIOCGWINSZ = getattr(termios, "TIOCGWINSZ", 1074295912) 92 packed = struct.pack(b"HHHH", 0, 0, 0, 0) 93 ioctl = fcntl.ioctl(child, TIOCGWINSZ, packed) 94 return struct.unpack(b"HHHH", ioctl)[0:2] 95 96 97class Terminal: 98 """ 99 I'm a virtual terminal 100 """ 101 102 def __init__( 103 self, 104 args=None, 105 executable=None, 106 shell=False, 107 cwd=None, 108 env=None, 109 preexec_fn=None, 110 # Terminal Size 111 rows=None, 112 cols=None, 113 # Logging options 114 log_stdin=None, 115 log_stdin_level="debug", 116 log_stdout=None, 117 log_stdout_level="debug", 118 log_stderr=None, 119 log_stderr_level="debug", 120 # sys.stdXYZ streaming options 121 stream_stdout=None, 122 stream_stderr=None, 123 # Used for tests 124 force_receive_encoding=__salt_system_encoding__, 125 ): 126 if not args and not executable: 127 raise TerminalException( 128 'You need to pass at least one of "args", "executable" ' 129 ) 130 self.args = args 131 self.executable = executable 132 self.shell = shell 133 self.cwd = cwd 134 self.env = env 135 self.preexec_fn = preexec_fn 136 self.receive_encoding = force_receive_encoding 137 138 if rows is None and cols is None: 139 rows, cols = self.__detect_parent_terminal_size() 140 elif rows is not None and cols is None: 141 _, cols = self.__detect_parent_terminal_size() 142 elif rows is None and cols is not None: 143 rows, _ = self.__detect_parent_terminal_size() 144 self.rows = rows 145 self.cols = cols 146 self.pid = None 147 self.stdin = None 148 self.stdout = None 149 self.stderr = None 150 151 self.child_fd = None 152 self.child_fde = None 153 154 self.partial_data_stdout = b"" 155 self.partial_data_stderr = b"" 156 157 self.closed = True 158 self.flag_eof_stdout = False 159 self.flag_eof_stderr = False 160 self.terminated = True 161 self.exitstatus = None 162 self.signalstatus = None 163 # status returned by os.waitpid 164 self.status = None 165 166 if stream_stdout is True: 167 self.stream_stdout = sys.stdout 168 elif stream_stdout is False: 169 self.stream_stdout = None 170 elif stream_stdout is not None: 171 if ( 172 not hasattr(stream_stdout, "write") 173 or not hasattr(stream_stdout, "flush") 174 or not hasattr(stream_stdout, "close") 175 ): 176 raise TerminalException( 177 "'stream_stdout' needs to have at least 3 methods, " 178 "'write()', 'flush()' and 'close()'." 179 ) 180 self.stream_stdout = stream_stdout 181 else: 182 raise TerminalException( 183 "Don't know how to handle '{}' as the VT's " 184 "'stream_stdout' parameter.".format(stream_stdout) 185 ) 186 187 if stream_stderr is True: 188 self.stream_stderr = sys.stderr 189 elif stream_stderr is False: 190 self.stream_stderr = None 191 elif stream_stderr is not None: 192 if ( 193 not hasattr(stream_stderr, "write") 194 or not hasattr(stream_stderr, "flush") 195 or not hasattr(stream_stderr, "close") 196 ): 197 raise TerminalException( 198 "'stream_stderr' needs to have at least 3 methods, " 199 "'write()', 'flush()' and 'close()'." 200 ) 201 self.stream_stderr = stream_stderr 202 else: 203 raise TerminalException( 204 "Don't know how to handle '{}' as the VT's " 205 "'stream_stderr' parameter.".format(stream_stderr) 206 ) 207 208 try: 209 self._spawn() 210 except Exception as err: # pylint: disable=W0703 211 # A lot can go wrong, so that's why we're catching the most general 212 # exception type 213 log.warning( 214 "Failed to spawn the VT: %s", err, exc_info_on_loglevel=logging.DEBUG 215 ) 216 raise TerminalException("Failed to spawn the VT. Error: {}".format(err)) 217 218 log.debug( 219 "Child Forked! PID: %s STDOUT_FD: %s STDERR_FD: %s", 220 self.pid, 221 self.child_fd, 222 self.child_fde, 223 ) 224 terminal_command = " ".join(self.args) 225 if ( 226 'decode("base64")' in terminal_command 227 or "base64.b64decode(" in terminal_command 228 ): 229 log.debug("VT: Salt-SSH SHIM Terminal Command executed. Logged to TRACE") 230 log.trace("Terminal Command: %s", terminal_command) 231 else: 232 log.debug("Terminal Command: %s", terminal_command) 233 234 # Setup logging after spawned in order to have a pid value 235 self.stdin_logger_level = LOG_LEVELS.get(log_stdin_level, log_stdin_level) 236 if log_stdin is True: 237 self.stdin_logger = logging.getLogger( 238 "{}.{}.PID-{}.STDIN".format(__name__, self.__class__.__name__, self.pid) 239 ) 240 elif log_stdin is not None: 241 if not isinstance(log_stdin, logging.Logger): 242 raise RuntimeError("'log_stdin' needs to subclass `logging.Logger`") 243 self.stdin_logger = log_stdin 244 else: 245 self.stdin_logger = None 246 247 self.stdout_logger_level = LOG_LEVELS.get(log_stdout_level, log_stdout_level) 248 if log_stdout is True: 249 self.stdout_logger = logging.getLogger( 250 "{}.{}.PID-{}.STDOUT".format( 251 __name__, self.__class__.__name__, self.pid 252 ) 253 ) 254 elif log_stdout is not None: 255 if not isinstance(log_stdout, logging.Logger): 256 raise RuntimeError("'log_stdout' needs to subclass `logging.Logger`") 257 self.stdout_logger = log_stdout 258 else: 259 self.stdout_logger = None 260 261 self.stderr_logger_level = LOG_LEVELS.get(log_stderr_level, log_stderr_level) 262 if log_stderr is True: 263 self.stderr_logger = logging.getLogger( 264 "{}.{}.PID-{}.STDERR".format( 265 __name__, self.__class__.__name__, self.pid 266 ) 267 ) 268 elif log_stderr is not None: 269 if not isinstance(log_stderr, logging.Logger): 270 raise RuntimeError("'log_stderr' needs to subclass `logging.Logger`") 271 self.stderr_logger = log_stderr 272 else: 273 self.stderr_logger = None 274 275 def send(self, data): 276 """ 277 Send data to the terminal. You are responsible to send any required 278 line feeds. 279 """ 280 return self._send(data) 281 282 def sendline(self, data, linesep=os.linesep): 283 """ 284 Send the provided data to the terminal appending a line feed. 285 """ 286 return self.send("{}{}".format(data, linesep)) 287 288 def recv(self, maxsize=None): 289 """ 290 Receive data from the terminal as a (``stdout``, ``stderr``) tuple. If 291 any of those is ``None`` we can no longer communicate with the 292 terminal's child process. 293 """ 294 if maxsize is None: 295 maxsize = 1024 296 elif maxsize < 1: 297 maxsize = 1 298 return self._recv(maxsize) 299 300 def close(self, terminate=True, kill=False): 301 """ 302 Close the communication with the terminal's child. 303 If ``terminate`` is ``True`` then additionally try to terminate the 304 terminal, and if ``kill`` is also ``True``, kill the terminal if 305 terminating it was not enough. 306 """ 307 if not self.closed: 308 if self.child_fd is not None: 309 os.close(self.child_fd) 310 self.child_fd = None 311 if self.child_fde is not None: 312 os.close(self.child_fde) 313 self.child_fde = None 314 time.sleep(0.1) 315 if terminate: 316 if not self.terminate(kill): 317 raise TerminalException("Failed to terminate child process.") 318 self.closed = True 319 320 @property 321 def has_unread_data(self): 322 return self.flag_eof_stderr is False or self.flag_eof_stdout is False 323 324 def _translate_newlines(self, data): 325 if data is None or not data: 326 return 327 # PTY's always return \r\n as the line feeds 328 return data.replace("\r\n", os.linesep) 329 330 def __enter__(self): 331 return self 332 333 def __exit__(self, exc_type, exc_value, traceback): 334 self.close(terminate=True, kill=True) 335 # Wait for the process to terminate, to avoid zombies. 336 if self.isalive(): 337 self.wait() 338 339 if mswindows: 340 341 def _execute(self): 342 raise NotImplementedError 343 344 def _spawn(self): 345 raise NotImplementedError 346 347 def _recv(self, maxsize): 348 raise NotImplementedError 349 350 def _send(self, data): 351 raise NotImplementedError 352 353 def send_signal(self, sig): 354 """ 355 Send a signal to the process 356 """ 357 # pylint: disable=E1101 358 if sig == signal.SIGTERM: 359 self.terminate() 360 elif sig == signal.CTRL_C_EVENT: 361 os.kill(self.pid, signal.CTRL_C_EVENT) 362 elif sig == signal.CTRL_BREAK_EVENT: 363 os.kill(self.pid, signal.CTRL_BREAK_EVENT) 364 else: 365 raise ValueError("Unsupported signal: {}".format(sig)) 366 # pylint: enable=E1101 367 368 def terminate(self, force=False): 369 """ 370 Terminates the process 371 """ 372 try: 373 win32api.TerminateProcess(self._handle, 1) 374 except OSError: 375 # ERROR_ACCESS_DENIED (winerror 5) is received when the 376 # process already died. 377 ecode = win32process.GetExitCodeProcess(self._handle) 378 if ecode == win32con.STILL_ACTIVE: 379 raise 380 self.exitstatus = ecode 381 382 kill = terminate 383 else: 384 385 def _spawn(self): 386 if not isinstance(self.args, str) and self.shell is True: 387 self.args = " ".join(self.args) 388 parent, child = pty.openpty() 389 err_parent, err_child = os.pipe() 390 child_name = os.ttyname(child) 391 proc = subprocess.Popen( # pylint: disable=subprocess-popen-preexec-fn 392 self.args, 393 preexec_fn=functools.partial( 394 self._preexec, child_name, self.rows, self.cols, self.preexec_fn 395 ), 396 shell=self.shell, # nosec 397 executable=self.executable, 398 cwd=self.cwd, 399 stdin=child, 400 stdout=child, 401 stderr=err_child, 402 env=self.env, 403 close_fds=True, 404 ) 405 os.close(child) 406 os.close(err_child) 407 self.child_fd = parent 408 self.child_fde = err_parent 409 self.pid = proc.pid 410 self.closed = False 411 self.terminated = False 412 413 @staticmethod 414 def _preexec(child_name, rows=80, cols=80, preexec_fn=None): 415 # Disconnect from controlling tty. Harmless if not already 416 # connected 417 try: 418 tty_fd = os.open("/dev/tty", os.O_RDWR | os.O_NOCTTY) 419 if tty_fd >= 0: 420 os.close(tty_fd) 421 # which exception, shouldn't we catch explicitly .. ? 422 except Exception: # pylint: disable=broad-except 423 # Already disconnected. This happens if running inside cron 424 pass 425 try: 426 os.setsid() 427 except OSError: 428 pass 429 try: 430 tty_fd = os.open("/dev/tty", os.O_RDWR | os.O_NOCTTY) 431 if tty_fd >= 0: 432 os.close(tty_fd) 433 raise TerminalException( 434 "Could not open child pty, {}".format(child_name) 435 ) 436 # which exception, shouldn't we catch explicitly .. ? 437 except Exception: # pylint: disable=broad-except 438 # Good! We are disconnected from a controlling tty. 439 pass 440 tty_fd = os.open(child_name, os.O_RDWR) 441 setwinsize(tty_fd, rows, cols) 442 if tty_fd < 0: 443 raise TerminalException( 444 "Could not open child pty, {}".format(child_name) 445 ) 446 else: 447 os.close(tty_fd) 448 if os.name != "posix": 449 tty_fd = os.open("/dev/tty", os.O_WRONLY) 450 if tty_fd < 0: 451 raise TerminalException("Could not open controlling tty, /dev/tty") 452 else: 453 os.close(tty_fd) 454 455 salt.utils.crypt.reinit_crypto() 456 457 if preexec_fn is not None: 458 preexec_fn() 459 460 def _send(self, data): 461 if self.child_fd is None: 462 return None 463 464 if not select.select([], [self.child_fd], [], 0)[1]: 465 return 0 466 467 try: 468 if self.stdin_logger: 469 self.stdin_logger.log(self.stdin_logger_level, data) 470 written = os.write(self.child_fd, data.encode(__salt_system_encoding__)) 471 except OSError as why: 472 if why.errno == errno.EPIPE: # broken pipe 473 os.close(self.child_fd) 474 self.child_fd = None 475 return 476 raise 477 return written 478 479 def _recv(self, maxsize): 480 rfds = [] 481 if self.child_fd: 482 rfds.append(self.child_fd) 483 if self.child_fde: 484 rfds.append(self.child_fde) 485 486 if not self.isalive(): 487 if not rfds: 488 self.close() 489 return None, None 490 rlist, _, _ = select.select(rfds, [], [], 0) 491 if not rlist: 492 self.flag_eof_stdout = self.flag_eof_stderr = True 493 log.debug("End of file(EOL). Brain-dead platform.") 494 if self.partial_data_stdout or self.partial_data_stderr: 495 # There is data that was received but for which 496 # decoding failed, attempt decoding again to generate 497 # relevant exception 498 self.close() 499 return ( 500 salt.utils.stringutils.to_unicode(self.partial_data_stdout), 501 salt.utils.stringutils.to_unicode(self.partial_data_stderr), 502 ) 503 self.close() 504 return None, None 505 506 stderr = "" 507 stdout = "" 508 509 if self.child_fd: 510 fd_flags = fcntl.fcntl(self.child_fd, fcntl.F_GETFL) 511 if self.child_fde: 512 fde_flags = fcntl.fcntl(self.child_fde, fcntl.F_GETFL) 513 if self.child_fd: 514 fcntl.fcntl(self.child_fd, fcntl.F_SETFL, fd_flags | os.O_NONBLOCK) 515 if self.child_fde: 516 fcntl.fcntl(self.child_fde, fcntl.F_SETFL, fde_flags | os.O_NONBLOCK) 517 518 rlist, _, _ = select.select(rfds, [], [], 0) 519 520 if not rlist: 521 if not self.isalive(): 522 self.flag_eof_stdout = self.flag_eof_stderr = True 523 log.debug("End of file(EOL). Very slow platform.") 524 return None, None 525 526 def read_and_decode_fd(fd, maxsize, partial_data_attr=None): 527 bytes_read = getattr(self, partial_data_attr, b"") 528 # Only read one byte if we already have some existing data 529 # to try and complete a split multibyte character 530 bytes_read += os.read(fd, maxsize if not bytes_read else 1) 531 try: 532 decoded_data = self._translate_newlines( 533 salt.utils.stringutils.to_unicode( 534 bytes_read, self.receive_encoding 535 ) 536 ) 537 if partial_data_attr is not None: 538 setattr(self, partial_data_attr, b"") 539 return decoded_data, False 540 except UnicodeDecodeError as ex: 541 max_multibyte_character_length = 4 542 if ex.start > ( 543 len(bytes_read) - max_multibyte_character_length 544 ) and ex.end == len(bytes_read): 545 # We weren't able to decode the received data possibly 546 # because it is a multibyte character split across 547 # blocks. Save what data we have to try and decode 548 # later. If the error wasn't caused by a multibyte 549 # character being split then the error start position 550 # should remain the same each time we get here but the 551 # length of the bytes_read will increase so we will 552 # give up and raise an exception instead. 553 if partial_data_attr is not None: 554 setattr(self, partial_data_attr, bytes_read) 555 else: 556 # We haven't been given anywhere to store partial 557 # data so raise the exception instead 558 raise 559 # No decoded data to return, but indicate that there 560 # is buffered data 561 return "", True 562 else: 563 raise 564 565 if self.child_fde in rlist and not self.flag_eof_stderr: 566 try: 567 stderr, partial_data = read_and_decode_fd( 568 self.child_fde, maxsize, "partial_data_stderr" 569 ) 570 571 if not stderr and not partial_data: 572 self.flag_eof_stderr = True 573 stderr = None 574 else: 575 if self.stream_stderr: 576 self.stream_stderr.write(stderr) 577 self.stream_stderr.flush() 578 579 if self.stderr_logger: 580 stripped = stderr.rstrip() 581 if stripped.startswith(os.linesep): 582 stripped = stripped[len(os.linesep) :] 583 if stripped: 584 self.stderr_logger.log( 585 self.stderr_logger_level, stripped 586 ) 587 except OSError: 588 os.close(self.child_fde) 589 self.child_fde = None 590 self.flag_eof_stderr = True 591 stderr = None 592 finally: 593 if self.child_fde is not None: 594 fcntl.fcntl(self.child_fde, fcntl.F_SETFL, fde_flags) 595 596 if self.child_fd in rlist and not self.flag_eof_stdout: 597 try: 598 stdout, partial_data = read_and_decode_fd( 599 self.child_fd, maxsize, "partial_data_stdout" 600 ) 601 602 if not stdout and not partial_data: 603 self.flag_eof_stdout = True 604 stdout = None 605 else: 606 if self.stream_stdout: 607 self.stream_stdout.write( 608 salt.utils.stringutils.to_str(stdout) 609 ) 610 self.stream_stdout.flush() 611 612 if self.stdout_logger: 613 stripped = stdout.rstrip() 614 if stripped.startswith(os.linesep): 615 stripped = stripped[len(os.linesep) :] 616 if stripped: 617 self.stdout_logger.log( 618 self.stdout_logger_level, stripped 619 ) 620 except OSError: 621 os.close(self.child_fd) 622 self.child_fd = None 623 self.flag_eof_stdout = True 624 stdout = None 625 finally: 626 if self.child_fd is not None: 627 fcntl.fcntl(self.child_fd, fcntl.F_SETFL, fd_flags) 628 # <---- Process STDOUT ------------------------------------------- 629 return stdout, stderr 630 631 def __detect_parent_terminal_size(self): 632 try: 633 TIOCGWINSZ = getattr(termios, "TIOCGWINSZ", 1074295912) 634 packed = struct.pack(b"HHHH", 0, 0, 0, 0) 635 ioctl = fcntl.ioctl(sys.stdin.fileno(), TIOCGWINSZ, packed) 636 return struct.unpack(b"HHHH", ioctl)[0:2] 637 except OSError: 638 # Return a default value of 24x80 639 return 24, 80 640 641 # <---- Internal API ------------------------------------------------- 642 643 # ----- Public API --------------------------------------------------> 644 def getwinsize(self): 645 """ 646 This returns the terminal window size of the child tty. The return 647 value is a tuple of (rows, cols). 648 649 Thank you for the shortcut PEXPECT 650 """ 651 if self.child_fd is None: 652 raise TerminalException( 653 "Can't check the size of the terminal since we're not " 654 "connected to the child process." 655 ) 656 return getwinsize(self.child_fd) 657 658 def setwinsize(self, child, rows=80, cols=80): 659 setwinsize(self.child_fd, rows, cols) 660 661 def isalive( 662 self, 663 _waitpid=os.waitpid, 664 _wnohang=os.WNOHANG, 665 _wifexited=os.WIFEXITED, 666 _wexitstatus=os.WEXITSTATUS, 667 _wifsignaled=os.WIFSIGNALED, 668 _wifstopped=os.WIFSTOPPED, 669 _wtermsig=os.WTERMSIG, 670 _os_error=os.error, 671 _errno_echild=errno.ECHILD, 672 _terminal_exception=TerminalException, 673 ): 674 """ 675 This tests if the child process is running or not. This is 676 non-blocking. If the child was terminated then this will read the 677 exitstatus or signalstatus of the child. This returns True if the 678 child process appears to be running or False if not. It can take 679 literally SECONDS for Solaris to return the right status. 680 """ 681 if self.terminated: 682 return False 683 684 if self.has_unread_data is False: 685 # This is for Linux, which requires the blocking form 686 # of waitpid to get status of a defunct process. 687 # This is super-lame. The flag_eof_* would have been set 688 # in recv(), so this should be safe. 689 waitpid_options = 0 690 else: 691 waitpid_options = _wnohang 692 693 try: 694 pid, status = _waitpid(self.pid, waitpid_options) 695 except _os_error: 696 err = sys.exc_info()[1] 697 # No child processes 698 if err.errno == _errno_echild: 699 raise _terminal_exception( 700 'isalive() encountered condition where "terminated" ' 701 "is 0, but there was no child process. Did someone " 702 "else call waitpid() on our process?" 703 ) 704 else: 705 raise 706 707 # I have to do this twice for Solaris. 708 # I can't even believe that I figured this out... 709 # If waitpid() returns 0 it means that no child process 710 # wishes to report, and the value of status is undefined. 711 if pid == 0: 712 try: 713 ### os.WNOHANG # Solaris! 714 pid, status = _waitpid(self.pid, waitpid_options) 715 except _os_error as exc: 716 # This should never happen... 717 if exc.errno == _errno_echild: 718 raise _terminal_exception( 719 "isalive() encountered condition that should " 720 "never happen. There was no child process. Did " 721 "someone else call waitpid() on our process?" 722 ) 723 else: 724 raise 725 726 # If pid is still 0 after two calls to waitpid() then the 727 # process really is alive. This seems to work on all platforms, 728 # except for Irix which seems to require a blocking call on 729 # waitpid or select, so I let recv take care of this situation 730 # (unfortunately, this requires waiting through the timeout). 731 if pid == 0: 732 return True 733 734 if pid == 0: 735 return True 736 737 if _wifexited(status): 738 self.status = status 739 self.exitstatus = _wexitstatus(status) 740 self.signalstatus = None 741 self.terminated = True 742 elif _wifsignaled(status): 743 self.status = status 744 self.exitstatus = None 745 self.signalstatus = _wtermsig(status) 746 self.terminated = True 747 elif _wifstopped(status): 748 raise _terminal_exception( 749 "isalive() encountered condition where child process is " 750 "stopped. This is not supported. Is some other process " 751 "attempting job control with our child pid?" 752 ) 753 return False 754 755 def terminate(self, force=False): 756 """ 757 This forces a child process to terminate. It starts nicely with 758 SIGHUP and SIGINT. If "force" is True then moves onto SIGKILL. This 759 returns True if the child was terminated. This returns False if the 760 child could not be terminated. 761 """ 762 if not self.closed: 763 self.close(terminate=False) 764 765 if not self.isalive(): 766 return True 767 try: 768 self.send_signal(signal.SIGHUP) 769 time.sleep(0.1) 770 if not self.isalive(): 771 return True 772 self.send_signal(signal.SIGCONT) 773 time.sleep(0.1) 774 if not self.isalive(): 775 return True 776 self.send_signal(signal.SIGINT) 777 time.sleep(0.1) 778 if not self.isalive(): 779 return True 780 if force: 781 self.send_signal(signal.SIGKILL) 782 time.sleep(0.1) 783 if not self.isalive(): 784 return True 785 else: 786 return False 787 return False 788 except OSError: 789 # I think there are kernel timing issues that sometimes cause 790 # this to happen. I think isalive() reports True, but the 791 # process is dead to the kernel. 792 # Make one last attempt to see if the kernel is up to date. 793 time.sleep(0.1) 794 if not self.isalive(): 795 return True 796 else: 797 return False 798 799 def wait(self): 800 """ 801 This waits until the child exits internally consuming any remaining 802 output from the child, thus, no blocking forever because the child 803 has unread data. 804 """ 805 if self.isalive(): 806 while self.isalive(): 807 stdout, stderr = self.recv() 808 if stdout is None: 809 break 810 if stderr is None: 811 break 812 else: 813 raise TerminalException("Cannot wait for dead child process.") 814 return self.exitstatus 815 816 def send_signal(self, sig): 817 """ 818 Send a signal to the process 819 """ 820 os.kill(self.pid, sig) 821 822 def kill(self): 823 """ 824 Kill the process with SIGKILL 825 """ 826 self.send_signal(signal.SIGKILL) 827