Download pyxtermjs/app.py from ar08/train: direct link, hf CLI and curl.
- Browser
- Download file 4.79 kB
-
https://huggingface.co/spaces/ar08/train/resolve/main/pyxtermjs/app.py
- Command line
-
hf download hf://spaces/ar08/train/pyxtermjs/app.py
-
curl -L -o app.py https://huggingface.co/spaces/ar08/train/resolve/main/pyxtermjs/app.py
4.79 kB
| #!/usr/bin/env python3 | |
| import argparse | |
| from flask import Flask, render_template | |
| from flask_socketio import SocketIO | |
| import pty | |
| import os | |
| import subprocess | |
| import select | |
| import termios | |
| import struct | |
| import fcntl | |
| import shlex | |
| import logging | |
| import sys | |
| logging.getLogger("werkzeug").setLevel(logging.ERROR) | |
| __version__ = "0.5.0.2" | |
| app = Flask(__name__, template_folder=".", static_folder=".", static_url_path="") | |
| app.config["SECRET_KEY"] = "secret!" | |
| app.config["fd"] = None | |
| app.config["child_pid"] = None | |
| socketio = SocketIO(app) | |
| def set_winsize(fd, row, col, xpix=0, ypix=0): | |
| logging.debug("setting window size with termios") | |
| winsize = struct.pack("HHHH", row, col, xpix, ypix) | |
| fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize) | |
| def read_and_forward_pty_output(): | |
| max_read_bytes = 1024 * 20 | |
| while True: | |
| socketio.sleep(0.01) | |
| if app.config["fd"]: | |
| timeout_sec = 0 | |
| (data_ready, _, _) = select.select([app.config["fd"]], [], [], timeout_sec) | |
| if data_ready: | |
| output = os.read(app.config["fd"], max_read_bytes).decode( | |
| errors="ignore" | |
| ) | |
| socketio.emit("pty-output", {"output": output}, namespace="/pty") | |
| def index(): | |
| return render_template("index.html") | |
| def pty_input(data): | |
| """write to the child pty. The pty sees this as if you are typing in a real | |
| terminal. | |
| """ | |
| if app.config["fd"]: | |
| logging.debug("received input from browser: %s" % data["input"]) | |
| os.write(app.config["fd"], data["input"].encode()) | |
| def resize(data): | |
| if app.config["fd"]: | |
| logging.debug(f"Resizing window to {data['rows']}x{data['cols']}") | |
| set_winsize(app.config["fd"], data["rows"], data["cols"]) | |
| def connect(): | |
| """new client connected""" | |
| logging.info("new client connected") | |
| if app.config["child_pid"]: | |
| # already started child process, don't start another | |
| return | |
| # create child process attached to a pty we can read from and write to | |
| (child_pid, fd) = pty.fork() | |
| if child_pid == 0: | |
| # this is the child process fork. | |
| # anything printed here will show up in the pty, including the output | |
| # of this subprocess | |
| subprocess.run(app.config["cmd"]) | |
| else: | |
| # this is the parent process fork. | |
| # store child fd and pid | |
| app.config["fd"] = fd | |
| app.config["child_pid"] = child_pid | |
| set_winsize(fd, 50, 50) | |
| cmd = " ".join(shlex.quote(c) for c in app.config["cmd"]) | |
| # logging/print statements must go after this because... I have no idea why | |
| # but if they come before the background task never starts | |
| socketio.start_background_task(target=read_and_forward_pty_output) | |
| logging.info("child pid is " + child_pid) | |
| logging.info( | |
| f"starting background task with command `{cmd}` to continously read " | |
| "and forward pty output to client" | |
| ) | |
| logging.info("task started") | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description=( | |
| "A fully functional terminal in your browser. " | |
| "https://github.com/cs01/pyxterm.js" | |
| ), | |
| formatter_class=argparse.ArgumentDefaultsHelpFormatter, | |
| ) | |
| parser.add_argument( | |
| "-p", "--port", default=5000, help="port to run server on", type=int | |
| ) | |
| parser.add_argument( | |
| "--host", | |
| default="127.0.0.1", | |
| help="host to run server on (use 0.0.0.0 to allow access from other hosts)", | |
| ) | |
| parser.add_argument("--debug", action="store_true", help="debug the server") | |
| parser.add_argument("--version", action="store_true", help="print version and exit") | |
| parser.add_argument( | |
| "--command", default="bash", help="Command to run in the terminal" | |
| ) | |
| parser.add_argument( | |
| "--cmd-args", | |
| default="", | |
| help="arguments to pass to command (i.e. --cmd-args='arg1 arg2 --flag')", | |
| ) | |
| args = parser.parse_args() | |
| if args.version: | |
| print(__version__) | |
| exit(0) | |
| app.config["cmd"] = [args.command] + shlex.split(args.cmd_args) | |
| green = "\033[92m" | |
| end = "\033[0m" | |
| log_format = ( | |
| green | |
| + "pyxtermjs > " | |
| + end | |
| + "%(levelname)s (%(funcName)s:%(lineno)s) %(message)s" | |
| ) | |
| logging.basicConfig( | |
| format=log_format, | |
| stream=sys.stdout, | |
| level=logging.DEBUG if args.debug else logging.INFO, | |
| ) | |
| logging.info(f"serving on http://{args.host}:{args.port}") | |
| socketio.run(app, debug=args.debug, port=args.port, host=args.host) | |
| if __name__ == "__main__": | |
| main() | |