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.
Files changed (31) hide show
  1. btwebterminal-2.9.0/PKG-INFO +66 -0
  2. btwebterminal-2.9.0/README.md +41 -0
  3. btwebterminal-2.9.0/VERSION.txt +1 -0
  4. btwebterminal-2.9.0/btwebterminal/__init__.py +0 -0
  5. btwebterminal-2.9.0/btwebterminal/app.py +42 -0
  6. btwebterminal-2.9.0/btwebterminal/logger.py +41 -0
  7. btwebterminal-2.9.0/btwebterminal/spyder_terminal/__init__.py +0 -0
  8. btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/__init__.py +5 -0
  9. btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/common.py +22 -0
  10. btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/logic/__init__.py +20 -0
  11. btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/logic/term_manager.py +131 -0
  12. btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/rest/__init__.py +21 -0
  13. btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/rest/term_rest.py +45 -0
  14. btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/routes.py +49 -0
  15. btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/tests/__init__.py +16 -0
  16. btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/tests/print_size.py +100 -0
  17. btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/tests/test_server.py +180 -0
  18. btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/web/__init__.py +20 -0
  19. btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/web/main_handler.py +29 -0
  20. btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/websockets/__init__.py +18 -0
  21. btwebterminal-2.9.0/btwebterminal/spyder_terminal/server/websockets/term_ws.py +51 -0
  22. btwebterminal-2.9.0/btwebterminal/webterminal.py +58 -0
  23. btwebterminal-2.9.0/btwebterminal.egg-info/PKG-INFO +66 -0
  24. btwebterminal-2.9.0/btwebterminal.egg-info/SOURCES.txt +30 -0
  25. btwebterminal-2.9.0/btwebterminal.egg-info/dependency_links.txt +1 -0
  26. btwebterminal-2.9.0/btwebterminal.egg-info/entry_points.txt +2 -0
  27. btwebterminal-2.9.0/btwebterminal.egg-info/not-zip-safe +1 -0
  28. btwebterminal-2.9.0/btwebterminal.egg-info/requires.txt +9 -0
  29. btwebterminal-2.9.0/btwebterminal.egg-info/top_level.txt +1 -0
  30. btwebterminal-2.9.0/setup.cfg +61 -0
  31. 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
@@ -0,0 +1,5 @@
1
+ """
2
+ This packages contains and implements a simple terminal server.
3
+
4
+ Creates virtual terminals via pexpect and websockets.
5
+ """
@@ -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,2 @@
1
+ [console_scripts]
2
+ bt-webterminal = btwebterminal.app:main
@@ -0,0 +1,9 @@
1
+ terminado<1.0.0,>=0.12.0
2
+ coloredlogs
3
+
4
+ [tests]
5
+ pytest
6
+ pytest-cov
7
+ coveralls
8
+ flake8
9
+ mypy
@@ -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)