[FIX] real child process cannot be fetched from host, because it may died befor fetching it, so signal handling cannot be handled directly.

This commit is contained in:
Benedek Racz 2020-01-25 22:54:40 +01:00
parent a9471a94e6
commit d46e700655
4 changed files with 60 additions and 39 deletions

View File

@ -43,6 +43,7 @@ import os
import traceback import traceback
import pkg_resources import pkg_resources
import psutil import psutil
import signal
from io import StringIO from io import StringIO
import ctypes import ctypes
@ -56,6 +57,7 @@ import socket
from .wexpect_util import init_logger from .wexpect_util import init_logger
from .wexpect_util import EOF_CHAR from .wexpect_util import EOF_CHAR
from .wexpect_util import SIGNAL_CHARS
# #
# System-wide constants # System-wide constants
@ -109,6 +111,7 @@ class ConsoleReaderBase:
self.host_process = psutil.Process(host_pid) self.host_process = psutil.Process(host_pid)
self.child_process = None self.child_process = None
self.child_pid = None self.child_pid = None
self.enable_signal_chars = True
logger.info("ConsoleReader started") logger.info("ConsoleReader started")
@ -130,6 +133,8 @@ class ConsoleReaderBase:
0, None, None, si) 0, None, None, si)
self.child_process = psutil.Process(self.child_pid) self.child_process = psutil.Process(self.child_pid)
logger.info(f'Child pid: {self.child_pid} Console pid: {self.console_pid}')
except: except:
logger.info(traceback.format_exc()) logger.info(traceback.format_exc())
return return
@ -176,6 +181,11 @@ class ConsoleReaderBase:
logger.debug(f'get_from_host: {s}') logger.debug(f'get_from_host: {s}')
else: else:
logger.spam(f'get_from_host: {s}') logger.spam(f'get_from_host: {s}')
if self.enable_signal_chars:
for sig,char in SIGNAL_CHARS.items():
if char in s:
self.child_process.send_signal(sig)
s = s.decode()
self.write(s) self.write(s)
if cursorPos.Y > maxconsoleY and not paused: if cursorPos.Y > maxconsoleY and not paused:
@ -201,7 +211,7 @@ class ConsoleReaderBase:
def isalive(self, process): def isalive(self, process):
"""True if the child is still alive, false otherwise""" """True if the child is still alive, false otherwise"""
try: try:
process.wait(timeout=0) self.exitstatus = process.wait(timeout=0)
return False return False
except psutil.TimeoutExpired: except psutil.TimeoutExpired:
return True return True
@ -481,7 +491,7 @@ class ConsoleReaderSocket(ConsoleReaderBase):
# timeout exception is setup # timeout exception is setup
if err == 'timed out': if err == 'timed out':
logger.debug('recv timed out, retry later') logger.debug('recv timed out, retry later')
return '' return b''
else: else:
raise raise
else: else:
@ -489,7 +499,7 @@ class ConsoleReaderSocket(ConsoleReaderBase):
raise Exception('orderly shutdown on server end') raise Exception('orderly shutdown on server end')
else: else:
# got a message do something :) # got a message do something :)
return msg.decode() return msg
class ConsoleReaderPipe(ConsoleReaderBase): class ConsoleReaderPipe(ConsoleReaderBase):
@ -526,6 +536,6 @@ class ConsoleReaderPipe(ConsoleReaderBase):
if avail > 0: if avail > 0:
resp = win32file.ReadFile(self.pipe, 4096) resp = win32file.ReadFile(self.pipe, 4096)
ret = resp[1] ret = resp[1]
return ret.decode() return ret
else: else:
return '' return b''

View File

@ -89,6 +89,7 @@ from .wexpect_util import TIMEOUT
from .wexpect_util import split_command_line from .wexpect_util import split_command_line
from .wexpect_util import init_logger from .wexpect_util import init_logger
from .wexpect_util import EOF_CHAR from .wexpect_util import EOF_CHAR
from .wexpect_util import SIGNAL_CHARS
logger = logging.getLogger('wexpect') logger = logging.getLogger('wexpect')
@ -389,26 +390,10 @@ class SpawnBase:
return self.console_process return self.console_process
def get_child_process(self, force=False): def get_child_process(self, force=False):
'''Fetches and returns the child process (and pid)
The console starts the *real* child. This function fetches this *real* child's process ID
and process handle. If the console process is slower,(the OS does not grant enough CPU for
that), the child, cannot be started, when we reach this function, therefore the
`self.get_console_process().children()` line will return an empty list. So we ask console's child
in a loop, while, we found a (the) child.
This loop cannot be an infinite loop. If the console's process has error before/during
starting the child. `self.get_console_process().children()` will throw error.
'''
if force or self.console_process is None: if force or self.console_process is None:
while True: self.child_process = self.get_console_process()
children = self.get_console_process().children() self.child_pid = self.child_process.pid
try: return self.child_process
self.child_process = children[0]
except IndexError:
time.sleep(.1)
continue
self.child_pid = self.child_process.pid
return self.child_process
def close(self): # File-like object. def close(self): # File-like object.
""" Closes the child console.""" """ Closes the child console."""
@ -444,7 +429,7 @@ class SpawnBase:
"""Sig == sigint for ctrl-c otherwise the child is terminated.""" """Sig == sigint for ctrl-c otherwise the child is terminated."""
try: try:
self.child_process.send_signal(sig) self.child_process.send_signal(sig)
except psutil._exceptions.NoSuchProcess as e: except psutil.NoSuchProcess as e:
logger.info('Child has already died. %s', e) logger.info('Child has already died. %s', e)
def wait(self, child=True, console=True): def wait(self, child=True, console=True):
@ -562,8 +547,7 @@ class SpawnBase:
"""This is like send(), but it adds a line feed (os.linesep). This """This is like send(), but it adds a line feed (os.linesep). This
returns the number of bytes written. """ returns the number of bytes written. """
n = self.send(s) n = self.send(s+'\r\n')
n = n + self.send(b'\r\n')
return n return n
def sendeof(self): def sendeof(self):
@ -580,7 +564,18 @@ class SpawnBase:
char = chr(4) char = chr(4)
self.send(char) self.send(char)
def send(self): def send(self, s, delaybeforesend=None):
"""Virtual definition
"""
if delaybeforesend is None:
delaybeforesend = self.delaybeforesend
if delaybeforesend:
time.sleep(delaybeforesend)
return self._send_impl(s)
def _send_impl(self, s):
"""Virtual definition """Virtual definition
""" """
raise NotImplementedError raise NotImplementedError
@ -773,6 +768,8 @@ class SpawnBase:
end_time = time.time() + timeout end_time = time.time() + timeout
if searchwindowsize == -1: if searchwindowsize == -1:
searchwindowsize = self.searchwindowsize searchwindowsize = self.searchwindowsize
logger.debug(f'searcher: {searcher}')
try: try:
incoming = self.buffer incoming = self.buffer
@ -916,14 +913,12 @@ class SpawnPipe(SpawnBase):
else: else:
raise raise
def send(self, s): def _send_impl(self, s):
"""This sends a string to the child process. This returns the number of """This sends a string to the child process. This returns the number of
bytes written. If a log file was set then the data is also written to bytes written. If a log file was set then the data is also written to
the log. """ the log. """
if isinstance(s, str): if isinstance(s, str):
s = str.encode(s) s = str.encode(s)
if self.delaybeforesend:
time.sleep(self.delaybeforesend)
try: try:
if s: if s:
logger.debug(f"Writing: {s}") logger.debug(f"Writing: {s}")
@ -942,6 +937,14 @@ class SpawnPipe(SpawnBase):
else: else:
raise raise
return len(s) return len(s)
def kill(self, sig=signal.SIGTERM):
"""Sig == sigint for ctrl-c otherwise the child is terminated."""
try:
logger.info(f'Sending kill signal: {sig}')
self.send(SIGNAL_CHARS[sig])
except EOF as e:
logger.info(e)
class SpawnSocket(SpawnBase): class SpawnSocket(SpawnBase):
@ -957,14 +960,12 @@ class SpawnSocket(SpawnBase):
super().__init__(command=command, args=args, timeout=timeout, maxread=maxread, super().__init__(command=command, args=args, timeout=timeout, maxread=maxread,
searchwindowsize=searchwindowsize, cwd=cwd, env=env, codepage=codepage, echo=echo, interact=interact) searchwindowsize=searchwindowsize, cwd=cwd, env=env, codepage=codepage, echo=echo, interact=interact)
def send(self, s): def _send_impl(self, s):
"""This sends a string to the child process. This returns the number of """This sends a string to the child process. This returns the number of
bytes written. If a log file was set then the data is also written to bytes written. If a log file was set then the data is also written to
the log. """ the log. """
if isinstance(s, str): if isinstance(s, str):
s = str.encode(s) s = str.encode(s)
if self.delaybeforesend:
time.sleep(self.delaybeforesend)
self.sock.sendall(s) self.sock.sendall(s)
return len(s) return len(s)
@ -1015,7 +1016,15 @@ class SpawnSocket(SpawnBase):
return '' return ''
return s.decode() return s.decode()
def kill(self, sig=signal.SIGTERM):
"""Sig == sigint for ctrl-c otherwise the child is terminated."""
try:
logger.info(f'Sending kill signal: {sig}')
self.send(SIGNAL_CHARS[sig])
except EOF as e:
logger.info(e)
class searcher_re (object): class searcher_re (object):
"""This is regular expression string search helper for the """This is regular expression string search helper for the

View File

@ -1125,19 +1125,15 @@ class Wtty:
logger.info(f"Fetch child's process and pid...") logger.info(f"Fetch child's process and pid...")
while True: while True:
logger.info(f"0")
msg = win32gui.GetMessage(0, 0, 0) msg = win32gui.GetMessage(0, 0, 0)
logger.info(f"0b")
childPid = msg[1][2] childPid = msg[1][2]
# Sometimes win32gui.GetMessage returns a bogus PID, so keep calling it # Sometimes win32gui.GetMessage returns a bogus PID, so keep calling it
# until we can successfully connect to the child or timeout is # until we can successfully connect to the child or timeout is
# reached # reached
if childPid: if childPid:
try: try:
logger.info(f"1")
self.__childProcess = win32api.OpenProcess( self.__childProcess = win32api.OpenProcess(
win32con.PROCESS_TERMINATE | win32con.PROCESS_QUERY_INFORMATION, False, childPid) win32con.PROCESS_TERMINATE | win32con.PROCESS_QUERY_INFORMATION, False, childPid)
logger.info(f"2")
self.__conProcess = win32api.OpenProcess( self.__conProcess = win32api.OpenProcess(
win32con.PROCESS_TERMINATE | win32con.PROCESS_QUERY_INFORMATION, False, self.conpid) win32con.PROCESS_TERMINATE | win32con.PROCESS_QUERY_INFORMATION, False, self.conpid)
except pywintypes.error as e: except pywintypes.error as e:

View File

@ -42,10 +42,16 @@ import traceback
import sys import sys
import os import os
import logging import logging
import signal
# platform does not define VEOF so assume CTRL-D # platform does not define VEOF so assume CTRL-D
EOF_CHAR = b'\x04' EOF_CHAR = b'\x04'
SIGNAL_CHARS = {
signal.SIGTERM: b'\x011', # Device control 1
signal.SIGINT: b'\x012', # Device control 2
}
SPAM = 5 SPAM = 5
logging.addLevelName(SPAM, "SPAM") logging.addLevelName(SPAM, "SPAM")
def spam(self, message, *args, **kws): def spam(self, message, *args, **kws):