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 <a...@gmx.de> + +- 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 <jupy...@googlegroups.com> @@ -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 <sar...@sarava.net> -# 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 <sar...@sarava.net> +# 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 <sar...@sarava.net> # 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)