Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-terminado for
openSUSE:Factory checked in at 2022-10-01 17:43:43
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-terminado (Old)
and /work/SRC/openSUSE:Factory/.python-terminado.new.2275 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-terminado"
Sat Oct 1 17:43:43 2022 rev:17 rq:1007303 version:0.16.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-terminado/python-terminado.changes
2022-07-13 13:46:01.514086059 +0200
+++
/work/SRC/openSUSE:Factory/.python-terminado.new.2275/python-terminado.changes
2022-10-01 17:44:09.229778957 +0200
@@ -1,0 +2,11 @@
+Fri Sep 30 15:16:04 UTC 2022 - Arun Persaud <[email protected]>
+
+- update to version 0.16.0:
+ * Bugs fixed
+ + Fix issue where large stdin writes can cause Tornado to hang
+ #189 (@KoopaKing)
+ * Maintenance and upkeep improvements
+ + Switch to using hatch version #186 (@blink1073)
+ + Fix flake8 v5 compat #179 (@blink1073)
+
+-------------------------------------------------------------------
Old:
----
terminado-0.15.0.tar.gz
New:
----
terminado-0.16.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-terminado.spec ++++++
--- /var/tmp/diff_new_pack.f0nljH/_old 2022-10-01 17:44:12.057784100 +0200
+++ /var/tmp/diff_new_pack.f0nljH/_new 2022-10-01 17:44:12.061784107 +0200
@@ -16,11 +16,8 @@
#
-%{?!python_module:%define python_module() python-%{**} python3-%{**}}
-# Disable tests until random testing race condition fixed, see:
https://github.com/jupyter/terminado/issues/21
-%bcond_with tests
Name: python-terminado
-Version: 0.15.0
+Version: 0.16.0
Release: 0
Summary: Terminals served to termjs using Tornado websockets
License: BSD-2-Clause
@@ -38,8 +35,8 @@
Requires: python-tornado >= 4
BuildArch: noarch
# SECTION test requirements
-BuildRequires: %{python_module pytest}
BuildRequires: %{python_module pytest-timeout}
+BuildRequires: %{python_module pytest}
# /SECTION
%python_subpackages
@@ -53,6 +50,7 @@
%prep
%setup -q -n terminado-%{version}
+sed -i '/addopts/ s/--durations 10 --color=yes//' pyproject.toml
%build
%pyproject_wheel
++++++ terminado-0.15.0.tar.gz -> terminado-0.16.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/terminado-0.15.0/.github/workflows/check-release.yml
new/terminado-0.16.0/.github/workflows/check-release.yml
--- old/terminado-0.15.0/.github/workflows/check-release.yml 2020-02-02
01:00:00.000000000 +0100
+++ new/terminado-0.16.0/.github/workflows/check-release.yml 2020-02-02
01:00:00.000000000 +0100
@@ -11,10 +11,6 @@
jobs:
check_release:
runs-on: ubuntu-latest
- strategy:
- matrix:
- group: [check_release, link_check]
- fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
@@ -22,10 +18,6 @@
run: |
pip install -e .
- name: Check Release
- if: ${{ matrix.group == 'check_release' }}
uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
- - name: Run Link Check
- if: ${{ matrix.group == 'link_check' }}
- uses: jupyter-server/jupyter_releaser/.github/actions/check-links@v1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/terminado-0.15.0/.github/workflows/test.yml
new/terminado-0.16.0/.github/workflows/test.yml
--- old/terminado-0.15.0/.github/workflows/test.yml 2020-02-02
01:00:00.000000000 +0100
+++ new/terminado-0.16.0/.github/workflows/test.yml 2020-02-02
01:00:00.000000000 +0100
@@ -94,14 +94,30 @@
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
- uses: jupyterlab/maintainer-tools/.github/actions/test-sdist@v1
+ check-links:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
+ - uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1
+
+ jupyter_server_terminals:
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@v2
+ - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
+ - uses: jupyterlab/maintainer-tools/.github/actions/downstream-test@v1
+ with:
+ package_name: jupyter_server_terminals
+
# Run "pre-commit run --all-files --hook-stage=manual"
pre-commit:
name: Run pre-commit hook
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- - name: Checkout
- uses: actions/checkout@v2
+ - uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v3
- name: Run pre-commit
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/terminado-0.15.0/.pre-commit-config.yaml
new/terminado-0.16.0/.pre-commit-config.yaml
--- old/terminado-0.15.0/.pre-commit-config.yaml 2020-02-02
01:00:00.000000000 +0100
+++ new/terminado-0.16.0/.pre-commit-config.yaml 2020-02-02
01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.2.0
+ rev: v4.3.0
hooks:
- id: end-of-file-fixer
- id: check-case-conflict
@@ -16,7 +16,7 @@
- id: trailing-whitespace
- repo: https://github.com/psf/black
- rev: 22.3.0
+ rev: 22.8.0
hooks:
- id: black
args: ["--line-length", "100"]
@@ -29,43 +29,39 @@
args: [--profile=black]
- repo: https://github.com/pre-commit/mirrors-prettier
- rev: v2.6.2
+ rev: v3.0.0-alpha.0
hooks:
- id: prettier
- repo: https://github.com/asottile/pyupgrade
- rev: v2.32.1
+ rev: v2.38.2
hooks:
- id: pyupgrade
args: [--py37-plus]
- repo: https://github.com/PyCQA/doc8
- rev: 0.11.1
+ rev: v1.0.0
hooks:
- id: doc8
args: [--max-line-length=200]
stages: [manual]
- repo: https://github.com/pycqa/flake8
- rev: 4.0.1
+ rev: 5.0.4
hooks:
- id: flake8
additional_dependencies:
- [
- "flake8-bugbear==20.1.4",
- "flake8-logging-format==0.6.0",
- "flake8-implicit-str-concat==0.2.0",
- ]
+ ["flake8-bugbear==22.6.22", "flake8-implicit-str-concat==0.2.0"]
stages: [manual]
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: "v0.950"
+ rev: "v0.971"
hooks:
- id: mypy
stages: [manual]
- repo: https://github.com/sirosen/check-jsonschema
- rev: 0.14.3
+ rev: 0.18.3
hooks:
- id: check-jsonschema
name: "Check GitHub Workflows"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/terminado-0.15.0/CHANGELOG.md
new/terminado-0.16.0/CHANGELOG.md
--- old/terminado-0.15.0/CHANGELOG.md 2020-02-02 01:00:00.000000000 +0100
+++ new/terminado-0.16.0/CHANGELOG.md 2020-02-02 01:00:00.000000000 +0100
@@ -2,12 +2,31 @@
<!-- <START NEW CHANGELOG ENTRY> -->
-## 0.15.0
+## 0.16.0
-No merged PRs
+([Full
Changelog](https://github.com/jupyter/terminado/compare/v0.15.0...7210e82a94596d7d8a00577169c09198efbe4633))
+
+### Bugs fixed
+
+- Fix issue where large stdin writes can cause Tornado to hang
[#189](https://github.com/jupyter/terminado/pull/189)
([@KoopaKing](https://github.com/KoopaKing))
+
+### Maintenance and upkeep improvements
+
+- Switch to using hatch version
[#186](https://github.com/jupyter/terminado/pull/186)
([@blink1073](https://github.com/blink1073))
+- Fix flake8 v5 compat [#179](https://github.com/jupyter/terminado/pull/179)
([@blink1073](https://github.com/blink1073))
+
+### Contributors to this release
+
+([GitHub contributors page for this
release](https://github.com/jupyter/terminado/graphs/contributors?from=2022-05-16&to=2022-09-29&type=c))
+
+[@blink1073](https://github.com/search?q=repo%3Ajupyter%2Fterminado+involves%3Ablink1073+updated%3A2022-05-16..2022-09-29&type=Issues)
|
[@KoopaKing](https://github.com/search?q=repo%3Ajupyter%2Fterminado+involves%3AKoopaKing+updated%3A2022-05-16..2022-09-29&type=Issues)
|
[@pre-commit-ci](https://github.com/search?q=repo%3Ajupyter%2Fterminado+involves%3Apre-commit-ci+updated%3A2022-05-16..2022-09-29&type=Issues)
<!-- <END NEW CHANGELOG ENTRY> -->
+## 0.15.0
+
+No merged PRs
+
## 0.13.3
([Full
Changelog](https://github.com/jupyter/terminado/compare/v0.13.2...2cccad61af7e7ec88e4db769664d8814985179d4))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/terminado-0.15.0/PKG-INFO
new/terminado-0.16.0/PKG-INFO
--- old/terminado-0.15.0/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
+++ new/terminado-0.16.0/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: terminado
-Version: 0.15.0
+Version: 0.16.0
Summary: Tornado websocket backend for the Xterm.js Javascript terminal
emulator library.
Project-URL: Homepage, https://github.com/jupyter/terminado
Author-email: Jupyter Development Team <[email protected]>
@@ -31,6 +31,7 @@
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+License-File: LICENSE
Classifier: Environment :: Web Environment
Classifier: License :: OSI Approved :: BSD License
Classifier: Programming Language :: Python :: 3
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/terminado-0.15.0/RELEASE.md
new/terminado-0.16.0/RELEASE.md
--- old/terminado-0.15.0/RELEASE.md 2020-02-02 01:00:00.000000000 +0100
+++ new/terminado-0.16.0/RELEASE.md 2020-02-02 01:00:00.000000000 +0100
@@ -4,14 +4,13 @@
```
git clean -dffx
-python setup.py sdist
-python setup.py bdist_wheel
-export script_version=`python setup.py --version 2>/dev/null`
+pip install pipx
+pipx run build
+export script_version=`pipx run hatch version 2>/dev/null`
git commit -a -m "Release $script_version"
git tag $script_version
git push --all
git push --tags
-pip install twine
-twine check dist/*
-twine upload dist/*
+pipx run twine check dist/*
+pipx run twine upload dist/*
```
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/terminado-0.15.0/demos/custom_exec.py
new/terminado-0.16.0/demos/custom_exec.py
--- old/terminado-0.15.0/demos/custom_exec.py 1970-01-01 01:00:00.000000000
+0100
+++ new/terminado-0.16.0/demos/custom_exec.py 2020-02-02 01:00:00.000000000
+0100
@@ -0,0 +1,43 @@
+"""Using a custom thread pool for subprocess writes.
+"""
+from concurrent import futures
+
+import tornado.web
+
+# This demo requires tornado_xstatic and XStatic-term.js
+import tornado_xstatic
+from common_demo_stuff import STATIC_DIR, TEMPLATE_DIR, run_and_show_browser
+
+from terminado import SingleTermManager, TermSocket
+
+
+class TerminalPageHandler(tornado.web.RequestHandler):
+ def get(self):
+ return self.render(
+ "termpage.html",
+ static=self.static_url,
+ xstatic=self.application.settings["xstatic_url"],
+ ws_url_path="/websocket",
+ )
+
+
+def main(argv):
+ with futures.ThreadPoolExecutor(max_workers=2) as custom_exec:
+ term_manager = SingleTermManager(shell_command=["bash"],
blocking_io_executor=custom_exec)
+ handlers = [
+ (r"/websocket", TermSocket, {"term_manager": term_manager}),
+ (r"/", TerminalPageHandler),
+ (r"/xstatic/(.*)", tornado_xstatic.XStaticFileHandler,
{"allowed_modules": ["termjs"]}),
+ ]
+ app = tornado.web.Application(
+ handlers,
+ static_path=STATIC_DIR,
+ template_path=TEMPLATE_DIR,
+ xstatic_url=tornado_xstatic.url_maker("/xstatic/"),
+ )
+ app.listen(8765, "localhost")
+ run_and_show_browser("http://localhost:8765/", term_manager)
+
+
+if __name__ == "__main__":
+ main([])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/terminado-0.15.0/demos/templates/termpage.html
new/terminado-0.16.0/demos/templates/termpage.html
--- old/terminado-0.15.0/demos/templates/termpage.html 2020-02-02
01:00:00.000000000 +0100
+++ new/terminado-0.16.0/demos/templates/termpage.html 2020-02-02
01:00:00.000000000 +0100
@@ -1,4 +1,4 @@
-<!DOCTYPE html>
+<!doctype html>
<head>
<meta charset="UTF-8" />
<title>pyxterm</title>
@@ -40,11 +40,11 @@
function calculate_size(element) {
var rows = Math.max(
2,
- Math.floor(element.innerHeight / termRowHeight) - 1
+ Math.floor(element.innerHeight / termRowHeight) - 1,
);
var cols = Math.max(
3,
- Math.floor(element.innerWidth / termColWidth) - 1
+ Math.floor(element.innerWidth / termColWidth) - 1,
);
console.log(
"resize:",
@@ -53,7 +53,7 @@
element.innerHeight,
element.innerWidth,
rows,
- cols
+ cols,
);
return { rows: rows, cols: cols };
}
@@ -71,7 +71,7 @@
geom.cols,
window.innerHeight,
window.innerWidth,
- ])
+ ]),
);
};
};
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/terminado-0.15.0/demos/templates/uimod.html
new/terminado-0.16.0/demos/templates/uimod.html
--- old/terminado-0.15.0/demos/templates/uimod.html 2020-02-02
01:00:00.000000000 +0100
+++ new/terminado-0.16.0/demos/templates/uimod.html 2020-02-02
01:00:00.000000000 +0100
@@ -1,4 +1,4 @@
-<!DOCTYPE html>
+<!doctype html>
<head>
<meta charset="UTF-8" />
<title>Terminado UIModule demo</title>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/terminado-0.15.0/pyproject.toml
new/terminado-0.16.0/pyproject.toml
--- old/terminado-0.15.0/pyproject.toml 2020-02-02 01:00:00.000000000 +0100
+++ new/terminado-0.16.0/pyproject.toml 2020-02-02 01:00:00.000000000 +0100
@@ -1,10 +1,10 @@
[build-system]
-requires = ["hatchling>=0.25"]
+requires = ["hatchling>=1.5"]
build-backend = "hatchling.build"
[project]
name = "terminado"
-version = "0.15.0"
+dynamic = ["version"]
license = { file = "LICENSE" }
description = "Tornado websocket backend for the Xterm.js Javascript terminal
emulator library."
classifiers = [ "Environment :: Web Environment", "License :: OSI Approved ::
BSD License", "Programming Language :: Python :: 3", "Topic :: Terminals ::
Terminal Emulators/X Terminals",]
@@ -28,22 +28,8 @@
[tool.jupyter-releaser]
skip = ["check-links"]
-[tool.tbump.version]
-current = "0.15.0"
-regex = '''
- (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
- ((?P<channel>a|b|rc|.dev)(?P<release>\d+))?
-'''
-
-[tool.tbump.git]
-message_template = "Bump to {new_version}"
-tag_template = "v{new_version}"
-
-[[tool.tbump.file]]
-src = "terminado/__init__.py"
-
-[[tool.tbump.file]]
-src = "pyproject.toml"
+[tool.hatch.version]
+path = "terminado/__init__.py"
[tool.pytest.ini_options]
addopts = "-raXs --durations 10 --color=yes --doctest-modules"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/terminado-0.15.0/terminado/__init__.py
new/terminado-0.16.0/terminado/__init__.py
--- old/terminado-0.15.0/terminado/__init__.py 2020-02-02 01:00:00.000000000
+0100
+++ new/terminado-0.16.0/terminado/__init__.py 2020-02-02 01:00:00.000000000
+0100
@@ -10,4 +10,4 @@
from .management import UniqueTermManager # noqa
from .websocket import TermSocket # noqa
-__version__ = "0.15.0"
+__version__ = "0.16.0"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/terminado-0.15.0/terminado/_static/terminado.js
new/terminado-0.16.0/terminado/_static/terminado.js
--- old/terminado-0.15.0/terminado/_static/terminado.js 2020-02-02
01:00:00.000000000 +0100
+++ new/terminado-0.16.0/terminado/_static/terminado.js 2020-02-02
01:00:00.000000000 +0100
@@ -18,7 +18,7 @@
size.cols,
window.innerHeight,
window.innerWidth,
- ])
+ ]),
);
term.on("data", function (data) {
ws.send(JSON.stringify(["stdin", data]));
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/terminado-0.15.0/terminado/management.py
new/terminado-0.16.0/terminado/management.py
--- old/terminado-0.15.0/terminado/management.py 2020-02-02
01:00:00.000000000 +0100
+++ new/terminado-0.16.0/terminado/management.py 2020-02-02
01:00:00.000000000 +0100
@@ -14,6 +14,7 @@
import signal
import warnings
from collections import deque
+from concurrent import futures
try:
from ptyprocess import PtyProcessUnicode
@@ -153,7 +154,13 @@
"""Base class for a terminal manager."""
def __init__(
- self, shell_command, server_url="", term_settings=None,
extra_env=None, ioloop=None
+ self,
+ shell_command,
+ server_url="",
+ term_settings=None,
+ extra_env=None,
+ ioloop=None,
+ blocking_io_executor=None,
):
self.shell_command = shell_command
self.server_url = server_url
@@ -163,6 +170,13 @@
self.ptys_by_fd = {}
+ if blocking_io_executor is None:
+ self._blocking_io_executor_is_external = False
+ self.blocking_io_executor =
futures.ThreadPoolExecutor(max_workers=1)
+ else:
+ self._blocking_io_executor_is_external = True
+ self.blocking_io_executor = blocking_io_executor
+
if ioloop is not None:
warnings.warn(
f"Setting {self.__class__.__name__}.ioloop is deprecated and
ignored",
@@ -259,6 +273,8 @@
async def shutdown(self):
await self.kill_all()
+ if not self._blocking_io_executor_is_external:
+ self.blocking_io_executor.shutdown(wait=False, cancel_futures=True)
async def kill_all(self):
futures = []
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/terminado-0.15.0/terminado/tests/basic_test.py
new/terminado-0.16.0/terminado/tests/basic_test.py
--- old/terminado-0.15.0/terminado/tests/basic_test.py 2020-02-02
01:00:00.000000000 +0100
+++ new/terminado-0.16.0/terminado/tests/basic_test.py 2020-02-02
01:00:00.000000000 +0100
@@ -1,287 +1,330 @@
-# basic_tests.py -- Basic unit tests for Terminado
-
-# Copyright (c) Jupyter Development Team
-# Copyright (c) 2014, Ramalingam Saravanan <[email protected]>
-# Distributed under the terms of the Simplified BSD License.
-
-
-import asyncio
-import datetime
-import json
-import os
-import re
-
-# We must set the policy for python >=3.8, see
https://www.tornadoweb.org/en/stable/#installation
-# Snippet from
https://github.com/tornadoweb/tornado/issues/2608#issuecomment-619524992
-import sys
-import unittest
-from sys import platform
-
-import pytest
-import tornado
-import tornado.httpserver
-import tornado.testing
-from tornado.ioloop import IOLoop
-
-from terminado import NamedTermManager, SingleTermManager, TermSocket,
UniqueTermManager
-
-if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and
sys.platform.startswith("win"):
- asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
-
-#
-# The timeout we use to assume no more messages are coming
-# from the sever.
-#
-DONE_TIMEOUT = 1.0
-os.environ["ASYNC_TEST_TIMEOUT"] = "20" # Global test case timeout
-
-MAX_TERMS = 3 # Testing thresholds
-
-
-class TestTermClient:
- """Test connection to a terminal manager"""
-
- __test__ = False
-
- def __init__(self, websocket):
- self.ws = websocket
- self.pending_read = None
-
- async def read_msg(self):
-
- # Because the Tornado Websocket client has no way to cancel
- # a pending read, we have to keep track of them...
- if self.pending_read is None:
- self.pending_read = self.ws.read_message()
-
- response = await self.pending_read
- self.pending_read = None
- if response:
- response = json.loads(response)
- return response
-
- async def read_all_msg(self, timeout=DONE_TIMEOUT):
- """Read messages until read times out"""
- msglist: list = []
- delta = datetime.timedelta(seconds=timeout)
- while True:
- try:
- mf = self.read_msg()
- msg = await tornado.gen.with_timeout(delta, mf)
- except tornado.gen.TimeoutError:
- return msglist
-
- msglist.append(msg)
-
- async def write_msg(self, msg):
- await self.ws.write_message(json.dumps(msg))
-
- async def read_stdout(self, timeout=DONE_TIMEOUT):
- """Read standard output until timeout read reached,
- return stdout and any non-stdout msgs received."""
- msglist = await self.read_all_msg(timeout)
- stdout = "".join([msg[1] for msg in msglist if msg[0] == "stdout"])
- othermsg = [msg for msg in msglist if msg[0] != "stdout"]
- return (stdout, othermsg)
-
- async def write_stdin(self, data):
- """Write to terminal stdin"""
- await self.write_msg(["stdin", data])
-
- async def get_pid(self):
- """Get process ID of terminal shell process"""
- await self.read_stdout() # Clear out any pending
- await self.write_stdin("echo $$\r")
- (stdout, extra) = await self.read_stdout()
- if os.name == "nt":
- match = re.search(r"echo \$\$\\.*?\\r\\n(\d+)", repr(stdout))
- assert match is not None
- pid = int(match.groups()[0])
- else:
- pid = int(stdout.split("\n")[1])
- return pid
-
- def close(self):
- self.ws.close()
-
-
-class TermTestCase(tornado.testing.AsyncHTTPTestCase):
-
- # Factory for TestTermClient, because it has to be async
- # See: https://github.com/tornadoweb/tornado/issues/1161
- async def get_term_client(self, path):
- port = self.get_http_port()
- url = "ws://127.0.0.1:%d%s" % (port, path)
- request = tornado.httpclient.HTTPRequest(
- url, headers={"Origin": "http://127.0.0.1:%d" % port}
- )
-
- ws = await tornado.websocket.websocket_connect(request)
- return TestTermClient(ws)
-
- async def get_term_clients(self, paths):
- return await asyncio.gather(*(self.get_term_client(path) for path in
paths))
-
- async def get_pids(self, tm_list):
- pids = []
- for tm in tm_list: # Must be sequential, in case terms are shared
- pid = await tm.get_pid()
- pids.append(pid)
-
- return pids
-
- def tearDown(self):
- run = IOLoop.current().run_sync
- run(self.named_tm.kill_all)
- run(self.single_tm.kill_all)
- run(self.unique_tm.kill_all)
- super().tearDown()
-
- def get_app(self):
- self.named_tm = NamedTermManager(
- shell_command=["bash"],
- max_terminals=MAX_TERMS,
- )
-
- self.single_tm = SingleTermManager(shell_command=["bash"])
-
- self.unique_tm = UniqueTermManager(
- shell_command=["bash"],
- max_terminals=MAX_TERMS,
- )
-
- named_tm = self.named_tm
-
- class NewTerminalHandler(tornado.web.RequestHandler):
- """Create a new named terminal, return redirect"""
-
- def get(self):
- name, terminal = named_tm.new_named_terminal()
- self.redirect("/named/" + name, permanent=False)
-
- return tornado.web.Application(
- [
- (r"/new", NewTerminalHandler),
- (r"/named/(\w+)", TermSocket, {"term_manager": self.named_tm}),
- (r"/single", TermSocket, {"term_manager": self.single_tm}),
- (r"/unique", TermSocket, {"term_manager": self.unique_tm}),
- ],
- debug=True,
- )
-
- test_urls = ("/named/term1", "/unique") + (("/single",) if os.name != "nt"
else ())
-
-
-class CommonTests(TermTestCase):
- @tornado.testing.gen_test
- async def test_basic(self):
- for url in self.test_urls:
- tm = await self.get_term_client(url)
- response = await tm.read_msg()
- self.assertEqual(response, ["setup", {}])
-
- # Check for initial shell prompt
- response = await tm.read_msg()
- self.assertEqual(response[0], "stdout")
- self.assertGreater(len(response[1]), 0)
- tm.close()
-
- @tornado.testing.gen_test
- async def test_basic_command(self):
- for url in self.test_urls:
- tm = await self.get_term_client(url)
- await tm.read_all_msg()
- await tm.write_stdin("whoami\n")
- (stdout, other) = await tm.read_stdout()
- if os.name == "nt":
- assert "whoami" in stdout
- else:
- assert stdout.startswith("who")
- assert other == []
- tm.close()
-
-
-class NamedTermTests(TermTestCase):
- def test_new(self):
- response = self.fetch("/new", follow_redirects=False)
- self.assertEqual(response.code, 302)
- url = response.headers["Location"]
-
- # Check that the new terminal was created
- name = url.split("/")[2]
- self.assertIn(name, self.named_tm.terminals)
-
- @tornado.testing.gen_test
- async def test_namespace(self):
- names = ["/named/1"] * 2 + ["/named/2"] * 2
- tms = await self.get_term_clients(names)
- pids = await self.get_pids(tms)
-
- self.assertEqual(pids[0], pids[1])
- self.assertEqual(pids[2], pids[3])
- self.assertNotEqual(pids[0], pids[3])
-
- terminal = self.named_tm.terminals["1"]
- killed = await terminal.terminate(True)
- assert killed
- assert not terminal.ptyproc.isalive()
- assert terminal.ptyproc.closed
-
- @tornado.testing.gen_test
- @pytest.mark.skipif("linux" not in platform, reason="It only works on
Linux")
- async def test_max_terminals(self):
- urls = ["/named/%d" % i for i in range(MAX_TERMS + 1)]
- tms = await self.get_term_clients(urls[:MAX_TERMS])
- _ = await self.get_pids(tms)
-
- # MAX_TERMS+1 should fail
- tm = await self.get_term_client(urls[MAX_TERMS])
- msg = await tm.read_msg()
- self.assertEqual(msg, None) # Connection closed
-
-
-class SingleTermTests(TermTestCase):
- @tornado.testing.gen_test
- async def test_single_process(self):
- tms = await self.get_term_clients(["/single", "/single"])
- pids = await self.get_pids(tms)
- self.assertEqual(pids[0], pids[1])
-
- assert self.single_tm.terminal is not None
- killed = await self.single_tm.terminal.terminate(True)
- assert killed
- assert self.single_tm.terminal.ptyproc.closed
-
-
-class UniqueTermTests(TermTestCase):
- @tornado.testing.gen_test
- async def test_unique_processes(self):
- tms = await self.get_term_clients(["/unique", "/unique"])
- pids = await self.get_pids(tms)
- self.assertNotEqual(pids[0], pids[1])
-
- @tornado.testing.gen_test
- @pytest.mark.skipif("linux" not in platform, reason="It only works on
Linux")
- async def test_max_terminals(self):
- tms = await self.get_term_clients(["/unique"] * MAX_TERMS)
- pids = await self.get_pids(tms)
- self.assertEqual(len(set(pids)), MAX_TERMS) # All PIDs unique
-
- # MAX_TERMS+1 should fail
- tm = await self.get_term_client("/unique")
- msg = await tm.read_msg()
- self.assertEqual(msg, None) # Connection closed
-
- # Close one
- tms[0].close()
- msg = await tms[0].read_msg() # Closed
- self.assertEqual(msg, None)
-
- # Should be able to open back up to MAX_TERMS
- tm = await self.get_term_client("/unique")
- msg = await tm.read_msg()
- self.assertEqual(msg[0], "setup")
-
-
-if __name__ == "__main__":
- unittest.main()
+# basic_tests.py -- Basic unit tests for Terminado
+
+# Copyright (c) Jupyter Development Team
+# Copyright (c) 2014, Ramalingam Saravanan <[email protected]>
+# Distributed under the terms of the Simplified BSD License.
+
+
+import asyncio
+import datetime
+import json
+import os
+import re
+
+# We must set the policy for python >=3.8, see
https://www.tornadoweb.org/en/stable/#installation
+# Snippet from
https://github.com/tornadoweb/tornado/issues/2608#issuecomment-619524992
+import sys
+import unittest
+from sys import platform
+
+import pytest
+import tornado
+import tornado.httpserver
+import tornado.testing
+from tornado.ioloop import IOLoop
+
+from terminado import NamedTermManager, SingleTermManager, TermSocket,
UniqueTermManager
+
+if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and
sys.platform.startswith("win"):
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
+
+#
+# The timeout we use to assume no more messages are coming
+# from the sever.
+#
+DONE_TIMEOUT = 1.0
+ASYNC_TEST_TIMEOUT = 30
+os.environ["ASYNC_TEST_TIMEOUT"] = str(ASYNC_TEST_TIMEOUT)
+
+MAX_TERMS = 3 # Testing thresholds
+
+
+class TestTermClient:
+ """Test connection to a terminal manager"""
+
+ __test__ = False
+
+ def __init__(self, websocket):
+ self.ws = websocket
+ self.pending_read = None
+
+ async def read_msg(self):
+
+ # Because the Tornado Websocket client has no way to cancel
+ # a pending read, we have to keep track of them...
+ if self.pending_read is None:
+ self.pending_read = self.ws.read_message()
+
+ response = await self.pending_read
+ self.pending_read = None
+ if response:
+ response = json.loads(response)
+ return response
+
+ async def read_all_msg(self, timeout=DONE_TIMEOUT):
+ """Read messages until read times out"""
+ msglist: list = []
+ delta = datetime.timedelta(seconds=timeout)
+ while True:
+ try:
+ mf = self.read_msg()
+ msg = await tornado.gen.with_timeout(delta, mf)
+ except tornado.gen.TimeoutError:
+ return msglist
+
+ msglist.append(msg)
+
+ async def write_msg(self, msg):
+ await self.ws.write_message(json.dumps(msg))
+
+ async def read_stdout(self, timeout=DONE_TIMEOUT):
+ """Read standard output until timeout read reached,
+ return stdout and any non-stdout msgs received."""
+ msglist = await self.read_all_msg(timeout)
+ stdout = "".join([msg[1] for msg in msglist if msg[0] == "stdout"])
+ othermsg = [msg for msg in msglist if msg[0] != "stdout"]
+ return (stdout, othermsg)
+
+ async def discard_stdout(self, timeout=DONE_TIMEOUT):
+ """Read standard output messages, discarding the data
+ as it's received. Return the number of bytes discarded
+ and any non-stdout msgs"""
+ othermsg: list = []
+ bytes_discarded = 0
+ delta = datetime.timedelta(seconds=timeout)
+ while True:
+ try:
+ mf = self.read_msg()
+ msg = await tornado.gen.with_timeout(delta, mf)
+ except tornado.gen.TimeoutError:
+ return bytes_discarded, othermsg
+ if msg[0] == "stdout":
+ bytes_discarded += len(msg[1])
+ else:
+ othermsg.append(msg)
+
+ async def write_stdin(self, data):
+ """Write to terminal stdin"""
+ await self.write_msg(["stdin", data])
+
+ async def get_pid(self):
+ """Get process ID of terminal shell process"""
+ await self.read_stdout() # Clear out any pending
+ await self.write_stdin("echo $$\r")
+ (stdout, extra) = await self.read_stdout()
+ if os.name == "nt":
+ match = re.search(r"echo \$\$\\.*?\\r\\n(\d+)", repr(stdout))
+ assert match is not None
+ pid = int(match.groups()[0])
+ else:
+ pid = int(stdout.split("\n")[1])
+ return pid
+
+ def close(self):
+ self.ws.close()
+
+
+class TermTestCase(tornado.testing.AsyncHTTPTestCase):
+
+ # Factory for TestTermClient, because it has to be async
+ # See: https://github.com/tornadoweb/tornado/issues/1161
+ async def get_term_client(self, path):
+ port = self.get_http_port()
+ url = "ws://127.0.0.1:%d%s" % (port, path)
+ request = tornado.httpclient.HTTPRequest(
+ url, headers={"Origin": "http://127.0.0.1:%d" % port}
+ )
+
+ ws = await tornado.websocket.websocket_connect(request)
+ return TestTermClient(ws)
+
+ async def get_term_clients(self, paths):
+ return await asyncio.gather(*(self.get_term_client(path) for path in
paths))
+
+ async def get_pids(self, tm_list):
+ pids = []
+ for tm in tm_list: # Must be sequential, in case terms are shared
+ pid = await tm.get_pid()
+ pids.append(pid)
+
+ return pids
+
+ def tearDown(self):
+ run = IOLoop.current().run_sync
+ run(self.named_tm.kill_all)
+ run(self.single_tm.kill_all)
+ run(self.unique_tm.kill_all)
+ super().tearDown()
+
+ def get_app(self):
+ self.named_tm = NamedTermManager(
+ shell_command=["bash"],
+ max_terminals=MAX_TERMS,
+ )
+
+ self.single_tm = SingleTermManager(shell_command=["bash"])
+
+ self.unique_tm = UniqueTermManager(
+ shell_command=["bash"],
+ max_terminals=MAX_TERMS,
+ )
+
+ named_tm = self.named_tm
+
+ class NewTerminalHandler(tornado.web.RequestHandler):
+ """Create a new named terminal, return redirect"""
+
+ def get(self):
+ name, terminal = named_tm.new_named_terminal()
+ self.redirect("/named/" + name, permanent=False)
+
+ return tornado.web.Application(
+ [
+ (r"/new", NewTerminalHandler),
+ (r"/named/(\w+)", TermSocket, {"term_manager": self.named_tm}),
+ (r"/single", TermSocket, {"term_manager": self.single_tm}),
+ (r"/unique", TermSocket, {"term_manager": self.unique_tm}),
+ ],
+ debug=True,
+ )
+
+ test_urls = ("/named/term1", "/unique") + (("/single",) if os.name != "nt"
else ())
+
+
+class CommonTests(TermTestCase):
+ @tornado.testing.gen_test
+ async def test_basic(self):
+ for url in self.test_urls:
+ tm = await self.get_term_client(url)
+ response = await tm.read_msg()
+ self.assertEqual(response, ["setup", {}])
+
+ # Check for initial shell prompt
+ response = await tm.read_msg()
+ self.assertEqual(response[0], "stdout")
+ self.assertGreater(len(response[1]), 0)
+ tm.close()
+
+ @tornado.testing.gen_test
+ async def test_basic_command(self):
+ for url in self.test_urls:
+ tm = await self.get_term_client(url)
+ await tm.read_all_msg()
+ await tm.write_stdin("whoami\n")
+ (stdout, other) = await tm.read_stdout()
+ if os.name == "nt":
+ assert "whoami" in stdout
+ else:
+ assert stdout.startswith("who")
+ assert other == []
+ tm.close()
+
+
+class NamedTermTests(TermTestCase):
+ def test_new(self):
+ response = self.fetch("/new", follow_redirects=False)
+ self.assertEqual(response.code, 302)
+ url = response.headers["Location"]
+
+ # Check that the new terminal was created
+ name = url.split("/")[2]
+ self.assertIn(name, self.named_tm.terminals)
+
+ @tornado.testing.gen_test
+ async def test_namespace(self):
+ names = ["/named/1"] * 2 + ["/named/2"] * 2
+ tms = await self.get_term_clients(names)
+ pids = await self.get_pids(tms)
+
+ self.assertEqual(pids[0], pids[1])
+ self.assertEqual(pids[2], pids[3])
+ self.assertNotEqual(pids[0], pids[3])
+
+ terminal = self.named_tm.terminals["1"]
+ killed = await terminal.terminate(True)
+ assert killed
+ assert not terminal.ptyproc.isalive()
+ assert terminal.ptyproc.closed
+
+ @tornado.testing.gen_test
+ @pytest.mark.skipif("linux" not in platform, reason="It only works on
Linux")
+ async def test_max_terminals(self):
+ urls = ["/named/%d" % i for i in range(MAX_TERMS + 1)]
+ tms = await self.get_term_clients(urls[:MAX_TERMS])
+ _ = await self.get_pids(tms)
+
+ # MAX_TERMS+1 should fail
+ tm = await self.get_term_client(urls[MAX_TERMS])
+ msg = await tm.read_msg()
+ self.assertEqual(msg, None) # Connection closed
+
+
+class SingleTermTests(TermTestCase):
+ @tornado.testing.gen_test
+ async def test_single_process(self):
+ tms = await self.get_term_clients(["/single", "/single"])
+ pids = await self.get_pids(tms)
+ self.assertEqual(pids[0], pids[1])
+
+ assert self.single_tm.terminal is not None
+ killed = await self.single_tm.terminal.terminate(True)
+ assert killed
+ assert self.single_tm.terminal.ptyproc.closed
+
+
+class UniqueTermTests(TermTestCase):
+ @tornado.testing.gen_test
+ async def test_unique_processes(self):
+ tms = await self.get_term_clients(["/unique", "/unique"])
+ pids = await self.get_pids(tms)
+ self.assertNotEqual(pids[0], pids[1])
+
+ @tornado.testing.gen_test
+ @pytest.mark.skipif("linux" not in platform, reason="It only works on
Linux")
+ async def test_max_terminals(self):
+ tms = await self.get_term_clients(["/unique"] * MAX_TERMS)
+ pids = await self.get_pids(tms)
+ self.assertEqual(len(set(pids)), MAX_TERMS) # All PIDs unique
+
+ # MAX_TERMS+1 should fail
+ tm = await self.get_term_client("/unique")
+ msg = await tm.read_msg()
+ self.assertEqual(msg, None) # Connection closed
+
+ # Close one
+ tms[0].close()
+ msg = await tms[0].read_msg() # Closed
+ self.assertEqual(msg, None)
+
+ # Should be able to open back up to MAX_TERMS
+ tm = await self.get_term_client("/unique")
+ msg = await tm.read_msg()
+ self.assertEqual(msg[0], "setup")
+
+ @tornado.testing.gen_test
+ @pytest.mark.timeout(timeout=ASYNC_TEST_TIMEOUT, method="thread")
+ async def test_large_io_doesnt_hang(self):
+ # This is a regression test for an error where Terminado hangs when
+ # the PTY buffer size is exceeded. While the buffer size varies from
+ # OS to OS, 30KBish seems like a reasonable amount and will trigger
+ # this on both OSX and Debian.
+ massive_payload = "ten bytes " * 3000
+ massive_payload = "echo " + massive_payload + "\n"
+ tm = await self.get_term_client("/unique")
+ # Clear all startup messages.
+ await tm.read_all_msg()
+ # Write a payload that doesn't fit in a single PTY buffer.
+ await tm.write_stdin(massive_payload)
+ # Verify that the server didn't hang when responding, and that
+ # we got a reasonable amount of data back (to tell us the read
+ # didn't just timeout.
+ bytes_discarded, other = await tm.discard_stdout()
+ # Echo wont actually output anything on Windows.
+ if "win" not in platform:
+ assert bytes_discarded > 10000
+ assert other == []
+ tm.close()
+
+
+if __name__ == "__main__":
+ unittest.main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/terminado-0.15.0/terminado/uimod_embed.js
new/terminado-0.16.0/terminado/uimod_embed.js
--- old/terminado-0.15.0/terminado/uimod_embed.js 2020-02-02
01:00:00.000000000 +0100
+++ new/terminado-0.16.0/terminado/uimod_embed.js 2020-02-02
01:00:00.000000000 +0100
@@ -17,5 +17,5 @@
make_terminal(container, { rows: rows, cols: cols }, ws_url);
}
},
- false
+ false,
);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/terminado-0.15.0/terminado/websocket.py
new/terminado-0.16.0/terminado/websocket.py
--- old/terminado-0.15.0/terminado/websocket.py 2020-02-02 01:00:00.000000000
+0100
+++ new/terminado-0.16.0/terminado/websocket.py 2020-02-02 01:00:00.000000000
+0100
@@ -4,12 +4,13 @@
# Copyright (c) 2014, Ramalingam Saravanan <[email protected]>
# Distributed under the terms of the Simplified BSD License.
-
import json
import logging
import os
import tornado.websocket
+from tornado import gen
+from tornado.concurrent import run_on_executor
def _cast_unicode(s):
@@ -26,6 +27,7 @@
self.term_name = ""
self.size = (None, None)
self.terminal = None
+ self._blocking_io_executor = term_manager.blocking_io_executor
self._logger = logging.getLogger(__name__)
self._user_command = ""
@@ -78,6 +80,7 @@
if content[0] == "stdout" and isinstance(content[1], str):
self.log_terminal_output(f"STDOUT: {content[1]}")
+ @gen.coroutine
def on_message(self, message):
"""Handle incoming websocket message
@@ -89,7 +92,7 @@
msg_type = command[0]
assert self.terminal is not None
if msg_type == "stdin":
- self.terminal.ptyproc.write(command[1])
+ yield self.stdin_to_ptyproc(command[1])
if self._enable_output_logging:
if command[1] == "\r":
self.log_terminal_output(f"STDIN: {self._user_command}")
@@ -125,3 +128,14 @@
:return:
"""
self._logger.debug(log)
+
+ @run_on_executor(executor="_blocking_io_executor")
+ def stdin_to_ptyproc(self, text):
+ """Handles stdin messages sent on the websocket.
+
+ This is a blocking call that should NOT be performed inside the
+ server primary event loop thread. Messages must be handled
+ asynchronously to prevent blocking on the PTY buffer.
+ """
+ if self.terminal is not None:
+ self.terminal.ptyproc.write(text)