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 2021-10-11 15:31:55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-terminado (Old) and /work/SRC/openSUSE:Factory/.python-terminado.new.2443 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-terminado" Mon Oct 11 15:31:55 2021 rev:12 rq:924566 version:0.12.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-terminado/python-terminado.changes 2021-05-15 01:24:13.099018513 +0200 +++ /work/SRC/openSUSE:Factory/.python-terminado.new.2443/python-terminado.changes 2021-10-11 15:32:56.946973084 +0200 @@ -1,0 +2,15 @@ +Sun Oct 10 19:56:06 UTC 2021 - Ben Greiner <c...@bnavigator.de> + +- update to version 0.12.1 + * correctly set the TERM variable + * Replace deprecated unittest aliases + * Clean up tests + * Log terminal output + * Switch select() to poll() in pty_read() + * Fix blocking of pty_read when there isn't pty data ready to + read + * Support creating terminal given name in kwargs. + * avoid persistent handle on IOLoop instance + * async/await syntax + +------------------------------------------------------------------- Old: ---- terminado-0.9.5.tar.gz New: ---- terminado-0.12.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-terminado.spec ++++++ --- /var/tmp/diff_new_pack.zvfZU0/_old 2021-10-11 15:32:57.390973796 +0200 +++ /var/tmp/diff_new_pack.zvfZU0/_new 2021-10-11 15:32:57.394973802 +0200 @@ -21,7 +21,7 @@ # Disable tests until random testing race condition fixed, see: https://github.com/jupyter/terminado/issues/21 %bcond_with tests Name: python-terminado -Version: 0.9.5 +Version: 0.12.1 Release: 0 Summary: Terminals served to termjs using Tornado websockets License: BSD-2-Clause ++++++ terminado-0.9.5.tar.gz -> terminado-0.12.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/terminado-0.9.5/PKG-INFO new/terminado-0.12.1/PKG-INFO --- old/terminado-0.9.5/PKG-INFO 2021-05-11 13:40:15.468093400 +0200 +++ new/terminado-0.12.1/PKG-INFO 2021-09-07 16:32:39.483855700 +0200 @@ -1,86 +1,11 @@ Metadata-Version: 2.1 Name: terminado -Version: 0.9.5 +Version: 0.12.1 Summary: Tornado websocket backend for the Xterm.js Javascript terminal emulator library. Home-page: https://github.com/jupyter/terminado Author: Jupyter Development Team Author-email: jupy...@googlegroups.com License: UNKNOWN -Description: This is a `Tornado <http://tornadoweb.org/>`_ websocket backend for the - `Xterm.js <https://xtermjs.org/>`_ Javascript terminal emulator - library. - - It evolved out of `pyxterm <https://github.com/mitotic/pyxterm>`_, which was - part of `GraphTerm <https://github.com/mitotic/graphterm>`_ (as lineterm.py), - v0.57.0 (2014-07-18), and ultimately derived from the public-domain `Ajaxterm - <http://antony.lesuisse.org/software/ajaxterm/>`_ code, v0.11 (2008-11-13) (also - on Github as part of `QWeb <https://github.com/antonylesuisse/qweb>`_). - - Modules: - - * ``terminado.management``: controls launching virtual terminals, - connecting them to Tornado's event loop, and closing them down. - * ``terminado.websocket``: Provides a websocket handler for communicating with - a terminal. - * ``terminado.uimodule``: Provides a ``Terminal`` Tornado `UI Module - <http://www.tornadoweb.org/en/stable/guide/templates.html#ui-modules>`_. - - JS: - - * ``terminado/_static/terminado.js``: A lightweight wrapper to set up a - term.js terminal with a websocket. - - Local Installation: - - $ pip install -e .[test] - - - Usage example: - - .. code:: python - - import os.path - import tornado.web - import tornado.ioloop - # This demo requires tornado_xstatic and XStatic-term.js - import tornado_xstatic - - import terminado - STATIC_DIR = os.path.join(os.path.dirname(terminado.__file__), "_static") - - 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") - - if __name__ == '__main__': - term_manager = terminado.SingleTermManager(shell_command=['bash']) - handlers = [ - (r"/websocket", terminado.TermSocket, - {'term_manager': term_manager}), - (r"/", TerminalPageHandler), - (r"/xstatic/(.*)", tornado_xstatic.XStaticFileHandler, - {'allowed_modules': ['termjs']}) - ] - app = tornado.web.Application(handlers, static_path=STATIC_DIR, - xstatic_url = tornado_xstatic.url_maker('/xstatic/')) - # Serve at http://localhost:8765/ N.B. Leaving out 'localhost' here will - # work, but it will listen on the public network interface as well. - # Given what terminado does, that would be rather a security hole. - app.listen(8765, 'localhost') - try: - tornado.ioloop.IOLoop.instance().start() - finally: - term_manager.shutdown() - - See the `demos directory <https://github.com/takluyver/terminado/tree/master/demos>`_ - for more examples. This is a simplified version of the ``single.py`` demo. - - Run the unit tests with: - - $ pytest - Platform: UNKNOWN Classifier: Environment :: Web Environment Classifier: License :: OSI Approved :: BSD License @@ -90,3 +15,81 @@ Requires-Python: >=3.6 Description-Content-Type: text/x-rst Provides-Extra: test +License-File: LICENSE + +This is a `Tornado <http://tornadoweb.org/>`_ websocket backend for the +`Xterm.js <https://xtermjs.org/>`_ Javascript terminal emulator +library. + +It evolved out of `pyxterm <https://github.com/mitotic/pyxterm>`_, which was +part of `GraphTerm <https://github.com/mitotic/graphterm>`_ (as lineterm.py), +v0.57.0 (2014-07-18), and ultimately derived from the public-domain `Ajaxterm +<http://antony.lesuisse.org/software/ajaxterm/>`_ code, v0.11 (2008-11-13) (also +on Github as part of `QWeb <https://github.com/antonylesuisse/qweb>`_). + +Modules: + +* ``terminado.management``: controls launching virtual terminals, + connecting them to Tornado's event loop, and closing them down. +* ``terminado.websocket``: Provides a websocket handler for communicating with + a terminal. +* ``terminado.uimodule``: Provides a ``Terminal`` Tornado `UI Module + <http://www.tornadoweb.org/en/stable/guide/templates.html#ui-modules>`_. + +JS: + +* ``terminado/_static/terminado.js``: A lightweight wrapper to set up a + term.js terminal with a websocket. + +Local Installation: + + $ pip install -e .[test] + + +Usage example: + +.. code:: python + + import os.path + import tornado.web + import tornado.ioloop + # This demo requires tornado_xstatic and XStatic-term.js + import tornado_xstatic + + import terminado + STATIC_DIR = os.path.join(os.path.dirname(terminado.__file__), "_static") + + 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") + + if __name__ == '__main__': + term_manager = terminado.SingleTermManager(shell_command=['bash']) + handlers = [ + (r"/websocket", terminado.TermSocket, + {'term_manager': term_manager}), + (r"/", TerminalPageHandler), + (r"/xstatic/(.*)", tornado_xstatic.XStaticFileHandler, + {'allowed_modules': ['termjs']}) + ] + app = tornado.web.Application(handlers, static_path=STATIC_DIR, + xstatic_url = tornado_xstatic.url_maker('/xstatic/')) + # Serve at http://localhost:8765/ N.B. Leaving out 'localhost' here will + # work, but it will listen on the public network interface as well. + # Given what terminado does, that would be rather a security hole. + app.listen(8765, 'localhost') + try: + tornado.ioloop.IOLoop.instance().start() + finally: + term_manager.shutdown() + +See the `demos directory <https://github.com/takluyver/terminado/tree/master/demos>`_ +for more examples. This is a simplified version of the ``single.py`` demo. + +Run the unit tests with: + + $ pytest + + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/terminado-0.9.5/setup.py new/terminado-0.12.1/setup.py --- old/terminado-0.9.5/setup.py 2021-05-11 13:35:48.000000000 +0200 +++ new/terminado-0.12.1/setup.py 2021-05-18 11:41:39.000000000 +0200 @@ -21,7 +21,7 @@ long_description_content_type="text/x-rst", install_requires = [ "ptyprocess;os_name!='nt'", - "pywinpty (>=0.5,<1);os_name=='nt'", + "pywinpty (>=1.1.0);os_name=='nt'", "tornado (>=4)", ], extras_require = dict(test=['pytest']), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/terminado-0.9.5/terminado/__init__.py new/terminado-0.12.1/terminado/__init__.py --- old/terminado-0.9.5/terminado/__init__.py 2021-05-11 13:40:05.000000000 +0200 +++ new/terminado-0.12.1/terminado/__init__.py 2021-09-07 16:32:30.000000000 +0200 @@ -12,4 +12,4 @@ # Prevent a warning about no attached handlers in Python 2 logging.getLogger(__name__).addHandler(logging.NullHandler()) -__version__ = '0.9.5' +__version__ = '0.12.1' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/terminado-0.9.5/terminado/management.py new/terminado-0.12.1/terminado/management.py --- old/terminado-0.9.5/terminado/management.py 2021-03-29 17:41:42.000000000 +0200 +++ new/terminado-0.12.1/terminado/management.py 2021-09-07 16:32:20.000000000 +0200 @@ -6,19 +6,15 @@ from __future__ import absolute_import, print_function -import sys -if sys.version_info[0] < 3: - byte_code = ord -else: - def byte_code(x): return x - unicode = str - +import asyncio from collections import deque import itertools import logging import os import signal import codecs +import warnings +import select try: from ptyprocess import PtyProcessUnicode @@ -28,12 +24,12 @@ from winpty import PtyProcess as PtyProcessUnicode preexec_fn = None -from tornado import gen from tornado.ioloop import IOLoop ENV_PREFIX = "PYXTERM_" # Environment variable prefix -DEFAULT_TERM_TYPE = "xterm" +# TERM is set according to xterm.js capabilities +DEFAULT_TERM_TYPE = "xterm-256color" class PtyWithClients(object): @@ -60,7 +56,7 @@ """Set the terminal size to that of the smallest client dimensions. A terminal not using the full space available is much nicer than a - terminal trying to use more than the available space, so we keep it + terminal trying to use more than the available space, so we keep it sized to the smallest client. """ minrows = mincols = 10001 @@ -89,8 +85,7 @@ pgid = os.getpgid(self.ptyproc.pid) os.killpg(pgid, sig) - @gen.coroutine - def terminate(self, force=False): + async def terminate(self, force=False): '''This forces a child process to terminate. It starts nicely with SIGHUP and SIGINT. If "force" is True then moves onto SIGKILL. This returns True if the child was terminated. This returns False if the @@ -102,34 +97,34 @@ signal.SIGTERM] loop = IOLoop.current() - def sleep(): return gen.sleep(self.ptyproc.delayafterterminate) + def sleep(): return asyncio.sleep(self.ptyproc.delayafterterminate) if not self.ptyproc.isalive(): - raise gen.Return(True) + return True try: for sig in signals: self.kill(sig) - yield sleep() + await sleep() if not self.ptyproc.isalive(): - raise gen.Return(True) + return True if force: self.kill(signal.SIGKILL) - yield sleep() + await sleep() if not self.ptyproc.isalive(): - raise gen.Return(True) + return True else: - raise gen.Return(False) - raise gen.Return(False) + return False + return False except OSError: # I think there are kernel timing issues that sometimes cause # this to happen. I think isalive() reports True, but the # process is dead to the kernel. # Make one last attempt to see if the kernel is up to date. - yield sleep() + await sleep() if not self.ptyproc.isalive(): - raise gen.Return(True) + return True else: - raise gen.Return(False) + return False def _update_removing(target, changes): @@ -142,6 +137,19 @@ target[k] = v +def _poll(fd, timeout: float = 0.1): + """Poll using poll() on posix systems and select() elsewhere (e.g., Windows) + """ + if os.name == "posix": + poller = select.poll() # noqa: ignore missing method on Windows + poller.register(fd, select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLERR) # read-only + return poller.poll(timeout * 1000) # milliseconds + else: + # poll() not supported on Windows + r, _, _ = select.select([fd], [], [], timeout) + return r + + class TermManagerBase(object): """Base class for a terminal manager.""" @@ -156,14 +164,17 @@ self.ptys_by_fd = {} if ioloop is not None: - self.ioloop = ioloop - else: - import tornado.ioloop - self.ioloop = tornado.ioloop.IOLoop.instance() + warnings.warn( + f"Setting {self.__class__.__name__}.ioloop is deprecated and ignored", + DeprecationWarning, + stacklevel=2, + ) def make_term_env(self, height=25, width=80, winheight=0, winwidth=0, **kwargs): """Build the environment variables for the process in the terminal.""" env = os.environ.copy() + # ignore any previously set TERM + # TERM is set according to xterm.js capabilities env["TERM"] = self.term_settings.get("type", DEFAULT_TERM_TYPE) dimensions = "%dx%d" % (width, height) if winwidth and winheight: @@ -194,7 +205,8 @@ """Connect a terminal to the tornado event loop to read data from it.""" fd = ptywclients.ptyproc.fd self.ptys_by_fd[fd] = ptywclients - self.ioloop.add_handler(fd, self.pty_read, self.ioloop.READ) + loop = IOLoop.current() + loop.add_handler(fd, self.pty_read, loop.READ) def on_eof(self, ptywclients): """Called when the pty has closed. @@ -203,13 +215,17 @@ fd = ptywclients.ptyproc.fd self.log.info("EOF on FD %d; stopping reading", fd) del self.ptys_by_fd[fd] - self.ioloop.remove_handler(fd) + IOLoop.current().remove_handler(fd) # This closes the fd, and should result in the process being reaped. ptywclients.ptyproc.close() def pty_read(self, fd, events=None): """Called by the event loop when there is pty data ready to read.""" + # prevent blocking on fd + if not _poll(fd, timeout=0.1): # 100ms + self.log.debug(f"Spurious pty_read() on fd {fd}") + return ptywclients = self.ptys_by_fd[fd] try: s = ptywclients.ptyproc.read(65536) @@ -240,18 +256,16 @@ """ pass - @gen.coroutine - def shutdown(self): - yield self.kill_all() + async def shutdown(self): + await self.kill_all() - @gen.coroutine - def kill_all(self): + async def kill_all(self): futures = [] for term in self.ptys_by_fd.values(): futures.append(term.terminate(force=True)) # wait for futures to finish - for f in futures: - yield f + if futures: + await asyncio.gather(*futures) class SingleTermManager(TermManagerBase): @@ -267,9 +281,8 @@ self.start_reading(self.terminal) return self.terminal - @gen.coroutine - def kill_all(self): - yield super(SingleTermManager, self).kill_all() + async def kill_all(self): + await super().kill_all() self.terminal = None @@ -344,7 +357,10 @@ return name def new_named_terminal(self, **kwargs): - name = self._next_available_name() + if 'name' in kwargs: + name = kwargs['name'] + else: + name = self._next_available_name() term = self.new_terminal(**kwargs) self.log.info("New terminal with automatic name: %s", name) term.term_name = name @@ -356,10 +372,9 @@ term = self.terminals[name] term.kill(sig) # This should lead to an EOF - @gen.coroutine - def terminate(self, name, force=False): + async def terminate(self, name, force=False): term = self.terminals[name] - yield term.terminate(force=force) + await term.terminate(force=force) def on_eof(self, ptywclients): super(NamedTermManager, self).on_eof(ptywclients) @@ -367,7 +382,6 @@ self.log.info("Terminal %s closed", name) self.terminals.pop(name, None) - @gen.coroutine - def kill_all(self): - yield super(NamedTermManager, self).kill_all() + async def kill_all(self): + await super().kill_all() self.terminals = {} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/terminado-0.9.5/terminado/tests/basic_test.py new/terminado-0.12.1/terminado/tests/basic_test.py --- old/terminado-0.9.5/terminado/tests/basic_test.py 2021-03-19 18:57:45.000000000 +0100 +++ new/terminado-0.12.1/terminado/tests/basic_test.py 2021-09-07 11:21:15.000000000 +0200 @@ -19,6 +19,8 @@ import os import re import signal +import pytest +from sys import platform # 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 @@ -41,111 +43,116 @@ self.ws = websocket self.pending_read = None - @tornado.gen.coroutine - def read_msg(self): + 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 = yield self.pending_read + response = await self.pending_read self.pending_read = None if response: response = json.loads(response) - raise tornado.gen.Return(response) + return response - @tornado.gen.coroutine - def read_all_msg(self, timeout=DONE_TIMEOUT): + async def read_all_msg(self, timeout=DONE_TIMEOUT): """Read messages until read times out""" msglist = [] delta = datetime.timedelta(seconds=timeout) while True: try: mf = self.read_msg() - msg = yield tornado.gen.with_timeout(delta, mf) + msg = await tornado.gen.with_timeout(delta, mf) except tornado.gen.TimeoutError: - raise tornado.gen.Return(msglist) + return msglist msglist.append(msg) - def write_msg(self, msg): - self.ws.write_message(json.dumps(msg)) + async def write_msg(self, msg): + await self.ws.write_message(json.dumps(msg)) - @tornado.gen.coroutine - def read_stdout(self, timeout=DONE_TIMEOUT): + async def read_stdout(self, timeout=DONE_TIMEOUT): """Read standard output until timeout read reached, return stdout and any non-stdout msgs received.""" - msglist = yield self.read_all_msg(timeout) + 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'] - raise tornado.gen.Return((stdout, othermsg)) + return (stdout, othermsg) - def write_stdin(self, data): + async def write_stdin(self, data): """Write to terminal stdin""" - self.write_msg(['stdin', data]) + await self.write_msg(['stdin', data]) - @tornado.gen.coroutine - def get_pid(self): + async def get_pid(self): """Get process ID of terminal shell process""" - yield self.read_stdout() # Clear out any pending - self.write_stdin("echo $$\r") - (stdout, extra) = yield self.read_stdout() + 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 \$\$\x1b\[0K\r\n(\d+)', stdout) + print(repr(stdout)) + match = re.search(r'echo \$\$\\x1b\[71X\\x1b\[71C\\r\\n(\d+)', repr(stdout)) + if match is None: + match = re.search(r'echo \$\$ \\r\\n(\d+)', repr(stdout)) + if match is None: + match = re.search( + r'echo \$\$ \\r\\n\\x1b\[\?25h\\x1b\[\?25l(\d+)', + repr(stdout)) pid = int(match.groups()[0]) else: + print('stdout=%r, extra=%r' % (stdout, extra)) pid = int(stdout.split('\n')[1]) - raise tornado.gen.Return(pid) + return pid def close(self): self.ws.close() class TermTestCase(tornado.testing.AsyncHTTPTestCase): - # Factory for TestTermClient, because it has to be a Tornado co-routine. + # Factory for TestTermClient, because it has to be async # See: https://github.com/tornadoweb/tornado/issues/1161 - @tornado.gen.coroutine - def get_term_client(self, path): + 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 = yield tornado.websocket.websocket_connect(request) - raise tornado.gen.Return(TestTermClient(ws)) + ws = await tornado.websocket.websocket_connect(request) + return TestTermClient(ws) - @tornado.gen.coroutine - def get_term_clients(self, paths): - tms = yield [self.get_term_client(path) for path in paths] - raise tornado.gen.Return(tms) + async def get_term_clients(self, paths): + return await asyncio.gather(*(self.get_term_client(path) for path in paths)) - @tornado.gen.coroutine - def get_pids(self, tm_list): + async def get_pids(self, tm_list): pids = [] - for tm in tm_list: # Must be sequential, in case terms are shared - pid = yield tm.get_pid() + for tm in tm_list: # Must be sequential, in case terms are shared + pid = await tm.get_pid() pids.append(pid) - raise tornado.gen.Return(pids) + return pids def tearDown(self): - self.named_tm.kill_all() - self.single_tm.kill_all() - self.unique_tm.kill_all() + 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, - ioloop=self.io_loop) - self.single_tm = SingleTermManager(shell_command=['bash'], - ioloop=self.io_loop) - self.unique_tm = UniqueTermManager(shell_command=['bash'], - max_terminals=MAX_TERMS, - ioloop=self.io_loop) + 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): @@ -159,29 +166,29 @@ (r"/unique", TermSocket, {'term_manager': self.unique_tm}) ], debug=True) - test_urls = ('/named/term1', '/unique', '/single') + test_urls = ('/named/term1', '/unique') + (('/single',) if os.name != 'nt' else tuple()) class CommonTests(TermTestCase): @tornado.testing.gen_test - def test_basic(self): + async def test_basic(self): for url in self.test_urls: - tm = yield self.get_term_client(url) - response = yield tm.read_msg() + tm = await self.get_term_client(url) + response = await tm.read_msg() self.assertEqual(response, ['setup', {}]) # Check for initial shell prompt - response = yield tm.read_msg() + response = await tm.read_msg() self.assertEqual(response[0], 'stdout') self.assertGreater(len(response[1]), 0) - tm.close() + tm.close() @tornado.testing.gen_test - def test_basic_command(self): + async def test_basic_command(self): for url in self.test_urls: - tm = yield self.get_term_client(url) - yield tm.read_all_msg() - tm.write_stdin("whoami\n") - (stdout, other) = yield tm.read_stdout() + 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: @@ -200,60 +207,62 @@ self.assertIn(name, self.named_tm.terminals) @tornado.testing.gen_test - def test_namespace(self): + async def test_namespace(self): names = ["/named/1"]*2 + ["/named/2"]*2 - tms = yield self.get_term_clients(names) - pids = yield self.get_pids(tms) + 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]) @tornado.testing.gen_test - def test_max_terminals(self): + @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 = yield self.get_term_clients(urls[:MAX_TERMS]) - pids = yield self.get_pids(tms) + tms = await self.get_term_clients(urls[:MAX_TERMS]) + pids = await self.get_pids(tms) # MAX_TERMS+1 should fail - tm = yield self.get_term_client(urls[MAX_TERMS]) - msg = yield tm.read_msg() + 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 - def test_single_process(self): - tms = yield self.get_term_clients(["/single", "/single"]) - pids = yield self.get_pids(tms) + 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]) class UniqueTermTests(TermTestCase): @tornado.testing.gen_test - def test_unique_processes(self): - tms = yield self.get_term_clients(["/unique", "/unique"]) - pids = yield self.get_pids(tms) + 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 - def test_max_terminals(self): - tms = yield self.get_term_clients(['/unique'] * MAX_TERMS) - pids = yield self.get_pids(tms) + @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 = yield self.get_term_client("/unique") - msg = yield tm.read_msg() + tm = await self.get_term_client("/unique") + msg = await tm.read_msg() self.assertEqual(msg, None) # Connection closed # Close one tms[0].close() - msg = yield tms[0].read_msg() # Closed - self.assertEquals(msg, None) + msg = await tms[0].read_msg() # Closed + self.assertEqual(msg, None) # Should be able to open back up to MAX_TERMS - tm = yield self.get_term_client("/unique") - msg = yield tm.read_msg() - self.assertEquals(msg[0], 'setup') + tm = await self.get_term_client("/unique") + msg = await tm.read_msg() + self.assertEqual(msg[0], 'setup') if __name__ == '__main__': unittest.main() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/terminado-0.9.5/terminado/websocket.py new/terminado-0.12.1/terminado/websocket.py --- old/terminado-0.9.5/terminado/websocket.py 2021-03-19 18:57:45.000000000 +0100 +++ new/terminado-0.12.1/terminado/websocket.py 2021-08-12 13:08:56.000000000 +0200 @@ -7,6 +7,8 @@ from __future__ import absolute_import, print_function # Python3-friendly imports +import os + try: from urllib.parse import urlparse except ImportError: @@ -14,6 +16,7 @@ import json import logging +import re import tornado.web import tornado.websocket @@ -35,6 +38,7 @@ self.terminal = None self._logger = logging.getLogger(__name__) + self._user_command = '' def origin_check(self, origin=None): """Deprecated: backward-compat for terminado <= 0.5.""" @@ -74,6 +78,12 @@ def send_json_message(self, content): json_msg = json.dumps(content) + pattern = re.compile(r'^(\w|\d)+') + try: + if pattern.search(content[1]): + self.log_terminal_output(f'STDOUT: {content[1]}') + except TypeError as e: + self._logger.error(f'not able to serialize: {e}') self.write_message(json_msg) def on_message(self, message): @@ -88,6 +98,11 @@ if msg_type == "stdin": self.terminal.ptyproc.write(command[1]) + if command[1] == '\r': + self.log_terminal_output(f'STDIN: {self._user_command}') + self._user_command = '' + else: + self._user_command += command[1] elif msg_type == "set_size": self.size = command[1:3] self.terminal.resize_to_smallest() @@ -110,3 +125,12 @@ self.send_json_message(['disconnect', 1]) self.close() self.terminal = None + + def log_terminal_output(self, log: str = ''): + """ + Logs the terminal input/output if the environment variable LOG_TERMINAL_OUTPUT is "true" + :param log: log line to write + :return: + """ + if str.lower(os.getenv("LOG_TERMINAL_OUTPUT", "false")) == "true": + self._logger.debug(log) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/terminado-0.9.5/terminado.egg-info/PKG-INFO new/terminado-0.12.1/terminado.egg-info/PKG-INFO --- old/terminado-0.9.5/terminado.egg-info/PKG-INFO 2021-05-11 13:40:15.000000000 +0200 +++ new/terminado-0.12.1/terminado.egg-info/PKG-INFO 2021-09-07 16:32:39.000000000 +0200 @@ -1,86 +1,11 @@ Metadata-Version: 2.1 Name: terminado -Version: 0.9.5 +Version: 0.12.1 Summary: Tornado websocket backend for the Xterm.js Javascript terminal emulator library. Home-page: https://github.com/jupyter/terminado Author: Jupyter Development Team Author-email: jupy...@googlegroups.com License: UNKNOWN -Description: This is a `Tornado <http://tornadoweb.org/>`_ websocket backend for the - `Xterm.js <https://xtermjs.org/>`_ Javascript terminal emulator - library. - - It evolved out of `pyxterm <https://github.com/mitotic/pyxterm>`_, which was - part of `GraphTerm <https://github.com/mitotic/graphterm>`_ (as lineterm.py), - v0.57.0 (2014-07-18), and ultimately derived from the public-domain `Ajaxterm - <http://antony.lesuisse.org/software/ajaxterm/>`_ code, v0.11 (2008-11-13) (also - on Github as part of `QWeb <https://github.com/antonylesuisse/qweb>`_). - - Modules: - - * ``terminado.management``: controls launching virtual terminals, - connecting them to Tornado's event loop, and closing them down. - * ``terminado.websocket``: Provides a websocket handler for communicating with - a terminal. - * ``terminado.uimodule``: Provides a ``Terminal`` Tornado `UI Module - <http://www.tornadoweb.org/en/stable/guide/templates.html#ui-modules>`_. - - JS: - - * ``terminado/_static/terminado.js``: A lightweight wrapper to set up a - term.js terminal with a websocket. - - Local Installation: - - $ pip install -e .[test] - - - Usage example: - - .. code:: python - - import os.path - import tornado.web - import tornado.ioloop - # This demo requires tornado_xstatic and XStatic-term.js - import tornado_xstatic - - import terminado - STATIC_DIR = os.path.join(os.path.dirname(terminado.__file__), "_static") - - 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") - - if __name__ == '__main__': - term_manager = terminado.SingleTermManager(shell_command=['bash']) - handlers = [ - (r"/websocket", terminado.TermSocket, - {'term_manager': term_manager}), - (r"/", TerminalPageHandler), - (r"/xstatic/(.*)", tornado_xstatic.XStaticFileHandler, - {'allowed_modules': ['termjs']}) - ] - app = tornado.web.Application(handlers, static_path=STATIC_DIR, - xstatic_url = tornado_xstatic.url_maker('/xstatic/')) - # Serve at http://localhost:8765/ N.B. Leaving out 'localhost' here will - # work, but it will listen on the public network interface as well. - # Given what terminado does, that would be rather a security hole. - app.listen(8765, 'localhost') - try: - tornado.ioloop.IOLoop.instance().start() - finally: - term_manager.shutdown() - - See the `demos directory <https://github.com/takluyver/terminado/tree/master/demos>`_ - for more examples. This is a simplified version of the ``single.py`` demo. - - Run the unit tests with: - - $ pytest - Platform: UNKNOWN Classifier: Environment :: Web Environment Classifier: License :: OSI Approved :: BSD License @@ -90,3 +15,81 @@ Requires-Python: >=3.6 Description-Content-Type: text/x-rst Provides-Extra: test +License-File: LICENSE + +This is a `Tornado <http://tornadoweb.org/>`_ websocket backend for the +`Xterm.js <https://xtermjs.org/>`_ Javascript terminal emulator +library. + +It evolved out of `pyxterm <https://github.com/mitotic/pyxterm>`_, which was +part of `GraphTerm <https://github.com/mitotic/graphterm>`_ (as lineterm.py), +v0.57.0 (2014-07-18), and ultimately derived from the public-domain `Ajaxterm +<http://antony.lesuisse.org/software/ajaxterm/>`_ code, v0.11 (2008-11-13) (also +on Github as part of `QWeb <https://github.com/antonylesuisse/qweb>`_). + +Modules: + +* ``terminado.management``: controls launching virtual terminals, + connecting them to Tornado's event loop, and closing them down. +* ``terminado.websocket``: Provides a websocket handler for communicating with + a terminal. +* ``terminado.uimodule``: Provides a ``Terminal`` Tornado `UI Module + <http://www.tornadoweb.org/en/stable/guide/templates.html#ui-modules>`_. + +JS: + +* ``terminado/_static/terminado.js``: A lightweight wrapper to set up a + term.js terminal with a websocket. + +Local Installation: + + $ pip install -e .[test] + + +Usage example: + +.. code:: python + + import os.path + import tornado.web + import tornado.ioloop + # This demo requires tornado_xstatic and XStatic-term.js + import tornado_xstatic + + import terminado + STATIC_DIR = os.path.join(os.path.dirname(terminado.__file__), "_static") + + 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") + + if __name__ == '__main__': + term_manager = terminado.SingleTermManager(shell_command=['bash']) + handlers = [ + (r"/websocket", terminado.TermSocket, + {'term_manager': term_manager}), + (r"/", TerminalPageHandler), + (r"/xstatic/(.*)", tornado_xstatic.XStaticFileHandler, + {'allowed_modules': ['termjs']}) + ] + app = tornado.web.Application(handlers, static_path=STATIC_DIR, + xstatic_url = tornado_xstatic.url_maker('/xstatic/')) + # Serve at http://localhost:8765/ N.B. Leaving out 'localhost' here will + # work, but it will listen on the public network interface as well. + # Given what terminado does, that would be rather a security hole. + app.listen(8765, 'localhost') + try: + tornado.ioloop.IOLoop.instance().start() + finally: + term_manager.shutdown() + +See the `demos directory <https://github.com/takluyver/terminado/tree/master/demos>`_ +for more examples. This is a simplified version of the ``single.py`` demo. + +Run the unit tests with: + + $ pytest + + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/terminado-0.9.5/terminado.egg-info/requires.txt new/terminado-0.12.1/terminado.egg-info/requires.txt --- old/terminado-0.9.5/terminado.egg-info/requires.txt 2021-05-11 13:40:15.000000000 +0200 +++ new/terminado-0.12.1/terminado.egg-info/requires.txt 2021-09-07 16:32:39.000000000 +0200 @@ -4,7 +4,7 @@ ptyprocess [:os_name == "nt"] -pywinpty<1,>=0.5 +pywinpty>=1.1.0 [test] pytest