Upload 4 files
Browse files- pyxtermjs/__init__.py +1 -0
- pyxtermjs/__main__.py +5 -0
- pyxtermjs/app.py +152 -0
- pyxtermjs/index.html +143 -0
pyxtermjs/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
__version__ = "0.1.0"
|
pyxtermjs/__main__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .app import main
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
if __name__ == "__main__":
|
| 5 |
+
exit(main())
|
pyxtermjs/app.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
import argparse
|
| 3 |
+
from flask import Flask, render_template
|
| 4 |
+
from flask_socketio import SocketIO
|
| 5 |
+
import pty
|
| 6 |
+
import os
|
| 7 |
+
import subprocess
|
| 8 |
+
import select
|
| 9 |
+
import termios
|
| 10 |
+
import struct
|
| 11 |
+
import fcntl
|
| 12 |
+
import shlex
|
| 13 |
+
import logging
|
| 14 |
+
import sys
|
| 15 |
+
|
| 16 |
+
logging.getLogger("werkzeug").setLevel(logging.ERROR)
|
| 17 |
+
|
| 18 |
+
__version__ = "0.5.0.2"
|
| 19 |
+
|
| 20 |
+
app = Flask(__name__, template_folder=".", static_folder=".", static_url_path="")
|
| 21 |
+
app.config["SECRET_KEY"] = "secret!"
|
| 22 |
+
app.config["fd"] = None
|
| 23 |
+
app.config["child_pid"] = None
|
| 24 |
+
socketio = SocketIO(app)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def set_winsize(fd, row, col, xpix=0, ypix=0):
|
| 28 |
+
logging.debug("setting window size with termios")
|
| 29 |
+
winsize = struct.pack("HHHH", row, col, xpix, ypix)
|
| 30 |
+
fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def read_and_forward_pty_output():
|
| 34 |
+
max_read_bytes = 1024 * 20
|
| 35 |
+
while True:
|
| 36 |
+
socketio.sleep(0.01)
|
| 37 |
+
if app.config["fd"]:
|
| 38 |
+
timeout_sec = 0
|
| 39 |
+
(data_ready, _, _) = select.select([app.config["fd"]], [], [], timeout_sec)
|
| 40 |
+
if data_ready:
|
| 41 |
+
output = os.read(app.config["fd"], max_read_bytes).decode(
|
| 42 |
+
errors="ignore"
|
| 43 |
+
)
|
| 44 |
+
socketio.emit("pty-output", {"output": output}, namespace="/pty")
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@app.route("/")
|
| 48 |
+
def index():
|
| 49 |
+
return render_template("index.html")
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
@socketio.on("pty-input", namespace="/pty")
|
| 53 |
+
def pty_input(data):
|
| 54 |
+
"""write to the child pty. The pty sees this as if you are typing in a real
|
| 55 |
+
terminal.
|
| 56 |
+
"""
|
| 57 |
+
if app.config["fd"]:
|
| 58 |
+
logging.debug("received input from browser: %s" % data["input"])
|
| 59 |
+
os.write(app.config["fd"], data["input"].encode())
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
@socketio.on("resize", namespace="/pty")
|
| 63 |
+
def resize(data):
|
| 64 |
+
if app.config["fd"]:
|
| 65 |
+
logging.debug(f"Resizing window to {data['rows']}x{data['cols']}")
|
| 66 |
+
set_winsize(app.config["fd"], data["rows"], data["cols"])
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
@socketio.on("connect", namespace="/pty")
|
| 70 |
+
def connect():
|
| 71 |
+
"""new client connected"""
|
| 72 |
+
logging.info("new client connected")
|
| 73 |
+
if app.config["child_pid"]:
|
| 74 |
+
# already started child process, don't start another
|
| 75 |
+
return
|
| 76 |
+
|
| 77 |
+
# create child process attached to a pty we can read from and write to
|
| 78 |
+
(child_pid, fd) = pty.fork()
|
| 79 |
+
if child_pid == 0:
|
| 80 |
+
# this is the child process fork.
|
| 81 |
+
# anything printed here will show up in the pty, including the output
|
| 82 |
+
# of this subprocess
|
| 83 |
+
subprocess.run(app.config["cmd"])
|
| 84 |
+
else:
|
| 85 |
+
# this is the parent process fork.
|
| 86 |
+
# store child fd and pid
|
| 87 |
+
app.config["fd"] = fd
|
| 88 |
+
app.config["child_pid"] = child_pid
|
| 89 |
+
set_winsize(fd, 50, 50)
|
| 90 |
+
cmd = " ".join(shlex.quote(c) for c in app.config["cmd"])
|
| 91 |
+
# logging/print statements must go after this because... I have no idea why
|
| 92 |
+
# but if they come before the background task never starts
|
| 93 |
+
socketio.start_background_task(target=read_and_forward_pty_output)
|
| 94 |
+
|
| 95 |
+
logging.info("child pid is " + child_pid)
|
| 96 |
+
logging.info(
|
| 97 |
+
f"starting background task with command `{cmd}` to continously read "
|
| 98 |
+
"and forward pty output to client"
|
| 99 |
+
)
|
| 100 |
+
logging.info("task started")
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def main():
|
| 104 |
+
parser = argparse.ArgumentParser(
|
| 105 |
+
description=(
|
| 106 |
+
"A fully functional terminal in your browser. "
|
| 107 |
+
"https://github.com/cs01/pyxterm.js"
|
| 108 |
+
),
|
| 109 |
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
| 110 |
+
)
|
| 111 |
+
parser.add_argument(
|
| 112 |
+
"-p", "--port", default=5000, help="port to run server on", type=int
|
| 113 |
+
)
|
| 114 |
+
parser.add_argument(
|
| 115 |
+
"--host",
|
| 116 |
+
default="127.0.0.1",
|
| 117 |
+
help="host to run server on (use 0.0.0.0 to allow access from other hosts)",
|
| 118 |
+
)
|
| 119 |
+
parser.add_argument("--debug", action="store_true", help="debug the server")
|
| 120 |
+
parser.add_argument("--version", action="store_true", help="print version and exit")
|
| 121 |
+
parser.add_argument(
|
| 122 |
+
"--command", default="bash", help="Command to run in the terminal"
|
| 123 |
+
)
|
| 124 |
+
parser.add_argument(
|
| 125 |
+
"--cmd-args",
|
| 126 |
+
default="",
|
| 127 |
+
help="arguments to pass to command (i.e. --cmd-args='arg1 arg2 --flag')",
|
| 128 |
+
)
|
| 129 |
+
args = parser.parse_args()
|
| 130 |
+
if args.version:
|
| 131 |
+
print(__version__)
|
| 132 |
+
exit(0)
|
| 133 |
+
app.config["cmd"] = [args.command] + shlex.split(args.cmd_args)
|
| 134 |
+
green = "\033[92m"
|
| 135 |
+
end = "\033[0m"
|
| 136 |
+
log_format = (
|
| 137 |
+
green
|
| 138 |
+
+ "pyxtermjs > "
|
| 139 |
+
+ end
|
| 140 |
+
+ "%(levelname)s (%(funcName)s:%(lineno)s) %(message)s"
|
| 141 |
+
)
|
| 142 |
+
logging.basicConfig(
|
| 143 |
+
format=log_format,
|
| 144 |
+
stream=sys.stdout,
|
| 145 |
+
level=logging.DEBUG if args.debug else logging.INFO,
|
| 146 |
+
)
|
| 147 |
+
logging.info(f"serving on http://{args.host}:{args.port}")
|
| 148 |
+
socketio.run(app, debug=args.debug, port=args.port, host=args.host)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
if __name__ == "__main__":
|
| 152 |
+
main()
|
pyxtermjs/index.html
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<html lang="en">
|
| 2 |
+
<head>
|
| 3 |
+
<meta charset="utf-8" />
|
| 4 |
+
|
| 5 |
+
<style>
|
| 6 |
+
html {
|
| 7 |
+
font-family: arial;
|
| 8 |
+
}
|
| 9 |
+
body{
|
| 10 |
+
background-color: black; /* Set background color to black */
|
| 11 |
+
color: white; /* Set text color to white */
|
| 12 |
+
margin: 0; /* Remove default margin */
|
| 13 |
+
padding: 0;
|
| 14 |
+
}
|
| 15 |
+
</style>
|
| 16 |
+
<link
|
| 17 |
+
rel="stylesheet"
|
| 18 |
+
href="https://unpkg.com/xterm@4.11.0/css/xterm.css"
|
| 19 |
+
/>
|
| 20 |
+
</head>
|
| 21 |
+
<body>
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
<span style="font-size: small"
|
| 25 |
+
>status:
|
| 26 |
+
<span style="font-size: small" id="status">connecting...</span></span
|
| 27 |
+
>
|
| 28 |
+
|
| 29 |
+
<div style="width: 100%; height: calc(100% - 50px)" id="terminal"></div>
|
| 30 |
+
|
| 31 |
+
<p style="text-align: right; font-size: small">
|
| 32 |
+
Get Rickrolled <a href="https://www.youtube.com/watch?v=o-YBDTqX_ZU"></a>
|
| 33 |
+
<a href="https://www.youtube.com/watch?v=o-YBDTqX_ZU">GitHub</a>
|
| 34 |
+
</p>
|
| 35 |
+
<!-- xterm -->
|
| 36 |
+
<script src="https://unpkg.com/xterm@4.11.0/lib/xterm.js"></script>
|
| 37 |
+
<script src="https://unpkg.com/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.js"></script>
|
| 38 |
+
<script src="https://unpkg.com/xterm-addon-web-links@0.4.0/lib/xterm-addon-web-links.js"></script>
|
| 39 |
+
<script src="https://unpkg.com/xterm-addon-search@0.8.0/lib/xterm-addon-sear
|
| 40 |
+
ch.js"></script>
|
| 41 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.min.js"></script>
|
| 42 |
+
|
| 43 |
+
<script>
|
| 44 |
+
const term = new Terminal({
|
| 45 |
+
cursorBlink: true,
|
| 46 |
+
macOptionIsMeta: true,
|
| 47 |
+
scrollback: true,
|
| 48 |
+
});
|
| 49 |
+
term.attachCustomKeyEventHandler(customKeyEventHandler);
|
| 50 |
+
//
|
| 51 |
+
const fit = new FitAddon.FitAddon();
|
| 52 |
+
term.loadAddon(fit);
|
| 53 |
+
term.loadAddon(new WebLinksAddon.WebLinksAddon());
|
| 54 |
+
term.loadAddon(new SearchAddon.SearchAddon());
|
| 55 |
+
|
| 56 |
+
term.open(document.getElementById("terminal"));
|
| 57 |
+
fit.fit();
|
| 58 |
+
term.resize(15, 50);
|
| 59 |
+
console.log(`size: ${term.cols} columns, ${term.rows} rows`);
|
| 60 |
+
fit.fit();
|
| 61 |
+
term.writeln("Welcome to AR-server");
|
| 62 |
+
term.writeln("");
|
| 63 |
+
term.writeln('')
|
| 64 |
+
term.writeln("You can copy with ctrl+shift+x");
|
| 65 |
+
term.writeln("You can paste with ctrl+shift+v");
|
| 66 |
+
term.writeln('')
|
| 67 |
+
term.onData((data) => {
|
| 68 |
+
console.log("browser terminal received new data:", data);
|
| 69 |
+
socket.emit("pty-input", { input: data });
|
| 70 |
+
});
|
| 71 |
+
|
| 72 |
+
const socket = io.connect("/pty");
|
| 73 |
+
const status = document.getElementById("status");
|
| 74 |
+
|
| 75 |
+
socket.on("pty-output", function (data) {
|
| 76 |
+
console.log("new output received from server:", data.output);
|
| 77 |
+
term.write(data.output);
|
| 78 |
+
});
|
| 79 |
+
|
| 80 |
+
socket.on("connect", () => {
|
| 81 |
+
fitToscreen();
|
| 82 |
+
status.innerHTML =
|
| 83 |
+
'<span style="background-color: lightgreen;">connected</span>';
|
| 84 |
+
});
|
| 85 |
+
|
| 86 |
+
socket.on("disconnect", () => {
|
| 87 |
+
status.innerHTML =
|
| 88 |
+
'<span style="background-color: #ff8383;">disconnected</span>';
|
| 89 |
+
});
|
| 90 |
+
|
| 91 |
+
function fitToscreen() {
|
| 92 |
+
fit.fit();
|
| 93 |
+
const dims = { cols: term.cols, rows: term.rows };
|
| 94 |
+
console.log("sending new dimensions to server's pty", dims);
|
| 95 |
+
socket.emit("resize", dims);
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
function debounce(func, wait_ms) {
|
| 99 |
+
let timeout;
|
| 100 |
+
return function (...args) {
|
| 101 |
+
const context = this;
|
| 102 |
+
clearTimeout(timeout);
|
| 103 |
+
timeout = setTimeout(() => func.apply(context, args), wait_ms);
|
| 104 |
+
};
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/**
|
| 108 |
+
* Handle copy and paste events
|
| 109 |
+
*/
|
| 110 |
+
function customKeyEventHandler(e) {
|
| 111 |
+
if (e.type !== "keydown") {
|
| 112 |
+
return true;
|
| 113 |
+
}
|
| 114 |
+
if (e.ctrlKey && e.shiftKey) {
|
| 115 |
+
const key = e.key.toLowerCase();
|
| 116 |
+
if (key === "v") {
|
| 117 |
+
// ctrl+shift+v: paste whatever is in the clipboard
|
| 118 |
+
navigator.clipboard.readText().then((toPaste) => {
|
| 119 |
+
term.writeText(toPaste);
|
| 120 |
+
});
|
| 121 |
+
return false;
|
| 122 |
+
} else if (key === "c" || key === "x") {
|
| 123 |
+
// ctrl+shift+x: copy whatever is highlighted to clipboard
|
| 124 |
+
|
| 125 |
+
// 'x' is used as an alternate to 'c' because ctrl+c is taken
|
| 126 |
+
// by the terminal (SIGINT) and ctrl+shift+c is taken by the browser
|
| 127 |
+
// (open devtools).
|
| 128 |
+
// I'm not aware of ctrl+shift+x being used by anything in the terminal
|
| 129 |
+
// or browser
|
| 130 |
+
const toCopy = term.getSelection();
|
| 131 |
+
navigator.clipboard.writeText(toCopy);
|
| 132 |
+
term.focus();
|
| 133 |
+
return false;
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
return true;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
const wait_ms = 50;
|
| 140 |
+
window.onresize = debounce(fitToscreen, wait_ms);
|
| 141 |
+
</script>
|
| 142 |
+
</body>
|
| 143 |
+
</html>
|