Skip to content

Instantly share code, notes, and snippets.

@GitSquared
Last active September 20, 2023 16:55
Show Gist options
  • Save GitSquared/2049d7e85eaddeeeaa44e8404fe0b0e1 to your computer and use it in GitHub Desktop.
Save GitSquared/2049d7e85eaddeeeaa44e8404fe0b0e1 to your computer and use it in GitHub Desktop.
Use your browser to access a remote terminal.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Remote terminal</title>
<link rel="stylesheet" href="xterm.css" />
<script src="terminal.class.js"></script>
<style>
div#terminal {
width: 500px;
height: 300px;
}
</style>
</head>
<body>
<div id="terminal-container"></div>
<script>
window.terminalRemote = new Terminal({
role: "client",
parentId: "terminal-container",
host: "127.0.0.1",
port: 3000
});
// Wait for terminal to be initialized then resize it to fit in the container
setTimeout(() => {
window.term.fit();
}, 500);
</script>
</body>
</html>
// Server demo (running node.js)
const Terminal = require("./terminal.class.js").Terminal;
let terminalServer = new Terminal({
role: "server",
shell: (process.platform === "win32") ? "cmd.exe" : "bash",
port: 3000
});
terminalServer.onclosed = (code, signal) => {
console.log("Terminal closed - "+code+", "+signal);
app.quit();
};
terminalServer.onopened = () => {
console.log("Connected to remote");
};
terminalServer.onresized = (cols, rows) => {
console.log("Resized terminal to "+cols+"x"+rows);
};
terminalServer.ondisconnected = () => {
console.log("Remote disconnected");
};
/* ****************************************************************
Create a terminal receiver or server.
Currently the server only accepts one connection at a time and
does not support SSL. If you wish to use to connect to a
terminal over the internet, PLEASE do not use this class as-is.
--SERVER-------------------------------------------------------
Server requirements:
- node-pty
- ws
Server usage:
terminalServer = new Terminal({
role: "server",
port: 3000, // 3000 if empty
shell: "bash" // Command to run, "bash" by default
})
Server events:
terminalServer.onopened // Connected to a receiver
.ondisconnected // Disconnected from receiver
.onclosed // Terminal was exited
// Args: code, signal
.onresized // Terminal was resized
// Args: cols, rows
Server methods:
None.
--CLIENT--------------------------------------------------------
Client requirements:
- xterm.js
- browser with WebSocket support
Client usage:
terminalClient = new Terminal({
role: "client",
parentId: "someid", // ID of the terminal container element
port: 3000, // 3000 if empty
host: "127.0.0.1" // Localhost by default
})
Client events:
None.
Client methods:
terminalClient.fit() // Resizes the terminal to match the container's size
.resize(cols, rows) // Manual resize
******************************************************************* */
class Terminal {
constructor(opts) {
if (opts.role === "client") {
if (!opts.parentId) throw "Missing options";
this.xTerm = require("xterm");
this.xTerm.loadAddon('attach');
this.xTerm.loadAddon('fit');
this.sendSizeToServer = () => {
let cols = this.term.cols.toString();
let rows = this.term.rows.toString();
while (cols.length < 3) {
cols = "0"+cols;
}
while (rows.length < 3) {
rows = "0"+rows;
}
this.socket.send("ESCAPED|-- RESIZE:"+cols+";"+rows);
};
this.term = new this.xTerm({
cols: 80,
rows: 24
});
this.term.open(document.getElementById(opts.parentId), true);
let sockHost = opts.host || "127.0.0.1";
let sockPort = opts.port || 3000;
this.socket = new WebSocket("ws://"+sockHost+":"+sockPort);
this.socket.onopen = () => {
this.term.attach(this.socket);
};
this.socket.onerror = (e) => {throw e};
this.fit = () => {
this.term.fit();
setTimeout(() => {
this.sendSizeToServer();
}, 50);
}
this.resize = (cols, rows) => {
this.term.resize(cols, rows);
this.sendSizeToServer();
}
} else if (opts.role === "server") {
this.Pty = require("node-pty");
this.Websocket = require("ws").Server;
this.onclosed = () => {};
this.onopened = () => {};
this.onresize = () => {};
this.ondisconnected = () => {};
this.tty = this.Pty.spawn(opts.shell || "bash", [], {
name: 'xterm-color',
cols: 80,
rows: 24,
cwd: process.env.PWD,
env: process.env
});
this.tty.on('exit', (code, signal) => {
this.onclosed(code, signal);
});
this.wss = new this.Websocket({
port: opts.port || 3000,
clientTracking: true,
verifyClient: (info) => {
if (this.wss.clients.length >= 1) {
return false;
} else {
return true;
}
}
});
this.wss.on('connection', (ws) => {
this.onopened();
ws.on('message', (msg) => {
if (msg.startsWith("ESCAPED|-- ")) {
if (msg.startsWith("ESCAPED|-- RESIZE:")) {
msg = msg.substr(18);
let cols = msg.slice(0, -4);
let rows = msg.substr(4);
this.tty.resize(Number(cols), Number(rows));
this.onresized(cols, rows);
}
} else {
this.tty.write(msg);
}
});
this.tty.on('data', (data) => {
try {
ws.send(data);
} catch (e) {
// Websocket closed
}
});
});
this.wss.on('close', () => {
this.ondisconnected();
});
} else {
throw "Unknow purpose";
}
}
}
module.exports = {
Terminal
};
@GitSquared
Copy link
Author

@ilayalmalem you can work around that by programmatically sending a \n to the PTY after the first time the receiver connects over websocket. It'll cause the shell to print out the prompt again

@ilayalmalem
Copy link

ilayalmalem commented Nov 8, 2020

@GitSquared Thank you! i managed to do the following:

ws.on('connection', () => {
    pty.send('clear\r');
})

This will actually hit Enter inside it.
You can also do something like:
pty.send('echo 10\r')
To execute "echo 10" in the pty.

@GitSquared
Copy link
Author

@ilayalmalem note that \r is Windows-specific.

@ilayalmalem
Copy link

@GitSquared strange, cause it seems to work just fine on my MacBook..

@djmalc
Copy link

djmalc commented Apr 27, 2021

seems to be a lot of things missing in this code. How does anyone get this to work?

@GitSquared
Copy link
Author

sorry @djmalc, i don't maintain gists. this code is outdated and will not work with latest versions of xterm.js/node-pty, i think.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment