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