pywinpty 3.0.3__cp313-cp313t-win_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pywinpty-3.0.3.dist-info/METADATA +162 -0
- pywinpty-3.0.3.dist-info/RECORD +12 -0
- pywinpty-3.0.3.dist-info/WHEEL +4 -0
- pywinpty-3.0.3.dist-info/licenses/LICENSE.txt +21 -0
- winpty/__init__.py +23 -0
- winpty/enums.py +54 -0
- winpty/ptyprocess.py +372 -0
- winpty/tests/__init__.py +2 -0
- winpty/tests/test_pty.py +164 -0
- winpty/tests/test_ptyprocess.py +268 -0
- winpty/winpty.cp313t-win_arm64.pyd +0 -0
- winpty/winpty.pyi +42 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pywinpty
|
|
3
|
+
Version: 3.0.3
|
|
4
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
5
|
+
Classifier: Programming Language :: Python
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
8
|
+
Classifier: Programming Language :: Python :: Free Threading
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Topic :: Terminals
|
|
11
|
+
Classifier: Topic :: Terminals :: Terminal Emulators/X Terminals
|
|
12
|
+
Classifier: Programming Language :: Rust
|
|
13
|
+
License-File: LICENSE.txt
|
|
14
|
+
Summary: Pseudo terminal support for Windows from Python.
|
|
15
|
+
Keywords: PTY,Windows,pseudo-terminal,PyO3
|
|
16
|
+
Author: Edgar Margffoy
|
|
17
|
+
Author-email: andfoy@gmail.com
|
|
18
|
+
Maintainer-email: Edgar Margffoy <andfoy@gmail.com>
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
21
|
+
Project-URL: Changelog, https://github.com/andfoy/pywinpty/blob/main/CHANGELOG.md
|
|
22
|
+
Project-URL: Repository, https://github.com/andfoy/pywinpty
|
|
23
|
+
|
|
24
|
+
# PyWinpty: Pseudoterminals for Windows in Python
|
|
25
|
+
|
|
26
|
+
[](./LICENSE.txt)
|
|
27
|
+
[](https://pypi.org/project/pywinpty/)
|
|
28
|
+
[](https://www.anaconda.com/download/)
|
|
29
|
+
[](https://www.anaconda.com/download/)
|
|
30
|
+
[](https://pepy.tech/project/pywinpty)
|
|
31
|
+
[](https://github.com/spyder-ide/pywinpty)
|
|
32
|
+
[](https://github.com/andfoy/pywinpty/actions/workflows/windows_build.yml)
|
|
33
|
+
|
|
34
|
+
*Copyright © 2017–2022 Spyder Project Contributors*
|
|
35
|
+
*Copyright © 2022– Edgar Andrés Margffoy Tuay*
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
## Overview
|
|
39
|
+
|
|
40
|
+
PyWinpty allows creating and communicating with Windows processes that receive input and print outputs via console input and output pipes. PyWinpty supports both the native [ConPTY](https://devblogs.microsoft.com/commandline/windows-command-line-introducing-the-windows-pseudo-console-conpty/) interface and the previous, fallback [winpty](https://github.com/rprichard/winpty) library.
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## Dependencies
|
|
44
|
+
To compile pywinpty sources, you must have [Rust](https://rustup.rs/) installed.
|
|
45
|
+
Optionally, you can also have Winpty's C header and library files available on your include path.
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
You can install this library by using conda or pip package managers, as it follows:
|
|
50
|
+
|
|
51
|
+
Using conda (Recommended):
|
|
52
|
+
```bash
|
|
53
|
+
conda install pywinpty
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Using pip:
|
|
57
|
+
```bash
|
|
58
|
+
pip install pywinpty
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Building from source
|
|
62
|
+
|
|
63
|
+
To build from sources, you will require both a working stable or nightly Rust toolchain with
|
|
64
|
+
target `x86_64-pc-windows-msvc`, which can be installed using [rustup](https://rustup.rs/).
|
|
65
|
+
|
|
66
|
+
Optionally, this library can be linked against winpty library, which you can install using conda-forge:
|
|
67
|
+
|
|
68
|
+
```batch
|
|
69
|
+
conda install winpty -c conda-forge
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
If you don't want to use conda, you will need to have the winpty binaries and headers available on your PATH.
|
|
73
|
+
|
|
74
|
+
Finally, pywinpty uses [Maturin](https://github.com/PyO3/maturin) as the build backend, which can be installed using `pip`:
|
|
75
|
+
|
|
76
|
+
```batch
|
|
77
|
+
pip install maturin
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
To test your compilation environment settings, you can build pywinpty sources locally, by
|
|
81
|
+
executing:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
maturin develop
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
This package depends on the following Rust crates:
|
|
88
|
+
|
|
89
|
+
* [PyO3](https://github.com/PyO3/pyo3): Library used to produce Python bindings from Rust code.
|
|
90
|
+
* [WinPTY-rs](https://github.com/andfoy/winpty-rs): Create and spawn processes inside a pseudoterminal in Windows from Rust.
|
|
91
|
+
* [Maturin](https://github.com/PyO3/maturin): Build system to build and publish Rust-based Python packages.
|
|
92
|
+
|
|
93
|
+
## Package usage
|
|
94
|
+
Pywinpty offers a single python wrapper around winpty library functions.
|
|
95
|
+
This implies that using a single object (``winpty.PTY``) it is possible to access to all functionality, as it follows:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
# High level usage using `spawn`
|
|
99
|
+
from winpty import PtyProcess
|
|
100
|
+
|
|
101
|
+
proc = PtyProcess.spawn('python')
|
|
102
|
+
proc.write('print("hello, world!")\r\n')
|
|
103
|
+
proc.write('exit()\r\n')
|
|
104
|
+
while proc.isalive():
|
|
105
|
+
print(proc.readline())
|
|
106
|
+
|
|
107
|
+
# Low level usage using the raw `PTY` object
|
|
108
|
+
from winpty import PTY
|
|
109
|
+
|
|
110
|
+
# Start a new winpty-agent process of size (cols, rows)
|
|
111
|
+
cols, rows = 80, 25
|
|
112
|
+
process = PTY(cols, rows)
|
|
113
|
+
|
|
114
|
+
# Spawn a new console process, e.g., CMD
|
|
115
|
+
process.spawn(br'C:\windows\system32\cmd.exe')
|
|
116
|
+
|
|
117
|
+
# Read console output (Unicode)
|
|
118
|
+
process.read()
|
|
119
|
+
|
|
120
|
+
# Write input to console (Unicode)
|
|
121
|
+
process.write(b'Text')
|
|
122
|
+
|
|
123
|
+
# Resize console size
|
|
124
|
+
new_cols, new_rows = 90, 30
|
|
125
|
+
process.set_size(new_cols, new_rows)
|
|
126
|
+
|
|
127
|
+
# Know if the process is alive
|
|
128
|
+
alive = process.isalive()
|
|
129
|
+
|
|
130
|
+
# End winpty-agent process
|
|
131
|
+
del process
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Running tests
|
|
135
|
+
We use pytest to run tests as it follows (after calling ``maturin develop``), the test suite depends
|
|
136
|
+
on pytest-lazy-fixture, which can be installed via pip:
|
|
137
|
+
|
|
138
|
+
```batch
|
|
139
|
+
pip install pytest pytest-lazy-fixture flaky
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
All the tests can be executed using the following command
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
python runtests.py
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
## Changelog
|
|
150
|
+
Visit our [CHANGELOG](CHANGELOG.md) file to learn more about our new features and improvements.
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
## Contribution guidelines
|
|
154
|
+
We follow PEP8 and PEP257 for pure python packages and Rust to compile extensions. We use MyPy type annotations for all functions and classes declared on this package. Feel free to send a PR or create an issue if you have any problem/question.
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
## Security contact information
|
|
158
|
+
|
|
159
|
+
To report a security vulnerability, please use the
|
|
160
|
+
[Tidelift security contact](https://tidelift.com/security).
|
|
161
|
+
Tidelift will coordinate the fix and disclosure.
|
|
162
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pywinpty-3.0.3.dist-info\METADATA,sha256=poyhYBPHwV2raTRVtLtL-_ogd-kuEP83l1cjQbN4bGA,5890
|
|
2
|
+
pywinpty-3.0.3.dist-info\WHEEL,sha256=HzMhMZ5m0Z89vUkS-V0VF24Ien1b8bNjE3scHQmaZDQ,98
|
|
3
|
+
pywinpty-3.0.3.dist-info\licenses\LICENSE.txt,sha256=-HjUdn-a0uQ9MIPvoAIBsADOk32e6GJuALpccqrJUeI,1088
|
|
4
|
+
winpty\__init__.py,sha256=laTi9sLjCLycyaQYO3wMvhZNIK2vh_pOvGTJT2Od4Qs,402
|
|
5
|
+
winpty\enums.py,sha256=KAm7XJFPk7nMWwUJxvYHlql92cr6y-Y24IHK3fc0JDU,1860
|
|
6
|
+
winpty\ptyprocess.py,sha256=hucCVfBridHRc4IPiEFrEkdFhU-UKSQbyZUa97EceDc,12209
|
|
7
|
+
winpty\tests\__init__.py,sha256=fzb9cDnPt2R3b_rWh6sqDgIbiQOZtBlfsV1aq-ULT2Q,53
|
|
8
|
+
winpty\tests\test_pty.py,sha256=AXujQybXhKr0t0UKpNBSqeAfsm4fHoU_ognbiocBF5I,3947
|
|
9
|
+
winpty\tests\test_ptyprocess.py,sha256=MeGp_P-zkseEp0iXciWh9Fa548hYGdvlcaHy_WNEwXo,6112
|
|
10
|
+
winpty\winpty.cp313t-win_arm64.pyd,sha256=xgirsSlLAUkOXe6FBnNq6FL5j-WpCBQhxEiz57EuuMY,523776
|
|
11
|
+
winpty\winpty.pyi,sha256=-VenowCvy8tjIZUCvvb5VZw5R1c6ATXEUfsiEyIZwjE,1176
|
|
12
|
+
pywinpty-3.0.3.dist-info\RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 Spyder IDE
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
winpty/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Pywinpty
|
|
4
|
+
========
|
|
5
|
+
This package provides low and high level APIs to create
|
|
6
|
+
pseudo terminals in Windows.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Local imports
|
|
10
|
+
from .winpty import PTY, WinptyError, __version__
|
|
11
|
+
from .ptyprocess import PtyProcess
|
|
12
|
+
from .enums import Backend, Encoding, MouseMode, AgentConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
PTY
|
|
16
|
+
PtyProcess
|
|
17
|
+
Backend
|
|
18
|
+
Encoding
|
|
19
|
+
MouseMode
|
|
20
|
+
AgentConfig
|
|
21
|
+
WinptyError
|
|
22
|
+
|
|
23
|
+
__version__
|
winpty/enums.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""General constants used to spawn a PTY."""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Backend:
|
|
7
|
+
"""Available PTY backends."""
|
|
8
|
+
ConPTY = 0
|
|
9
|
+
WinPTY = 1
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Encoding:
|
|
13
|
+
"""Available byte encodings to communicate with a PTY."""
|
|
14
|
+
UTF8 = 'utf-8'
|
|
15
|
+
UTF16 = 'utf-16'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MouseMode:
|
|
19
|
+
"""Mouse capture settings for the winpty backend."""
|
|
20
|
+
|
|
21
|
+
# QuickEdit mode is initially disabled, and the agent does not send mouse
|
|
22
|
+
# mode sequences to the terminal. If it receives mouse input, though, it
|
|
23
|
+
# still writes MOUSE_EVENT_RECORD values into CONIN.
|
|
24
|
+
WINPTY_MOUSE_MODE_NONE = 0
|
|
25
|
+
|
|
26
|
+
# QuickEdit mode is initially enabled. As CONIN enters or leaves mouse
|
|
27
|
+
# input mode (i.e. where ENABLE_MOUSE_INPUT is on and
|
|
28
|
+
# ENABLE_QUICK_EDIT_MODE is off), the agent enables or disables mouse
|
|
29
|
+
# input on the terminal.
|
|
30
|
+
WINPTY_MOUSE_MODE_AUTO = 1
|
|
31
|
+
|
|
32
|
+
# QuickEdit mode is initially disabled, and the agent enables the
|
|
33
|
+
# terminal's mouse input mode. It does not disable terminal
|
|
34
|
+
# mouse mode (until exit).
|
|
35
|
+
WINPTY_MOUSE_MODE_FORCE = 2
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AgentConfig:
|
|
39
|
+
"""General configuration settings for the winpty backend."""
|
|
40
|
+
|
|
41
|
+
# Create a new screen buffer (connected to the "conerr" terminal pipe) and
|
|
42
|
+
# pass it to child processes as the STDERR handle. This flag also prevents
|
|
43
|
+
# the agent from reopening CONOUT$ when it polls -- regardless of whether
|
|
44
|
+
# the active screen buffer changes, winpty continues to monitor the
|
|
45
|
+
# original primary screen buffer.
|
|
46
|
+
WINPTY_FLAG_CONERR = 0x1
|
|
47
|
+
|
|
48
|
+
# Don't output escape sequences.
|
|
49
|
+
WINPTY_FLAG_PLAIN_OUTPUT = 0x2
|
|
50
|
+
|
|
51
|
+
# Do output color escape sequences. These escapes are output by default,
|
|
52
|
+
# but are suppressed with WINPTY_FLAG_PLAIN_OUTPUT.
|
|
53
|
+
# Use this flag to re-enable them.
|
|
54
|
+
WINPTY_FLAG_COLOR_ESCAPES = 0x4
|
winpty/ptyprocess.py
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# Standard library imports
|
|
4
|
+
import os
|
|
5
|
+
import shlex
|
|
6
|
+
import signal
|
|
7
|
+
import socket
|
|
8
|
+
import subprocess
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
from shutil import which
|
|
12
|
+
|
|
13
|
+
# Local imports
|
|
14
|
+
from .winpty import PTY
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PtyProcess(object):
|
|
18
|
+
"""This class represents a process running in a pseudoterminal.
|
|
19
|
+
|
|
20
|
+
The main constructor is the :meth:`spawn` classmethod.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, pty):
|
|
24
|
+
assert isinstance(pty, PTY)
|
|
25
|
+
self.pty = pty
|
|
26
|
+
self.pid = pty.pid
|
|
27
|
+
# self.fd = pty.fd
|
|
28
|
+
self.argv = None
|
|
29
|
+
self.env = None
|
|
30
|
+
self.launch_dir = None
|
|
31
|
+
|
|
32
|
+
self.read_blocking = bool(int(os.environ.get('PYWINPTY_BLOCK', 1)))
|
|
33
|
+
self.closed = False
|
|
34
|
+
self.flag_eof = False
|
|
35
|
+
|
|
36
|
+
# Used by terminate() to give kernel time to update process status.
|
|
37
|
+
# Time in seconds.
|
|
38
|
+
self.delayafterterminate = 0.1
|
|
39
|
+
# Used by close() to give kernel time to update process status.
|
|
40
|
+
# Time in seconds.
|
|
41
|
+
self.delayafterclose = 0.1
|
|
42
|
+
|
|
43
|
+
# Set up our file reader sockets.
|
|
44
|
+
self._server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
45
|
+
self._server.bind(("127.0.0.1", 0))
|
|
46
|
+
address = self._server.getsockname()
|
|
47
|
+
self._server.listen(1)
|
|
48
|
+
|
|
49
|
+
# Read from the pty in a thread.
|
|
50
|
+
self._thread = threading.Thread(target=_read_in_thread,
|
|
51
|
+
args=(address, self.pty, self.read_blocking))
|
|
52
|
+
self._thread.daemon = True
|
|
53
|
+
self._thread.start()
|
|
54
|
+
|
|
55
|
+
self.fileobj, _ = self._server.accept()
|
|
56
|
+
self.fd = self.fileobj.fileno()
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def spawn(cls, argv, cwd=None, env=None, dimensions=(24, 80),
|
|
60
|
+
backend=None):
|
|
61
|
+
"""Start the given command in a child process in a pseudo terminal.
|
|
62
|
+
|
|
63
|
+
This does all the setting up the pty, and returns an instance of
|
|
64
|
+
PtyProcess.
|
|
65
|
+
|
|
66
|
+
Dimensions of the psuedoterminal used for the subprocess can be
|
|
67
|
+
specified as a tuple (rows, cols), or the default (24, 80) will be
|
|
68
|
+
used.
|
|
69
|
+
"""
|
|
70
|
+
if isinstance(argv, str):
|
|
71
|
+
argv = shlex.split(argv, posix=False)
|
|
72
|
+
|
|
73
|
+
if not isinstance(argv, (list, tuple)):
|
|
74
|
+
raise TypeError("Expected a list or tuple for argv, got %r" % argv)
|
|
75
|
+
|
|
76
|
+
# Shallow copy of argv so we can modify it
|
|
77
|
+
_argv: list[str] = list(argv[:])
|
|
78
|
+
command = _argv[0]
|
|
79
|
+
env = env or os.environ
|
|
80
|
+
|
|
81
|
+
path = env.get('PATH', os.defpath)
|
|
82
|
+
command_with_path = which(command, path=path)
|
|
83
|
+
if command_with_path is None:
|
|
84
|
+
raise FileNotFoundError(
|
|
85
|
+
'The command was not found or was not ' +
|
|
86
|
+
'executable: %s.' % command
|
|
87
|
+
)
|
|
88
|
+
command = command_with_path
|
|
89
|
+
_argv[0] = command
|
|
90
|
+
cmdline = ' ' + subprocess.list2cmdline(_argv[1:])
|
|
91
|
+
cwd = cwd or os.getcwd()
|
|
92
|
+
|
|
93
|
+
backend = backend or os.environ.get('PYWINPTY_BACKEND', None)
|
|
94
|
+
backend = int(backend) if backend is not None else backend
|
|
95
|
+
|
|
96
|
+
proc = PTY(dimensions[1], dimensions[0],
|
|
97
|
+
backend=backend)
|
|
98
|
+
|
|
99
|
+
# Create the environment string.
|
|
100
|
+
envStrs = []
|
|
101
|
+
for (key, value) in env.items():
|
|
102
|
+
envStrs.append('%s=%s' % (key, value))
|
|
103
|
+
env = '\0'.join(envStrs) + '\0'
|
|
104
|
+
|
|
105
|
+
# command = bytes(command, encoding)
|
|
106
|
+
# cwd = bytes(cwd, encoding)
|
|
107
|
+
# cmdline = bytes(cmdline, encoding)
|
|
108
|
+
# env = bytes(env, encoding)
|
|
109
|
+
|
|
110
|
+
if len(_argv) == 1:
|
|
111
|
+
proc.spawn(command, cwd=cwd, env=env)
|
|
112
|
+
else:
|
|
113
|
+
proc.spawn(command, cwd=cwd, env=env, cmdline=cmdline)
|
|
114
|
+
|
|
115
|
+
inst = cls(proc)
|
|
116
|
+
inst._winsize = dimensions
|
|
117
|
+
|
|
118
|
+
# Set some informational attributes
|
|
119
|
+
inst.argv = _argv
|
|
120
|
+
if env is not None:
|
|
121
|
+
inst.env = env
|
|
122
|
+
if cwd is not None:
|
|
123
|
+
inst.launch_dir = cwd
|
|
124
|
+
|
|
125
|
+
return inst
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def exitstatus(self):
|
|
129
|
+
"""The exit status of the process.
|
|
130
|
+
"""
|
|
131
|
+
return self.pty.get_exitstatus()
|
|
132
|
+
|
|
133
|
+
def fileno(self):
|
|
134
|
+
"""This returns the file descriptor of the pty for the child.
|
|
135
|
+
"""
|
|
136
|
+
return self.fd
|
|
137
|
+
|
|
138
|
+
def close(self, force=False):
|
|
139
|
+
"""This closes the connection with the child application. Note that
|
|
140
|
+
calling close() more than once is valid. This emulates standard Python
|
|
141
|
+
behavior with files. Set force to True if you want to make sure that
|
|
142
|
+
the child is terminated (SIGKILL is sent if the child ignores
|
|
143
|
+
SIGINT)."""
|
|
144
|
+
if not self.closed:
|
|
145
|
+
self.fileobj.close()
|
|
146
|
+
self._server.close()
|
|
147
|
+
# Give kernel time to update process status.
|
|
148
|
+
time.sleep(self.delayafterclose)
|
|
149
|
+
if self.isalive():
|
|
150
|
+
if not self.terminate(force):
|
|
151
|
+
raise IOError('Could not terminate the child.')
|
|
152
|
+
self.fd = -1
|
|
153
|
+
self.closed = True
|
|
154
|
+
# del self.pty
|
|
155
|
+
|
|
156
|
+
def __del__(self):
|
|
157
|
+
"""This makes sure that no system resources are left open. Python only
|
|
158
|
+
garbage collects Python objects. OS file descriptors are not Python
|
|
159
|
+
objects, so they must be handled explicitly. If the child file
|
|
160
|
+
descriptor was opened outside of this class (passed to the constructor)
|
|
161
|
+
then this does not close it.
|
|
162
|
+
"""
|
|
163
|
+
# It is possible for __del__ methods to execute during the
|
|
164
|
+
# teardown of the Python VM itself. Thus self.close() may
|
|
165
|
+
# trigger an exception because os.close may be None.
|
|
166
|
+
try:
|
|
167
|
+
self.close()
|
|
168
|
+
except Exception:
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
def flush(self):
|
|
172
|
+
"""This does nothing. It is here to support the interface for a
|
|
173
|
+
File-like object. """
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
def isatty(self):
|
|
177
|
+
"""This returns True if the file descriptor is open and connected to a
|
|
178
|
+
tty(-like) device, else False."""
|
|
179
|
+
return self.isalive()
|
|
180
|
+
|
|
181
|
+
def read(self, size=1024):
|
|
182
|
+
"""Read and return at most ``size`` characters from the pty.
|
|
183
|
+
|
|
184
|
+
Can block if there is nothing to read. Raises :exc:`EOFError` if the
|
|
185
|
+
terminal was closed.
|
|
186
|
+
"""
|
|
187
|
+
# try:
|
|
188
|
+
# data = self.pty.read(size, blocking=self.read_blocking)
|
|
189
|
+
# except Exception as e:
|
|
190
|
+
# if "EOF" in str(e):
|
|
191
|
+
# raise EOFError(e) from e
|
|
192
|
+
# return data
|
|
193
|
+
data = self.fileobj.recv(size)
|
|
194
|
+
if not data:
|
|
195
|
+
self.flag_eof = True
|
|
196
|
+
raise EOFError('Pty is closed')
|
|
197
|
+
|
|
198
|
+
if data == b'0011Ignore':
|
|
199
|
+
data = b''
|
|
200
|
+
|
|
201
|
+
err = True
|
|
202
|
+
while err and data:
|
|
203
|
+
try:
|
|
204
|
+
data.decode('utf-8')
|
|
205
|
+
err = False
|
|
206
|
+
except UnicodeDecodeError:
|
|
207
|
+
data += self.fileobj.recv(1)
|
|
208
|
+
return data.decode('utf-8')
|
|
209
|
+
|
|
210
|
+
def readline(self):
|
|
211
|
+
"""Read one line from the pseudoterminal as bytes.
|
|
212
|
+
|
|
213
|
+
Can block if there is nothing to read. Raises :exc:`EOFError` if the
|
|
214
|
+
terminal was closed.
|
|
215
|
+
"""
|
|
216
|
+
buf = []
|
|
217
|
+
while 1:
|
|
218
|
+
try:
|
|
219
|
+
ch = self.read(1)
|
|
220
|
+
except EOFError:
|
|
221
|
+
return ''.join(buf)
|
|
222
|
+
buf.append(ch)
|
|
223
|
+
if ch == '\n':
|
|
224
|
+
return ''.join(buf)
|
|
225
|
+
|
|
226
|
+
def write(self, s):
|
|
227
|
+
"""Write the string ``s`` to the pseudoterminal.
|
|
228
|
+
|
|
229
|
+
Returns the number of bytes written.
|
|
230
|
+
"""
|
|
231
|
+
if not self.pty.isalive():
|
|
232
|
+
raise EOFError('Pty is closed')
|
|
233
|
+
|
|
234
|
+
nbytes = self.pty.write(s)
|
|
235
|
+
return nbytes
|
|
236
|
+
|
|
237
|
+
def terminate(self, force=False):
|
|
238
|
+
"""This forces a child process to terminate."""
|
|
239
|
+
if not self.isalive():
|
|
240
|
+
return True
|
|
241
|
+
self.kill(signal.SIGINT)
|
|
242
|
+
try:
|
|
243
|
+
self.pty.cancel_io()
|
|
244
|
+
except Exception:
|
|
245
|
+
pass
|
|
246
|
+
time.sleep(self.delayafterterminate)
|
|
247
|
+
if not self.isalive():
|
|
248
|
+
return True
|
|
249
|
+
if force:
|
|
250
|
+
self.kill(signal.SIGTERM)
|
|
251
|
+
time.sleep(self.delayafterterminate)
|
|
252
|
+
if not self.isalive():
|
|
253
|
+
return True
|
|
254
|
+
else:
|
|
255
|
+
return False
|
|
256
|
+
|
|
257
|
+
def wait(self):
|
|
258
|
+
"""This waits until the child exits. This is a blocking call. This will
|
|
259
|
+
not read any data from the child.
|
|
260
|
+
"""
|
|
261
|
+
while self.isalive():
|
|
262
|
+
time.sleep(0.1)
|
|
263
|
+
return self.exitstatus
|
|
264
|
+
|
|
265
|
+
def isalive(self):
|
|
266
|
+
"""This tests if the child process is running or not. This is
|
|
267
|
+
non-blocking. If the child was terminated then this will read the
|
|
268
|
+
exitstatus or signalstatus of the child. This returns True if the child
|
|
269
|
+
process appears to be running or False if not.
|
|
270
|
+
"""
|
|
271
|
+
alive = self.pty.isalive()
|
|
272
|
+
self.closed = not alive
|
|
273
|
+
return alive
|
|
274
|
+
|
|
275
|
+
def kill(self, sig):
|
|
276
|
+
"""Kill the process with the given signal.
|
|
277
|
+
"""
|
|
278
|
+
if self.pid is None:
|
|
279
|
+
return
|
|
280
|
+
os.kill(self.pid, sig)
|
|
281
|
+
|
|
282
|
+
def sendcontrol(self, char):
|
|
283
|
+
'''Helper method that wraps send() with mnemonic access for sending control
|
|
284
|
+
character to the child (such as Ctrl-C or Ctrl-D). For example, to send
|
|
285
|
+
Ctrl-G (ASCII 7, bell, '\a')::
|
|
286
|
+
child.sendcontrol('g')
|
|
287
|
+
See also, sendintr() and sendeof().
|
|
288
|
+
'''
|
|
289
|
+
char = char.lower()
|
|
290
|
+
a = ord(char)
|
|
291
|
+
if 97 <= a <= 122:
|
|
292
|
+
a = a - ord('a') + 1
|
|
293
|
+
byte = bytes([a]).decode("ascii")
|
|
294
|
+
return self.pty.write(byte), byte
|
|
295
|
+
d = {'@': 0, '`': 0,
|
|
296
|
+
'[': 27, '{': 27,
|
|
297
|
+
'\\': 28, '|': 28,
|
|
298
|
+
']': 29, '}': 29,
|
|
299
|
+
'^': 30, '~': 30,
|
|
300
|
+
'_': 31,
|
|
301
|
+
'?': 127}
|
|
302
|
+
if char not in d:
|
|
303
|
+
return 0, ''
|
|
304
|
+
|
|
305
|
+
byte = bytes([d[char]]).decode("ascii")
|
|
306
|
+
return self.pty.write(byte), byte
|
|
307
|
+
|
|
308
|
+
def sendeof(self):
|
|
309
|
+
"""This sends an EOF to the child. This sends a character which causes
|
|
310
|
+
the pending parent output buffer to be sent to the waiting child
|
|
311
|
+
program without waiting for end-of-line. If it is the first character
|
|
312
|
+
of the line, the read() in the user program returns 0, which signifies
|
|
313
|
+
end-of-file. This means to work as expected a sendeof() has to be
|
|
314
|
+
called at the beginning of a line. This method does not send a newline.
|
|
315
|
+
It is the responsibility of the caller to ensure the eof is sent at the
|
|
316
|
+
beginning of a line."""
|
|
317
|
+
# Send control character 4 (Ctrl-D)
|
|
318
|
+
self.pty.write('\x04')
|
|
319
|
+
|
|
320
|
+
def sendintr(self):
|
|
321
|
+
"""This sends a SIGINT to the child. It does not require
|
|
322
|
+
the SIGINT to be the first character on a line. """
|
|
323
|
+
# Send control character 3 (Ctrl-C)
|
|
324
|
+
self.pty.write('\x03')
|
|
325
|
+
|
|
326
|
+
def eof(self):
|
|
327
|
+
"""This returns True if the EOF exception was ever raised.
|
|
328
|
+
"""
|
|
329
|
+
return self.flag_eof
|
|
330
|
+
|
|
331
|
+
def getwinsize(self):
|
|
332
|
+
"""Return the window size of the pseudoterminal as a tuple (rows, cols).
|
|
333
|
+
"""
|
|
334
|
+
return self._winsize
|
|
335
|
+
|
|
336
|
+
def setwinsize(self, rows, cols):
|
|
337
|
+
"""Set the terminal window size of the child tty.
|
|
338
|
+
"""
|
|
339
|
+
self._winsize = (rows, cols)
|
|
340
|
+
self.pty.set_size(cols, rows)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _read_in_thread(address, pty: PTY, blocking: bool):
|
|
344
|
+
"""Read data from the pty in a thread.
|
|
345
|
+
"""
|
|
346
|
+
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
347
|
+
client.connect(address)
|
|
348
|
+
|
|
349
|
+
call = 0
|
|
350
|
+
|
|
351
|
+
while 1:
|
|
352
|
+
try:
|
|
353
|
+
data = pty.read(blocking=blocking) or '0011Ignore'
|
|
354
|
+
try:
|
|
355
|
+
client.send(bytes(data, 'utf-8'))
|
|
356
|
+
except socket.error:
|
|
357
|
+
break
|
|
358
|
+
|
|
359
|
+
# Handle end of file.
|
|
360
|
+
if pty.iseof():
|
|
361
|
+
try:
|
|
362
|
+
client.send(b'')
|
|
363
|
+
except socket.error:
|
|
364
|
+
pass
|
|
365
|
+
break
|
|
366
|
+
|
|
367
|
+
call += 1
|
|
368
|
+
except Exception as e:
|
|
369
|
+
break
|
|
370
|
+
time.sleep(1e-3)
|
|
371
|
+
|
|
372
|
+
client.close()
|
winpty/tests/__init__.py
ADDED
winpty/tests/test_pty.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""winpty wrapper tests."""
|
|
3
|
+
|
|
4
|
+
# Standard library imports
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
# Third party imports
|
|
9
|
+
from winpty import PTY, WinptyError
|
|
10
|
+
from winpty.enums import Backend
|
|
11
|
+
from winpty.ptyprocess import which
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
CMD = which('cmd').lower()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def pty_factory(backend):
|
|
19
|
+
if os.environ.get('CI_RUNNING', None) == '1':
|
|
20
|
+
if backend == Backend.ConPTY:
|
|
21
|
+
os.environ['CONPTY_CI'] = '1'
|
|
22
|
+
elif backend == Backend.WinPTY:
|
|
23
|
+
os.environ.pop('CONPTY_CI', None)
|
|
24
|
+
|
|
25
|
+
@pytest.fixture(scope='function')
|
|
26
|
+
def pty_fixture():
|
|
27
|
+
pty = PTY(80, 20, backend=backend)
|
|
28
|
+
# loc = bytes(os.getcwd(), 'utf8')
|
|
29
|
+
assert pty.spawn(CMD)
|
|
30
|
+
time.sleep(0.3)
|
|
31
|
+
# if backend == Backend.ConPTY:
|
|
32
|
+
# pty.write("\x1b[?1;0c\x1b[0;0R")
|
|
33
|
+
return pty
|
|
34
|
+
return pty_fixture
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
conpty_provider = pty_factory(Backend.ConPTY)
|
|
38
|
+
winpty_provider = pty_factory(Backend.WinPTY)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.fixture(scope='module', params=['WinPTY', 'ConPTY'])
|
|
42
|
+
def pty_fixture(request):
|
|
43
|
+
backend = request.param
|
|
44
|
+
if os.environ.get('CI_RUNNING', None) == '1':
|
|
45
|
+
if backend == 'ConPTY':
|
|
46
|
+
os.environ['CI'] = '1'
|
|
47
|
+
os.environ['CONPTY_CI'] = '1'
|
|
48
|
+
if backend == 'WinPTY':
|
|
49
|
+
os.environ.pop('CI', None)
|
|
50
|
+
os.environ.pop('CONPTY_CI', None)
|
|
51
|
+
|
|
52
|
+
backend = getattr(Backend, backend)
|
|
53
|
+
def _pty_factory():
|
|
54
|
+
try:
|
|
55
|
+
pty = PTY(80, 25, backend=backend)
|
|
56
|
+
except WinptyError:
|
|
57
|
+
pytest.skip()
|
|
58
|
+
return None
|
|
59
|
+
assert pty.spawn(CMD)
|
|
60
|
+
time.sleep(0.3)
|
|
61
|
+
return pty
|
|
62
|
+
return _pty_factory
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# @pytest.fixture(scope='function', params=[
|
|
66
|
+
# pytest.lazy_fixture('conpty_provider'),
|
|
67
|
+
# pytest.lazy_fixture('winpty_provider')])
|
|
68
|
+
# def pty_fixture(request):
|
|
69
|
+
# pty = request.param
|
|
70
|
+
# return pty
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_read(pty_fixture, capsys):
|
|
74
|
+
pty = pty_fixture()
|
|
75
|
+
loc = os.getcwd()
|
|
76
|
+
readline = ''
|
|
77
|
+
|
|
78
|
+
with capsys.disabled():
|
|
79
|
+
start_time = time.time()
|
|
80
|
+
while loc not in readline:
|
|
81
|
+
if time.time() - start_time > 5:
|
|
82
|
+
break
|
|
83
|
+
readline += pty.read()
|
|
84
|
+
assert loc in readline or 'cmd' in readline
|
|
85
|
+
del pty
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_write(pty_fixture):
|
|
89
|
+
pty = pty_fixture()
|
|
90
|
+
line = pty.read()
|
|
91
|
+
|
|
92
|
+
str_text = 'Eggs, ham and spam ünicode'
|
|
93
|
+
# text = bytes(str_text, 'utf-8')
|
|
94
|
+
num_bytes = pty.write(str_text)
|
|
95
|
+
|
|
96
|
+
line = ''
|
|
97
|
+
start_time = time.time()
|
|
98
|
+
while str_text not in line:
|
|
99
|
+
if time.time() - start_time > 5:
|
|
100
|
+
break
|
|
101
|
+
line += pty.read()
|
|
102
|
+
|
|
103
|
+
assert str_text in line
|
|
104
|
+
del pty
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_isalive(pty_fixture):
|
|
108
|
+
pty = pty_fixture()
|
|
109
|
+
pty.write('exit\r\n')
|
|
110
|
+
|
|
111
|
+
text = 'exit'
|
|
112
|
+
line = ''
|
|
113
|
+
while text not in line:
|
|
114
|
+
try:
|
|
115
|
+
line += pty.read()
|
|
116
|
+
except Exception:
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
while pty.isalive():
|
|
120
|
+
try:
|
|
121
|
+
pty.read()
|
|
122
|
+
# continue
|
|
123
|
+
except Exception:
|
|
124
|
+
break
|
|
125
|
+
|
|
126
|
+
assert not pty.isalive()
|
|
127
|
+
del pty
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# def test_agent_spawn_fail(pty_fixture):
|
|
131
|
+
# pty = pty_fixture
|
|
132
|
+
# try:
|
|
133
|
+
# pty.spawn(CMD)
|
|
134
|
+
# assert False
|
|
135
|
+
# except WinptyError:
|
|
136
|
+
# pass
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# @pytest.mark.parametrize(
|
|
140
|
+
# 'backend_name,backend',
|
|
141
|
+
# [("ConPTY", Backend.ConPTY), ('WinPTY', Backend.WinPTY)])
|
|
142
|
+
# def test_pty_create_size_fail(backend_name, backend):
|
|
143
|
+
# try:
|
|
144
|
+
# PTY(80, -25, backend=backend)
|
|
145
|
+
# assert False
|
|
146
|
+
# except WinptyError:
|
|
147
|
+
# pass
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# def test_agent_resize_fail(pty_fixture):
|
|
151
|
+
# pty = pty_fixture()
|
|
152
|
+
# try:
|
|
153
|
+
# pty.set_size(-80, 70)
|
|
154
|
+
# assert False
|
|
155
|
+
# except WinptyError:
|
|
156
|
+
# pass
|
|
157
|
+
# finally:
|
|
158
|
+
# del pty
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_agent_resize(pty_fixture):
|
|
162
|
+
pty = pty_fixture()
|
|
163
|
+
pty.set_size(80, 70)
|
|
164
|
+
del pty
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""winpty wrapper tests."""
|
|
3
|
+
|
|
4
|
+
# Standard library imports
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
import signal
|
|
8
|
+
import time
|
|
9
|
+
import sys
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
# Third party imports
|
|
13
|
+
import pytest
|
|
14
|
+
from flaky import flaky
|
|
15
|
+
|
|
16
|
+
# Local imports
|
|
17
|
+
from winpty import WinptyError
|
|
18
|
+
from winpty.enums import Backend
|
|
19
|
+
from winpty.ptyprocess import PtyProcess, which
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture(scope='module', params=['WinPTY', 'ConPTY'])
|
|
24
|
+
def pty_fixture(request):
|
|
25
|
+
backend = request.param
|
|
26
|
+
if os.environ.get('CI_RUNNING', None) == '1':
|
|
27
|
+
if backend == 'ConPTY':
|
|
28
|
+
os.environ['CI'] = '1'
|
|
29
|
+
os.environ['CONPTY_CI'] = '1'
|
|
30
|
+
if backend == 'WinPTY':
|
|
31
|
+
os.environ.pop('CI', None)
|
|
32
|
+
os.environ.pop('CONPTY_CI', None)
|
|
33
|
+
|
|
34
|
+
backend = getattr(Backend, backend)
|
|
35
|
+
def _pty_factory(cmd=None, env=None):
|
|
36
|
+
cmd = cmd or 'cmd'
|
|
37
|
+
try:
|
|
38
|
+
pty = PtyProcess.spawn(cmd, env=env, backend=backend)
|
|
39
|
+
except WinptyError:
|
|
40
|
+
pytest.skip()
|
|
41
|
+
return None
|
|
42
|
+
return pty
|
|
43
|
+
# time.sleep(10)
|
|
44
|
+
_pty_factory.backend = request.param
|
|
45
|
+
return _pty_factory
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@flaky(max_runs=40, min_passes=1)
|
|
49
|
+
def test_read(pty_fixture):
|
|
50
|
+
pty = pty_fixture()
|
|
51
|
+
loc = os.getcwd()
|
|
52
|
+
data = ''
|
|
53
|
+
tries = 0
|
|
54
|
+
while loc not in data and tries < 10:
|
|
55
|
+
try:
|
|
56
|
+
data += pty.read()
|
|
57
|
+
except EOFError:
|
|
58
|
+
pass
|
|
59
|
+
tries += 1
|
|
60
|
+
assert loc in data
|
|
61
|
+
pty.terminate()
|
|
62
|
+
time.sleep(2)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@flaky(max_runs=40, min_passes=1)
|
|
66
|
+
def test_write(pty_fixture):
|
|
67
|
+
pty = pty_fixture()
|
|
68
|
+
|
|
69
|
+
text = 'Eggs, ham and spam ünicode'
|
|
70
|
+
pty.write(text)
|
|
71
|
+
|
|
72
|
+
data = ''
|
|
73
|
+
tries = 0
|
|
74
|
+
while text not in data and tries < 10:
|
|
75
|
+
try:
|
|
76
|
+
data += pty.read()
|
|
77
|
+
except EOFError:
|
|
78
|
+
pass
|
|
79
|
+
tries += 1
|
|
80
|
+
assert text in data
|
|
81
|
+
pty.terminate()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@pytest.mark.xfail(reason="It fails sometimes due to long strings")
|
|
85
|
+
@flaky(max_runs=40, min_passes=1)
|
|
86
|
+
def test_isalive(pty_fixture):
|
|
87
|
+
pty = pty_fixture()
|
|
88
|
+
time.sleep(4)
|
|
89
|
+
|
|
90
|
+
pty.write('echo \"foo\"\r\nexit\r\n')
|
|
91
|
+
data = ''
|
|
92
|
+
while True:
|
|
93
|
+
try:
|
|
94
|
+
print('Stuck')
|
|
95
|
+
data += pty.read()
|
|
96
|
+
except EOFError:
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
regex = re.compile(".*foo.*")
|
|
100
|
+
assert regex.findall(data)
|
|
101
|
+
assert not pty.isalive()
|
|
102
|
+
pty.terminate()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@pytest.mark.xfail(reason="It fails sometimes due to long strings")
|
|
106
|
+
@flaky(max_runs=40, min_passes=1)
|
|
107
|
+
def test_readline(pty_fixture):
|
|
108
|
+
env = os.environ.copy()
|
|
109
|
+
env['foo'] = 'bar'
|
|
110
|
+
pty = pty_fixture(env=env)
|
|
111
|
+
|
|
112
|
+
# Ensure that the echo print has its own CRLF
|
|
113
|
+
pty.write('cls\r\n')
|
|
114
|
+
pty.write('echo %foo%\r\n')
|
|
115
|
+
|
|
116
|
+
data = ''
|
|
117
|
+
tries = 0
|
|
118
|
+
while 'bar' not in data and tries < 10:
|
|
119
|
+
data = pty.readline()
|
|
120
|
+
tries += 1
|
|
121
|
+
|
|
122
|
+
assert 'bar' in data
|
|
123
|
+
|
|
124
|
+
pty.terminate()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_close(pty_fixture):
|
|
128
|
+
pty = pty_fixture()
|
|
129
|
+
pty.close()
|
|
130
|
+
assert not pty.isalive()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_flush(pty_fixture):
|
|
134
|
+
pty = pty_fixture()
|
|
135
|
+
pty.flush()
|
|
136
|
+
pty.terminate()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_intr(pty_fixture):
|
|
140
|
+
pty = pty_fixture(cmd=[sys.executable, 'import time; time.sleep(10)'])
|
|
141
|
+
pty.sendintr()
|
|
142
|
+
assert pty.wait() != 0
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_send_control(pty_fixture):
|
|
146
|
+
pty = pty_fixture(cmd=[sys.executable, 'import time; time.sleep(10)'])
|
|
147
|
+
pty.sendcontrol('d')
|
|
148
|
+
assert pty.wait() != 0
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@pytest.mark.skipif(which('cat') is None, reason="Requires cat on the PATH")
|
|
152
|
+
def test_send_eof(pty_fixture):
|
|
153
|
+
cat = pty_fixture('cat')
|
|
154
|
+
cat.sendeof()
|
|
155
|
+
assert cat.wait() == 0
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_isatty(pty_fixture):
|
|
159
|
+
pty = pty_fixture()
|
|
160
|
+
assert pty.isatty()
|
|
161
|
+
pty.terminate()
|
|
162
|
+
assert not pty.isatty()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_wait(pty_fixture):
|
|
166
|
+
pty = pty_fixture(cmd=[sys.executable, '--version'])
|
|
167
|
+
assert pty.wait() == 0
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_exit_status(pty_fixture):
|
|
171
|
+
pty = pty_fixture(cmd=[sys.executable])
|
|
172
|
+
pty.write('import sys;sys.exit(1)\r\n')
|
|
173
|
+
pty.wait()
|
|
174
|
+
assert pty.exitstatus == 1
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# @pytest.mark.timeout(30)
|
|
178
|
+
def test_kill_sigterm(pty_fixture):
|
|
179
|
+
pty = pty_fixture()
|
|
180
|
+
pty.write('echo \"foo\"\r\nsleep 1000\r\n')
|
|
181
|
+
pty.read()
|
|
182
|
+
pty.kill(signal.SIGTERM)
|
|
183
|
+
|
|
184
|
+
while True:
|
|
185
|
+
try:
|
|
186
|
+
pty.read()
|
|
187
|
+
except EOFError:
|
|
188
|
+
break
|
|
189
|
+
|
|
190
|
+
assert not pty.isalive()
|
|
191
|
+
assert pty.exitstatus == signal.SIGTERM
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# @pytest.mark.timeout(30)
|
|
195
|
+
def test_terminate(pty_fixture):
|
|
196
|
+
pty = pty_fixture()
|
|
197
|
+
pty.write('echo \"foo\"\r\nsleep 1000\r\n')
|
|
198
|
+
pty.read()
|
|
199
|
+
pty.terminate()
|
|
200
|
+
|
|
201
|
+
while True:
|
|
202
|
+
try:
|
|
203
|
+
pty.read()
|
|
204
|
+
except EOFError:
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
assert not pty.isalive()
|
|
208
|
+
assert pty.closed
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# @pytest.mark.timeout(30)
|
|
212
|
+
def test_terminate_force(pty_fixture):
|
|
213
|
+
pty = pty_fixture()
|
|
214
|
+
pty.write('echo \"foo\"\r\nsleep 1000\r\n')
|
|
215
|
+
pty.read()
|
|
216
|
+
pty.terminate(force=True)
|
|
217
|
+
|
|
218
|
+
while True:
|
|
219
|
+
try:
|
|
220
|
+
pty.read()
|
|
221
|
+
except EOFError:
|
|
222
|
+
break
|
|
223
|
+
|
|
224
|
+
assert not pty.isalive()
|
|
225
|
+
assert pty.closed
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_terminate_loop(pty_fixture):
|
|
229
|
+
pty = pty_fixture()
|
|
230
|
+
loop = asyncio.SelectorEventLoop()
|
|
231
|
+
asyncio.set_event_loop(loop)
|
|
232
|
+
|
|
233
|
+
def reader():
|
|
234
|
+
try:
|
|
235
|
+
data = pty.read()
|
|
236
|
+
except EOFError:
|
|
237
|
+
loop.remove_reader(pty.fd)
|
|
238
|
+
loop.stop()
|
|
239
|
+
|
|
240
|
+
loop.add_reader(pty.fd, reader)
|
|
241
|
+
loop.call_soon(pty.write, 'echo \"foo\"\r\nsleep 1000\r\n')
|
|
242
|
+
loop.call_soon(pty.terminate, True)
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
loop.run_forever()
|
|
246
|
+
finally:
|
|
247
|
+
loop.close()
|
|
248
|
+
|
|
249
|
+
assert not pty.isalive()
|
|
250
|
+
assert pty.closed
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def test_getwinsize(pty_fixture):
|
|
254
|
+
pty = pty_fixture()
|
|
255
|
+
assert pty.getwinsize() == (24, 80)
|
|
256
|
+
pty.terminate()
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_setwinsize(pty_fixture):
|
|
260
|
+
pty = pty_fixture()
|
|
261
|
+
pty.setwinsize(50, 110)
|
|
262
|
+
assert pty.getwinsize() == (50, 110)
|
|
263
|
+
pty.terminate()
|
|
264
|
+
|
|
265
|
+
pty = PtyProcess.spawn('cmd', dimensions=(60, 120))
|
|
266
|
+
assert pty.getwinsize() == (60, 120)
|
|
267
|
+
pty.terminate()
|
|
268
|
+
|
|
Binary file
|
winpty/winpty.pyi
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""Stub typing declarations for the native PTY object."""
|
|
4
|
+
|
|
5
|
+
# Standard library imports
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
# Local imports
|
|
9
|
+
from .enums import MouseMode, AgentConfig
|
|
10
|
+
|
|
11
|
+
__version__: str
|
|
12
|
+
|
|
13
|
+
class WinptyError(Exception): ...
|
|
14
|
+
|
|
15
|
+
class PTY:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
cols: int,
|
|
19
|
+
rows: int,
|
|
20
|
+
backend: Optional[int] = None,
|
|
21
|
+
mouse_mode: int = MouseMode.WINPTY_MOUSE_MODE_NONE,
|
|
22
|
+
timeout: int = 30000,
|
|
23
|
+
agent_config: int = AgentConfig.WINPTY_FLAG_COLOR_ESCAPES,
|
|
24
|
+
): ...
|
|
25
|
+
def spawn(
|
|
26
|
+
self,
|
|
27
|
+
appname: str,
|
|
28
|
+
cmdline: Optional[str] = None,
|
|
29
|
+
cwd: Optional[str] = None,
|
|
30
|
+
env: Optional[str] = None,
|
|
31
|
+
) -> bool: ...
|
|
32
|
+
def set_size(self, cols: int, rows: int): ...
|
|
33
|
+
def read(self, blocking: bool = False) -> str: ...
|
|
34
|
+
def write(self, to_write: str) -> int: ...
|
|
35
|
+
def isalive(self) -> bool: ...
|
|
36
|
+
def get_exitstatus(self) -> Optional[int]: ...
|
|
37
|
+
def iseof(self) -> bool: ...
|
|
38
|
+
def cancel_io(self) -> bool: ...
|
|
39
|
+
@property
|
|
40
|
+
def pid(self) -> Optional[int]: ...
|
|
41
|
+
@property
|
|
42
|
+
def fd(self) -> Optional[int]: ...
|