[ADD] logging into spawn class; [FIX] more fixes around process finishing

This commit is contained in:
Benedek Racz 2020-01-22 11:17:07 +01:00
parent 4a7385e4a1
commit 54d961489a
3 changed files with 105 additions and 58 deletions

View File

@ -42,6 +42,7 @@ import logging
import os import os
import traceback import traceback
import pkg_resources import pkg_resources
import psutil
from io import StringIO from io import StringIO
import ctypes import ctypes
@ -54,6 +55,8 @@ import winerror
import win32pipe import win32pipe
import socket import socket
from .wexpect_util import init_logger
# #
# System-wide constants # System-wide constants
# #
@ -72,28 +75,8 @@ except: # pragma: no cover
# console manipulation. # console manipulation.
# #
logger = logging.getLogger('wexpect') logger = logging.getLogger('wexpect')
def init_logger():
logger = logging.getLogger('wexpect')
os.environ['WEXPECT_LOGGER_LEVEL'] = 'DEBUG'
try:
logger_level = os.environ['WEXPECT_LOGGER_LEVEL']
try:
logger_filename = os.environ['WEXPECT_LOGGER_FILENAME']
except KeyError:
pid = os.getpid()
logger_filename = f'./.wlog/wexpect_{pid}'
logger.setLevel(logger_level)
logger_filename = f'{logger_filename}.log'
os.makedirs(os.path.dirname(logger_filename), exist_ok=True)
fh = logging.FileHandler(logger_filename, 'w', 'utf-8')
formatter = logging.Formatter('%(asctime)s - %(filename)s:%(lineno)d - %(levelname)s - %(message)s')
fh.setFormatter(formatter)
logger.addHandler(fh)
except KeyError:
logger.setLevel(logging.ERROR)
init_logger() init_logger(logger)
class ConsoleReaderBase: class ConsoleReaderBase:
"""Consol class (aka. client-side python class) for the child. """Consol class (aka. client-side python class) for the child.
@ -101,7 +84,7 @@ class ConsoleReaderBase:
This class initialize the console starts the child in it and reads the console periodically. This class initialize the console starts the child in it and reads the console periodically.
""" """
def __init__(self, path, parent_pid, cp=None, window_size_x=80, window_size_y=25, def __init__(self, path, host_pid, cp=None, window_size_x=80, window_size_y=25,
buffer_size_x=80, buffer_size_y=16000, **kwargs): buffer_size_x=80, buffer_size_y=16000, **kwargs):
"""Initialize the console starts the child in it and reads the console periodically. """Initialize the console starts the child in it and reads the console periodically.
@ -121,7 +104,9 @@ class ConsoleReaderBase:
self.consin = None self.consin = None
self.consout = None self.consout = None
self.local_echo = True self.local_echo = True
self.pid = os.getpid() self.console_pid = os.getpid()
self.host_pid = host_pid
self.host_process = psutil.Process(host_pid)
logger.info("ConsoleReader started") logger.info("ConsoleReader started")
@ -151,6 +136,7 @@ class ConsoleReaderBase:
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
time.sleep(.1) time.sleep(.1)
finally: finally:
self.terminate_child()
time.sleep(.1) time.sleep(.1)
self.send_to_host(self.readConsoleToCursor()) self.send_to_host(self.readConsoleToCursor())
time.sleep(.1) time.sleep(.1)
@ -161,26 +147,20 @@ class ConsoleReaderBase:
paused = False paused = False
while True: while True:
if not self.isalive(self.host_process):
logger.info('Host process has been died.')
return
if win32process.GetExitCodeProcess(self.__childProcess) != win32con.STILL_ACTIVE:
logger.info('Child finished.')
return
consinfo = self.consout.GetConsoleScreenBufferInfo() consinfo = self.consout.GetConsoleScreenBufferInfo()
cursorPos = consinfo['CursorPosition'] cursorPos = consinfo['CursorPosition']
self.send_to_host(self.readConsoleToCursor()) self.send_to_host(self.readConsoleToCursor())
s = self.get_from_host() s = self.get_from_host()
self.write(s) self.write(s)
if win32process.GetExitCodeProcess(self.__childProcess) != win32con.STILL_ACTIVE:
logger.info('Child finished.')
time.sleep(.1)
try:
win32process.TerminateProcess(self.__childProcess, 0)
except pywintypes.error as e:
""" 'Access denied' happens always? Perhaps if not running as admin (or UAC
enabled under Vista/7). Don't log. Child process will exit regardless when
calling sys.exit
"""
if e.args[0] != winerror.ERROR_ACCESS_DENIED:
logger.info(e)
return
if cursorPos.Y > maxconsoleY and not paused: if cursorPos.Y > maxconsoleY and not paused:
logger.info('cursorPos %s' % cursorPos) logger.info('cursorPos %s' % cursorPos)
self.suspendThread() self.suspendThread()
@ -192,7 +172,27 @@ class ConsoleReaderBase:
paused = False paused = False
time.sleep(.1) time.sleep(.1)
def terminate_child(self):
try:
win32process.TerminateProcess(self.__childProcess, 0)
except pywintypes.error as e:
""" 'Access denied' happens always? Perhaps if not running as admin (or UAC
enabled under Vista/7). Don't log. Child process will exit regardless when
calling sys.exit
"""
# if e.args[0] != winerror.ERROR_ACCESS_DENIED:
logger.info(e)
return
def isalive(self, process):
"""True if the child is still alive, false otherwise"""
try:
process.wait(timeout=0)
return False
except psutil.TimeoutExpired:
return True
def write(self, s): def write(self, s):
"""Writes input into the child consoles input buffer.""" """Writes input into the child consoles input buffer."""
@ -439,6 +439,7 @@ class ConsoleReaderSocket(ConsoleReaderBase):
def send_to_host(self, msg): def send_to_host(self, msg):
# convert to bytes # convert to bytes
msg_bytes = str.encode(msg) msg_bytes = str.encode(msg)
logger.debug(f'Sending msg: {msg_bytes}')
self.connection.sendall(msg_bytes) self.connection.sendall(msg_bytes)
def get_from_host(self): def get_from_host(self):
@ -482,6 +483,7 @@ class ConsoleReaderPipe(ConsoleReaderBase):
def send_to_host(self, msg): def send_to_host(self, msg):
# convert to bytes # convert to bytes
msg_bytes = str.encode(msg) msg_bytes = str.encode(msg)
logger.debug(f'Sending msg: {msg_bytes}')
win32file.WriteFile(self.pipe, msg_bytes) win32file.WriteFile(self.pipe, msg_bytes)
def get_from_host(self): def get_from_host(self):

View File

@ -73,23 +73,29 @@ import traceback
import types import types
import psutil import psutil
import signal import signal
import socket
import logging
import pywintypes import pywintypes
import win32process import win32process
import win32con import win32con
import win32api
import win32file import win32file
import winerror import winerror
import win32pipe import win32pipe
import socket
from .wexpect_util import ExceptionPexpect from .wexpect_util import ExceptionPexpect
from .wexpect_util import EOF from .wexpect_util import EOF
from .wexpect_util import TIMEOUT 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
logger = logging.getLogger('wexpect')
init_logger(logger)
def run (command, timeout=-1, withexitstatus=False, events=None, extra_args=None, logfile=None, cwd=None, env=None): def run (command, timeout=-1, withexitstatus=False, events=None, extra_args=None, logfile=None,
cwd=None, env=None, **kwargs):
""" """
This function runs the given command; waits for it to finish; then This function runs the given command; waits for it to finish; then
@ -152,9 +158,9 @@ def run (command, timeout=-1, withexitstatus=False, events=None, extra_args=None
dictionary passed to a callback. """ dictionary passed to a callback. """
if timeout == -1: if timeout == -1:
child = SpawnSocket(command, maxread=2000, logfile=logfile, cwd=cwd, env=env) child = SpawnSocket(command, maxread=2000, logfile=logfile, cwd=cwd, env=env, **kwargs)
else: else:
child = SpawnSocket(command, timeout=timeout, maxread=2000, logfile=logfile, cwd=cwd, env=env) child = SpawnSocket(command, timeout=timeout, maxread=2000, logfile=logfile, cwd=cwd, env=env, **kwargs)
if events is not None: if events is not None:
patterns = list(events.keys()) patterns = list(events.keys())
responses = list(events.values()) responses = list(events.values())
@ -198,7 +204,7 @@ def run (command, timeout=-1, withexitstatus=False, events=None, extra_args=None
class SpawnBase: class SpawnBase:
def __init__(self, command, args=[], timeout=30, maxread=60000, searchwindowsize=None, def __init__(self, command, args=[], timeout=30, maxread=60000, searchwindowsize=None,
logfile=None, cwd=None, env=None, codepage=None, echo=True, **kwargs): logfile=None, cwd=None, env=None, codepage=None, echo=True, safe_exit=True, **kwargs):
"""This starts the given command in a child process. This does all the """This starts the given command in a child process. This does all the
fork/exec type of stuff for a pty. This is called by __init__. If args fork/exec type of stuff for a pty. This is called by __init__. If args
is empty then command will be parsed (split on spaces) and args will be is empty then command will be parsed (split on spaces) and args will be
@ -218,6 +224,7 @@ class SpawnBase:
self.child_process = None self.child_process = None
self.child_pid = None self.child_pid = None
self.safe_exit = safe_exit
self.searcher = None self.searcher = None
self.ignorecase = False self.ignorecase = False
self.before = None self.before = None
@ -260,25 +267,19 @@ class SpawnBase:
command_with_path = shutil.which(self.command) command_with_path = shutil.which(self.command)
if command_with_path is None: if command_with_path is None:
raise ExceptionPexpect ('The command was not found or was not executable: %s.' % self.command) logger.warning('The command was not found or was not executable: %s.' % self.command)
raise ExceptionPexpect ('The command was not found or was not executable: %s.' % self.command)
self.command = command_with_path self.command = command_with_path
self.args[0] = self.command self.args[0] = self.command
self.name = '<' + ' '.join (self.args) + '>' self.name = '<' + ' '.join (self.args) + '>'
if self.cwd is not None:
os.chdir(self.cwd)
if self.cwd is not None:
# Restore the original working dir
os.chdir(self.ocwd)
self.terminated = False self.terminated = False
self.closed = False self.closed = False
self.child_fd = self.startChild(self.args, self.env) self.child_fd = self.startChild(self.args, self.env)
self.get_child_process() self.get_child_process()
logger.info(f'Child pid: {self.child_pid} Console pid: {self.console_pid}')
self.connect_to_child() self.connect_to_child()
def __del__(self): def __del__(self):
@ -288,6 +289,8 @@ class SpawnBase:
try: try:
self.terminate() self.terminate()
self.disconnect_from_child() self.disconnect_from_child()
if self.safe_exit:
self.wait()
except: except:
traceback.print_exc() traceback.print_exc()
@ -370,7 +373,10 @@ class SpawnBase:
def kill(self, sig=signal.SIGTERM): def kill(self, sig=signal.SIGTERM):
"""Sig == sigint for ctrl-c otherwise the child is terminated.""" """Sig == sigint for ctrl-c otherwise the child is terminated."""
self.child_process.send_signal(sig) try:
self.child_process.send_signal(sig)
except psutil._exceptions.NoSuchProcess as e:
logger.info('Child has already died. %s', e)
def wait(self, child=True, console=True): def wait(self, child=True, console=True):
if child: if child:
@ -766,7 +772,7 @@ class SpawnPipe(SpawnBase):
def connect_to_child(self): def connect_to_child(self):
pipe_name = 'wexpect_{}'.format(self.console_pid) pipe_name = 'wexpect_{}'.format(self.console_pid)
pipe_full_path = r'\\.\pipe\{}'.format(pipe_name) pipe_full_path = r'\\.\pipe\{}'.format(pipe_name)
# print('Trying to connect to pipe: {}'.format(pipe_full_path)) logger.debug(f'Trying to connect to pipe: {pipe_full_path}')
while True: while True:
try: try:
self.pipe = win32file.CreateFile( self.pipe = win32file.CreateFile(
@ -778,14 +784,14 @@ class SpawnPipe(SpawnBase):
0, 0,
None None
) )
# print("pipe found!") logger.debug('Pipe found')
res = win32pipe.SetNamedPipeHandleState(self.pipe, win32pipe.PIPE_READMODE_MESSAGE, None, None) res = win32pipe.SetNamedPipeHandleState(self.pipe, win32pipe.PIPE_READMODE_MESSAGE, None, None)
# if res == 0: if res == 0:
# print(f"SetNamedPipeHandleState return code: {res}") logger.debug(f"SetNamedPipeHandleState return code: {res}")
return return
except pywintypes.error as e: except pywintypes.error as e:
if e.args[0] == winerror.ERROR_FILE_NOT_FOUND: #2 if e.args[0] == winerror.ERROR_FILE_NOT_FOUND: #2
# print("no pipe, trying again in a bit later") logger.debug("no pipe, trying again in a bit later")
time.sleep(0.2) time.sleep(0.2)
else: else:
raise raise
@ -806,6 +812,7 @@ class SpawnPipe(SpawnBase):
This is a wrapper around Wtty.read(). """ This is a wrapper around Wtty.read(). """
if self.closed: if self.closed:
logger.info('I/O operation on closed file in read_nonblocking().')
raise ValueError ('I/O operation on closed file in read_nonblocking().') raise ValueError ('I/O operation on closed file in read_nonblocking().')
try: try:
@ -815,22 +822,27 @@ class SpawnPipe(SpawnBase):
# the last output. # the last output.
# The flag_child_finished flag shows that this is the second trial, where we raise the EOF. # The flag_child_finished flag shows that this is the second trial, where we raise the EOF.
if self.flag_child_finished: if self.flag_child_finished:
logger.info("EOF('self.flag_child_finished')")
raise EOF('self.flag_child_finished') raise EOF('self.flag_child_finished')
if not self.isalive(): if not self.isalive():
self.flag_child_finished = True self.flag_child_finished = True
logger.info('self.isalive() == False: Child has been died, lets do a last read!')
try: try:
s = win32file.ReadFile(self.pipe, size)[1] s = win32file.ReadFile(self.pipe, size)[1]
logger.debug(f's: {s}')
return s.decode() return s.decode()
except pywintypes.error as e: except pywintypes.error as e:
if e.args[0] == winerror.ERROR_BROKEN_PIPE: #109 if e.args[0] == winerror.ERROR_BROKEN_PIPE: #109
self.flag_eof = True self.flag_eof = True
logger.info("EOF('broken pipe, bye bye')")
raise EOF('broken pipe, bye bye') raise EOF('broken pipe, bye bye')
elif e.args[0] == winerror.ERROR_NO_DATA: elif e.args[0] == winerror.ERROR_NO_DATA:
'''232 (0xE8) '''232 (0xE8)
The pipe is being closed. The pipe is being closed.
''' '''
self.flag_eof = True self.flag_eof = True
logger.info("EOF('The pipe is being closed.')")
raise EOF('The pipe is being closed.') raise EOF('The pipe is being closed.')
else: else:
raise raise
@ -905,6 +917,8 @@ class SpawnPipe(SpawnBase):
"wexpect.console_reader.logger.info('Console finished2.');" "wexpect.console_reader.logger.info('Console finished2.');"
) )
logger.info(f'Console starter command:{commandLine}')
_, _, self.console_pid, __otid = win32process.CreateProcess(None, commandLine, None, None, False, _, _, self.console_pid, __otid = win32process.CreateProcess(None, commandLine, None, None, False,
win32process.CREATE_NEW_CONSOLE, None, None, si) win32process.CREATE_NEW_CONSOLE, None, None, si)
@ -933,6 +947,7 @@ class SpawnSocket(SpawnBase):
def connect_to_child(self): def connect_to_child(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect((self.host, self.port)) self.sock.connect((self.host, self.port))
self.sock.settimeout(.2)
def disconnect_from_child(self): def disconnect_from_child(self):
if self.sock: if self.sock:
@ -951,6 +966,7 @@ class SpawnSocket(SpawnBase):
This is a wrapper around Wtty.read(). """ This is a wrapper around Wtty.read(). """
if self.closed: if self.closed:
logger.info('I/O operation on closed file in read_nonblocking().')
raise ValueError ('I/O operation on closed file in read_nonblocking().') raise ValueError ('I/O operation on closed file in read_nonblocking().')
try: try:
@ -960,14 +976,20 @@ class SpawnSocket(SpawnBase):
# the last output. # the last output.
# The flag_child_finished flag shows that this is the second trial, where we raise the EOF. # The flag_child_finished flag shows that this is the second trial, where we raise the EOF.
if self.flag_child_finished: if self.flag_child_finished:
logger.info("EOF('self.flag_child_finished')")
raise EOF('self.flag_child_finished') raise EOF('self.flag_child_finished')
if not self.isalive(): if not self.isalive():
self.flag_child_finished = True self.flag_child_finished = True
logger.info('self.isalive() == False: Child has been died, lets do a last read!')
logger.debug(f'Reading socket...')
s = self.sock.recv(size) s = self.sock.recv(size)
logger.debug(f's: {s}')
except EOF: except EOF:
self.flag_eof = True self.flag_eof = True
raise raise
except socket.timeout:
return ''
return s.decode() return s.decode()
@ -1012,6 +1034,8 @@ class SpawnSocket(SpawnBase):
"wexpect.console_reader.logger.info('Console finished2.');" "wexpect.console_reader.logger.info('Console finished2.');"
) )
logger.info(f'Console starter command:{commandLine}')
_, _, self.console_pid, __otid = win32process.CreateProcess(None, commandLine, None, None, False, _, _, self.console_pid, __otid = win32process.CreateProcess(None, commandLine, None, None, False,
win32process.CREATE_NEW_CONSOLE, None, None, si) win32process.CREATE_NEW_CONSOLE, None, None, si)

View File

@ -40,6 +40,27 @@ Wexpect Copyright (c) 2019 Benedek Racz
import re import re
import traceback import traceback
import sys import sys
import os
import logging
def init_logger(logger):
os.environ['WEXPECT_LOGGER_LEVEL'] = 'DEBUG'
try:
logger_level = os.environ['WEXPECT_LOGGER_LEVEL']
try:
logger_filename = os.environ['WEXPECT_LOGGER_FILENAME']
except KeyError:
pid = os.getpid()
logger_filename = f'./.wlog/wexpect_{pid}'
logger.setLevel(logger_level)
logger_filename = f'{logger_filename}.log'
os.makedirs(os.path.dirname(logger_filename), exist_ok=True)
fh = logging.FileHandler(logger_filename, 'w', 'utf-8')
formatter = logging.Formatter('%(asctime)s - %(filename)s:%(lineno)d - %(levelname)s - %(message)s')
fh.setFormatter(formatter)
logger.addHandler(fh)
except KeyError:
logger.setLevel(logging.ERROR)
def split_command_line(command_line, escape_char = '^'): def split_command_line(command_line, escape_char = '^'):
"""This splits a command line into a list of arguments. It splits arguments """This splits a command line into a list of arguments. It splits arguments