作ったもの
ブラウザ上で動くターミナルエミュレータを作りました。
構成
サーバーサイドはgoで実装し、クライアントサイドはtypescriptで実装しました。
入出力はwebsocketでやり取りします。
実装の概要
ptyを作成してbashと接続する。
ターミナルエミュレーターが端末上で動くプログラムと入出力をやり取りする場合には、pty(仮想端末)と言われるものを使います。1
ptyはopenpty()関数を使うことによって作成します。
goにはopenptyに対応する関数は見つからなかったので、cgoを使って呼び出します。
openptyを呼ぶと、amasterとaslaveという2つの値が得られるので、
aslaveの方を端末上で動くプログラムの標準入出力に割り当て、amasterの方にブラウザから受け取った入力を入れます。
var amaster C.int
var aslave C.int
if errno := C.openpty((*C.int)(unsafe.Pointer(&amaster)), (*C.int)(unsafe.Pointer(&aslave)), nil, nil, nil); errno != 0 {
panic(fmt.Sprintf("errorno: %d\n", errno))
}
sysattr := syscall.SysProcAttr{Setsid: true}
attr := syscall.ProcAttr{
Files: []uintptr{uintptr(aslave), uintptr(aslave), uintptr(aslave)},
Env: []string{"TERM=vt100"},
Sys: &sysattr,
}
pid, err := syscall.ForkExec("/usr/bin/bash", []string{}, &attr)
if err != nil {
panic(err)
}
この状態でamasterの方に書き込めば、シェルに値が送られ、
またシェルの結果をamasterから読み込むことができます。
amasterへ読み書きするにはos.NewFileをつかって行えます
file := os.NewFile(uintptr(amaster), "ptymaster")
file.Write([]byte("ls"))
各種キーをシェルに送る
各種キーでアルファベットなどはそのまま対応するASCIIコードをamasterに入力してやればいいです。
ブラウザ側で
Backspaceキーが押されたときはBS(0x08)、
Enterキーが押されたときはCR(0x13)をおくればよいです。
Crtlキーに対応する
Ctrl+なにかのキーの場合は該当するASCIIコードから0x40を引いた値を送ってやります。
(例:Ctrl+Cだと0x03になる)
実際にシグナルを送る部分はptyがいい感じにやってくれてるらしく、これを実装するだけでCtrl+Cを押すと現在実行中のプログラムが終了するようになります。
特殊文字とエスケープシーケンスを扱う
ただこれだけだと、ただ文字は追記されるだけで、BSで文字を削除することすらできません。
既に入力した文字を削除する場合、別の場所に文字を表示したい場合などは、特別な文字が送られてくるのでクライアント側でうまく扱ってやる必要があります。
とりあえず以下の特殊文字を実装するとそれっぽくなります。
- CR(0x13) カーソルを行頭に戻します
- LF(0x10) カーソルを次の行に移動します
- BS(0x08) カーソルを一文字分左に移動します
とりあえずここまで実装すればタブ補完上下キーによる履歴表示あたりは動くはずです。
もう少し凝ったものを動かしたい場合、エスケープシーケンスを実装する必要があります。
エスケープシーケンスとは、複数の文字の組み合わせで端末を制御するものです。
筆者は以下のサイトを参考にいい感じに実装しました。
http://bkclass.web.fc2.com/doc_vt100.html
https://www.vt100.net/docs/vt510-rm/chapter4.html
https://en.wikipedia.org/wiki/ANSI_escape_code
終わりに
コードは公開してあるので興味ある方はどうぞ。
https://github.com/sdkawata/shell-server
(セキュリティ的な安全確認はしてないので、グローバルからアクセスできない場所で動かしてください)
-
ptyとは何かというと説明が難しい(というか筆者もよくわかってない)ですが、色々と機能のついたパイプのようなものと思っておくのがいいと思います。具体的にどんな機能がついているかについては https://qiita.com/angel_p_57/items/ff1c0d054714b7982ca50 が参考になります。 ↩