btwebterminal 2.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- btwebterminal-2.9.0/PKG-INFO +66 -0
- btwebterminal-2.9.0/README.md +41 -0
- btwebterminal-2.9.0/VERSION.txt +1 -0
- btwebterminal-2.9.0/btwebterminal/__init__.py +0 -0
- btwebterminal-2.9.0/btwebterminal/app.py +42 -0
- btwebterminal-2.9.0/btwebterminal/logger.py +41 -0
- btwebterminal-2.9.0/btwebterminal/spyder_terminal/__init__.py +0 -0
- btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/__init__.py +5 -0
- btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/common.py +22 -0
- btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/logic/__init__.py +20 -0
- btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/logic/term_manager.py +131 -0
- btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/rest/__init__.py +21 -0
- btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/rest/term_rest.py +45 -0
- btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/routes.py +49 -0
- btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/tests/__init__.py +16 -0
- btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/tests/print_size.py +100 -0
- btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/tests/test_server.py +180 -0
- btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/web/__init__.py +20 -0
- btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/web/main_handler.py +29 -0
- btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/websockets/__init__.py +18 -0
- btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/websockets/term_ws.py +51 -0
- btwebterminal-2.9.0/btwebterminal/webterminal.py +58 -0
- btwebterminal-2.9.0/btwebterminal.egg-info/PKG-INFO +66 -0
- btwebterminal-2.9.0/btwebterminal.egg-info/SOURCES.txt +30 -0
- btwebterminal-2.9.0/btwebterminal.egg-info/dependency_links.txt +1 -0
- btwebterminal-2.9.0/btwebterminal.egg-info/entry_points.txt +2 -0
- btwebterminal-2.9.0/btwebterminal.egg-info/not-zip-safe +1 -0
- btwebterminal-2.9.0/btwebterminal.egg-info/requires.txt +9 -0
- btwebterminal-2.9.0/btwebterminal.egg-info/top_level.txt +1 -0
- btwebterminal-2.9.0/setup.cfg +61 -0
- btwebterminal-2.9.0/setup.py +33 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: btwebterminal
|
|
3
|
+
Version: 2.9.0
|
|
4
|
+
Summary: Webterminal Component for Bert's Interactive Lesson Loader
|
|
5
|
+
Home-page: https://github.com/berttejeda/bert.webterminal
|
|
6
|
+
Author: Engelbert Tejeda
|
|
7
|
+
Author-email: berttejeda@gmail.com
|
|
8
|
+
Keywords: browser-based,webterminal,xtermjs
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Information Technology
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Natural Language :: English
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
15
|
+
Requires-Python: >=3.7
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: terminado<1.0.0,>=0.12.0
|
|
18
|
+
Requires-Dist: coloredlogs
|
|
19
|
+
Provides-Extra: tests
|
|
20
|
+
Requires-Dist: pytest; extra == "tests"
|
|
21
|
+
Requires-Dist: pytest-cov; extra == "tests"
|
|
22
|
+
Requires-Dist: coveralls; extra == "tests"
|
|
23
|
+
Requires-Dist: flake8; extra == "tests"
|
|
24
|
+
Requires-Dist: mypy; extra == "tests"
|
|
25
|
+
|
|
26
|
+
# Overview
|
|
27
|
+
|
|
28
|
+
This is the Webterminal agent to be used with various projects I've developed, e.g.
|
|
29
|
+
|
|
30
|
+
- [bert.dashboard](https://github.com/berttejeda/bert.dashboard)
|
|
31
|
+
- [bert.bill](https://github.com/berttejeda/bert.bill) (superceded by above)
|
|
32
|
+
- [bert.slidev - webterminal addon](https://github.com/berttejeda/bert.slidev)
|
|
33
|
+
|
|
34
|
+
This agent allows the app's [xtermjs]((https://github.com/xtermjs/xterm.js/)) Webterminal React component to connect \
|
|
35
|
+
to a local bash process on your computer.
|
|
36
|
+
|
|
37
|
+
You can get this Webterminal agent running either by:
|
|
38
|
+
|
|
39
|
+
- Running the pre-built docker image:
|
|
40
|
+
```shell
|
|
41
|
+
docker run -it --name webterminal --rm -p 10001:10001 berttejeda/bill-webterminal
|
|
42
|
+
```
|
|
43
|
+
- Running `docker-compose up -d` from this project
|
|
44
|
+
- Install btdashboard with `pip install btdashboard` and running `btdashboard`, OR \
|
|
45
|
+
Cloning the [berttejeda/bert.dashboard](https://github.com/berttejeda/bert.dashboard) project, installing all requirements, and \
|
|
46
|
+
running `python btdashboard/app.py -aio` \
|
|
47
|
+
Doing so will launch a local websocket that forwards keystrokes to a bash process on your system
|
|
48
|
+
- Install bertdotbill with `pip install bertdotbill` and running `bill -aio`, OR \
|
|
49
|
+
Cloning the [bert.bill](https://github.com/berttejeda/bert.bill) project, installing all requirements, and \
|
|
50
|
+
running `python bertdotbill/app.py -aio` \
|
|
51
|
+
Doing so will launch a local websocket that forwards keystrokes to a bash process on your system
|
|
52
|
+
|
|
53
|
+
Either of the commands above will start the websocket and bash process on localhost:10001, \
|
|
54
|
+
but you can change the port if you like.
|
|
55
|
+
|
|
56
|
+
You can then connect to the agent through the Web UI.
|
|
57
|
+
|
|
58
|
+
# Architecture
|
|
59
|
+
|
|
60
|
+
- Utilizes [spyder-terminal](https://github.com/spyder-ide/spyder-terminal) component
|
|
61
|
+
|
|
62
|
+
# Features
|
|
63
|
+
|
|
64
|
+
- You can practice the lesson material with your own OS/system
|
|
65
|
+
- Simply **click** on a command, and it will be sent and executed on the underlying shell via web terminal!
|
|
66
|
+
- Default shell is bash (for now)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Overview
|
|
2
|
+
|
|
3
|
+
This is the Webterminal agent to be used with various projects I've developed, e.g.
|
|
4
|
+
|
|
5
|
+
- [bert.dashboard](https://github.com/berttejeda/bert.dashboard)
|
|
6
|
+
- [bert.bill](https://github.com/berttejeda/bert.bill) (superceded by above)
|
|
7
|
+
- [bert.slidev - webterminal addon](https://github.com/berttejeda/bert.slidev)
|
|
8
|
+
|
|
9
|
+
This agent allows the app's [xtermjs]((https://github.com/xtermjs/xterm.js/)) Webterminal React component to connect \
|
|
10
|
+
to a local bash process on your computer.
|
|
11
|
+
|
|
12
|
+
You can get this Webterminal agent running either by:
|
|
13
|
+
|
|
14
|
+
- Running the pre-built docker image:
|
|
15
|
+
```shell
|
|
16
|
+
docker run -it --name webterminal --rm -p 10001:10001 berttejeda/bill-webterminal
|
|
17
|
+
```
|
|
18
|
+
- Running `docker-compose up -d` from this project
|
|
19
|
+
- Install btdashboard with `pip install btdashboard` and running `btdashboard`, OR \
|
|
20
|
+
Cloning the [berttejeda/bert.dashboard](https://github.com/berttejeda/bert.dashboard) project, installing all requirements, and \
|
|
21
|
+
running `python btdashboard/app.py -aio` \
|
|
22
|
+
Doing so will launch a local websocket that forwards keystrokes to a bash process on your system
|
|
23
|
+
- Install bertdotbill with `pip install bertdotbill` and running `bill -aio`, OR \
|
|
24
|
+
Cloning the [bert.bill](https://github.com/berttejeda/bert.bill) project, installing all requirements, and \
|
|
25
|
+
running `python bertdotbill/app.py -aio` \
|
|
26
|
+
Doing so will launch a local websocket that forwards keystrokes to a bash process on your system
|
|
27
|
+
|
|
28
|
+
Either of the commands above will start the websocket and bash process on localhost:10001, \
|
|
29
|
+
but you can change the port if you like.
|
|
30
|
+
|
|
31
|
+
You can then connect to the agent through the Web UI.
|
|
32
|
+
|
|
33
|
+
# Architecture
|
|
34
|
+
|
|
35
|
+
- Utilizes [spyder-terminal](https://github.com/spyder-ide/spyder-terminal) component
|
|
36
|
+
|
|
37
|
+
# Features
|
|
38
|
+
|
|
39
|
+
- You can practice the lesson material with your own OS/system
|
|
40
|
+
- Simply **click** on a command, and it will be sent and executed on the underlying shell via web terminal!
|
|
41
|
+
- Default shell is bash (for now)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.9.0
|
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Spawn a websocket that handles forking of shell sessions for attachment by the bertdotbill WebTerminal UI element.
|
|
3
|
+
The code for this was taken from [spyder-terminal](https://github.com/spyder-ide/spyder-terminal).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import os
|
|
8
|
+
from btwebterminal.webterminal import WebTerminal
|
|
9
|
+
|
|
10
|
+
def main():
|
|
11
|
+
"""The main entrypoint
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
parser = argparse.ArgumentParser(description="btwebterminal - A websocket-based shell session handler")
|
|
15
|
+
|
|
16
|
+
parser.add_argument('--host',
|
|
17
|
+
default=os.environ.get('WEBTERMINAL_HOST', '0.0.0.0'),
|
|
18
|
+
help='Host to listen on (default: 0.0.0.0 or WEBTERMINAL_HOST env var)')
|
|
19
|
+
|
|
20
|
+
parser.add_argument('--port',
|
|
21
|
+
type=int,
|
|
22
|
+
default=int(os.environ.get('WEBTERMINAL_PORT', 10001)),
|
|
23
|
+
help='Port to listen on (default: 10001 or WEBTERMINAL_PORT env var)')
|
|
24
|
+
|
|
25
|
+
parser.add_argument('--shell',
|
|
26
|
+
default=os.environ.get('WEBTERMINAL_SHELL'),
|
|
27
|
+
help='Shell to spawn, can also be set with WEBTERMINAL_SHELL env var')
|
|
28
|
+
|
|
29
|
+
parser.add_argument('--debug',
|
|
30
|
+
action='store_true',
|
|
31
|
+
default=os.environ.get('WEBTERMINAL_DEBUG', 'false').lower() == 'true',
|
|
32
|
+
help='Enable debug mode (default: False or WEBTERMINAL_DEBUG=true env var)')
|
|
33
|
+
|
|
34
|
+
args = parser.parse_args()
|
|
35
|
+
|
|
36
|
+
WebTerminal().start(host=args.host, port=args.port, shell=args.shell, debug=args.debug)
|
|
37
|
+
|
|
38
|
+
if __name__ == '__main__':
|
|
39
|
+
main()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import logging.handlers
|
|
3
|
+
import coloredlogs
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
class Logger:
|
|
8
|
+
|
|
9
|
+
def __init__(self, **kwargs):
|
|
10
|
+
env_debug_is_on = os.environ.get('WEBTERMINAL_DEBUG', '').lower() in [
|
|
11
|
+
't', 'true', '1', 'on', 'y', 'yes']
|
|
12
|
+
self.debug = kwargs.get('debug', False) or env_debug_is_on
|
|
13
|
+
self.FORMAT_STR = "%(asctime)s %(name)s [%(levelname)s]: %(message)s"
|
|
14
|
+
self.formatter = logging.Formatter(
|
|
15
|
+
self.FORMAT_STR,
|
|
16
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
17
|
+
)
|
|
18
|
+
self.logfile_path = kwargs.get('logfile_path')
|
|
19
|
+
self.logfile_write_mode = kwargs.get('logfile_write_mode', 'a')
|
|
20
|
+
|
|
21
|
+
def init_logger(self, name=None, debug=False):
|
|
22
|
+
# Setup Logging
|
|
23
|
+
logger = logging.getLogger(name)
|
|
24
|
+
# TODO Find a better approach to this hacky method
|
|
25
|
+
if '--debug' in ' '.join(sys.argv) or self.debug:
|
|
26
|
+
logging_level = logging.DEBUG
|
|
27
|
+
else:
|
|
28
|
+
logging_level = logging.INFO
|
|
29
|
+
logger.setLevel(logging_level)
|
|
30
|
+
stdout_handler = logging.StreamHandler(sys.stdout)
|
|
31
|
+
stdout_handler.setFormatter(self.formatter)
|
|
32
|
+
logger.addHandler(stdout_handler)
|
|
33
|
+
if self.logfile_path:
|
|
34
|
+
# create one handler for print and one for export
|
|
35
|
+
file_handler = logging.FileHandler(self.logfile_path, self.logfile_write_mode)
|
|
36
|
+
file_handler.setFormatter(self.formatter)
|
|
37
|
+
logger.addHandler(file_handler)
|
|
38
|
+
coloredlogs.install(logger=logger,
|
|
39
|
+
fmt=self.FORMAT_STR,
|
|
40
|
+
level=logging_level)
|
|
41
|
+
return logger
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
"""General server constants and utillty functions."""
|
|
3
|
+
|
|
4
|
+
import tornado
|
|
5
|
+
import os.path
|
|
6
|
+
import btwebterminal.spyder_terminal.server.routes as routes
|
|
7
|
+
from btwebterminal.spyder_terminal.server.logic.term_manager import TermManager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_app(shell, close_future=None, **kwargs):
|
|
11
|
+
"""Create and return a tornado Web Application instance."""
|
|
12
|
+
debug = kwargs.get('debug')
|
|
13
|
+
serve_traceback = kwargs.get('serve_traceback')
|
|
14
|
+
autoreload = kwargs.get('autoreload')
|
|
15
|
+
settings = {"static_path": os.path.join(
|
|
16
|
+
os.path.dirname(__file__), "static")}
|
|
17
|
+
application = tornado.web.Application(routes.gen_routes(close_future),
|
|
18
|
+
debug=debug,
|
|
19
|
+
serve_traceback=serve_traceback,
|
|
20
|
+
autoreload=autoreload, **settings)
|
|
21
|
+
application.term_manager = TermManager([shell])
|
|
22
|
+
return application
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
logic module.
|
|
5
|
+
|
|
6
|
+
=========
|
|
7
|
+
Provides:
|
|
8
|
+
1. Creation and handling of system terminal emulators
|
|
9
|
+
2. Execution of bash/cmd commands
|
|
10
|
+
|
|
11
|
+
How to use the documentation
|
|
12
|
+
----------------------------
|
|
13
|
+
Documentation is available in one form: docstrings provided
|
|
14
|
+
with the code
|
|
15
|
+
|
|
16
|
+
Copyright (c) 2016, Edgar A. Margffoy.
|
|
17
|
+
MIT, see LICENSE for more details.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from . import term_manager
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""Term manager."""
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
import signal
|
|
7
|
+
import hashlib
|
|
8
|
+
import shlex
|
|
9
|
+
import tornado.web
|
|
10
|
+
import tornado.gen
|
|
11
|
+
import tornado.ioloop
|
|
12
|
+
from terminado.management import TermManagerBase, PtyWithClients
|
|
13
|
+
from urllib.parse import unquote
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
LOGGER = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
WINDOWS = os.name == 'nt'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PtyReader(PtyWithClients):
|
|
22
|
+
"""Wrapper around PtyWithClients."""
|
|
23
|
+
|
|
24
|
+
def resize_to_smallest(self, rows, cols):
|
|
25
|
+
"""Set the terminal size to that of the smallest client dimensions.
|
|
26
|
+
|
|
27
|
+
A terminal not using the full space available is much nicer than a
|
|
28
|
+
terminal trying to use more than the available space, so we keep it
|
|
29
|
+
sized to the smallest client.
|
|
30
|
+
"""
|
|
31
|
+
minrows = mincols = 10001
|
|
32
|
+
if rows is not None and rows < minrows:
|
|
33
|
+
minrows = rows
|
|
34
|
+
if cols is not None and cols < mincols:
|
|
35
|
+
mincols = cols
|
|
36
|
+
|
|
37
|
+
if minrows == 10001 or mincols == 10001:
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
rows, cols = self.ptyproc.getwinsize()
|
|
41
|
+
if (rows, cols) != (minrows, mincols):
|
|
42
|
+
LOGGER.debug("Resizing PTY to {0}x{1}".format(mincols, minrows))
|
|
43
|
+
self.ptyproc.setwinsize(minrows, mincols)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TermManager(TermManagerBase):
|
|
47
|
+
"""Wrapper around pexpect to execute local commands."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, shell_command, **kwargs):
|
|
50
|
+
"""Create a new terminal handler instance."""
|
|
51
|
+
super().__init__(shell_command, **kwargs)
|
|
52
|
+
self.consoles = {}
|
|
53
|
+
|
|
54
|
+
def new_terminal(self, **kwargs):
|
|
55
|
+
"""Make a new terminal, return a :class:`PtyReader` instance."""
|
|
56
|
+
options = self.term_settings.copy()
|
|
57
|
+
options['shell_command'] = self.shell_command
|
|
58
|
+
options.update(kwargs)
|
|
59
|
+
argv = options['shell_command']
|
|
60
|
+
|
|
61
|
+
# Convert list to string for terminado if needed
|
|
62
|
+
if isinstance(argv, list):
|
|
63
|
+
# Flatten and ensure all items are strings
|
|
64
|
+
flat_argv = []
|
|
65
|
+
for item in argv:
|
|
66
|
+
if isinstance(item, list):
|
|
67
|
+
flat_argv.extend(str(i) for i in item)
|
|
68
|
+
else:
|
|
69
|
+
flat_argv.append(str(item))
|
|
70
|
+
# Use shlex.quote() for proper shell escaping on Windows
|
|
71
|
+
argv = ' '.join(shlex.quote(arg) for arg in flat_argv)
|
|
72
|
+
|
|
73
|
+
env = self.make_term_env(**options)
|
|
74
|
+
cwd = options.get('cwd', None)
|
|
75
|
+
LOGGER.debug("Spawning new terminal: {0} in {1}".format(argv, cwd))
|
|
76
|
+
return PtyReader(argv, env, cwd)
|
|
77
|
+
|
|
78
|
+
@tornado.gen.coroutine
|
|
79
|
+
def client_disconnected(self, pid, socket):
|
|
80
|
+
"""Send terminal SIGHUP when client disconnects."""
|
|
81
|
+
self.log.info("Websocket closed, sending SIGHUP to terminal.")
|
|
82
|
+
term = self.consoles[pid]
|
|
83
|
+
term.clients.remove(socket)
|
|
84
|
+
try:
|
|
85
|
+
if WINDOWS:
|
|
86
|
+
term.kill()
|
|
87
|
+
self.pty_read(term.ptyproc.fd)
|
|
88
|
+
return
|
|
89
|
+
term.killpg(signal.SIGHUP)
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
del self.consoles[pid]
|
|
93
|
+
|
|
94
|
+
@tornado.gen.coroutine
|
|
95
|
+
def create_term(self, rows, cols, cwd=None):
|
|
96
|
+
"""Create a new virtual terminal."""
|
|
97
|
+
LOGGER.debug("create_term called with rows={0}, cols={1}, cwd={2}".format(rows, cols, cwd))
|
|
98
|
+
pid = hashlib.md5(str(time.time()).encode('utf-8')).hexdigest()[0:6]
|
|
99
|
+
# We need to do percent decoding for reading the cwd through a cookie
|
|
100
|
+
# For further information see spyder-ide/spyder-terminal#225
|
|
101
|
+
cwd = unquote(cwd)
|
|
102
|
+
LOGGER.debug("Decoded CWD: {0}".format(cwd))
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
pty = self.new_terminal(cwd=cwd, height=rows, width=cols)
|
|
106
|
+
pty.resize_to_smallest(rows, cols)
|
|
107
|
+
self.consoles[pid] = pty
|
|
108
|
+
LOGGER.info("Terminal created with PID {0} for CWD {1}".format(pid, cwd))
|
|
109
|
+
return pid
|
|
110
|
+
except Exception as e:
|
|
111
|
+
LOGGER.error("Error in TermManager.create_term: {0}".format(str(e)), exc_info=True)
|
|
112
|
+
raise
|
|
113
|
+
|
|
114
|
+
@tornado.gen.coroutine
|
|
115
|
+
def start_term(self, pid, socket):
|
|
116
|
+
"""Start reading a virtual terminal."""
|
|
117
|
+
term = self.consoles[pid]
|
|
118
|
+
self.start_reading(term)
|
|
119
|
+
term.clients.append(socket)
|
|
120
|
+
|
|
121
|
+
@tornado.gen.coroutine
|
|
122
|
+
def execute(self, pid, cmd):
|
|
123
|
+
"""Write characters to terminal."""
|
|
124
|
+
term = self.consoles[pid]
|
|
125
|
+
term.ptyproc.write(cmd)
|
|
126
|
+
|
|
127
|
+
@tornado.gen.coroutine
|
|
128
|
+
def resize_term(self, pid, rows, cols):
|
|
129
|
+
"""Resize terminal."""
|
|
130
|
+
term = self.consoles[pid]
|
|
131
|
+
term.resize_to_smallest(rows, cols)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
rest module.
|
|
5
|
+
|
|
6
|
+
=========
|
|
7
|
+
|
|
8
|
+
Provides:
|
|
9
|
+
1. Asynchronous execution of JSON services
|
|
10
|
+
|
|
11
|
+
How to use the documentation
|
|
12
|
+
----------------------------
|
|
13
|
+
Documentation is available in one form: docstrings provided
|
|
14
|
+
with the code
|
|
15
|
+
|
|
16
|
+
Copyright (c) 2016, Edgar A. Margffoy.
|
|
17
|
+
MIT, see LICENSE for more details.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from . import term_rest
|
|
21
|
+
term_rest
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# -*- coding: iso-8859-15 -*-
|
|
2
|
+
|
|
3
|
+
"""Main HTTP routes request handlers."""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import tornado.web
|
|
7
|
+
import tornado.escape
|
|
8
|
+
from os import getcwd
|
|
9
|
+
|
|
10
|
+
LOGGER = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MainHandler(tornado.web.RequestHandler):
|
|
14
|
+
"""Handles creation of new terminals."""
|
|
15
|
+
|
|
16
|
+
@tornado.gen.coroutine
|
|
17
|
+
def post(self):
|
|
18
|
+
"""POST verb: Create a new terminal."""
|
|
19
|
+
LOGGER.debug("Received POST request to create terminal")
|
|
20
|
+
try:
|
|
21
|
+
rows = int(self.get_argument('rows', default=23))
|
|
22
|
+
cols = int(self.get_argument('cols', default=73))
|
|
23
|
+
cwd = self.get_cookie('cwd', default=getcwd())
|
|
24
|
+
LOGGER.info('Terminal creation request - CWD: {0}, Size: ({1}, {2})'.format(cwd, cols, rows))
|
|
25
|
+
|
|
26
|
+
LOGGER.debug("Calling term_manager.create_term...")
|
|
27
|
+
pid = yield self.application.term_manager.create_term(rows, cols, cwd)
|
|
28
|
+
LOGGER.info('Terminal created successfully with PID: {0}'.format(pid))
|
|
29
|
+
|
|
30
|
+
self.write(pid)
|
|
31
|
+
except Exception as e:
|
|
32
|
+
LOGGER.error("Failed to create terminal: {0}".format(str(e)), exc_info=True)
|
|
33
|
+
self.set_status(500)
|
|
34
|
+
self.write({"error": "Failed to create terminal", "details": str(e)})
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ResizeHandler(tornado.web.RequestHandler):
|
|
38
|
+
"""Handles resizing of terminals."""
|
|
39
|
+
|
|
40
|
+
@tornado.gen.coroutine
|
|
41
|
+
def post(self, pid):
|
|
42
|
+
"""POST verb: Resize a terminal."""
|
|
43
|
+
rows = int(self.get_argument('rows', default=23))
|
|
44
|
+
cols = int(self.get_argument('cols', default=73))
|
|
45
|
+
self.application.term_manager.resize_term(pid, rows, cols)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# -*- coding: iso-8859-15 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
routes.
|
|
5
|
+
|
|
6
|
+
======
|
|
7
|
+
|
|
8
|
+
This module establishes and defines the Web Handlers and Websockets
|
|
9
|
+
that are associated with a specific URL routing name. New routing
|
|
10
|
+
associations must be defined here.
|
|
11
|
+
|
|
12
|
+
Notes
|
|
13
|
+
-----
|
|
14
|
+
For more information regarding routing URL and valid regular expressions
|
|
15
|
+
visit: http://www.tornadoweb.org/en/stable/guide/structure.html
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import btwebterminal.spyder_terminal.server.web as web
|
|
19
|
+
import btwebterminal.spyder_terminal.server.rest as rest
|
|
20
|
+
import btwebterminal.spyder_terminal.server.websockets as websockets
|
|
21
|
+
|
|
22
|
+
# Define new rest associations
|
|
23
|
+
REST = [
|
|
24
|
+
(r"/api/terminals", rest.term_rest.MainHandler),
|
|
25
|
+
(r"/api/terminals/(.*)/size", rest.term_rest.ResizeHandler)
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
# Define new websocket routes
|
|
29
|
+
WS = [
|
|
30
|
+
(r"/terminals/(.*)", websockets.term_ws.MainSocket)
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
# Define new web rendering route associations
|
|
34
|
+
WEB = [
|
|
35
|
+
(r'/', web.main_handler.MainHandler)
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
ROUTES = REST + WS + WEB
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def gen_routes(close_future):
|
|
42
|
+
"""Return a list of HTML redirection routes."""
|
|
43
|
+
if close_future is not None:
|
|
44
|
+
ws = []
|
|
45
|
+
for route in WS:
|
|
46
|
+
ws.append((route[0], route[1],
|
|
47
|
+
dict(close_future=close_future)))
|
|
48
|
+
return REST + ws + WEB
|
|
49
|
+
return ROUTES
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tests module.
|
|
3
|
+
|
|
4
|
+
=========
|
|
5
|
+
|
|
6
|
+
Provides:
|
|
7
|
+
1. Websocket and HTTP server methods tests.
|
|
8
|
+
|
|
9
|
+
How to use the documentation
|
|
10
|
+
----------------------------
|
|
11
|
+
Documentation is available in one form: docstrings provided
|
|
12
|
+
with the code
|
|
13
|
+
|
|
14
|
+
Copyright (c) 2016, Edgar A. Margffoy.
|
|
15
|
+
MIT, see LICENSE for more details.
|
|
16
|
+
"""
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Print console size on UNIX and Windows systems.
|
|
5
|
+
|
|
6
|
+
Taken from: https://gist.github.com/jtriley/1108174
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import shlex
|
|
11
|
+
import struct
|
|
12
|
+
import platform
|
|
13
|
+
import subprocess
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_terminal_size():
|
|
17
|
+
"""
|
|
18
|
+
Get width and height of console.
|
|
19
|
+
|
|
20
|
+
Works on Linux, OS X, Windows and Cygwin
|
|
21
|
+
Based on:
|
|
22
|
+
http://stackoverflow.com/questions/566746/how-to-get-console-window-width-in-python
|
|
23
|
+
"""
|
|
24
|
+
current_os = platform.system()
|
|
25
|
+
tuple_xy = None
|
|
26
|
+
if current_os == 'Windows':
|
|
27
|
+
tuple_xy = _get_terminal_size_windows()
|
|
28
|
+
if tuple_xy is None:
|
|
29
|
+
tuple_xy = _get_terminal_size_tput()
|
|
30
|
+
# needed for window's python in cygwin's xterm!
|
|
31
|
+
if current_os in ['Linux', 'Darwin'] or current_os.startswith('CYGWIN'):
|
|
32
|
+
tuple_xy = _get_terminal_size_linux()
|
|
33
|
+
if tuple_xy is None:
|
|
34
|
+
print("default")
|
|
35
|
+
tuple_xy = (80, 25) # default value
|
|
36
|
+
return tuple_xy
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _get_terminal_size_windows():
|
|
40
|
+
try:
|
|
41
|
+
from ctypes import windll, create_string_buffer
|
|
42
|
+
# stdin handle is -10
|
|
43
|
+
# stdout handle is -11
|
|
44
|
+
# stderr handle is -12
|
|
45
|
+
h = windll.kernel32.GetStdHandle(-12)
|
|
46
|
+
csbi = create_string_buffer(22)
|
|
47
|
+
res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi)
|
|
48
|
+
if res:
|
|
49
|
+
(bufx, bufy, curx, cury, wattr,
|
|
50
|
+
left, top, right, bottom,
|
|
51
|
+
maxx, maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw)
|
|
52
|
+
sizex = right - left + 1
|
|
53
|
+
sizey = bottom - top + 1
|
|
54
|
+
return sizex, sizey
|
|
55
|
+
except:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _get_terminal_size_tput():
|
|
60
|
+
"""Get terminal width.
|
|
61
|
+
|
|
62
|
+
src: http://stackoverflow.com/questions/263890/
|
|
63
|
+
how-do-i-find-the-width-height-of-a-terminal-window
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
cols = int(subprocess.check_call(shlex.split('tput cols')))
|
|
67
|
+
rows = int(subprocess.check_call(shlex.split('tput lines')))
|
|
68
|
+
return (cols, rows)
|
|
69
|
+
except:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _get_terminal_size_linux():
|
|
74
|
+
def ioctl_GWINSZ(fd):
|
|
75
|
+
try:
|
|
76
|
+
import fcntl
|
|
77
|
+
import termios
|
|
78
|
+
cr = struct.unpack('hh',
|
|
79
|
+
fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234'))
|
|
80
|
+
return cr
|
|
81
|
+
except:
|
|
82
|
+
pass
|
|
83
|
+
cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)
|
|
84
|
+
if not cr:
|
|
85
|
+
try:
|
|
86
|
+
fd = os.open(os.ctermid(), os.O_RDONLY)
|
|
87
|
+
cr = ioctl_GWINSZ(fd)
|
|
88
|
+
os.close(fd)
|
|
89
|
+
except:
|
|
90
|
+
pass
|
|
91
|
+
if not cr:
|
|
92
|
+
try:
|
|
93
|
+
cr = (os.environ['LINES'], os.environ['COLUMNS'])
|
|
94
|
+
except:
|
|
95
|
+
return None
|
|
96
|
+
return int(cr[1]), int(cr[0])
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
if __name__ == "__main__":
|
|
100
|
+
print(get_terminal_size())
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
|
|
2
|
+
"""
|
|
3
|
+
Tornado server-side tests.
|
|
4
|
+
|
|
5
|
+
Note: This uses tornado.testing unittest style tests
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import os.path as osp
|
|
11
|
+
from urllib.parse import urlencode
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
from flaky import flaky
|
|
16
|
+
from tornado import testing, websocket, gen
|
|
17
|
+
from tornado.concurrent import Future
|
|
18
|
+
from bertdotbill.spyder.utils.programs import find_program
|
|
19
|
+
|
|
20
|
+
sys.path.append(osp.realpath(osp.dirname(__file__) + "/.."))
|
|
21
|
+
|
|
22
|
+
from spyder_terminal.server.common import create_app
|
|
23
|
+
|
|
24
|
+
LOCATION = os.path.realpath(os.path.join(os.getcwd(),
|
|
25
|
+
os.path.dirname(__file__)))
|
|
26
|
+
LOCATION_SLASH = LOCATION.replace('\\', '/')
|
|
27
|
+
|
|
28
|
+
LINE_END = '\n'
|
|
29
|
+
SHELL = 'bash'
|
|
30
|
+
WINDOWS = os.name == 'nt'
|
|
31
|
+
|
|
32
|
+
if WINDOWS:
|
|
33
|
+
LINE_END = '\r\n'
|
|
34
|
+
SHELL = 'cmd'
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TerminalServerTests(testing.AsyncHTTPTestCase):
|
|
38
|
+
"""Main server tests."""
|
|
39
|
+
|
|
40
|
+
def get_app(self):
|
|
41
|
+
"""Return HTTP/WS server."""
|
|
42
|
+
self.close_future = Future()
|
|
43
|
+
return create_app(SHELL, self.close_future)
|
|
44
|
+
|
|
45
|
+
def _mk_connection(self, pid):
|
|
46
|
+
return websocket.websocket_connect(
|
|
47
|
+
'ws://127.0.0.1:{0}/terminals/{1}'.format(
|
|
48
|
+
self.get_http_port(), pid)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@gen.coroutine
|
|
52
|
+
def close(self, ws):
|
|
53
|
+
"""
|
|
54
|
+
Close a websocket connection and wait for the server side.
|
|
55
|
+
|
|
56
|
+
If we don't wait here, there are sometimes leak warnings in the
|
|
57
|
+
tests.
|
|
58
|
+
"""
|
|
59
|
+
ws.close()
|
|
60
|
+
yield self.close_future
|
|
61
|
+
|
|
62
|
+
@testing.gen_test
|
|
63
|
+
def test_main_get(self):
|
|
64
|
+
"""Test if HTML source is rendered."""
|
|
65
|
+
response = yield self.http_client.fetch(
|
|
66
|
+
self.get_url('/'),
|
|
67
|
+
method="GET"
|
|
68
|
+
)
|
|
69
|
+
self.assertEqual(response.code, 200)
|
|
70
|
+
|
|
71
|
+
@testing.gen_test
|
|
72
|
+
def test_main_post(self):
|
|
73
|
+
"""Test that POST requests to root are forbidden."""
|
|
74
|
+
try:
|
|
75
|
+
yield self.http_client.fetch(
|
|
76
|
+
self.get_url('/'),
|
|
77
|
+
method="POST",
|
|
78
|
+
body=''
|
|
79
|
+
)
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
@testing.gen_test
|
|
84
|
+
def test_create_terminal(self):
|
|
85
|
+
"""Test terminal creation."""
|
|
86
|
+
data = {'rows': '25', 'cols': '80'}
|
|
87
|
+
response = yield self.http_client.fetch(
|
|
88
|
+
self.get_url('/api/terminals'),
|
|
89
|
+
method="POST",
|
|
90
|
+
body=urlencode(data)
|
|
91
|
+
)
|
|
92
|
+
self.assertEqual(response.code, 200)
|
|
93
|
+
|
|
94
|
+
@flaky(max_runs=3)
|
|
95
|
+
@testing.gen_test
|
|
96
|
+
def test_terminal_communication(self):
|
|
97
|
+
"""Test terminal creation."""
|
|
98
|
+
data = {'rows': '25', 'cols': '100'}
|
|
99
|
+
response = yield self.http_client.fetch(
|
|
100
|
+
self.get_url('/api/terminals'),
|
|
101
|
+
method="POST",
|
|
102
|
+
body=urlencode(data)
|
|
103
|
+
)
|
|
104
|
+
pid = response.body.decode('utf-8')
|
|
105
|
+
sock = yield self._mk_connection(pid)
|
|
106
|
+
msg = yield sock.read_message()
|
|
107
|
+
print(msg)
|
|
108
|
+
test_msg = 'pwd'
|
|
109
|
+
sock.write_message(' ' + test_msg)
|
|
110
|
+
msg = ''
|
|
111
|
+
while test_msg not in msg:
|
|
112
|
+
msg += yield sock.read_message()
|
|
113
|
+
print(msg)
|
|
114
|
+
msg = ''.join(msg.rstrip())
|
|
115
|
+
self.assertTrue(test_msg in msg)
|
|
116
|
+
yield self.close(sock)
|
|
117
|
+
|
|
118
|
+
@testing.gen_test
|
|
119
|
+
def test_terminal_closing(self):
|
|
120
|
+
"""Test terminal destruction."""
|
|
121
|
+
data = {'rows': '25', 'cols': '80'}
|
|
122
|
+
response = yield self.http_client.fetch(
|
|
123
|
+
self.get_url('/api/terminals'),
|
|
124
|
+
method="POST",
|
|
125
|
+
body=urlencode(data)
|
|
126
|
+
)
|
|
127
|
+
pid = response.body.decode('utf-8')
|
|
128
|
+
sock = yield self._mk_connection(pid)
|
|
129
|
+
_ = yield sock.read_message()
|
|
130
|
+
yield self.close(sock)
|
|
131
|
+
try:
|
|
132
|
+
sock.write_message(' This shall not work')
|
|
133
|
+
except AttributeError:
|
|
134
|
+
pass
|
|
135
|
+
yield self.close(sock)
|
|
136
|
+
|
|
137
|
+
@flaky(max_runs=3)
|
|
138
|
+
@pytest.mark.timeout(10)
|
|
139
|
+
@testing.gen_test
|
|
140
|
+
@pytest.mark.skipif(os.name == 'nt', reason="Doesn't work on Windows")
|
|
141
|
+
def test_terminal_resize(self):
|
|
142
|
+
"""Test terminal resizing."""
|
|
143
|
+
data = {'rows': '25', 'cols': '80'}
|
|
144
|
+
response = yield self.http_client.fetch(
|
|
145
|
+
self.get_url('/api/terminals'),
|
|
146
|
+
method="POST",
|
|
147
|
+
body=urlencode(data)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
pid = response.body.decode('utf-8')
|
|
151
|
+
sock = yield self._mk_connection(pid)
|
|
152
|
+
_ = yield sock.read_message()
|
|
153
|
+
|
|
154
|
+
data = {'rows': '23', 'cols': '73'}
|
|
155
|
+
response = yield self.http_client.fetch(
|
|
156
|
+
self.get_url('/api/terminals/{0}/size'.format(pid)),
|
|
157
|
+
method="POST",
|
|
158
|
+
body=urlencode(data)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
sock.write_message('cd {0}{1}'.format(LOCATION_SLASH, LINE_END))
|
|
162
|
+
|
|
163
|
+
# Use the current python interpreter to execute print_size.py if it
|
|
164
|
+
# can be determined by sys.executable. Otherwise just hope that there
|
|
165
|
+
# is a `python` in the shell's path which works with the script.
|
|
166
|
+
python_bin = sys.executable or "python"
|
|
167
|
+
python_exec = python_bin + ' print_size.py' + LINE_END
|
|
168
|
+
sock.write_message(python_exec)
|
|
169
|
+
|
|
170
|
+
expected_size = '(73, 23)'
|
|
171
|
+
msg = ''
|
|
172
|
+
fail_retry = 50
|
|
173
|
+
tries = 0
|
|
174
|
+
while expected_size not in msg:
|
|
175
|
+
if tries == fail_retry:
|
|
176
|
+
break
|
|
177
|
+
msg = yield sock.read_message()
|
|
178
|
+
tries += 1
|
|
179
|
+
self.assertIn(expected_size, msg)
|
|
180
|
+
yield self.close(sock)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
web module.
|
|
5
|
+
|
|
6
|
+
=========
|
|
7
|
+
|
|
8
|
+
Provides:
|
|
9
|
+
1. Asynchronous execution of Web Rendering
|
|
10
|
+
|
|
11
|
+
How to use the documentation
|
|
12
|
+
----------------------------
|
|
13
|
+
Documentation is available in one form: docstrings provided
|
|
14
|
+
with the code
|
|
15
|
+
|
|
16
|
+
Copyright (c) 2016, Edgar A. Margffoy.
|
|
17
|
+
MIT, see LICENSE for more details.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from . import main_handler
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# -*- coding: iso-8859-15 -*-
|
|
2
|
+
|
|
3
|
+
"""Basic static index.html HTTP handler."""
|
|
4
|
+
import tornado.web
|
|
5
|
+
import tornado.escape
|
|
6
|
+
from os import getcwd
|
|
7
|
+
from urllib.parse import quote
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MainHandler(tornado.web.RequestHandler):
|
|
11
|
+
"""Handles index request."""
|
|
12
|
+
|
|
13
|
+
def initialize(self, db=None):
|
|
14
|
+
"""Stump initialization function."""
|
|
15
|
+
self.db = db
|
|
16
|
+
|
|
17
|
+
@tornado.gen.coroutine
|
|
18
|
+
def get(self):
|
|
19
|
+
"""Get static index.html page."""
|
|
20
|
+
cwd = self.get_argument('path', getcwd())
|
|
21
|
+
# We need to do percent encoding for sending the cwd through a cookie
|
|
22
|
+
# For further information see spyder-ide/spyder-terminal#225
|
|
23
|
+
self.set_cookie('cwd', quote(cwd))
|
|
24
|
+
self.render('../static/build/index.html')
|
|
25
|
+
|
|
26
|
+
@tornado.gen.coroutine
|
|
27
|
+
def post(self):
|
|
28
|
+
"""POST verb: Forbidden."""
|
|
29
|
+
self.set_status(403)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
web module.
|
|
3
|
+
|
|
4
|
+
=========
|
|
5
|
+
|
|
6
|
+
Provides:
|
|
7
|
+
1. Asynchronous execution of Websockets
|
|
8
|
+
|
|
9
|
+
How to use the documentation
|
|
10
|
+
----------------------------
|
|
11
|
+
Documentation is available in one form: docstrings provided
|
|
12
|
+
with the code
|
|
13
|
+
|
|
14
|
+
Copyright (c) 2016, Edgar A. Margffoy.
|
|
15
|
+
MIT, see LICENSE for more details.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from . import term_ws
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# -*- coding: iso-8859-15 -*-
|
|
2
|
+
|
|
3
|
+
"""Websocket handling class."""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import tornado.escape
|
|
7
|
+
import tornado.websocket
|
|
8
|
+
|
|
9
|
+
LOGGER = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MainSocket(tornado.websocket.WebSocketHandler):
|
|
13
|
+
"""Handles long polling communication between xterm.js and server."""
|
|
14
|
+
|
|
15
|
+
def check_origin(self, origin):
|
|
16
|
+
return True
|
|
17
|
+
|
|
18
|
+
def initialize(self, close_future=None):
|
|
19
|
+
"""Base class initialization."""
|
|
20
|
+
self.close_future = close_future
|
|
21
|
+
|
|
22
|
+
def open(self, pid):
|
|
23
|
+
"""Open a Websocket associated to a console."""
|
|
24
|
+
LOGGER.info("WebSocket opened: {0}".format(pid))
|
|
25
|
+
self.pid = pid
|
|
26
|
+
self.application.term_manager.start_term(pid, self)
|
|
27
|
+
LOGGER.info("TTY On!")
|
|
28
|
+
|
|
29
|
+
def on_preclose(self):
|
|
30
|
+
"""Close console communication."""
|
|
31
|
+
LOGGER.info('Wassup!')
|
|
32
|
+
|
|
33
|
+
def on_close(self):
|
|
34
|
+
"""Close console communication."""
|
|
35
|
+
LOGGER.info('TTY Off!')
|
|
36
|
+
LOGGER.info("WebSocket closed: {0}".format(self.pid))
|
|
37
|
+
self.application.term_manager.client_disconnected(self.pid, self)
|
|
38
|
+
if self.close_future is not None:
|
|
39
|
+
self.close_future.set_result(("Done!"))
|
|
40
|
+
|
|
41
|
+
def on_message(self, message):
|
|
42
|
+
"""Execute a command on console."""
|
|
43
|
+
self.application.term_manager.execute(self.pid, message)
|
|
44
|
+
|
|
45
|
+
def on_pty_read(self, text):
|
|
46
|
+
"""Read data from pty; send to frontend."""
|
|
47
|
+
self.write_message(text)
|
|
48
|
+
|
|
49
|
+
def on_pty_died(self):
|
|
50
|
+
"""Close websocket if terminal was closed externally."""
|
|
51
|
+
self.close()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tornado.web
|
|
3
|
+
import tornado.ioloop
|
|
4
|
+
from btwebterminal.spyder_terminal.server.common import create_app
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from btwebterminal.logger import Logger
|
|
8
|
+
logger = Logger().init_logger(None)
|
|
9
|
+
|
|
10
|
+
class WebTerminal:
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
def _get_default_shell(self):
|
|
16
|
+
"""Get the appropriate shell command for the OS."""
|
|
17
|
+
if os.name == 'nt': # Windows
|
|
18
|
+
# Try Git Bash first
|
|
19
|
+
localappdata = os.environ.get('LOCALAPPDATA', '')
|
|
20
|
+
git_bash_paths = [
|
|
21
|
+
(Path(localappdata) / "Programs\\Git\\git-cmd.exe"),
|
|
22
|
+
Path("C:\\Program Files\\Git\\git-cmd.exe"),
|
|
23
|
+
Path("C:\\Program Files (x86)\\Git\\git-cmd.exe"),
|
|
24
|
+
]
|
|
25
|
+
for git_bash in git_bash_paths:
|
|
26
|
+
if git_bash.exists():
|
|
27
|
+
return [git_bash.as_posix(), "--no-cd", "--command=usr/bin/bash.exe", "-l", "-i"]
|
|
28
|
+
# Fallback to cmd.exe
|
|
29
|
+
return ["cmd.exe"]
|
|
30
|
+
else:
|
|
31
|
+
# Unix-like systems
|
|
32
|
+
return ["/bin/bash"]
|
|
33
|
+
|
|
34
|
+
def start(self, host, port, shell=None, debug=False):
|
|
35
|
+
clr = 'cls'
|
|
36
|
+
webterminal_shell_name = 'bash'
|
|
37
|
+
logger.info(f'Server is now at: {host}:{port}')
|
|
38
|
+
logger.info(f'Shell: {webterminal_shell_name}')
|
|
39
|
+
|
|
40
|
+
# Use provided shell or detect default
|
|
41
|
+
if shell is None:
|
|
42
|
+
shell = self._get_default_shell()
|
|
43
|
+
|
|
44
|
+
application = create_app(shell,
|
|
45
|
+
debug=debug,
|
|
46
|
+
serve_traceback=debug,
|
|
47
|
+
autoreload=debug)
|
|
48
|
+
ioloop = tornado.ioloop.IOLoop.instance()
|
|
49
|
+
application.listen(port, address=host)
|
|
50
|
+
try:
|
|
51
|
+
ioloop.start()
|
|
52
|
+
except KeyboardInterrupt:
|
|
53
|
+
pass
|
|
54
|
+
finally:
|
|
55
|
+
logger.info("Closing server...\n")
|
|
56
|
+
ioloop.run_sync(application.term_manager.shutdown)
|
|
57
|
+
tornado.ioloop.IOLoop.instance().stop()
|
|
58
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: btwebterminal
|
|
3
|
+
Version: 2.9.0
|
|
4
|
+
Summary: Webterminal Component for Bert's Interactive Lesson Loader
|
|
5
|
+
Home-page: https://github.com/berttejeda/bert.webterminal
|
|
6
|
+
Author: Engelbert Tejeda
|
|
7
|
+
Author-email: berttejeda@gmail.com
|
|
8
|
+
Keywords: browser-based,webterminal,xtermjs
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Information Technology
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Natural Language :: English
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
15
|
+
Requires-Python: >=3.7
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: terminado<1.0.0,>=0.12.0
|
|
18
|
+
Requires-Dist: coloredlogs
|
|
19
|
+
Provides-Extra: tests
|
|
20
|
+
Requires-Dist: pytest; extra == "tests"
|
|
21
|
+
Requires-Dist: pytest-cov; extra == "tests"
|
|
22
|
+
Requires-Dist: coveralls; extra == "tests"
|
|
23
|
+
Requires-Dist: flake8; extra == "tests"
|
|
24
|
+
Requires-Dist: mypy; extra == "tests"
|
|
25
|
+
|
|
26
|
+
# Overview
|
|
27
|
+
|
|
28
|
+
This is the Webterminal agent to be used with various projects I've developed, e.g.
|
|
29
|
+
|
|
30
|
+
- [bert.dashboard](https://github.com/berttejeda/bert.dashboard)
|
|
31
|
+
- [bert.bill](https://github.com/berttejeda/bert.bill) (superceded by above)
|
|
32
|
+
- [bert.slidev - webterminal addon](https://github.com/berttejeda/bert.slidev)
|
|
33
|
+
|
|
34
|
+
This agent allows the app's [xtermjs]((https://github.com/xtermjs/xterm.js/)) Webterminal React component to connect \
|
|
35
|
+
to a local bash process on your computer.
|
|
36
|
+
|
|
37
|
+
You can get this Webterminal agent running either by:
|
|
38
|
+
|
|
39
|
+
- Running the pre-built docker image:
|
|
40
|
+
```shell
|
|
41
|
+
docker run -it --name webterminal --rm -p 10001:10001 berttejeda/bill-webterminal
|
|
42
|
+
```
|
|
43
|
+
- Running `docker-compose up -d` from this project
|
|
44
|
+
- Install btdashboard with `pip install btdashboard` and running `btdashboard`, OR \
|
|
45
|
+
Cloning the [berttejeda/bert.dashboard](https://github.com/berttejeda/bert.dashboard) project, installing all requirements, and \
|
|
46
|
+
running `python btdashboard/app.py -aio` \
|
|
47
|
+
Doing so will launch a local websocket that forwards keystrokes to a bash process on your system
|
|
48
|
+
- Install bertdotbill with `pip install bertdotbill` and running `bill -aio`, OR \
|
|
49
|
+
Cloning the [bert.bill](https://github.com/berttejeda/bert.bill) project, installing all requirements, and \
|
|
50
|
+
running `python bertdotbill/app.py -aio` \
|
|
51
|
+
Doing so will launch a local websocket that forwards keystrokes to a bash process on your system
|
|
52
|
+
|
|
53
|
+
Either of the commands above will start the websocket and bash process on localhost:10001, \
|
|
54
|
+
but you can change the port if you like.
|
|
55
|
+
|
|
56
|
+
You can then connect to the agent through the Web UI.
|
|
57
|
+
|
|
58
|
+
# Architecture
|
|
59
|
+
|
|
60
|
+
- Utilizes [spyder-terminal](https://github.com/spyder-ide/spyder-terminal) component
|
|
61
|
+
|
|
62
|
+
# Features
|
|
63
|
+
|
|
64
|
+
- You can practice the lesson material with your own OS/system
|
|
65
|
+
- Simply **click** on a command, and it will be sent and executed on the underlying shell via web terminal!
|
|
66
|
+
- Default shell is bash (for now)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
VERSION.txt
|
|
3
|
+
setup.cfg
|
|
4
|
+
setup.py
|
|
5
|
+
btwebterminal/__init__.py
|
|
6
|
+
btwebterminal/app.py
|
|
7
|
+
btwebterminal/logger.py
|
|
8
|
+
btwebterminal/webterminal.py
|
|
9
|
+
btwebterminal.egg-info/PKG-INFO
|
|
10
|
+
btwebterminal.egg-info/SOURCES.txt
|
|
11
|
+
btwebterminal.egg-info/dependency_links.txt
|
|
12
|
+
btwebterminal.egg-info/entry_points.txt
|
|
13
|
+
btwebterminal.egg-info/not-zip-safe
|
|
14
|
+
btwebterminal.egg-info/requires.txt
|
|
15
|
+
btwebterminal.egg-info/top_level.txt
|
|
16
|
+
btwebterminal/spyder_terminal/__init__.py
|
|
17
|
+
btwebterminal/spyder_terminal/server/__init__.py
|
|
18
|
+
btwebterminal/spyder_terminal/server/common.py
|
|
19
|
+
btwebterminal/spyder_terminal/server/routes.py
|
|
20
|
+
btwebterminal/spyder_terminal/server/logic/__init__.py
|
|
21
|
+
btwebterminal/spyder_terminal/server/logic/term_manager.py
|
|
22
|
+
btwebterminal/spyder_terminal/server/rest/__init__.py
|
|
23
|
+
btwebterminal/spyder_terminal/server/rest/term_rest.py
|
|
24
|
+
btwebterminal/spyder_terminal/server/tests/__init__.py
|
|
25
|
+
btwebterminal/spyder_terminal/server/tests/print_size.py
|
|
26
|
+
btwebterminal/spyder_terminal/server/tests/test_server.py
|
|
27
|
+
btwebterminal/spyder_terminal/server/web/__init__.py
|
|
28
|
+
btwebterminal/spyder_terminal/server/web/main_handler.py
|
|
29
|
+
btwebterminal/spyder_terminal/server/websockets/__init__.py
|
|
30
|
+
btwebterminal/spyder_terminal/server/websockets/term_ws.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
btwebterminal
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[check-manifest]
|
|
2
|
+
ignore =
|
|
3
|
+
Dockerfile
|
|
4
|
+
entrypoint.sh
|
|
5
|
+
openssl.cnf
|
|
6
|
+
|
|
7
|
+
[metadata]
|
|
8
|
+
name = btwebterminal
|
|
9
|
+
author = Engelbert Tejeda
|
|
10
|
+
author_email = berttejeda@gmail.com
|
|
11
|
+
description = Webterminal Component for Bert's Interactive Lesson Loader
|
|
12
|
+
version = file: VERSION.txt
|
|
13
|
+
url = https://github.com/berttejeda/bert.webterminal
|
|
14
|
+
keywords =
|
|
15
|
+
browser-based
|
|
16
|
+
webterminal
|
|
17
|
+
xtermjs
|
|
18
|
+
classifiers =
|
|
19
|
+
Development Status :: 3 - Alpha
|
|
20
|
+
Intended Audience :: Developers
|
|
21
|
+
Intended Audience :: Information Technology
|
|
22
|
+
License :: OSI Approved :: MIT License
|
|
23
|
+
Natural Language :: English
|
|
24
|
+
Programming Language :: Python :: 3.7
|
|
25
|
+
long_description = file: README.md
|
|
26
|
+
long_description_content_type = text/markdown
|
|
27
|
+
license_files =
|
|
28
|
+
LICENSE.txt
|
|
29
|
+
|
|
30
|
+
[options]
|
|
31
|
+
include_package_data = true
|
|
32
|
+
python_requires = >= 3.7
|
|
33
|
+
setup_requires =
|
|
34
|
+
setuptools >= 40.6
|
|
35
|
+
pip >= 10
|
|
36
|
+
wheel >= 0.31
|
|
37
|
+
packages = find:
|
|
38
|
+
zip_safe = False
|
|
39
|
+
scripts =
|
|
40
|
+
install_requires =
|
|
41
|
+
terminado>=0.12.0,<1.0.0
|
|
42
|
+
coloredlogs
|
|
43
|
+
|
|
44
|
+
[options.extras_require]
|
|
45
|
+
tests =
|
|
46
|
+
pytest
|
|
47
|
+
pytest-cov
|
|
48
|
+
coveralls
|
|
49
|
+
flake8
|
|
50
|
+
mypy
|
|
51
|
+
|
|
52
|
+
[options.entry_points]
|
|
53
|
+
console_scripts =
|
|
54
|
+
bt-webterminal=btwebterminal.app:main
|
|
55
|
+
|
|
56
|
+
[options.data_files]
|
|
57
|
+
|
|
58
|
+
[egg_info]
|
|
59
|
+
tag_build =
|
|
60
|
+
tag_date = 0
|
|
61
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import configparser as ConfigParser
|
|
2
|
+
import os
|
|
3
|
+
from setuptools import setup
|
|
4
|
+
import sysconfig
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
gui_dirname = 'bill.gui'
|
|
8
|
+
|
|
9
|
+
cfg = ConfigParser.ConfigParser()
|
|
10
|
+
cfg.read('setup.cfg')
|
|
11
|
+
my_package_name = cfg.get('metadata', 'name')
|
|
12
|
+
|
|
13
|
+
if sys.platform == 'win32':
|
|
14
|
+
site_packages_path = 'scripts'
|
|
15
|
+
else:
|
|
16
|
+
site_packages_path = 'bin'
|
|
17
|
+
|
|
18
|
+
data_files_path = site_packages_path
|
|
19
|
+
|
|
20
|
+
def tree(src):
|
|
21
|
+
result = []
|
|
22
|
+
data_file_path = os.path.join(data_files_path, src)
|
|
23
|
+
for root, dirs, files in os.walk(src):
|
|
24
|
+
for file in files:
|
|
25
|
+
if os.path.sep in root:
|
|
26
|
+
sub_root = root.split(os.path.sep, 1)[-1]
|
|
27
|
+
file = os.path.join(sub_root, file)
|
|
28
|
+
result.append(os.path.join(src, file))
|
|
29
|
+
return [(data_file_path, result)]
|
|
30
|
+
|
|
31
|
+
DATA_FILES = tree(gui_dirname)
|
|
32
|
+
|
|
33
|
+
setup(data_files=DATA_FILES)
|