mirror of
https://github.com/clearml/wexpect-venv
synced 2025-02-24 13:23:09 +00:00
[ADD] Basic features added
This commit is contained in:
parent
d2c07a8827
commit
c7a4e092c8
1940
wexpect.py
1940
wexpect.py
File diff suppressed because it is too large
Load Diff
16
wexpect/__init__.py
Normal file
16
wexpect/__init__.py
Normal file
@ -0,0 +1,16 @@
|
||||
# __init__.py
|
||||
|
||||
from .wexpect_util import split_command_line
|
||||
from .wexpect_util import join_args
|
||||
from .wexpect_util import ExceptionPexpect
|
||||
from .wexpect_util import EOF
|
||||
from .wexpect_util import TIMEOUT
|
||||
|
||||
from .console_reader import ConsoleReaderSocket
|
||||
from .console_reader import ConsoleReaderPipe
|
||||
|
||||
from .spawn import Spawn
|
||||
from .spawn import Spawn as spawn
|
||||
|
||||
__all__ = ['split_command_line', 'join_args', 'ExceptionPexpect', 'EOF', 'TIMEOUT',
|
||||
'ConsoleReaderSocket', 'ConsoleReaderPipe', 'spawn', 'Spawn']
|
@ -1,34 +1,65 @@
|
||||
"""Wexpect is a Windows variant of pexpect https://pexpect.readthedocs.io.
|
||||
|
||||
Wexpect is a Python module for spawning child applications and controlling
|
||||
them automatically.
|
||||
|
||||
console_reader Implements a virtual terminal, and starts the child program.
|
||||
The main wexpect.Spawn class connect to this class to reach the child's terminal.
|
||||
|
||||
Credits: Noah Spurrier, Richard Holden, Marco Molteni, Kimberley Burchett,
|
||||
Robert Stone, Hartmut Goebel, Chad Schroeder, Erick Tryzelaar, Dave Kirby, Ids
|
||||
vander Molen, George Todd, Noel Taylor, Nicolas D. Cesar, Alexander Gattin,
|
||||
Geoffrey Marshall, Francisco Lourenco, Glen Mabey, Karthik Gurusamy, Fernando
|
||||
Perez, Corey Minyard, Jon Cohen, Guillaume Chazarain, Andrew Ryan, Nick
|
||||
Craig-Wood, Andrew Stone, Jorgen Grahn, Benedek Racz
|
||||
|
||||
Free, open source, and all that good stuff.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
Wexpect Copyright (c) 2019 Benedek Racz
|
||||
|
||||
"""
|
||||
|
||||
import time
|
||||
import sys
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import types
|
||||
import traceback
|
||||
import signal
|
||||
import pkg_resources
|
||||
from io import StringIO
|
||||
|
||||
from ctypes import windll
|
||||
import ctypes
|
||||
import pywintypes
|
||||
from win32com.shell.shell import SHGetSpecialFolderPath
|
||||
import win32console
|
||||
import win32process
|
||||
import win32con
|
||||
import win32gui
|
||||
import win32api
|
||||
import win32file
|
||||
import winerror
|
||||
import win32pipe
|
||||
|
||||
__version__ = 'test'
|
||||
import socket
|
||||
|
||||
#
|
||||
# System-wide constants
|
||||
#
|
||||
screenbufferfillchar = '\4'
|
||||
maxconsoleY = 8000
|
||||
default_port = 4321
|
||||
|
||||
# The version is handled by the package: pbr, which derives the version from the git tags.
|
||||
try:
|
||||
@ -52,25 +83,19 @@ try:
|
||||
except KeyError:
|
||||
logger.setLevel(logging.ERROR)
|
||||
|
||||
# Test the logger
|
||||
#logger.info('wexpect imported; logger working')
|
||||
|
||||
|
||||
class ConsoleReader:
|
||||
class ConsoleReaderBase:
|
||||
"""Consol class (aka. client-side python class) for the child.
|
||||
|
||||
This class initialize the console starts the child in it and reads the console periodically.
|
||||
"""
|
||||
|
||||
def __init__(self, path, parent_pid, parent_tid, cp=None):
|
||||
"""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,
|
||||
buffer_size_x=80, buffer_size_y=16000, **kwargs):
|
||||
"""Initialize the console starts the child in it and reads the console periodically.
|
||||
|
||||
Args:
|
||||
path (str): Child's executable with arguments.
|
||||
parent_pid (int): Parent (aka. host) process process-ID
|
||||
parent_tid (int): Parent (aka. host) process thread-ID
|
||||
cp (:obj:, optional): Output console code page.
|
||||
"""
|
||||
self.lastRead = 0
|
||||
@ -79,25 +104,27 @@ class ConsoleReader:
|
||||
self.totalRead = 0
|
||||
self.__buffer = StringIO()
|
||||
self.__currentReadCo = win32console.PyCOORDType(0, 0)
|
||||
|
||||
self.pipe = None
|
||||
self.connection = None
|
||||
self.consin = None
|
||||
self.consout = None
|
||||
self.local_echo = True
|
||||
|
||||
logger.info("ConsoleReader started")
|
||||
logger.info("parent_tid %s" % parent_tid)
|
||||
self.create_pipe()
|
||||
|
||||
if cp:
|
||||
try:
|
||||
logger.info("Setting console output code page to %s" % cp)
|
||||
win32console.SetConsoleOutputCP(cp)
|
||||
logger.info("Console output code page: %s" % windll.kernel32.GetConsoleOutputCP())
|
||||
logger.info("Console output code page: %s" % ctypes.windll.kernel32.GetConsoleOutputCP())
|
||||
except Exception as e:
|
||||
logger.info(e)
|
||||
|
||||
try:
|
||||
self.create_connection(**kwargs)
|
||||
logger.info('Spawning %s' % path)
|
||||
try:
|
||||
self.initConsole()
|
||||
|
||||
time.sleep(1)
|
||||
si = win32process.GetStartupInfo()
|
||||
self.__childProcess, _, childPid, self.__tid = win32process.CreateProcess(None, path, None, None, False,
|
||||
0, None, None, si)
|
||||
@ -111,14 +138,18 @@ class ConsoleReader:
|
||||
time.sleep(.1)
|
||||
return
|
||||
|
||||
time.sleep(.1)
|
||||
time.sleep(.2)
|
||||
self.write('ls')
|
||||
self.write(os.linesep)
|
||||
|
||||
paused = False
|
||||
|
||||
|
||||
while True:
|
||||
consinfo = self.consout.GetConsoleScreenBufferInfo()
|
||||
cursorPos = consinfo['CursorPosition']
|
||||
self.send_to_host(self.readConsoleToCursor())
|
||||
s = self.get_from_host()
|
||||
self.write(s)
|
||||
|
||||
if win32process.GetExitCodeProcess(self.__childProcess) != win32con.STILL_ACTIVE:
|
||||
time.sleep(.1)
|
||||
@ -131,6 +162,10 @@ class ConsoleReader:
|
||||
"""
|
||||
if e.args[0] != winerror.ERROR_ACCESS_DENIED:
|
||||
logger.info(e)
|
||||
|
||||
time.sleep(.1)
|
||||
self.send_to_host(self.readConsoleToCursor())
|
||||
time.sleep(.1)
|
||||
return
|
||||
|
||||
if cursorPos.Y > maxconsoleY and not paused:
|
||||
@ -144,36 +179,63 @@ class ConsoleReader:
|
||||
paused = False
|
||||
|
||||
time.sleep(.1)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
except:
|
||||
logger.error(traceback.format_exc())
|
||||
time.sleep(.1)
|
||||
finally:
|
||||
self.close_connection()
|
||||
|
||||
def create_pipe(self):
|
||||
pid = win32process.GetCurrentProcessId()
|
||||
pipe_name = 'wexpect_pipe_c2s_{}'.format(pid)
|
||||
pipe_full_path = r'\\.\pipe\{}'.format(pipe_name)
|
||||
logger.info('Start pipe server: %s', pipe_full_path)
|
||||
self.pipe = win32pipe.CreateNamedPipe(
|
||||
pipe_full_path,
|
||||
win32pipe.PIPE_ACCESS_DUPLEX,
|
||||
win32pipe.PIPE_TYPE_MESSAGE | win32pipe.PIPE_READMODE_MESSAGE | win32pipe.PIPE_WAIT,
|
||||
1, 65536, 65536, 0, None)
|
||||
logger.info("waiting for client")
|
||||
win32pipe.ConnectNamedPipe(self.pipe, None)
|
||||
logger.info('got client')
|
||||
def write(self, s):
|
||||
"""Writes input into the child consoles input buffer."""
|
||||
|
||||
if len(s) == 0:
|
||||
return 0
|
||||
if s[-1] == '\n':
|
||||
s = s[:-1]
|
||||
records = [self.createKeyEvent(c) for c in str(s)]
|
||||
if not self.consout:
|
||||
return ""
|
||||
|
||||
def write_pipe(self, msg):
|
||||
# convert to bytes
|
||||
msg_bytes = str.encode(msg)
|
||||
win32file.WriteFile(self.pipe, msg_bytes)
|
||||
# Store the current cursor position to hide characters in local echo disabled mode (workaround).
|
||||
consinfo = self.consout.GetConsoleScreenBufferInfo()
|
||||
startCo = consinfo['CursorPosition']
|
||||
|
||||
# Send the string to console input
|
||||
wrote = self.consin.WriteConsoleInput(records)
|
||||
|
||||
# Wait until all input has been recorded by the console.
|
||||
ts = time.time()
|
||||
while self.consin.PeekConsoleInput(8) != ():
|
||||
if time.time() > ts + len(s) * .1 + .5:
|
||||
break
|
||||
time.sleep(.05)
|
||||
|
||||
def initConsole(self, consout=None):
|
||||
# Hide characters in local echo disabled mode (workaround).
|
||||
if not self.local_echo:
|
||||
self.consout.FillConsoleOutputCharacter(screenbufferfillchar, len(s), startCo)
|
||||
|
||||
return wrote
|
||||
|
||||
def createKeyEvent(self, char):
|
||||
"""Creates a single key record corrosponding to
|
||||
the ascii character char."""
|
||||
|
||||
evt = win32console.PyINPUT_RECORDType(win32console.KEY_EVENT)
|
||||
evt.KeyDown = True
|
||||
evt.Char = char
|
||||
evt.RepeatCount = 1
|
||||
return evt
|
||||
|
||||
def initConsole(self, consout=None, window_size_x=80, window_size_y=25, buffer_size_x=80,
|
||||
buffer_size_y=16000):
|
||||
if not consout:
|
||||
consout=self.getConsoleOut()
|
||||
|
||||
rect = win32console.PySMALL_RECTType(0, 0, 79, 24)
|
||||
self.consin = win32console.GetStdHandle(win32console.STD_INPUT_HANDLE)
|
||||
|
||||
rect = win32console.PySMALL_RECTType(0, 0, window_size_x-1, window_size_y-1)
|
||||
consout.SetConsoleWindowInfo(True, rect)
|
||||
size = win32console.PyCOORDType(80, 16000)
|
||||
size = win32console.PyCOORDType(buffer_size_x, buffer_size_y)
|
||||
consout.SetConsoleScreenBufferSize(size)
|
||||
pos = win32console.PyCOORDType(0, 0)
|
||||
# Use NUL as fill char because it displays as whitespace
|
||||
@ -238,11 +300,6 @@ class ConsoleReader:
|
||||
consinfo = self.consout.GetConsoleScreenBufferInfo()
|
||||
endCo = consinfo['CursorPosition']
|
||||
endCo= self.getCoord(0 + self.getOffset(endCo))
|
||||
# endCo.Y = endCo.Y+1
|
||||
# logger.info(endCo.Y+1)
|
||||
|
||||
logger.info(startCo)
|
||||
logger.info(endCo)
|
||||
|
||||
buff = []
|
||||
self.lastRead = 0
|
||||
@ -266,7 +323,6 @@ class ConsoleReader:
|
||||
|
||||
startCo = endPoint
|
||||
|
||||
logger.info(repr(s))
|
||||
return ''.join(buff)
|
||||
|
||||
|
||||
@ -304,7 +360,6 @@ class ConsoleReader:
|
||||
raw = raw[self.__consSize.X:]
|
||||
raw = ''.join(rawlist)
|
||||
s = self.parseData(raw)
|
||||
logger.debug(s)
|
||||
for i, line in enumerate(reversed(rawlist)):
|
||||
if line.endswith(screenbufferfillchar):
|
||||
# Record the Y offset where the most recent line break was detected
|
||||
@ -318,8 +373,6 @@ class ConsoleReader:
|
||||
logger.debug('isSamePos and self.lastReadData == s')
|
||||
s = ''
|
||||
|
||||
logger.debug('s: %r' % s)
|
||||
|
||||
if s:
|
||||
lastReadData = self.lastReadData
|
||||
pos = self.getOffset(self.__currentReadCo)
|
||||
@ -328,8 +381,6 @@ class ConsoleReader:
|
||||
# Detect changed lines
|
||||
self.__buffer.seek(pos)
|
||||
buf = self.__buffer.read()
|
||||
logger.debug('buf: %r' % buf)
|
||||
logger.debug('raw: %r' % raw)
|
||||
if raw.startswith(buf):
|
||||
# Line has grown
|
||||
rawslice = raw[len(buf):]
|
||||
@ -340,8 +391,7 @@ class ConsoleReader:
|
||||
self.lastRead = lastRead
|
||||
else:
|
||||
# Cursor has been repositioned
|
||||
s = '\r' + s
|
||||
logger.debug('s: %r' % s)
|
||||
s = '\r' + s
|
||||
self.__buffer.seek(pos)
|
||||
self.__buffer.truncate()
|
||||
self.__buffer.write(raw)
|
||||
@ -351,88 +401,80 @@ class ConsoleReader:
|
||||
|
||||
return s
|
||||
|
||||
class ConsoleReaderSocket(ConsoleReaderBase):
|
||||
|
||||
def client(path, pid, tid):
|
||||
try:
|
||||
w = ConsoleReader(path, pid, tid)
|
||||
time.sleep(1)
|
||||
w.write_pipe(w.readConsoleToCursor())
|
||||
except Exception:
|
||||
tb = traceback.format_exc()
|
||||
logger.error(tb)
|
||||
|
||||
|
||||
def pipe_client(conpid):
|
||||
pipe_name = 'wexpect_pipe_c2s_{}'.format(conpid)
|
||||
pipe_full_path = r'\\.\pipe\{}'.format(pipe_name)
|
||||
print('Trying to connect to pipe: {}'.format(pipe_full_path))
|
||||
quit = False
|
||||
|
||||
while not quit:
|
||||
|
||||
def create_connection(self, **kwargs):
|
||||
|
||||
self.port = kwargs['port']
|
||||
# Create a TCP/IP socket
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
server_address = ('localhost', self.port)
|
||||
self.sock.bind(server_address)
|
||||
logger.info(f'Socket started at port: {self.port}')
|
||||
|
||||
# Listen for incoming connections
|
||||
self.sock.listen(1)
|
||||
self.connection, client_address = self.sock.accept()
|
||||
self.connection.settimeout(.2)
|
||||
logger.info(f'Client connected: {client_address}')
|
||||
|
||||
def close_connection(self):
|
||||
if self.connection:
|
||||
self.connection.close()
|
||||
|
||||
def send_to_host(self, msg):
|
||||
# convert to bytes
|
||||
msg_bytes = str.encode(msg)
|
||||
self.connection.sendall(msg_bytes)
|
||||
|
||||
def get_from_host(self):
|
||||
try:
|
||||
handle = win32file.CreateFile(
|
||||
pipe_full_path,
|
||||
win32file.GENERIC_READ | win32file.GENERIC_WRITE,
|
||||
0,
|
||||
None,
|
||||
win32file.OPEN_EXISTING,
|
||||
0,
|
||||
None
|
||||
)
|
||||
res = win32pipe.SetNamedPipeHandleState(handle, win32pipe.PIPE_READMODE_MESSAGE, None, None)
|
||||
if res == 0:
|
||||
print(f"SetNamedPipeHandleState return code: {res}")
|
||||
while True:
|
||||
resp = win32file.ReadFile(handle, 64*1024)
|
||||
print(f"message: {resp}")
|
||||
except pywintypes.error as e:
|
||||
if e.args[0] == 2:
|
||||
print("no pipe, trying again in a bit")
|
||||
time.sleep(0.2)
|
||||
elif e.args[0] == 109:
|
||||
print("broken pipe, bye bye")
|
||||
quit = True
|
||||
|
||||
|
||||
def main():
|
||||
pass
|
||||
msg = self.connection.recv(4096)
|
||||
except socket.timeout as e:
|
||||
err = e.args[0]
|
||||
# this next if/else is a bit redundant, but illustrates how the
|
||||
# timeout exception is setup
|
||||
if err == 'timed out':
|
||||
logger.debug('recv timed out, retry later')
|
||||
return ''
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
if len(msg) == 0:
|
||||
raise Exception('orderly shutdown on server end')
|
||||
else:
|
||||
# got a message do something :)
|
||||
return msg.decode()
|
||||
|
||||
|
||||
class ConsoleReaderPipe(ConsoleReaderBase):
|
||||
def create_connection(self):
|
||||
pid = win32process.GetCurrentProcessId()
|
||||
pipe_name = 'wexpect_{}'.format(pid)
|
||||
pipe_full_path = r'\\.\pipe\{}'.format(pipe_name)
|
||||
logger.info('Start pipe server: %s', pipe_full_path)
|
||||
self.pipe = win32pipe.CreateNamedPipe(
|
||||
pipe_full_path,
|
||||
win32pipe.PIPE_ACCESS_DUPLEX,
|
||||
win32pipe.PIPE_TYPE_MESSAGE | win32pipe.PIPE_READMODE_MESSAGE | win32pipe.PIPE_WAIT,
|
||||
1, 65536, 65536, 0, None)
|
||||
logger.info("waiting for client")
|
||||
win32pipe.ConnectNamedPipe(self.pipe, None)
|
||||
logger.info('got client')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
si = win32process.GetStartupInfo()
|
||||
si.dwFlags = win32process.STARTF_USESHOWWINDOW
|
||||
si.wShowWindow = win32con.SW_HIDE
|
||||
pyargs = ['-c']
|
||||
|
||||
dirname = os.path.dirname(sys.executable
|
||||
if getattr(sys, 'frozen', False) else
|
||||
os.path.abspath(__file__))
|
||||
|
||||
# client('uname')
|
||||
|
||||
pid = win32process.GetCurrentProcessId()
|
||||
tid = win32api.GetCurrentThreadId()
|
||||
|
||||
commandLine = '"%s" %s "%s"' % (os.path.join(dirname, 'python.exe')
|
||||
if getattr(sys, 'frozen', False) else
|
||||
os.path.join(os.path.dirname(sys.executable), 'python.exe'),
|
||||
' '.join(pyargs),
|
||||
"import sys;"
|
||||
"sys.path.append('D:\\\\bt\\\\wexpect');"
|
||||
"import console_reader;"
|
||||
"import time;"
|
||||
"console_reader.client('uname', {tid}, {pid});".format(pid=pid, tid=tid)
|
||||
)
|
||||
|
||||
def close_connection(self):
|
||||
if self.pipe:
|
||||
raise Exception(f'Unimplemented close')
|
||||
|
||||
print(commandLine)
|
||||
def send_to_host(self, msg):
|
||||
# convert to bytes
|
||||
msg_bytes = str.encode(msg)
|
||||
win32file.WriteFile(self.pipe, msg_bytes)
|
||||
|
||||
__oproc, _, conpid, __otid = win32process.CreateProcess(None, commandLine, None, None, False,
|
||||
win32process.CREATE_NEW_CONSOLE, None, None, si)
|
||||
# time.sleep(3)
|
||||
pipe_client(conpid)
|
||||
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
def get_from_host(self):
|
||||
resp = win32file.ReadFile(self.pipe, 64*1024)
|
||||
ret = resp[1]
|
||||
return ret
|
||||
|
||||
|
849
wexpect/spawn.py
Normal file
849
wexpect/spawn.py
Normal file
@ -0,0 +1,849 @@
|
||||
"""Wexpect is a Windows variant of pexpect https://pexpect.readthedocs.io.
|
||||
|
||||
Wexpect is a Python module for spawning child applications and controlling
|
||||
them automatically. Wexpect can be used for automating interactive applications
|
||||
such as ssh, ftp, passwd, telnet, etc. It can be used to a automate setup
|
||||
scripts for duplicating software package installations on different servers. It
|
||||
can be used for automated software testing. Wexpect is in the spirit of Don
|
||||
Libes' Expect, but Wexpect is pure Python. Other Expect-like modules for Python
|
||||
require TCL and Expect or require C extensions to be compiled. Wexpect does not
|
||||
use C, Expect, or TCL extensions.
|
||||
|
||||
There are two main interfaces to Wexpect -- the function, run() and the class,
|
||||
spawn. You can call the run() function to execute a command and return the
|
||||
output. This is a handy replacement for os.system().
|
||||
|
||||
For example::
|
||||
|
||||
wexpect.run('ls -la')
|
||||
|
||||
The more powerful interface is the spawn class. You can use this to spawn an
|
||||
external child command and then interact with the child by sending lines and
|
||||
expecting responses.
|
||||
|
||||
For example::
|
||||
|
||||
child = wexpect.spawn('scp foo myname@host.example.com:.')
|
||||
child.expect('Password:')
|
||||
child.sendline(mypassword)
|
||||
|
||||
This works even for commands that ask for passwords or other input outside of
|
||||
the normal stdio streams.
|
||||
|
||||
Spawn file is the main (aka. host) class of the wexpect. The user call Spawn, which
|
||||
start the console_reader as a subprocess, which starts the read child.
|
||||
|
||||
Credits: Noah Spurrier, Richard Holden, Marco Molteni, Kimberley Burchett,
|
||||
Robert Stone, Hartmut Goebel, Chad Schroeder, Erick Tryzelaar, Dave Kirby, Ids
|
||||
vander Molen, George Todd, Noel Taylor, Nicolas D. Cesar, Alexander Gattin,
|
||||
Geoffrey Marshall, Francisco Lourenco, Glen Mabey, Karthik Gurusamy, Fernando
|
||||
Perez, Corey Minyard, Jon Cohen, Guillaume Chazarain, Andrew Ryan, Nick
|
||||
Craig-Wood, Andrew Stone, Jorgen Grahn, Benedek Racz
|
||||
|
||||
Free, open source, and all that good stuff.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
Wexpect Copyright (c) 2019 Benedek Racz
|
||||
|
||||
"""
|
||||
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
import shutil
|
||||
import re
|
||||
import traceback
|
||||
|
||||
import pywintypes
|
||||
import win32process
|
||||
import win32con
|
||||
import win32api
|
||||
import win32file
|
||||
import winerror
|
||||
import win32pipe
|
||||
import socket
|
||||
|
||||
from wexpect_util import ExceptionPexpect
|
||||
from wexpect_util import EOF
|
||||
from wexpect_util import TIMEOUT
|
||||
from wexpect_util import split_command_line
|
||||
|
||||
|
||||
class Spawn:
|
||||
def __init__(self, command, args=[], timeout=30, maxread=60000, searchwindowsize=None,
|
||||
logfile=None, cwd=None, env=None, codepage=None, echo=True):
|
||||
"""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
|
||||
is empty then command will be parsed (split on spaces) and args will be
|
||||
set to parsed arguments.
|
||||
|
||||
The pid and child_fd of this object get set by this method.
|
||||
Note that it is difficult for this method to fail.
|
||||
You cannot detect if the child process cannot start.
|
||||
So the only way you can tell if the child process started
|
||||
or not is to try to read from the file descriptor. If you get
|
||||
EOF immediately then it means that the child is already dead.
|
||||
That may not necessarily be bad because you may haved spawned a child
|
||||
that performs some task; creates no stdout output; and then dies.
|
||||
"""
|
||||
self.searcher = None
|
||||
self.ignorecase = False
|
||||
self.before = None
|
||||
self.after = None
|
||||
self.match = None
|
||||
self.match_index = None
|
||||
self.terminated = True
|
||||
self.exitstatus = None
|
||||
self.status = None # status returned by os.waitpid
|
||||
self.flag_eof = False
|
||||
self.flag_child_finished = False
|
||||
self.pid = None
|
||||
self.child_fd = -1 # initially closed
|
||||
self.timeout = timeout
|
||||
self.delimiter = EOF
|
||||
self.cwd = cwd
|
||||
self.env = env
|
||||
self.maxread = maxread # max bytes to read at one time into buffer
|
||||
self.delaybeforesend = 0.05 # Sets sleep time used just before sending data to child. Time in seconds.
|
||||
self.delayafterterminate = 0.1 # Sets delay in terminate() method to allow kernel time to update process status. Time in seconds.
|
||||
self.flag_child_finished = False
|
||||
self.buffer = '' # This is the read buffer. See maxread.
|
||||
self.searchwindowsize = searchwindowsize # Anything before searchwindowsize point is preserved, but not searched.
|
||||
|
||||
|
||||
# If command is an int type then it may represent a file descriptor.
|
||||
if type(command) == type(0):
|
||||
raise ExceptionPexpect ('Command is an int type. If this is a file descriptor then maybe you want to use fdpexpect.fdspawn which takes an existing file descriptor instead of a command string.')
|
||||
|
||||
if type (args) != type([]):
|
||||
raise TypeError ('The argument, args, must be a list.')
|
||||
|
||||
if args == []:
|
||||
self.args = split_command_line(command)
|
||||
self.command = self.args[0]
|
||||
else:
|
||||
self.args = args[:] # work with a copy
|
||||
self.args.insert (0, command)
|
||||
self.command = command
|
||||
|
||||
command_with_path = shutil.which(self.command)
|
||||
if command_with_path is None:
|
||||
raise ExceptionPexpect ('The command was not found or was not executable: %s.' % self.command)
|
||||
self.command = command_with_path
|
||||
self.args[0] = self.command
|
||||
|
||||
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.closed = False
|
||||
|
||||
self.child_fd = self.startChild(self.args, self.env)
|
||||
self.connect_to_child('localhost', 4321)
|
||||
|
||||
def __del__(self):
|
||||
"""This makes sure that no system resources are left open. Python only
|
||||
garbage collects Python objects, not the child console."""
|
||||
|
||||
try:
|
||||
self.terminate()
|
||||
except:
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
|
||||
"""This returns a human-readable string that represents the state of
|
||||
the object. """
|
||||
|
||||
s = []
|
||||
s.append(repr(self))
|
||||
s.append('command: ' + str(self.command))
|
||||
s.append('args: ' + str(self.args))
|
||||
s.append('searcher: ' + str(self.searcher))
|
||||
s.append('buffer (last 100 chars): ' + str(self.buffer)[-100:])
|
||||
s.append('before (last 100 chars): ' + str(self.before)[-100:])
|
||||
s.append('after: ' + str(self.after))
|
||||
s.append('match: ' + str(self.match))
|
||||
s.append('match_index: ' + str(self.match_index))
|
||||
s.append('exitstatus: ' + str(self.exitstatus))
|
||||
s.append('flag_eof: ' + str(self.flag_eof))
|
||||
s.append('pid: ' + str(self.pid))
|
||||
s.append('child_fd: ' + str(self.child_fd))
|
||||
s.append('closed: ' + str(self.closed))
|
||||
s.append('timeout: ' + str(self.timeout))
|
||||
s.append('delimiter: ' + str(self.delimiter))
|
||||
s.append('maxread: ' + str(self.maxread))
|
||||
s.append('ignorecase: ' + str(self.ignorecase))
|
||||
s.append('searchwindowsize: ' + str(self.searchwindowsize))
|
||||
s.append('delaybeforesend: ' + str(self.delaybeforesend))
|
||||
s.append('delayafterterminate: ' + str(self.delayafterterminate))
|
||||
return '\n'.join(s)
|
||||
|
||||
def fileno (self): # File-like object.
|
||||
"""There is no child fd."""
|
||||
|
||||
return 0
|
||||
|
||||
def terminate(self, force=False):
|
||||
"""Terminate the child. Force not used. """
|
||||
|
||||
if not self.isalive():
|
||||
return True
|
||||
|
||||
win32api.TerminateProcess(self.conproc, 1)
|
||||
time.sleep(self.delayafterterminate)
|
||||
if not self.isalive():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def close(self, force=True): # File-like object.
|
||||
""" Closes the child console."""
|
||||
|
||||
self.closed = self.terminate(force)
|
||||
if not self.closed:
|
||||
raise ExceptionPexpect ('close() could not terminate the child using terminate()')
|
||||
self.closed = True
|
||||
|
||||
def read (self, size = -1): # File-like object.
|
||||
"""This reads at most "size" bytes from the file (less if the read hits
|
||||
EOF before obtaining size bytes). If the size argument is negative or
|
||||
omitted, read all data until EOF is reached. The bytes are returned as
|
||||
a string object. An empty string is returned when EOF is encountered
|
||||
immediately. """
|
||||
|
||||
if size == 0:
|
||||
return ''
|
||||
if size < 0:
|
||||
self.expect (self.delimiter) # delimiter default is EOF
|
||||
return self.before
|
||||
|
||||
# I could have done this more directly by not using expect(), but
|
||||
# I deliberately decided to couple read() to expect() so that
|
||||
# I would catch any bugs early and ensure consistant behavior.
|
||||
# It's a little less efficient, but there is less for me to
|
||||
# worry about if I have to later modify read() or expect().
|
||||
# Note, it's OK if size==-1 in the regex. That just means it
|
||||
# will never match anything in which case we stop only on EOF.
|
||||
cre = re.compile('.{%d}' % size, re.DOTALL)
|
||||
index = self.expect ([cre, self.delimiter]) # delimiter default is EOF
|
||||
if index == 0:
|
||||
return self.after ### self.before should be ''. Should I assert this?
|
||||
return self.before
|
||||
|
||||
def readline (self, size = -1): # File-like object.
|
||||
|
||||
"""This reads and returns one entire line. A trailing newline is kept
|
||||
in the string, but may be absent when a file ends with an incomplete
|
||||
line. Note: This readline() looks for a \\r\\n pair even on UNIX
|
||||
because this is what the pseudo tty device returns. So contrary to what
|
||||
you may expect you will receive the newline as \\r\\n. An empty string
|
||||
is returned when EOF is hit immediately. Currently, the size argument is
|
||||
mostly ignored, so this behavior is not standard for a file-like
|
||||
object. If size is 0 then an empty string is returned. """
|
||||
|
||||
if size == 0:
|
||||
return ''
|
||||
index = self.expect (['\r\n', self.delimiter]) # delimiter default is EOF
|
||||
if index == 0:
|
||||
return self.before + '\r\n'
|
||||
else:
|
||||
return self.before
|
||||
|
||||
def __iter__ (self): # File-like object.
|
||||
|
||||
"""This is to support iterators over a file-like object.
|
||||
"""
|
||||
|
||||
return self
|
||||
|
||||
def read_nonblocking (self, size = 1):
|
||||
"""This reads at most size characters from the child application. If
|
||||
the end of file is read then an EOF exception will be raised.
|
||||
|
||||
This is not effected by the 'size' parameter, so if you call
|
||||
read_nonblocking(size=100, timeout=30) and only one character is
|
||||
available right away then one character will be returned immediately.
|
||||
It will not wait for 30 seconds for another 99 characters to come in.
|
||||
|
||||
This is a wrapper around Wtty.read(). """
|
||||
|
||||
if self.closed:
|
||||
raise ValueError ('I/O operation on closed file in read_nonblocking().')
|
||||
|
||||
try:
|
||||
# The real child and it's console are two different process. The console dies 0.1 sec
|
||||
# later to be able to read the child's last output (before EOF). So here we check
|
||||
# isalive() (which checks the real child.) and try a last read on the console. To catch
|
||||
# the last output.
|
||||
# The flag_child_finished flag shows that this is the second trial, where we raise the EOF.
|
||||
if self.flag_child_finished:
|
||||
raise EOF('self.flag_child_finished')
|
||||
if not self.isalive():
|
||||
self.flag_child_finished = True
|
||||
|
||||
s = self.sock.recv(size)
|
||||
except EOF:
|
||||
self.flag_eof = True
|
||||
raise
|
||||
|
||||
return s.decode()
|
||||
|
||||
def __next__ (self): # File-like object.
|
||||
|
||||
"""This is to support iterators over a file-like object.
|
||||
"""
|
||||
|
||||
result = self.readline()
|
||||
if self.after == self.delimiter:
|
||||
raise StopIteration
|
||||
return result
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.terminate()
|
||||
|
||||
def readlines (self, sizehint = -1): # File-like object.
|
||||
|
||||
"""This reads until EOF using readline() and returns a list containing
|
||||
the lines thus read. The optional "sizehint" argument is ignored. """
|
||||
|
||||
lines = []
|
||||
while True:
|
||||
line = self.readline()
|
||||
if not line:
|
||||
break
|
||||
lines.append(line)
|
||||
return lines
|
||||
|
||||
def isatty(self): # File-like object.
|
||||
"""The child is always created with a console."""
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def pipe_client(self, conpid):
|
||||
pipe_name = 'wexpect_{}'.format(conpid)
|
||||
pipe_full_path = r'\\.\pipe\{}'.format(pipe_name)
|
||||
print('Trying to connect to pipe: {}'.format(pipe_full_path))
|
||||
quit = False
|
||||
|
||||
while not quit:
|
||||
try:
|
||||
handle = win32file.CreateFile(
|
||||
pipe_full_path,
|
||||
win32file.GENERIC_READ | win32file.GENERIC_WRITE,
|
||||
0,
|
||||
None,
|
||||
win32file.OPEN_EXISTING,
|
||||
0,
|
||||
None
|
||||
)
|
||||
print("pipe found!")
|
||||
res = win32pipe.SetNamedPipeHandleState(handle, win32pipe.PIPE_READMODE_MESSAGE, None, None)
|
||||
if res == 0:
|
||||
print(f"SetNamedPipeHandleState return code: {res}")
|
||||
while True:
|
||||
resp = win32file.ReadFile(handle, 64*1024)
|
||||
print(f"message: {resp}")
|
||||
win32file.WriteFile(handle, b'back')
|
||||
except pywintypes.error as e:
|
||||
if e.args[0] == winerror.ERROR_FILE_NOT_FOUND: #2
|
||||
print("no pipe, trying again in a bit later")
|
||||
time.sleep(0.2)
|
||||
elif e.args[0] == winerror.ERROR_BROKEN_PIPE: #109
|
||||
print("broken pipe, bye bye")
|
||||
quit = True
|
||||
elif e.args[0] == winerror.ERROR_NO_DATA:
|
||||
'''232 (0xE8)
|
||||
The pipe is being closed.
|
||||
'''
|
||||
print("The pipe is being closed.")
|
||||
quit = True
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
def connect_to_child(self, host, port):
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.sock.connect((host, port))
|
||||
|
||||
|
||||
def startChild(self, args, env):
|
||||
si = win32process.GetStartupInfo()
|
||||
si.dwFlags = win32process.STARTF_USESHOWWINDOW
|
||||
si.wShowWindow = win32con.SW_HIDE
|
||||
|
||||
dirname = os.path.dirname(sys.executable
|
||||
if getattr(sys, 'frozen', False) else
|
||||
os.path.abspath(__file__))
|
||||
spath = [os.path.dirname(dirname)]
|
||||
pyargs = ['-c']
|
||||
if getattr(sys, 'frozen', False):
|
||||
# If we are running 'frozen', add library.zip and lib\library.zip
|
||||
# to sys.path
|
||||
# py2exe: Needs appropriate 'zipfile' option in setup script and
|
||||
# 'bundle_files' 3
|
||||
spath.append(os.path.join(dirname, 'library.zip'))
|
||||
spath.append(os.path.join(dirname, 'library.zip',
|
||||
os.path.basename(os.path.splitext(sys.executable)[0])))
|
||||
if os.path.isdir(os.path.join(dirname, 'lib')):
|
||||
dirname = os.path.join(dirname, 'lib')
|
||||
spath.append(os.path.join(dirname, 'library.zip'))
|
||||
spath.append(os.path.join(dirname, 'library.zip',
|
||||
os.path.basename(os.path.splitext(sys.executable)[0])))
|
||||
pyargs.insert(0, '-S') # skip 'import site'
|
||||
|
||||
|
||||
pid = win32process.GetCurrentProcessId()
|
||||
|
||||
commandLine = '"%s" %s "%s"' % (os.path.join(dirname, 'python.exe')
|
||||
if getattr(sys, 'frozen', False) else
|
||||
os.path.join(os.path.dirname(sys.executable), 'python.exe'),
|
||||
' '.join(pyargs),
|
||||
"import sys;"
|
||||
f"sys.path = {spath} + sys.path;"
|
||||
"import wexpect;"
|
||||
"import time;"
|
||||
f"wexpect.ConsoleReaderSocket(wexpect.join_args({args}), {pid}, port=4321);"
|
||||
)
|
||||
|
||||
print(commandLine)
|
||||
|
||||
self.conproc, _, conpid, __otid = win32process.CreateProcess(None, commandLine, None, None, False,
|
||||
win32process.CREATE_NEW_CONSOLE, None, None, si)
|
||||
|
||||
|
||||
def isalive(self, console=True):
|
||||
"""True if the child is still alive, false otherwise"""
|
||||
|
||||
if console:
|
||||
return win32process.GetExitCodeProcess(self.conproc) == win32con.STILL_ACTIVE
|
||||
else:
|
||||
return win32process.GetExitCodeProcess(self.__childProcess) == win32con.STILL_ACTIVE
|
||||
|
||||
|
||||
def write(self, s): # File-like object.
|
||||
|
||||
"""This is similar to send() except that there is no return value.
|
||||
"""
|
||||
|
||||
self.send(s)
|
||||
|
||||
def writelines (self, sequence): # File-like object.
|
||||
|
||||
"""This calls write() for each element in the sequence. The sequence
|
||||
can be any iterable object producing strings, typically a list of
|
||||
strings. This does not add line separators There is no return value.
|
||||
"""
|
||||
|
||||
for s in sequence:
|
||||
self.write(s)
|
||||
|
||||
def sendline(self, s=''):
|
||||
|
||||
"""This is like send(), but it adds a line feed (os.linesep). This
|
||||
returns the number of bytes written. """
|
||||
|
||||
n = self.send(s)
|
||||
n = n + self.send(b'\r\n')
|
||||
return n
|
||||
|
||||
def sendeof(self):
|
||||
|
||||
"""This sends an EOF to the child. This sends a character which causes
|
||||
the pending parent output buffer to be sent to the waiting child
|
||||
program without waiting for end-of-line. If it is the first character
|
||||
of the line, the read() in the user program returns 0, which signifies
|
||||
end-of-file. This means to work as expected a sendeof() has to be
|
||||
called at the beginning of a line. This method does not send a newline.
|
||||
It is the responsibility of the caller to ensure the eof is sent at the
|
||||
beginning of a line. """
|
||||
|
||||
# platform does not define VEOF so assume CTRL-D
|
||||
char = chr(4)
|
||||
self.send(char)
|
||||
|
||||
def send(self, s):
|
||||
"""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
|
||||
the log. """
|
||||
|
||||
time.sleep(self.delaybeforesend)
|
||||
self.sock.sendall(s)
|
||||
return len(s)
|
||||
|
||||
|
||||
def compile_pattern_list(self, patterns):
|
||||
|
||||
"""This compiles a pattern-string or a list of pattern-strings.
|
||||
Patterns must be a StringType, EOF, TIMEOUT, SRE_Pattern, or a list of
|
||||
those. Patterns may also be None which results in an empty list (you
|
||||
might do this if waiting for an EOF or TIMEOUT condition without
|
||||
expecting any pattern).
|
||||
|
||||
This is used by expect() when calling expect_list(). Thus expect() is
|
||||
nothing more than::
|
||||
|
||||
cpl = self.compile_pattern_list(pl)
|
||||
return self.expect_list(cpl, timeout)
|
||||
|
||||
If you are using expect() within a loop it may be more
|
||||
efficient to compile the patterns first and then call expect_list().
|
||||
This avoid calls in a loop to compile_pattern_list()::
|
||||
|
||||
cpl = self.compile_pattern_list(my_pattern)
|
||||
while some_condition:
|
||||
...
|
||||
i = self.expect_list(clp, timeout)
|
||||
...
|
||||
"""
|
||||
|
||||
if patterns is None:
|
||||
return []
|
||||
if type(patterns) is not list:
|
||||
patterns = [patterns]
|
||||
|
||||
compile_flags = re.DOTALL # Allow dot to match \n
|
||||
if self.ignorecase:
|
||||
compile_flags = compile_flags | re.IGNORECASE
|
||||
compiled_pattern_list = []
|
||||
for p in patterns:
|
||||
if type(p) in (str,):
|
||||
compiled_pattern_list.append(re.compile(p, compile_flags))
|
||||
elif p is EOF:
|
||||
compiled_pattern_list.append(EOF)
|
||||
elif p is TIMEOUT:
|
||||
compiled_pattern_list.append(TIMEOUT)
|
||||
elif type(p) is type(re.compile('')):
|
||||
compiled_pattern_list.append(p)
|
||||
else:
|
||||
raise TypeError ('Argument must be one of StringTypes, EOF, TIMEOUT, SRE_Pattern, or a list of those type. %s' % str(type(p)))
|
||||
|
||||
return compiled_pattern_list
|
||||
|
||||
def expect(self, pattern, timeout = -1, searchwindowsize=None):
|
||||
|
||||
"""This seeks through the stream until a pattern is matched. The
|
||||
pattern is overloaded and may take several types. The pattern can be a
|
||||
StringType, EOF, a compiled re, or a list of any of those types.
|
||||
Strings will be compiled to re types. This returns the index into the
|
||||
pattern list. If the pattern was not a list this returns index 0 on a
|
||||
successful match. This may raise exceptions for EOF or TIMEOUT. To
|
||||
avoid the EOF or TIMEOUT exceptions add EOF or TIMEOUT to the pattern
|
||||
list. That will cause expect to match an EOF or TIMEOUT condition
|
||||
instead of raising an exception.
|
||||
|
||||
If you pass a list of patterns and more than one matches, the first match
|
||||
in the stream is chosen. If more than one pattern matches at that point,
|
||||
the leftmost in the pattern list is chosen. For example::
|
||||
|
||||
# the input is 'foobar'
|
||||
index = p.expect (['bar', 'foo', 'foobar'])
|
||||
# returns 1 ('foo') even though 'foobar' is a "better" match
|
||||
|
||||
Please note, however, that buffering can affect this behavior, since
|
||||
input arrives in unpredictable chunks. For example::
|
||||
|
||||
# the input is 'foobar'
|
||||
index = p.expect (['foobar', 'foo'])
|
||||
# returns 0 ('foobar') if all input is available at once,
|
||||
# but returs 1 ('foo') if parts of the final 'bar' arrive late
|
||||
|
||||
After a match is found the instance attributes 'before', 'after' and
|
||||
'match' will be set. You can see all the data read before the match in
|
||||
'before'. You can see the data that was matched in 'after'. The
|
||||
re.MatchObject used in the re match will be in 'match'. If an error
|
||||
occurred then 'before' will be set to all the data read so far and
|
||||
'after' and 'match' will be None.
|
||||
|
||||
If timeout is -1 then timeout will be set to the self.timeout value.
|
||||
|
||||
A list entry may be EOF or TIMEOUT instead of a string. This will
|
||||
catch these exceptions and return the index of the list entry instead
|
||||
of raising the exception. The attribute 'after' will be set to the
|
||||
exception type. The attribute 'match' will be None. This allows you to
|
||||
write code like this::
|
||||
|
||||
index = p.expect (['good', 'bad', wexpect.EOF, wexpect.TIMEOUT])
|
||||
if index == 0:
|
||||
do_something()
|
||||
elif index == 1:
|
||||
do_something_else()
|
||||
elif index == 2:
|
||||
do_some_other_thing()
|
||||
elif index == 3:
|
||||
do_something_completely_different()
|
||||
|
||||
instead of code like this::
|
||||
|
||||
try:
|
||||
index = p.expect (['good', 'bad'])
|
||||
if index == 0:
|
||||
do_something()
|
||||
elif index == 1:
|
||||
do_something_else()
|
||||
except EOF:
|
||||
do_some_other_thing()
|
||||
except TIMEOUT:
|
||||
do_something_completely_different()
|
||||
|
||||
These two forms are equivalent. It all depends on what you want. You
|
||||
can also just expect the EOF if you are waiting for all output of a
|
||||
child to finish. For example::
|
||||
|
||||
p = wexpect.spawn('/bin/ls')
|
||||
p.expect (wexpect.EOF)
|
||||
print p.before
|
||||
|
||||
If you are trying to optimize for speed then see expect_list().
|
||||
"""
|
||||
|
||||
compiled_pattern_list = self.compile_pattern_list(pattern)
|
||||
return self.expect_list(compiled_pattern_list, timeout, searchwindowsize)
|
||||
|
||||
def expect_list(self, pattern_list, timeout = -1, searchwindowsize = -1):
|
||||
|
||||
"""This takes a list of compiled regular expressions and returns the
|
||||
index into the pattern_list that matched the child output. The list may
|
||||
also contain EOF or TIMEOUT (which are not compiled regular
|
||||
expressions). This method is similar to the expect() method except that
|
||||
expect_list() does not recompile the pattern list on every call. This
|
||||
may help if you are trying to optimize for speed, otherwise just use
|
||||
the expect() method. This is called by expect(). If timeout==-1 then
|
||||
the self.timeout value is used. If searchwindowsize==-1 then the
|
||||
self.searchwindowsize value is used. """
|
||||
|
||||
return self.expect_loop(searcher_re(pattern_list), timeout, searchwindowsize)
|
||||
|
||||
def expect_exact(self, pattern_list, timeout = -1, searchwindowsize = -1):
|
||||
|
||||
"""This is similar to expect(), but uses plain string matching instead
|
||||
of compiled regular expressions in 'pattern_list'. The 'pattern_list'
|
||||
may be a string; a list or other sequence of strings; or TIMEOUT and
|
||||
EOF.
|
||||
|
||||
This call might be faster than expect() for two reasons: string
|
||||
searching is faster than RE matching and it is possible to limit the
|
||||
search to just the end of the input buffer.
|
||||
|
||||
This method is also useful when you don't want to have to worry about
|
||||
escaping regular expression characters that you want to match."""
|
||||
|
||||
if not isinstance(pattern_list, list):
|
||||
pattern_list = [pattern_list]
|
||||
|
||||
for p in pattern_list:
|
||||
if type(p) not in (str,) and p not in (TIMEOUT, EOF):
|
||||
raise TypeError ('Argument must be one of StringTypes, EOF, TIMEOUT, or a list of those type. %s' % str(type(p)))
|
||||
|
||||
return self.expect_loop(searcher_string(pattern_list), timeout, searchwindowsize)
|
||||
|
||||
def expect_loop(self, searcher, timeout = -1, searchwindowsize = -1):
|
||||
|
||||
"""This is the common loop used inside expect. The 'searcher' should be
|
||||
an instance of searcher_re or searcher_string, which describes how and what
|
||||
to search for in the input.
|
||||
|
||||
See expect() for other arguments, return value and exceptions. """
|
||||
|
||||
self.searcher = searcher
|
||||
|
||||
if timeout == -1:
|
||||
timeout = self.timeout
|
||||
if timeout is not None:
|
||||
end_time = time.time() + timeout
|
||||
if searchwindowsize == -1:
|
||||
searchwindowsize = self.searchwindowsize
|
||||
|
||||
try:
|
||||
incoming = self.buffer
|
||||
freshlen = len(incoming)
|
||||
while True: # Keep reading until exception or return.
|
||||
index = searcher.search(incoming, freshlen, searchwindowsize)
|
||||
if index >= 0:
|
||||
self.buffer = incoming[searcher.end : ]
|
||||
self.before = incoming[ : searcher.start]
|
||||
self.after = incoming[searcher.start : searcher.end]
|
||||
self.match = searcher.match
|
||||
self.match_index = index
|
||||
return self.match_index
|
||||
# No match at this point
|
||||
if timeout is not None and end_time < time.time():
|
||||
raise TIMEOUT ('Timeout exceeded in expect_any().')
|
||||
# Still have time left, so read more data
|
||||
c = self.read_nonblocking(self.maxread)
|
||||
freshlen = len(c)
|
||||
time.sleep (0.01)
|
||||
incoming += c
|
||||
except EOF as e:
|
||||
self.buffer = ''
|
||||
self.before = incoming
|
||||
self.after = EOF
|
||||
index = searcher.eof_index
|
||||
if index >= 0:
|
||||
self.match = EOF
|
||||
self.match_index = index
|
||||
return self.match_index
|
||||
else:
|
||||
self.match = None
|
||||
self.match_index = None
|
||||
raise EOF (str(e) + '\n' + str(self))
|
||||
except TIMEOUT as e:
|
||||
self.buffer = incoming
|
||||
self.before = incoming
|
||||
self.after = TIMEOUT
|
||||
index = searcher.timeout_index
|
||||
if index >= 0:
|
||||
self.match = TIMEOUT
|
||||
self.match_index = index
|
||||
return self.match_index
|
||||
else:
|
||||
self.match = None
|
||||
self.match_index = None
|
||||
raise TIMEOUT (str(e) + '\n' + str(self))
|
||||
except:
|
||||
self.before = incoming
|
||||
self.after = None
|
||||
self.match = None
|
||||
self.match_index = None
|
||||
raise
|
||||
|
||||
|
||||
class searcher_re (object):
|
||||
|
||||
"""This is regular expression string search helper for the
|
||||
spawn.expect_any() method.
|
||||
|
||||
Attributes:
|
||||
|
||||
eof_index - index of EOF, or -1
|
||||
timeout_index - index of TIMEOUT, or -1
|
||||
|
||||
After a successful match by the search() method the following attributes
|
||||
are available:
|
||||
|
||||
start - index into the buffer, first byte of match
|
||||
end - index into the buffer, first byte after match
|
||||
match - the re.match object returned by a succesful re.search
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, patterns):
|
||||
|
||||
"""This creates an instance that searches for 'patterns' Where
|
||||
'patterns' may be a list or other sequence of compiled regular
|
||||
expressions, or the EOF or TIMEOUT types."""
|
||||
|
||||
self.eof_index = -1
|
||||
self.timeout_index = -1
|
||||
self._searches = []
|
||||
for n, s in zip(list(range(len(patterns))), patterns):
|
||||
if s is EOF:
|
||||
self.eof_index = n
|
||||
continue
|
||||
if s is TIMEOUT:
|
||||
self.timeout_index = n
|
||||
continue
|
||||
self._searches.append((n, s))
|
||||
|
||||
def __str__(self):
|
||||
|
||||
"""This returns a human-readable string that represents the state of
|
||||
the object."""
|
||||
|
||||
ss = [ (n,' %d: re.compile("%s")' % (n,str(s.pattern))) for n,s in self._searches]
|
||||
ss.append((-1,'searcher_re:'))
|
||||
if self.eof_index >= 0:
|
||||
ss.append ((self.eof_index,' %d: EOF' % self.eof_index))
|
||||
if self.timeout_index >= 0:
|
||||
ss.append ((self.timeout_index,' %d: TIMEOUT' % self.timeout_index))
|
||||
ss.sort()
|
||||
ss = list(zip(*ss))[1]
|
||||
return '\n'.join(ss)
|
||||
|
||||
def search(self, buffer, freshlen, searchwindowsize=None):
|
||||
|
||||
"""This searches 'buffer' for the first occurence of one of the regular
|
||||
expressions. 'freshlen' must indicate the number of bytes at the end of
|
||||
'buffer' which have not been searched before.
|
||||
|
||||
See class spawn for the 'searchwindowsize' argument.
|
||||
|
||||
If there is a match this returns the index of that string, and sets
|
||||
'start', 'end' and 'match'. Otherwise, returns -1."""
|
||||
|
||||
absurd_match = len(buffer)
|
||||
first_match = absurd_match
|
||||
# 'freshlen' doesn't help here -- we cannot predict the
|
||||
# length of a match, and the re module provides no help.
|
||||
if searchwindowsize is None:
|
||||
searchstart = 0
|
||||
else:
|
||||
searchstart = max(0, len(buffer)-searchwindowsize)
|
||||
for index, s in self._searches:
|
||||
match = s.search(buffer, searchstart)
|
||||
if match is None:
|
||||
continue
|
||||
n = match.start()
|
||||
if n < first_match:
|
||||
first_match = n
|
||||
the_match = match
|
||||
best_index = index
|
||||
if first_match == absurd_match:
|
||||
return -1
|
||||
self.start = first_match
|
||||
self.match = the_match
|
||||
self.end = self.match.end()
|
||||
return best_index
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
p = Spawn('cmd')
|
||||
|
||||
p.sendline(b'ls')
|
||||
time.sleep(.5)
|
||||
data = p.expect('>')
|
||||
print(data)
|
||||
print(p.before)
|
||||
data = p.expect('>')
|
||||
print(data)
|
||||
print(p.before)
|
||||
|
||||
except:
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
p.terminate()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
105
wexpect/wexpect_util.py
Normal file
105
wexpect/wexpect_util.py
Normal file
@ -0,0 +1,105 @@
|
||||
"""Wexpect is a Windows variant of pexpect https://pexpect.readthedocs.io.
|
||||
|
||||
Wexpect is a Python module for spawning child applications and controlling
|
||||
them automatically.
|
||||
|
||||
wexpect util contains small functions, and classes, which are used in multiple classes.
|
||||
The command line argument parsers, and the Exceptions placed here.
|
||||
|
||||
Credits: Noah Spurrier, Richard Holden, Marco Molteni, Kimberley Burchett,
|
||||
Robert Stone, Hartmut Goebel, Chad Schroeder, Erick Tryzelaar, Dave Kirby, Ids
|
||||
vander Molen, George Todd, Noel Taylor, Nicolas D. Cesar, Alexander Gattin,
|
||||
Geoffrey Marshall, Francisco Lourenco, Glen Mabey, Karthik Gurusamy, Fernando
|
||||
Perez, Corey Minyard, Jon Cohen, Guillaume Chazarain, Andrew Ryan, Nick
|
||||
Craig-Wood, Andrew Stone, Jorgen Grahn, Benedek Racz
|
||||
|
||||
Free, open source, and all that good stuff.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
Wexpect Copyright (c) 2019 Benedek Racz
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
import ctypes
|
||||
import traceback
|
||||
import sys
|
||||
|
||||
def split_command_line(command_line):
|
||||
'''https://stackoverflow.com/a/35900070/2506522
|
||||
'''
|
||||
|
||||
nargs = ctypes.c_int()
|
||||
ctypes.windll.shell32.CommandLineToArgvW.restype = ctypes.POINTER(ctypes.c_wchar_p)
|
||||
lpargs = ctypes.windll.shell32.CommandLineToArgvW(command_line, ctypes.byref(nargs))
|
||||
args = [lpargs[i] for i in range(nargs.value)]
|
||||
if ctypes.windll.kernel32.LocalFree(lpargs):
|
||||
raise AssertionError
|
||||
return args
|
||||
|
||||
def join_args(args):
|
||||
"""Joins arguments into a command line. It quotes all arguments that contain
|
||||
spaces or any of the characters ^!$%&()[]{}=;'+,`~"""
|
||||
commandline = []
|
||||
for arg in args:
|
||||
if re.search('[\^!$%&()[\]{}=;\'+,`~\s]', arg):
|
||||
arg = '"%s"' % arg
|
||||
commandline.append(arg)
|
||||
return ' '.join(commandline)
|
||||
|
||||
|
||||
class ExceptionPexpect(Exception):
|
||||
"""Base class for all exceptions raised by this module.
|
||||
"""
|
||||
|
||||
def __init__(self, value):
|
||||
|
||||
self.value = value
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return str(self.value)
|
||||
|
||||
def get_trace(self):
|
||||
"""This returns an abbreviated stack trace with lines that only concern
|
||||
the caller. In other words, the stack trace inside the Wexpect module
|
||||
is not included. """
|
||||
|
||||
tblist = traceback.extract_tb(sys.exc_info()[2])
|
||||
tblist = [item for item in tblist if self.__filter_not_wexpect(item)]
|
||||
tblist = traceback.format_list(tblist)
|
||||
return ''.join(tblist)
|
||||
|
||||
def __filter_not_wexpect(self, trace_list_item):
|
||||
"""This returns True if list item 0 the string 'wexpect.py' in it. """
|
||||
|
||||
if trace_list_item[0].find('wexpect.py') == -1:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class EOF(ExceptionPexpect):
|
||||
"""Raised when EOF is read from a child. This usually means the child has exited.
|
||||
The user can wait to EOF, which means he waits the end of the execution of the child process."""
|
||||
|
||||
class TIMEOUT(ExceptionPexpect):
|
||||
"""Raised when a read time exceeds the timeout. """
|
||||
|
Loading…
Reference in New Issue
Block a user