xterm-pty
This is an addon that adds a PTY layer to xterm.js. It is useful to run an Emscripten'ed CUI program.
See the demo site: https://xterm-pty.netlify.app/.
How to use
If you want to use this library to run a CUI program built with Emscripten, you can go to Section "Emscripten integration".
Install xterm-pty
as usual.
$ npm i xterm-pty
Use LineDisciplineAddon.write
and LineDisciplineAddon.onData
instead of Terminal.write
and Terminal.onData
of xterm.js.
// Start an xterm.js instance
const xterm = new Terminal();
// Create master/slave objects
const { master, slave } = openpty();
// Connect the master object to xterm.js
xterm.loadAddon(ldiscAddon);
// Use slave.write instead of xterm.write
slave.write("Hello, world!\nInput your name:");
// Use slave.onReadable and slave.read instead of xterm.onData
slave.onReadable(() => {
xterm.write(`Hi, ${ slave.read().trim() }!\n`);
});
Result:
Hello, world!
Input your name: Yusuke
Hi, Yusuke!
■
Emscripten integration
Assume you want to run example.c in xterm.js.
- Compile it with Emscripten. Note that you need to specify
-sNO_EXIT_RUNTIME=1 -sFORCE_FILESYSTEM=1
.
emcc -sNO_EXIT_RUNTIME=1 -sFORCE_FILESYSTEM=1 -o example-core.js example.c
This will generate two files, example-core.js and example-core.wasm.
- Write a Web Worker script to run example-core.js as follows.
// example.worker.js
importScripts("https://cdn.jsdelivr.net/npm/[email protected]/workerTools.js");
onmessage = (msg) => {
importScripts(location.origin + "/example-core.js");
emscriptenHack(new TtyClient(msg.data));
};
The function emscriptenHack
intercepts some syscall events like TTY read/write, ioctl
, and select
in the Emscripten runtime. The helper class TtyClient
sends TTY requests to the server that works in the main thread (UI thread).
- Write a HTML as follows.
<html>
<head>
<title>demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/xterm.css">
</head>
<body>
<div id="terminal"></div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/xterm.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/index.js"></script>
<script>
const xterm = new Terminal();
xterm.open(document.getElementById("terminal"));
const { master, slave } = openpty();
xterm.loadAddon(master);
const worker = new Worker("./example.worker.js");
new TtyServer(slave).start(worker);
</script>
</body>
</html>
It sets up xterm.js, and invokes example.worker.js as a Web Worker. The helper class TtyServer
accepts TTY request from the client that works in the Web Worker.
- Serve all four files, example.html, example.worker.js, example-core.js, and example-core.wasm, with the following custom headers, and access the server with Google Chrome or Firefox.
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
These headers are needed to enable SharedArrayBuffer
. Note that you need to use Google Chrome or Firefox.
Here is a minimal server script in Ruby:
require "webrick"
class Server < WEBrick::HTTPServer
def service(req, res)
super
res["Cross-Origin-Opener-Policy"] = "same-origin"
res["Cross-Origin-Embedder-Policy"] = "require-corp"
end
end
Server.new(Port: 8080, DocumentRoot: ".").start
Caveats
Currently, emscriptenHack
rewrites the Emscripten runtime to monitor the following system events.
- eead from the file descripter 0
- write to the file descripters 1 and 2
- ioctl for TCGETS (getting termios), TCSETS and families (setting termios), and TIOCGWINSZ (gettins window size)
- select for TTY (waiting for stdin to be readable)
The hack highly depends on the internal implementation of the Emscripten runtime. We confirmed that it worked on Emscripten 2.0.13, which is bundled in Ubuntu 21.10.
Also, to use xterm-pty to run an Emscripten'ed process, you need to run it in a Web Worker. This is because the Emscripten runtime is written as synchronous Javascript code. When an Emscripten'ed process attempts to read its stdin, the runtime invokes a callback to receive input. The callback function is not async
, but need to wait synchronously for input from xterm.js. To achieve this, we need to use SharedArrayBuffer
and Atomics.wait
. This is why the response header described above is needed.
Unfortunately, xterm-pty does not support dynamic change of window size; you cannot use xterm-pty with xterm-addon-fit. We need to send SIGWINCH signal to tell an Emscripten'ed process when the terminal is resized, but Emscripten does not support signals yet.
Component flow
From xterm.js to the Emscripten runtime:
graph TB
subgraph Main thread
A(xterm.js) -->|Terminal.onData| B[PTY Master]
subgraph "PTY"
B -->|LineDiscipline.writeFromLower| C[LineDiscipline]
C -->|LineDiscipline.onWriteToUpper| D[PTY Slave]
end
D -->|Slave.read| E[TtyServer]
end
subgraph Web Worker thread
E -->|SharedArrayBuffer| F[TtyClient]
F -->|TtyClient.onRead| G[emscriptenHack]
G -->|dirty hack| H(Emscripten runtime)
end
From the Emscripten runtime to xterm.js:
graph BT
subgraph Main thread
B[PTY Master] -->|Terminal.write| A(xterm.js)
subgraph "PTY"
C[LineDiscipline] -->|LineDiscipline.onWriteToLower| B
D[PTY Slave] -->|LineDiscipline.writeFromUpper| C
end
E[TtyServer] -->|Slave.write| D
end
subgraph Web Worker thread
F[TtyClient] -->|Worker.postMessage| E
G[emscriptenHack] -->|TtyClient.onWrite| F
H(Emscripten runtime) -->|dirty hack| G
end