node-pty xterm.js websocket を利用したブラウザで動くShellの作成

node-pty xterm.js websocket を利用したブラウザで動くShellの作成

2022-12-1319 min read

目次

  1. 概要
  2. 利用するモジュールの説明
  3. 大まかな構成
  4. ソース
  5. typescriptで書き直す
  6. 参考にしたサイト

概要

node-pty xterm.js websocket を利用したブラウザで動くShellの作成をしてみました。

利用するモジュールの説明

node-pty

microsoft/node-pty: Fork pseudoterminals in Node.JS

node-ptyは、Node.jsでターミナルエミュレータを実装するためのモジュールです。

OSのテキストベースの端末エミュレータ(例えば、xtermやgnome-terminalなど)をラップして、Node.jsから呼び出すことができるようにします。 そのため、Node.jsで書かれたアプリケーションからターミナルを実行したり、ターミナルからのデータを読み取ったりすることができます。

node-ptyは、コマンドを実行したり、ターミナルを操作したりするためのAPIを提供しています。 また、node-ptyはオープンソースのモジュールであり、GitHub上で開発されています。

xterm.js

xtermjs/xterm.js: A terminal for the web

xterm.jsは、webブラウザ上で動作するターミナルエミュレータのライブラリです。 ターミナルエミュレータは、コンピュータでコマンドを入力し、その結果を表示するためのテキストベースの画面を提供するものです。 JavaScriptで書かれており、webブラウザ上で動作するようになっています。 xterm.jsを使用すると、webアプリケーションやサイトにターミナルエミュレータの機能を組み込むことができます。

ws

websockets/ws: Simple to use, blazing fast and thoroughly tested WebSocket client and server for Node.js

"ws"は、WebSocketを実装するためのJavaScriptライブラリです。

WebSocketは、クライアントとサーバー間でリアルタイムでデータをやり取りするためのプロトコルです。WebSocketを使用すると、サーバーからのイベントを受信したり、クライアントからのデータを送信したりすることができます。

"ws"ライブラリは、WebSocketを使用するためのAPIを提供しており、Node.jsでWebSocketを扱うためによく使われます。

大まかな構成

まず、node-ptyを使用してサーバーサイドでターミナルを起動し、xterm.jsを使用してクライアント側でターミナルのようなインターフェースを表示します。

次に、WebSocketを使用して、サーバーとクライアント間でデータをやり取りするようにします。 これにより、クライアントからサーバーへのコマンドを送信したり、サーバーからのターミナルの出力をクライアントに表示することができます。

具体的には、まずサーバーサイドでnode-ptyを使用してターミナルを起動します。 そして、WebSocketサーバーを起動します。次に、クライアント側でxterm.jsを使用してターミナルのようなインターフェースを表示し、WebSocketを使用してサーバーと通信するようにします。 クライアントからのコマンドを受け取ると、サーバーはnode-ptyを使用してそのコマンドを実行し、その結果をクライアントに送信します。 これを繰り返すことで、ブラウザ上で動作するシェルを実装することができます。

ソース

package.json

{
  "scripts": {
    "dev": "node index.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "node-pty": "^0.10.1",
    "ws": "^8.11.0",
    "xterm": "^5.0.0",
    "xterm-addon-fit": "^0.6.0",
    "xterm-addon-ligatures": "^0.6.0",
    "xterm-addon-search": "^0.10.0",
    "xterm-addon-serialize": "^0.8.0",
    "xterm-addon-unicode11": "^0.4.0",
    "xterm-addon-web-links": "^0.7.0"
  }
}

index.html

<html>
  <head>
    <meta charset="utf-8">
    <style>
      html {
        height: 100%;
      }
      body {
        height: 100%;
        margin: 0;
      }
      .fullheight {
        height: 100%;
        background: black;
      }
    </style>
    <link rel="stylesheet" href="node_modules/xterm/css/xterm.css" />
    <script type="text/javascript" src="node_modules/xterm/lib/xterm.js" charset="utf-8"></script>
    <script type="text/javascript" src="node_modules/xterm-addon-fit/lib/xterm-addon-fit.js" charset="utf-8"></script>
    <script type="text/javascript" src="node_modules/xterm-addon-ligatures/lib/xterm-addon-ligatures.js" charset="utf-8"></script>
    <script type="text/javascript" src="node_modules/xterm-addon-search/lib/xterm-addon-search.js" charset="utf-8"></script>
    <script type="text/javascript" src="node_modules/xterm-addon-web-links/lib/xterm-addon-web-links.js" charset="utf-8"></script>
    <script type="text/javascript" src="node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js" charset="utf-8"></script>
    <script type="text/javascript" src="node_modules/xterm-addon-serialize/lib/xterm-addon-serialize.js" charset="utf-8"></script>
  </head>
  <body>
    <div class="fullheight" id="terminal"></div>
    <script type="text/javascript" src="cli.js" charset="utf-8"></script>
  </body>
</html>

index.js

const express = require("express");
const app = express();
const server = require("http").Server(app);
const nodePty = require("node-pty");
const WebSocket = require("ws");

app.use("/", express.static("."));
const wss = new WebSocket.Server({ server });

wss.on("connection", (ws) => {
  let pty = nodePty.spawn("bash", ["--login"], {
    name: "xterm-color",
    cols: 80,
    rows: 24,
    cwd: process.env.HOME,
    env: process.env,
  });
  pty.onData((data) => {
    ws.send(JSON.stringify({ output: data }));
  });
  ws.on("message", (message) => {
    console.log("received: %s", message);
    m = JSON.parse(message);
    if (m.input) {
      pty.write(m.input);
    } else if (m.resize) {
      pty.resize(m.resize[0], m.resize[1]);
    }
  });
});

server.listen(process.env.PORT || 8999, () => {
  console.log(`Server started on port ${server.address().port} :)`);
});

cli.js

const term = new Terminal({
  cols: 80,
  rows: 24,
  allowProposedApi: true,
});
term.open(document.getElementById("terminal"));

// addons
const fitAddon = new FitAddon.FitAddon();
// const ligaturesAddon = new LigaturesAddon.LigaturesAddon();
const searchAddon = new SearchAddon.SearchAddon(); 
const webLinksAddon = new WebLinksAddon.WebLinksAddon(); 
const unicode11Addon = new Unicode11Addon.Unicode11Addon(); 
const serializeAddon = new SerializeAddon.SerializeAddon(); 

[
  fitAddon,
  // ligaturesAddon,
  searchAddon,
  webLinksAddon,
  unicode11Addon,
  serializeAddon,
].map((e) => term.loadAddon(e));

term.unicode.activeVersion = '11';

const ws = new WebSocket(`ws://${location.hostname}:8999`);

ws.addEventListener("open", () => {
  console.info("WebSocket connected");
});
ws.addEventListener("message", (event) => {
  console.debug("Message from server ", event.data);
  try {
    let output = JSON.parse(event.data);
    term.write(output.output, () => {
      console.log(serializeAddon.serialize());
    });
  } catch (e) {
    console.error(e);
  }
});


term.onData((data) => ws.send(JSON.stringify({ input: data })));

window.addEventListener("resize", () => {
  fitAddon.fit();
});

fitAddon.fit();

term.onResize((size) => {
  console.debug("resize");
  const resizer = JSON.stringify({ resizer: [size.cols, size.rows] });
  ws.send(resizer);
});

JS版のソースです。

https://github.com/s-yoshiki/node-websh/tree/8528ff6d61a2100afefba662584b3d7c306d7408

TypeScriptで書き直す

TypeScriptで書き直しました。

s-yoshiki/node-websh: node-pty xterm.js websocket を利用したブラウザで動くShell

参考にしたサイト

dews/webssh: xterm + node-pty + websocket

【Node.js + Express】WebSocketを使ってみる( + 全クライアントに一斉送信) - とある科学の備忘録

xterm.jsでキーボード入力を受け付ける方法 - haku-maiのブログ

Node.js Stream を使いこなす - Qiita

Tags
javascript(103)
linux(54)
amazon%20aws(47)
node.js(43)
%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0(36)
typescript(33)
%E7%94%BB%E5%83%8F%E5%87%A6%E7%90%86(30)
html5(29)
php(24)
centos(24)
python(22)
%E7%AB%B6%E6%8A%80%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0(21)
mac(21)
mysql(19)
canvas(18)
opencv(17)
%E9%9B%91%E8%AB%87(16)
docker(16)
wordpress(15)
atcoder(14)
apache(12)
%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92(12)
%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9(12)
amazon%20s3(12)
red%20hat(12)
ubuntu(11)
github(10)
git(10)
vue.js(10)
%E7%94%BB%E5%83%8F%E5%87%A6%E7%90%86100%E6%9C%AC%E3%83%8E%E3%83%83%E3%82%AF(10)
mariadb(10)
aws%20cdk(9)
css3(8)
%E5%8F%AF%E8%A6%96%E5%8C%96(8)
%E5%B0%8F%E3%83%8D%E3%82%BF(8)
amazon%20lightsail(7)
react(7)
%E3%83%96%E3%83%AD%E3%82%B0(6)
cms(6)
oracle(6)
perl(6)
gitlab(6)
next.js(6)
prisma(6)
iam(5)
amazon%20ec2(5)
%E8%B3%87%E6%A0%BC%E8%A9%A6%E9%A8%93(5)
aws%20amplify(5)
nestjs(5)
curl(4)
Author
githubzennqiita
ただの備忘録です。※このブログの内容は個人の見解であり、所属する組織等の見解ではないです。