first commit

This commit is contained in:
Ayxan
2022-05-23 00:16:32 +04:00
commit d660f2a4ca
24786 changed files with 4428337 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
"""
Pywinpty
========
This package provides low and high level APIs to create
pseudo terminals in Windows.
"""
# Local imports
from .winpty import PTY, WinptyError, __version__
from .ptyprocess import PtyProcess
from .enums import Backend, Encoding, MouseMode, AgentConfig
PTY
PtyProcess
Backend
Encoding
MouseMode
AgentConfig
WinptyError
__version__

View File

@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
"""General constants used to spawn a PTY."""
class Backend:
"""Available PTY backends."""
ConPTY = 0
WinPTY = 1
class Encoding:
"""Available byte encodings to communicate with a PTY."""
UTF8 = 'utf-8'
UTF16 = 'utf-16'
class MouseMode:
"""Mouse capture settings for the winpty backend."""
# QuickEdit mode is initially disabled, and the agent does not send mouse
# mode sequences to the terminal. If it receives mouse input, though, it
# still writes MOUSE_EVENT_RECORD values into CONIN.
WINPTY_MOUSE_MODE_NONE = 0
# QuickEdit mode is initially enabled. As CONIN enters or leaves mouse
# input mode (i.e. where ENABLE_MOUSE_INPUT is on and
# ENABLE_QUICK_EDIT_MODE is off), the agent enables or disables mouse
# input on the terminal.
WINPTY_MOUSE_MODE_AUTO = 1
# QuickEdit mode is initially disabled, and the agent enables the
# terminal's mouse input mode. It does not disable terminal
# mouse mode (until exit).
WINPTY_MOUSE_MODE_FORCE = 2
class AgentConfig:
"""General configuration settings for the winpty backend."""
# Create a new screen buffer (connected to the "conerr" terminal pipe) and
# pass it to child processes as the STDERR handle. This flag also prevents
# the agent from reopening CONOUT$ when it polls -- regardless of whether
# the active screen buffer changes, winpty continues to monitor the
# original primary screen buffer.
WINPTY_FLAG_CONERR = 0x1
# Don't output escape sequences.
WINPTY_FLAG_PLAIN_OUTPUT = 0x2
# Do output color escape sequences. These escapes are output by default,
# but are suppressed with WINPTY_FLAG_PLAIN_OUTPUT.
# Use this flag to reenable them.
WINPTY_FLAG_COLOR_ESCAPES = 0x4

View File

@@ -0,0 +1,365 @@
# -*- coding: utf-8 -*-
# Standard library imports
import codecs
import os
import shlex
import signal
import socket
import subprocess
import threading
import time
from shutil import which
# Local imports
from .winpty import PTY
class PtyProcess(object):
"""This class represents a process running in a pseudoterminal.
The main constructor is the :meth:`spawn` classmethod.
"""
def __init__(self, pty):
assert isinstance(pty, PTY)
self.pty = pty
self.pid = pty.pid
# self.fd = pty.fd
self.read_blocking = bool(int(os.environ.get('PYWINPTY_BLOCK', 1)))
self.closed = False
self.flag_eof = False
# Used by terminate() to give kernel time to update process status.
# Time in seconds.
self.delayafterterminate = 0.1
# Used by close() to give kernel time to update process status.
# Time in seconds.
self.delayafterclose = 0.1
# Set up our file reader sockets.
self._server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._server.bind(("127.0.0.1", 0))
address = self._server.getsockname()
self._server.listen(1)
# Read from the pty in a thread.
self._thread = threading.Thread(target=_read_in_thread,
args=(address, self.pty, self.read_blocking))
self._thread.daemon = True
self._thread.start()
self.fileobj, _ = self._server.accept()
self.fd = self.fileobj.fileno()
@classmethod
def spawn(cls, argv, cwd=None, env=None, dimensions=(24, 80),
backend=None):
"""Start the given command in a child process in a pseudo terminal.
This does all the setting up the pty, and returns an instance of
PtyProcess.
Dimensions of the psuedoterminal used for the subprocess can be
specified as a tuple (rows, cols), or the default (24, 80) will be
used.
"""
if isinstance(argv, str):
argv = shlex.split(argv, posix=False)
if not isinstance(argv, (list, tuple)):
raise TypeError("Expected a list or tuple for argv, got %r" % argv)
# Shallow copy of argv so we can modify it
argv = argv[:]
command = argv[0]
env = env or os.environ
path = env.get('PATH', os.defpath)
command_with_path = which(command, path=path)
if command_with_path is None:
raise FileNotFoundError(
'The command was not found or was not ' +
'executable: %s.' % command
)
command = command_with_path
argv[0] = command
cmdline = ' ' + subprocess.list2cmdline(argv[1:])
cwd = cwd or os.getcwd()
backend = backend or os.environ.get('PYWINPTY_BACKEND', None)
backend = int(backend) if backend is not None else backend
proc = PTY(dimensions[1], dimensions[0],
backend=backend)
# Create the environemnt string.
envStrs = []
for (key, value) in env.items():
envStrs.append('%s=%s' % (key, value))
env = '\0'.join(envStrs) + '\0'
# command = bytes(command, encoding)
# cwd = bytes(cwd, encoding)
# cmdline = bytes(cmdline, encoding)
# env = bytes(env, encoding)
if len(argv) == 1:
proc.spawn(command, cwd=cwd, env=env)
else:
proc.spawn(command, cwd=cwd, env=env, cmdline=cmdline)
inst = cls(proc)
inst._winsize = dimensions
# Set some informational attributes
inst.argv = argv
if env is not None:
inst.env = env
if cwd is not None:
inst.launch_dir = cwd
return inst
@property
def exitstatus(self):
"""The exit status of the process.
"""
return self.pty.get_exitstatus()
def fileno(self):
"""This returns the file descriptor of the pty for the child.
"""
return self.fd
def close(self, force=False):
"""This closes the connection with the child application. Note that
calling close() more than once is valid. This emulates standard Python
behavior with files. Set force to True if you want to make sure that
the child is terminated (SIGKILL is sent if the child ignores
SIGINT)."""
if not self.closed:
self.fileobj.close()
self._server.close()
# Give kernel time to update process status.
time.sleep(self.delayafterclose)
if self.isalive():
if not self.terminate(force):
raise IOError('Could not terminate the child.')
self.fd = -1
self.closed = True
# del self.pty
def __del__(self):
"""This makes sure that no system resources are left open. Python only
garbage collects Python objects. OS file descriptors are not Python
objects, so they must be handled explicitly. If the child file
descriptor was opened outside of this class (passed to the constructor)
then this does not close it.
"""
# It is possible for __del__ methods to execute during the
# teardown of the Python VM itself. Thus self.close() may
# trigger an exception because os.close may be None.
try:
self.close()
except Exception:
pass
def flush(self):
"""This does nothing. It is here to support the interface for a
File-like object. """
pass
def isatty(self):
"""This returns True if the file descriptor is open and connected to a
tty(-like) device, else False."""
return self.isalive()
def read(self, size=1024):
"""Read and return at most ``size`` characters from the pty.
Can block if there is nothing to read. Raises :exc:`EOFError` if the
terminal was closed.
"""
# try:
# data = self.pty.read(size, blocking=self.read_blocking)
# except Exception as e:
# if "EOF" in str(e):
# raise EOFError(e) from e
# return data
data = self.fileobj.recv(size)
if not data:
self.flag_eof = True
raise EOFError('Pty is closed')
if data == b'0011Ignore':
data = ''
err = True
while err and data:
try:
data.decode('utf-8')
err = False
except UnicodeDecodeError:
data += self.fileobj.recv(1)
return data.decode('utf-8')
def readline(self):
"""Read one line from the pseudoterminal as bytes.
Can block if there is nothing to read. Raises :exc:`EOFError` if the
terminal was closed.
"""
buf = []
while 1:
try:
ch = self.read(1)
except EOFError:
return ''.join(buf)
buf.append(ch)
if ch == '\n':
return ''.join(buf)
def write(self, s):
"""Write the string ``s`` to the pseudoterminal.
Returns the number of bytes written.
"""
if not self.pty.isalive():
raise EOFError('Pty is closed')
nbytes = self.pty.write(s)
return nbytes
def terminate(self, force=False):
"""This forces a child process to terminate."""
if not self.isalive():
return True
self.kill(signal.SIGINT)
time.sleep(self.delayafterterminate)
if not self.isalive():
return True
if force:
self.kill(signal.SIGTERM)
time.sleep(self.delayafterterminate)
if not self.isalive():
return True
else:
return False
def wait(self):
"""This waits until the child exits. This is a blocking call. This will
not read any data from the child.
"""
while self.isalive():
time.sleep(0.1)
return self.exitstatus
def isalive(self):
"""This tests if the child process is running or not. This is
non-blocking. If the child was terminated then this will read the
exitstatus or signalstatus of the child. This returns True if the child
process appears to be running or False if not.
"""
alive = self.pty.isalive()
self.closed = not alive
return alive
def kill(self, sig=None):
"""Kill the process with the given signal.
"""
os.kill(self.pid, sig)
def sendcontrol(self, char):
'''Helper method that wraps send() with mnemonic access for sending control
character to the child (such as Ctrl-C or Ctrl-D). For example, to send
Ctrl-G (ASCII 7, bell, '\a')::
child.sendcontrol('g')
See also, sendintr() and sendeof().
'''
char = char.lower()
a = ord(char)
if 97 <= a <= 122:
a = a - ord('a') + 1
byte = str(bytes([a]))
return self.pty.write(byte), byte
d = {'@': 0, '`': 0,
'[': 27, '{': 27,
'\\': 28, '|': 28,
']': 29, '}': 29,
'^': 30, '~': 30,
'_': 31,
'?': 127}
if char not in d:
return 0, ''
byte = str(bytes([d[char]]))
return self.pty.write(byte), byte
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."""
# Send control character 4 (Ctrl-D)
self.pty.write('\x04'), '\x04'
def sendintr(self):
"""This sends a SIGINT to the child. It does not require
the SIGINT to be the first character on a line. """
# Send control character 3 (Ctrl-C)
self.pty.write('\x03'), '\x03'
def eof(self):
"""This returns True if the EOF exception was ever raised.
"""
return self.flag_eof
def getwinsize(self):
"""Return the window size of the pseudoterminal as a tuple (rows, cols).
"""
return self._winsize
def setwinsize(self, rows, cols):
"""Set the terminal window size of the child tty.
"""
self._winsize = (rows, cols)
self.pty.set_size(cols, rows)
def _read_in_thread(address, pty, blocking):
"""Read data from the pty in a thread.
"""
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(address)
call = 0
while 1:
try:
data = pty.read(4096, blocking=blocking) or b'0011Ignore'
try:
client.send(bytes(data, 'utf-8'))
except socket.error:
break
# Handle end of file.
if pty.iseof():
try:
client.send(b'')
except socket.error:
pass
finally:
break
call += 1
except Exception as e:
break
time.sleep(1e-3)
client.close()

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
"""winpty module tests."""

View File

@@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
"""winpty wrapper tests."""
# Standard library imports
import os
import time
# Third party imports
from winpty import PTY, WinptyError
from winpty.enums import Backend
from winpty.ptyprocess import which
import pytest
CMD = which('cmd').lower()
def pty_factory(backend):
if os.environ.get('CI_RUNNING', None) == '1':
if backend == Backend.ConPTY:
os.environ['CONPTY_CI'] = '1'
elif backend == Backend.WinPTY:
os.environ.pop('CONPTY_CI', None)
@pytest.fixture(scope='function')
def pty_fixture():
pty = PTY(80, 20, backend=backend)
# loc = bytes(os.getcwd(), 'utf8')
assert pty.spawn(CMD)
time.sleep(0.3)
return pty
return pty_fixture
conpty_provider = pty_factory(Backend.ConPTY)
winpty_provider = pty_factory(Backend.WinPTY)
@pytest.fixture(scope='module', params=['WinPTY', 'ConPTY'])
def pty_fixture(request):
backend = request.param
if os.environ.get('CI_RUNNING', None) == '1':
if backend == 'ConPTY':
os.environ['CI'] = '1'
os.environ['CONPTY_CI'] = '1'
if backend == 'WinPTY':
os.environ.pop('CI', None)
os.environ.pop('CONPTY_CI', None)
backend = getattr(Backend, backend)
def _pty_factory():
pty = PTY(80, 25, backend=backend)
assert pty.spawn(CMD)
time.sleep(0.3)
return pty
return _pty_factory
# @pytest.fixture(scope='function', params=[
# pytest.lazy_fixture('conpty_provider'),
# pytest.lazy_fixture('winpty_provider')])
# def pty_fixture(request):
# pty = request.param
# return pty
def test_read(pty_fixture, capsys):
pty = pty_fixture()
loc = os.getcwd()
readline = ''
with capsys.disabled():
start_time = time.time()
while loc not in readline:
if time.time() - start_time > 5:
break
readline += pty.read()
assert loc in readline or 'cmd' in readline
del pty
def test_write(pty_fixture):
pty = pty_fixture()
line = pty.read()
str_text = 'Eggs, ham and spam ünicode'
# text = bytes(str_text, 'utf-8')
num_bytes = pty.write(str_text)
line = ''
start_time = time.time()
while str_text not in line:
if time.time() - start_time > 5:
break
line += pty.read()
assert str_text in line
del pty
def test_isalive(pty_fixture):
pty = pty_fixture()
pty.write('exit\r\n')
text = 'exit'
line = ''
while text not in line:
try:
line += pty.read()
except Exception:
break
while pty.isalive():
try:
pty.read()
# continue
except Exception:
break
assert not pty.isalive()
del pty
# def test_agent_spawn_fail(pty_fixture):
# pty = pty_fixture
# try:
# pty.spawn(CMD)
# assert False
# except WinptyError:
# pass
@pytest.mark.parametrize(
'backend_name,backend',
[("ConPTY", Backend.ConPTY), ('WinPTY', Backend.WinPTY)])
def test_pty_create_size_fail(backend_name, backend):
try:
PTY(80, -25, backend=backend)
assert False
except WinptyError:
pass
def test_agent_resize_fail(pty_fixture):
pty = pty_fixture()
try:
pty.set_size(-80, 70)
assert False
except WinptyError:
pass
finally:
del pty
def test_agent_resize(pty_fixture):
pty = pty_fixture()
pty.set_size(80, 70)
del pty

View File

@@ -0,0 +1,257 @@
# -*- coding: utf-8 -*-
"""winpty wrapper tests."""
# Standard library imports
import asyncio
import os
import signal
import time
import sys
import re
# Third party imports
import pytest
from flaky import flaky
# Local imports
from winpty.enums import Backend
from winpty.ptyprocess import PtyProcess, which
@pytest.fixture(scope='module', params=['WinPTY', 'ConPTY'])
def pty_fixture(request):
backend = request.param
if os.environ.get('CI_RUNNING', None) == '1':
if backend == 'ConPTY':
os.environ['CI'] = '1'
os.environ['CONPTY_CI'] = '1'
if backend == 'WinPTY':
os.environ.pop('CI', None)
os.environ.pop('CONPTY_CI', None)
backend = getattr(Backend, backend)
def _pty_factory(cmd=None, env=None):
cmd = cmd or 'cmd'
return PtyProcess.spawn(cmd, env=env, backend=backend)
_pty_factory.backend = request.param
return _pty_factory
@flaky(max_runs=40, min_passes=1)
def test_read(pty_fixture):
pty = pty_fixture()
loc = os.getcwd()
data = ''
tries = 0
while loc not in data and tries < 10:
try:
data += pty.read()
except EOFError:
pass
tries += 1
assert loc in data
pty.terminate()
time.sleep(2)
@flaky(max_runs=40, min_passes=1)
def test_write(pty_fixture):
pty = pty_fixture()
text = 'Eggs, ham and spam ünicode'
pty.write(text)
data = ''
tries = 0
while text not in data and tries < 10:
try:
data += pty.read()
except EOFError:
pass
tries += 1
assert text in data
pty.terminate()
@pytest.mark.xfail(reason="It fails sometimes due to long strings")
@flaky(max_runs=40, min_passes=1)
def test_isalive(pty_fixture):
pty = pty_fixture()
pty.write('echo \"foo\"\r\nexit\r\n')
data = ''
while True:
try:
print('Stuck')
data += pty.read()
except EOFError:
break
regex = re.compile(".*foo.*")
assert regex.findall(data)
assert not pty.isalive()
pty.terminate()
@pytest.mark.xfail(reason="It fails sometimes due to long strings")
@flaky(max_runs=40, min_passes=1)
def test_readline(pty_fixture):
env = os.environ.copy()
env['foo'] = 'bar'
pty = pty_fixture(env=env)
# Ensure that the echo print has its own CRLF
pty.write('cls\r\n')
pty.write('echo %foo%\r\n')
data = ''
tries = 0
while 'bar' not in data and tries < 10:
data = pty.readline()
tries += 1
assert 'bar' in data
pty.terminate()
def test_close(pty_fixture):
pty = pty_fixture()
pty.close()
assert not pty.isalive()
def test_flush(pty_fixture):
pty = pty_fixture()
pty.flush()
pty.terminate()
def test_intr(pty_fixture):
pty = pty_fixture(cmd=[sys.executable, 'import time; time.sleep(10)'])
pty.sendintr()
assert pty.wait() != 0
def test_send_control(pty_fixture):
pty = pty_fixture(cmd=[sys.executable, 'import time; time.sleep(10)'])
pty.sendcontrol('d')
assert pty.wait() != 0
@pytest.mark.skipif(which('cat') is None, reason="Requires cat on the PATH")
def test_send_eof(pty_fixture):
cat = pty_fixture('cat')
cat.sendeof()
assert cat.wait() == 0
def test_isatty(pty_fixture):
pty = pty_fixture()
assert pty.isatty()
pty.terminate()
assert not pty.isatty()
def test_wait(pty_fixture):
pty = pty_fixture(cmd=[sys.executable, '--version'])
assert pty.wait() == 0
def test_exit_status(pty_fixture):
pty = pty_fixture(cmd=[sys.executable])
pty.write('import sys;sys.exit(1)\r\n')
pty.wait()
assert pty.exitstatus == 1
@pytest.mark.timeout(30)
def test_kill_sigterm(pty_fixture):
pty = pty_fixture()
pty.write('echo \"foo\"\r\nsleep 1000\r\n')
pty.read()
pty.kill(signal.SIGTERM)
while True:
try:
pty.read()
except EOFError:
break
assert not pty.isalive()
assert pty.exitstatus == signal.SIGTERM
@pytest.mark.timeout(30)
def test_terminate(pty_fixture):
pty = pty_fixture()
pty.write('echo \"foo\"\r\nsleep 1000\r\n')
pty.read()
pty.terminate()
while True:
try:
pty.read()
except EOFError:
break
assert not pty.isalive()
@pytest.mark.timeout(30)
def test_terminate_force(pty_fixture):
pty = pty_fixture()
pty.write('echo \"foo\"\r\nsleep 1000\r\n')
pty.read()
pty.terminate(force=True)
while True:
try:
pty.read()
except EOFError:
break
assert not pty.isalive()
def test_terminate_loop(pty_fixture):
pty = pty_fixture()
loop = asyncio.SelectorEventLoop()
asyncio.set_event_loop(loop)
def reader():
try:
data = pty.read()
except EOFError:
loop.remove_reader(pty.fd)
loop.stop()
loop.add_reader(pty.fd, reader)
loop.call_soon(pty.write, 'echo \"foo\"\r\nsleep 1000\r\n')
loop.call_soon(pty.terminate, True)
try:
loop.run_forever()
finally:
loop.close()
assert not pty.isalive()
def test_getwinsize(pty_fixture):
pty = pty_fixture()
assert pty.getwinsize() == (24, 80)
pty.terminate()
def test_setwinsize(pty_fixture):
pty = pty_fixture()
pty.setwinsize(50, 110)
assert pty.getwinsize() == (50, 110)
pty.terminate()
pty = PtyProcess.spawn('cmd', dimensions=(60, 120))
assert pty.getwinsize() == (60, 120)
pty.terminate()

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
"""Stub typing declarations for the native PTY object."""
# Standard library imports
from typing import Optional
# Local imports
from .enums import Backend, Encoding, MouseMode, AgentConfig
class PTY:
def __init__(self, cols: int, rows: int,
backend: Optional[int] = None,
encoding: Optional[str] = Encoding.UTF8,
mouse_mode: int = MouseMode.WINPTY_MOUSE_MODE_NONE,
timeout: int = 30000,
agent_config: int = AgentConfig.WINPTY_FLAG_COLOR_ESCAPES):
...
def spawn(self,
appname: bytes,
cmdline: Optional[bytes] = None,
cwd: Optional[bytes] = None,
env: Optional[bytes] = None) -> bool:
...
def set_size(self, cols: int, rows: int): ...
def read(self,
length: Optional[int] = 1000,
blocking: bool = False) -> bytes:
...
def read_stderr(self,
length: Optional[int] = 1000,
blocking: bool = False) -> bytes:
...
def write(self, to_write: bytes) -> int: ...
def isalive(self) -> bool: ...
def get_exitstatus(self) -> Optional[int]: ...
def iseof(self) -> bool: ...
@property
def pid(self) -> Optional[int]: ...
@property
def fd(self) -> Optional[int]: ...