Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
0
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

goでブラウザ上で動くターミナルエミュレータを作った話

作ったもの

ブラウザ上で動くターミナルエミュレータを作りました。

shell-server.gif

構成

サーバーサイドは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
(セキュリティ的な安全確認はしてないので、グローバルからアクセスできない場所で動かしてください)


  1. ptyとは何かというと説明が難しい(というか筆者もよくわかってない)ですが、色々と機能のついたパイプのようなものと思っておくのがいいと思います。具体的にどんな機能がついているかについては https://qiita.com/angel_p_57/items/ff1c0d054714b7982ca50 が参考になります。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
0
Help us understand the problem. What are the problem?