Uploaded Test files
This commit is contained in:
		
							parent
							
								
									f584ad9d97
								
							
						
					
					
						commit
						2e81cb7d99
					
				
					 16627 changed files with 2065359 additions and 102444 deletions
				
			
		
							
								
								
									
										15
									
								
								venv/Lib/site-packages/terminado/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								venv/Lib/site-packages/terminado/__init__.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
"""Terminals served to xterm.js using Tornado websockets"""
 | 
			
		||||
 | 
			
		||||
# Copyright (c) Jupyter Development Team
 | 
			
		||||
# Copyright (c) 2014, Ramalingam Saravanan <sarava@sarava.net>
 | 
			
		||||
# Distributed under the terms of the Simplified BSD License.
 | 
			
		||||
 | 
			
		||||
from .websocket import TermSocket
 | 
			
		||||
from .management import (TermManagerBase, SingleTermManager,
 | 
			
		||||
                         UniqueTermManager, NamedTermManager)
 | 
			
		||||
 | 
			
		||||
import logging
 | 
			
		||||
# Prevent a warning about no attached handlers in Python 2
 | 
			
		||||
logging.getLogger(__name__).addHandler(logging.NullHandler())
 | 
			
		||||
 | 
			
		||||
__version__ = '0.9.1'
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										362
									
								
								venv/Lib/site-packages/terminado/management.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										362
									
								
								venv/Lib/site-packages/terminado/management.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,362 @@
 | 
			
		|||
"""Terminal management for exposing terminals to a web interface using Tornado.
 | 
			
		||||
"""
 | 
			
		||||
# Copyright (c) Jupyter Development Team
 | 
			
		||||
# Copyright (c) 2014, Ramalingam Saravanan <sarava@sarava.net>
 | 
			
		||||
# Distributed under the terms of the Simplified BSD License.
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
from collections import deque
 | 
			
		||||
import itertools
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
import signal
 | 
			
		||||
 | 
			
		||||
try:
 | 
			
		||||
    from ptyprocess import PtyProcessUnicode
 | 
			
		||||
except ImportError:
 | 
			
		||||
    from winpty import PtyProcess as PtyProcessUnicode
 | 
			
		||||
 | 
			
		||||
from tornado import gen
 | 
			
		||||
from tornado.ioloop import IOLoop
 | 
			
		||||
 | 
			
		||||
ENV_PREFIX = "PYXTERM_"         # Environment variable prefix
 | 
			
		||||
 | 
			
		||||
DEFAULT_TERM_TYPE = "xterm"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PtyWithClients(object):
 | 
			
		||||
    def __init__(self, argv, env=[], cwd=None):
 | 
			
		||||
        self.clients = []
 | 
			
		||||
        # If you start the process and then construct this object from it,
 | 
			
		||||
        #  output generated by the process prior to the object's creation
 | 
			
		||||
        #  is lost.  Hence the change from 0.8.3.
 | 
			
		||||
        # Buffer output until a client connects; then let the client
 | 
			
		||||
        #  drain the buffer.
 | 
			
		||||
        # We keep the same read_buffer as before
 | 
			
		||||
        self.read_buffer = deque([], maxlen=10)
 | 
			
		||||
        self.preopen_buffer = deque([])
 | 
			
		||||
        self.ptyproc = PtyProcessUnicode.spawn(argv, env=env, cwd=cwd)
 | 
			
		||||
 | 
			
		||||
    def resize_to_smallest(self):
 | 
			
		||||
        """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 
 | 
			
		||||
        sized to the smallest client.
 | 
			
		||||
        """
 | 
			
		||||
        minrows = mincols = 10001
 | 
			
		||||
        for client in self.clients:
 | 
			
		||||
            rows, cols = client.size
 | 
			
		||||
            if rows is not None and rows < minrows:
 | 
			
		||||
                minrows = rows
 | 
			
		||||
            if cols is not None and cols < mincols:
 | 
			
		||||
                mincols = cols
 | 
			
		||||
 | 
			
		||||
        if minrows == 10001 or mincols == 10001:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        rows, cols = self.ptyproc.getwinsize()
 | 
			
		||||
        if (rows, cols) != (minrows, mincols):
 | 
			
		||||
            self.ptyproc.setwinsize(minrows, mincols)
 | 
			
		||||
 | 
			
		||||
    def kill(self, sig=signal.SIGTERM):
 | 
			
		||||
        """Send a signal to the process in the pty"""
 | 
			
		||||
        self.ptyproc.kill(sig)
 | 
			
		||||
 | 
			
		||||
    def killpg(self, sig=signal.SIGTERM):
 | 
			
		||||
        """Send a signal to the process group of the process in the pty"""
 | 
			
		||||
        if os.name == 'nt':
 | 
			
		||||
            return self.ptyproc.kill(sig)
 | 
			
		||||
        pgid = os.getpgid(self.ptyproc.pid)
 | 
			
		||||
        os.killpg(pgid, sig)
 | 
			
		||||
 | 
			
		||||
    @gen.coroutine
 | 
			
		||||
    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
 | 
			
		||||
        child could not be terminated. '''
 | 
			
		||||
        if os.name == 'nt':
 | 
			
		||||
            signals = [signal.SIGINT, signal.SIGTERM]
 | 
			
		||||
        else:
 | 
			
		||||
            signals = [signal.SIGHUP, signal.SIGCONT, signal.SIGINT,
 | 
			
		||||
                       signal.SIGTERM]
 | 
			
		||||
 | 
			
		||||
        loop = IOLoop.current()
 | 
			
		||||
        def sleep(): return gen.sleep(self.ptyproc.delayafterterminate)
 | 
			
		||||
 | 
			
		||||
        if not self.ptyproc.isalive():
 | 
			
		||||
            raise gen.Return(True)
 | 
			
		||||
        try:
 | 
			
		||||
            for sig in signals:
 | 
			
		||||
                self.kill(sig)
 | 
			
		||||
                yield sleep()
 | 
			
		||||
                if not self.ptyproc.isalive():
 | 
			
		||||
                    raise gen.Return(True)
 | 
			
		||||
            if force:
 | 
			
		||||
                self.kill(signal.SIGKILL)
 | 
			
		||||
                yield sleep()
 | 
			
		||||
                if not self.ptyproc.isalive():
 | 
			
		||||
                    raise gen.Return(True)
 | 
			
		||||
                else:
 | 
			
		||||
                    raise gen.Return(False)
 | 
			
		||||
            raise gen.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()
 | 
			
		||||
            if not self.ptyproc.isalive():
 | 
			
		||||
                raise gen.Return(True)
 | 
			
		||||
            else:
 | 
			
		||||
                raise gen.Return(False)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _update_removing(target, changes):
 | 
			
		||||
    """Like dict.update(), but remove keys where the value is None.
 | 
			
		||||
    """
 | 
			
		||||
    for k, v in changes.items():
 | 
			
		||||
        if v is None:
 | 
			
		||||
            target.pop(k, None)
 | 
			
		||||
        else:
 | 
			
		||||
            target[k] = v
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TermManagerBase(object):
 | 
			
		||||
    """Base class for a terminal manager."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, shell_command, server_url="", term_settings={},
 | 
			
		||||
                 extra_env=None, ioloop=None):
 | 
			
		||||
        self.shell_command = shell_command
 | 
			
		||||
        self.server_url = server_url
 | 
			
		||||
        self.term_settings = term_settings
 | 
			
		||||
        self.extra_env = extra_env
 | 
			
		||||
        self.log = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
        self.ptys_by_fd = {}
 | 
			
		||||
 | 
			
		||||
        if ioloop is not None:
 | 
			
		||||
            self.ioloop = ioloop
 | 
			
		||||
        else:
 | 
			
		||||
            import tornado.ioloop
 | 
			
		||||
            self.ioloop = tornado.ioloop.IOLoop.instance()
 | 
			
		||||
 | 
			
		||||
    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()
 | 
			
		||||
        env["TERM"] = self.term_settings.get("type", DEFAULT_TERM_TYPE)
 | 
			
		||||
        dimensions = "%dx%d" % (width, height)
 | 
			
		||||
        if winwidth and winheight:
 | 
			
		||||
            dimensions += ";%dx%d" % (winwidth, winheight)
 | 
			
		||||
        env[ENV_PREFIX+"DIMENSIONS"] = dimensions
 | 
			
		||||
        env["COLUMNS"] = str(width)
 | 
			
		||||
        env["LINES"] = str(height)
 | 
			
		||||
 | 
			
		||||
        if self.server_url:
 | 
			
		||||
            env[ENV_PREFIX+"URL"] = self.server_url
 | 
			
		||||
 | 
			
		||||
        if self.extra_env:
 | 
			
		||||
            _update_removing(env, self.extra_env)
 | 
			
		||||
 | 
			
		||||
        return env
 | 
			
		||||
 | 
			
		||||
    def new_terminal(self, **kwargs):
 | 
			
		||||
        """Make a new terminal, return a :class:`PtyWithClients` instance."""
 | 
			
		||||
        options = self.term_settings.copy()
 | 
			
		||||
        options['shell_command'] = self.shell_command
 | 
			
		||||
        options.update(kwargs)
 | 
			
		||||
        argv = options['shell_command']
 | 
			
		||||
        env = self.make_term_env(**options)
 | 
			
		||||
        cwd = options.get('cwd', None)
 | 
			
		||||
        return PtyWithClients(argv, env, cwd)
 | 
			
		||||
 | 
			
		||||
    def start_reading(self, ptywclients):
 | 
			
		||||
        """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)
 | 
			
		||||
 | 
			
		||||
    def on_eof(self, ptywclients):
 | 
			
		||||
        """Called when the pty has closed.
 | 
			
		||||
        """
 | 
			
		||||
        # Stop trying to read from that terminal
 | 
			
		||||
        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)
 | 
			
		||||
 | 
			
		||||
        # 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."""
 | 
			
		||||
        ptywclients = self.ptys_by_fd[fd]
 | 
			
		||||
        try:
 | 
			
		||||
            s = ptywclients.ptyproc.read(65536)
 | 
			
		||||
            client_list = ptywclients.clients
 | 
			
		||||
            ptywclients.read_buffer.append(s)
 | 
			
		||||
            if not client_list:
 | 
			
		||||
                # No one to consume our output: buffer it.
 | 
			
		||||
                ptywclients.preopen_buffer.append(s)
 | 
			
		||||
                return
 | 
			
		||||
            for client in ptywclients.clients:
 | 
			
		||||
                client.on_pty_read(s)
 | 
			
		||||
        except EOFError:
 | 
			
		||||
            self.on_eof(ptywclients)
 | 
			
		||||
            for client in ptywclients.clients:
 | 
			
		||||
                client.on_pty_died()
 | 
			
		||||
 | 
			
		||||
    def get_terminal(self, url_component=None):
 | 
			
		||||
        """Override in a subclass to give a terminal to a new websocket connection
 | 
			
		||||
 | 
			
		||||
        The :class:`TermSocket` handler works with zero or one URL components
 | 
			
		||||
        (capturing groups in the URL spec regex). If it receives one, it is
 | 
			
		||||
        passed as the ``url_component`` parameter; otherwise, this is None.
 | 
			
		||||
        """
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    def client_disconnected(self, websocket):
 | 
			
		||||
        """Override this to e.g. kill terminals on client disconnection.
 | 
			
		||||
        """
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    @gen.coroutine
 | 
			
		||||
    def shutdown(self):
 | 
			
		||||
        yield self.kill_all()
 | 
			
		||||
 | 
			
		||||
    @gen.coroutine
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SingleTermManager(TermManagerBase):
 | 
			
		||||
    """All connections to the websocket share a common terminal."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, **kwargs):
 | 
			
		||||
        super(SingleTermManager, self).__init__(**kwargs)
 | 
			
		||||
        self.terminal = None
 | 
			
		||||
 | 
			
		||||
    def get_terminal(self, url_component=None):
 | 
			
		||||
        if self.terminal is None:
 | 
			
		||||
            self.terminal = self.new_terminal()
 | 
			
		||||
            self.start_reading(self.terminal)
 | 
			
		||||
        return self.terminal
 | 
			
		||||
 | 
			
		||||
    @gen.coroutine
 | 
			
		||||
    def kill_all(self):
 | 
			
		||||
        yield super(SingleTermManager, self).kill_all()
 | 
			
		||||
        self.terminal = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MaxTerminalsReached(Exception):
 | 
			
		||||
    def __init__(self, max_terminals):
 | 
			
		||||
        self.max_terminals = max_terminals
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return "Cannot create more than %d terminals" % self.max_terminals
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UniqueTermManager(TermManagerBase):
 | 
			
		||||
    """Give each websocket a unique terminal to use."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, max_terminals=None, **kwargs):
 | 
			
		||||
        super(UniqueTermManager, self).__init__(**kwargs)
 | 
			
		||||
        self.max_terminals = max_terminals
 | 
			
		||||
 | 
			
		||||
    def get_terminal(self, url_component=None):
 | 
			
		||||
        if self.max_terminals and len(self.ptys_by_fd) >= self.max_terminals:
 | 
			
		||||
            raise MaxTerminalsReached(self.max_terminals)
 | 
			
		||||
 | 
			
		||||
        term = self.new_terminal()
 | 
			
		||||
        self.start_reading(term)
 | 
			
		||||
        return term
 | 
			
		||||
 | 
			
		||||
    def client_disconnected(self, websocket):
 | 
			
		||||
        """Send terminal SIGHUP when client disconnects."""
 | 
			
		||||
        self.log.info("Websocket closed, sending SIGHUP to terminal.")
 | 
			
		||||
        if websocket.terminal:
 | 
			
		||||
            if os.name == 'nt':
 | 
			
		||||
                websocket.terminal.kill()
 | 
			
		||||
                # Immediately call the pty reader to process
 | 
			
		||||
                # the eof and free up space
 | 
			
		||||
                self.pty_read(websocket.terminal.ptyproc.fd)
 | 
			
		||||
                return
 | 
			
		||||
            websocket.terminal.killpg(signal.SIGHUP)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NamedTermManager(TermManagerBase):
 | 
			
		||||
    """Share terminals between websockets connected to the same endpoint.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def __init__(self, max_terminals=None, **kwargs):
 | 
			
		||||
        super(NamedTermManager, self).__init__(**kwargs)
 | 
			
		||||
        self.max_terminals = max_terminals
 | 
			
		||||
        self.terminals = {}
 | 
			
		||||
 | 
			
		||||
    def get_terminal(self, term_name):
 | 
			
		||||
        assert term_name is not None
 | 
			
		||||
 | 
			
		||||
        if term_name in self.terminals:
 | 
			
		||||
            return self.terminals[term_name]
 | 
			
		||||
 | 
			
		||||
        if self.max_terminals and len(self.terminals) >= self.max_terminals:
 | 
			
		||||
            raise MaxTerminalsReached(self.max_terminals)
 | 
			
		||||
 | 
			
		||||
        # Create new terminal
 | 
			
		||||
        self.log.info("New terminal with specified name: %s", term_name)
 | 
			
		||||
        term = self.new_terminal()
 | 
			
		||||
        term.term_name = term_name
 | 
			
		||||
        self.terminals[term_name] = term
 | 
			
		||||
        self.start_reading(term)
 | 
			
		||||
        return term
 | 
			
		||||
 | 
			
		||||
    name_template = "%d"
 | 
			
		||||
 | 
			
		||||
    def _next_available_name(self):
 | 
			
		||||
        for n in itertools.count(start=1):
 | 
			
		||||
            name = self.name_template % n
 | 
			
		||||
            if name not in self.terminals:
 | 
			
		||||
                return name
 | 
			
		||||
 | 
			
		||||
    def new_named_terminal(self, **kwargs):
 | 
			
		||||
        name = self._next_available_name()
 | 
			
		||||
        term = self.new_terminal(**kwargs)
 | 
			
		||||
        self.log.info("New terminal with automatic name: %s", name)
 | 
			
		||||
        term.term_name = name
 | 
			
		||||
        self.terminals[name] = term
 | 
			
		||||
        self.start_reading(term)
 | 
			
		||||
        return name, term
 | 
			
		||||
 | 
			
		||||
    def kill(self, name, sig=signal.SIGTERM):
 | 
			
		||||
        term = self.terminals[name]
 | 
			
		||||
        term.kill(sig)   # This should lead to an EOF
 | 
			
		||||
 | 
			
		||||
    @gen.coroutine
 | 
			
		||||
    def terminate(self, name, force=False):
 | 
			
		||||
        term = self.terminals[name]
 | 
			
		||||
        yield term.terminate(force=force)
 | 
			
		||||
 | 
			
		||||
    def on_eof(self, ptywclients):
 | 
			
		||||
        super(NamedTermManager, self).on_eof(ptywclients)
 | 
			
		||||
        name = ptywclients.term_name
 | 
			
		||||
        self.log.info("Terminal %s closed", name)
 | 
			
		||||
        self.terminals.pop(name, None)
 | 
			
		||||
 | 
			
		||||
    @gen.coroutine
 | 
			
		||||
    def kill_all(self):
 | 
			
		||||
        yield super(NamedTermManager, self).kill_all()
 | 
			
		||||
        self.terminals = {}
 | 
			
		||||
							
								
								
									
										0
									
								
								venv/Lib/site-packages/terminado/tests/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								venv/Lib/site-packages/terminado/tests/__init__.py
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										259
									
								
								venv/Lib/site-packages/terminado/tests/basic_test.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										259
									
								
								venv/Lib/site-packages/terminado/tests/basic_test.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,259 @@
 | 
			
		|||
# basic_tests.py -- Basic unit tests for Terminado
 | 
			
		||||
 | 
			
		||||
# Copyright (c) Jupyter Development Team
 | 
			
		||||
# Copyright (c) 2014, Ramalingam Saravanan <sarava@sarava.net>
 | 
			
		||||
# Distributed under the terms of the Simplified BSD License.
 | 
			
		||||
 | 
			
		||||
from __future__ import absolute_import, print_function
 | 
			
		||||
 | 
			
		||||
import unittest
 | 
			
		||||
from terminado import *
 | 
			
		||||
import tornado
 | 
			
		||||
import tornado.httpserver
 | 
			
		||||
from tornado.httpclient import HTTPError
 | 
			
		||||
from tornado.ioloop import IOLoop
 | 
			
		||||
import tornado.testing
 | 
			
		||||
import datetime
 | 
			
		||||
import logging
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
import signal
 | 
			
		||||
 | 
			
		||||
# 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, asyncio
 | 
			
		||||
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(object):
 | 
			
		||||
    """Test connection to a terminal manager"""
 | 
			
		||||
    def __init__(self, websocket):
 | 
			
		||||
        self.ws = websocket
 | 
			
		||||
        self.pending_read = None
 | 
			
		||||
 | 
			
		||||
    @tornado.gen.coroutine
 | 
			
		||||
    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
 | 
			
		||||
        self.pending_read = None
 | 
			
		||||
        if response:
 | 
			
		||||
            response = json.loads(response)
 | 
			
		||||
        raise tornado.gen.Return(response)
 | 
			
		||||
 | 
			
		||||
    @tornado.gen.coroutine
 | 
			
		||||
    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)
 | 
			
		||||
            except tornado.gen.TimeoutError:
 | 
			
		||||
                raise tornado.gen.Return(msglist)
 | 
			
		||||
 | 
			
		||||
            msglist.append(msg)
 | 
			
		||||
 | 
			
		||||
    def write_msg(self, msg):
 | 
			
		||||
        self.ws.write_message(json.dumps(msg))
 | 
			
		||||
 | 
			
		||||
    @tornado.gen.coroutine
 | 
			
		||||
    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)
 | 
			
		||||
        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))
 | 
			
		||||
 | 
			
		||||
    def write_stdin(self, data):
 | 
			
		||||
        """Write to terminal stdin"""
 | 
			
		||||
        self.write_msg(['stdin', data])
 | 
			
		||||
 | 
			
		||||
    @tornado.gen.coroutine
 | 
			
		||||
    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()
 | 
			
		||||
        if os.name == 'nt':
 | 
			
		||||
            match = re.search(r'echo \$\$\x1b\[0K\r\n(\d+)', stdout)
 | 
			
		||||
            pid = int(match.groups()[0])
 | 
			
		||||
        else:
 | 
			
		||||
            pid = int(stdout.split('\n')[1])
 | 
			
		||||
        raise tornado.gen.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.
 | 
			
		||||
    # See:  https://github.com/tornadoweb/tornado/issues/1161
 | 
			
		||||
    @tornado.gen.coroutine
 | 
			
		||||
    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))
 | 
			
		||||
 | 
			
		||||
    @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)
 | 
			
		||||
 | 
			
		||||
    @tornado.gen.coroutine
 | 
			
		||||
    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()
 | 
			
		||||
            pids.append(pid)
 | 
			
		||||
 | 
			
		||||
        raise tornado.gen.Return(pids)
 | 
			
		||||
 | 
			
		||||
    def tearDown(self):
 | 
			
		||||
        self.named_tm.kill_all()
 | 
			
		||||
        self.single_tm.kill_all()
 | 
			
		||||
        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)
 | 
			
		||||
        
 | 
			
		||||
        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')
 | 
			
		||||
 | 
			
		||||
class CommonTests(TermTestCase):
 | 
			
		||||
    @tornado.testing.gen_test
 | 
			
		||||
    def test_basic(self):
 | 
			
		||||
        for url in self.test_urls:
 | 
			
		||||
            tm = yield self.get_term_client(url)
 | 
			
		||||
            response = yield tm.read_msg()                    
 | 
			
		||||
            self.assertEqual(response, ['setup', {}])
 | 
			
		||||
 | 
			
		||||
            # Check for initial shell prompt
 | 
			
		||||
            response = yield tm.read_msg()
 | 
			
		||||
            self.assertEqual(response[0], 'stdout')
 | 
			
		||||
            self.assertGreater(len(response[1]), 0)
 | 
			
		||||
            tm.close()        
 | 
			
		||||
 | 
			
		||||
    @tornado.testing.gen_test
 | 
			
		||||
    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()
 | 
			
		||||
            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
 | 
			
		||||
    def test_namespace(self):
 | 
			
		||||
        names = ["/named/1"]*2 + ["/named/2"]*2
 | 
			
		||||
        tms = yield self.get_term_clients(names)
 | 
			
		||||
        pids = yield 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):
 | 
			
		||||
        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)
 | 
			
		||||
 | 
			
		||||
        # MAX_TERMS+1 should fail
 | 
			
		||||
        tm = yield self.get_term_client(urls[MAX_TERMS])
 | 
			
		||||
        msg = yield 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)
 | 
			
		||||
        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)
 | 
			
		||||
        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)
 | 
			
		||||
        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()
 | 
			
		||||
        self.assertEqual(msg, None)             # Connection closed
 | 
			
		||||
 | 
			
		||||
        # Close one
 | 
			
		||||
        tms[0].close()
 | 
			
		||||
        msg = yield tms[0].read_msg()           # Closed
 | 
			
		||||
        self.assertEquals(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')
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    unittest.main()
 | 
			
		||||
							
								
								
									
										27
									
								
								venv/Lib/site-packages/terminado/uimodule.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								venv/Lib/site-packages/terminado/uimodule.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,27 @@
 | 
			
		|||
"""A Tornado UI module for a terminal backed by terminado.
 | 
			
		||||
 | 
			
		||||
See the Tornado docs for information on UI modules:
 | 
			
		||||
http://www.tornadoweb.org/en/stable/guide/templates.html#ui-modules
 | 
			
		||||
"""
 | 
			
		||||
# Copyright (c) Jupyter Development Team
 | 
			
		||||
# Copyright (c) 2014, Ramalingam Saravanan <sarava@sarava.net>
 | 
			
		||||
# Distributed under the terms of the Simplified BSD License.
 | 
			
		||||
 | 
			
		||||
import os.path
 | 
			
		||||
import tornado.web
 | 
			
		||||
 | 
			
		||||
class Terminal(tornado.web.UIModule):
 | 
			
		||||
    def render(self, ws_url, cols=80, rows=25):
 | 
			
		||||
        return ('<div class="terminado-container" '
 | 
			
		||||
                'data-ws-url="{ws_url}" '
 | 
			
		||||
                'data-rows="{rows}" data-cols="{cols}"/>').format(
 | 
			
		||||
                        ws_url=ws_url, rows=rows, cols=cols)
 | 
			
		||||
 | 
			
		||||
    def javascript_files(self):
 | 
			
		||||
        # TODO: Can we calculate these dynamically?
 | 
			
		||||
        return ['/xstatic/termjs/term.js', '/static/terminado.js']
 | 
			
		||||
 | 
			
		||||
    def embedded_javascript(self):
 | 
			
		||||
        file = os.path.join(os.path.dirname(__file__), 'uimod_embed.js')
 | 
			
		||||
        with open(file) as f:
 | 
			
		||||
            return f.read()
 | 
			
		||||
							
								
								
									
										112
									
								
								venv/Lib/site-packages/terminado/websocket.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								venv/Lib/site-packages/terminado/websocket.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,112 @@
 | 
			
		|||
"""Tornado websocket handler to serve a terminal interface.
 | 
			
		||||
"""
 | 
			
		||||
# Copyright (c) Jupyter Development Team
 | 
			
		||||
# Copyright (c) 2014, Ramalingam Saravanan <sarava@sarava.net>
 | 
			
		||||
# Distributed under the terms of the Simplified BSD License.
 | 
			
		||||
 | 
			
		||||
from __future__ import absolute_import, print_function
 | 
			
		||||
 | 
			
		||||
# Python3-friendly imports
 | 
			
		||||
try:
 | 
			
		||||
    from urllib.parse import urlparse
 | 
			
		||||
except ImportError:
 | 
			
		||||
    from urlparse import urlparse
 | 
			
		||||
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
import tornado.web
 | 
			
		||||
import tornado.websocket
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _cast_unicode(s):
 | 
			
		||||
    if isinstance(s, bytes):
 | 
			
		||||
        return s.decode('utf-8')
 | 
			
		||||
    return s
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TermSocket(tornado.websocket.WebSocketHandler):
 | 
			
		||||
    """Handler for a terminal websocket"""
 | 
			
		||||
 | 
			
		||||
    def initialize(self, term_manager):
 | 
			
		||||
        self.term_manager = term_manager
 | 
			
		||||
        self.term_name = ""
 | 
			
		||||
        self.size = (None, None)
 | 
			
		||||
        self.terminal = None
 | 
			
		||||
 | 
			
		||||
        self._logger = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
    def origin_check(self, origin=None):
 | 
			
		||||
        """Deprecated: backward-compat for terminado <= 0.5."""
 | 
			
		||||
        return self.check_origin(origin or self.request.headers.get('Origin'))
 | 
			
		||||
 | 
			
		||||
    def open(self, url_component=None):
 | 
			
		||||
        """Websocket connection opened.
 | 
			
		||||
 | 
			
		||||
        Call our terminal manager to get a terminal, and connect to it as a
 | 
			
		||||
        client.
 | 
			
		||||
        """
 | 
			
		||||
        # Jupyter has a mixin to ping websockets and keep connections through
 | 
			
		||||
        # proxies alive. Call super() to allow that to set up:
 | 
			
		||||
        super(TermSocket, self).open(url_component)
 | 
			
		||||
 | 
			
		||||
        self._logger.info("TermSocket.open: %s", url_component)
 | 
			
		||||
 | 
			
		||||
        url_component = _cast_unicode(url_component)
 | 
			
		||||
        self.term_name = url_component or 'tty'
 | 
			
		||||
        self.terminal = self.term_manager.get_terminal(url_component)
 | 
			
		||||
        self.terminal.clients.append(self)
 | 
			
		||||
        self.send_json_message(["setup", {}])
 | 
			
		||||
        self._logger.info("TermSocket.open: Opened %s", self.term_name)
 | 
			
		||||
        # Now drain the preopen buffer, if it exists.
 | 
			
		||||
        buffered = ""
 | 
			
		||||
        while True:
 | 
			
		||||
            if not self.terminal.preopen_buffer:
 | 
			
		||||
                break
 | 
			
		||||
            s = self.terminal.preopen_buffer.popleft()
 | 
			
		||||
            buffered += s
 | 
			
		||||
        if buffered:
 | 
			
		||||
            self.on_pty_read(buffered)
 | 
			
		||||
 | 
			
		||||
    def on_pty_read(self, text):
 | 
			
		||||
        """Data read from pty; send to frontend"""
 | 
			
		||||
        self.send_json_message(['stdout', text])
 | 
			
		||||
 | 
			
		||||
    def send_json_message(self, content):
 | 
			
		||||
        json_msg = json.dumps(content)
 | 
			
		||||
        self.write_message(json_msg)
 | 
			
		||||
 | 
			
		||||
    def on_message(self, message):
 | 
			
		||||
        """Handle incoming websocket message
 | 
			
		||||
 | 
			
		||||
        We send JSON arrays, where the first element is a string indicating
 | 
			
		||||
        what kind of message this is. Data associated with the message follows.
 | 
			
		||||
        """
 | 
			
		||||
        ##logging.info("TermSocket.on_message: %s - (%s) %s", self.term_name, type(message), len(message) if isinstance(message, bytes) else message[:250])
 | 
			
		||||
        command = json.loads(message)
 | 
			
		||||
        msg_type = command[0]
 | 
			
		||||
 | 
			
		||||
        if msg_type == "stdin":
 | 
			
		||||
            self.terminal.ptyproc.write(command[1])
 | 
			
		||||
        elif msg_type == "set_size":
 | 
			
		||||
            self.size = command[1:3]
 | 
			
		||||
            self.terminal.resize_to_smallest()
 | 
			
		||||
 | 
			
		||||
    def on_close(self):
 | 
			
		||||
        """Handle websocket closing.
 | 
			
		||||
 | 
			
		||||
        Disconnect from our terminal, and tell the terminal manager we're
 | 
			
		||||
        disconnecting.
 | 
			
		||||
        """
 | 
			
		||||
        self._logger.info("Websocket closed")
 | 
			
		||||
        if self.terminal:
 | 
			
		||||
            self.terminal.clients.remove(self)
 | 
			
		||||
            self.terminal.resize_to_smallest()
 | 
			
		||||
        self.term_manager.client_disconnected(self)
 | 
			
		||||
 | 
			
		||||
    def on_pty_died(self):
 | 
			
		||||
        """Terminal closed: tell the frontend, and close the socket.
 | 
			
		||||
        """
 | 
			
		||||
        self.send_json_message(['disconnect', 1])
 | 
			
		||||
        self.close()
 | 
			
		||||
        self.terminal = None
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue