-- Views
May 01, 26
スライド概要
ブラウザでbashを動かして理解す る「ターミナルの仕組み」 2025/12/17 俺の忘年会2025 #orestudy
自己紹介:murasuke ・株式会社 ツールラボ 開発部 所属 PHPで自社Webサービスの開発をやっています ・ボイジャー2号と1号の間にうまれました ・今年、人生で初めてカンファレンスや勉強会に参加 (ちょっとだけ勇気を出して)本当に良かった!!! ・今日も一緒にターミナルに詳しくなりましょう!
今年1年のまとめとして、ブラウザでターミナルを動かします! 今年やったこと ● マウスでお絵描きシェルスクリプト ● ターミナル上で動画再生 ● (実はJavaScriptで「テトリス」も作ったけど、未発表)
今年1年のまとめとして、ブラウザでターミナルを動かします! ということで 一年の締めくくりとして「ブラウザでターミナルを動かしてみます!」 ターミナルとシェルの間を「つなげる」コードを説明しながら、両者の関係 を理解しましょう ● 「黒い画面=シェル」なのか? ● ターミナル / PTY、TTY / シェルって何? ● 入出力の流れをもとに、理解しようと思います
ターミナルとは① ビデオ端末 (VT100など): ディスプレイに文字を表示するようになった端末 ・キーを押したらその文字を即時で 表示する仕組みではない 接続先のコンピューターから エコーバックを受けて、文字を 表示するようになっている ・エスケープシーケンスによる 表示の制御、文字装飾も できるようになった
ターミナルとは② 赤枠:は擬似ターミナル(PTY)と接続しているプロセス STAT「+(フォワグラウンドプロセス)」ターミナルとつながっているプロセス 青枠:はコンピューターに接続したキーボード入力を待っているプロセス /sbin/agetty はログインプロンプトを表示し、ユーザーのログインを待ち受けるプロセス
ターミナルとは③ ビデオ端末 (VT100など): キーボード、ディスプレイを制御するTTY経由でShellとつながっています TTYをShell側から見ると ・画面サイズ、カーソルの位置といったプロパティーを持つ「画面」に見える ・「キーボード入力」 ⇒ Shellには「標準入力(ファイル)」からの入力に変換 ・「標準出力(ファイル)」への書き込み ⇒ 画面へ出力される ⇒ read/write でアクセス可能なファイル風IOを持った「入出力装置 」である
疑似端末 PTY (Pseudo Terminal) とは? 物理的な端末がない環境で、端末のように振る舞うプロセス間通信チャネルのこと ・相手が物理端末では無いため「ドライバ」として の機能は持たない ・masterはターミナルエミュレーターと接続して 入出力を行う機能を持つ擬似ファイル ・slave は「端末のように振る舞う」特別なファイ ル(ShellからはTTYに「見える」) ・Ctrl+C、Ctrl+Zのような割り込み(ジョブ管理)は管理者権限のないプロセスから はできないので、PTY(カーネル側)で行われる
プログラムの全体像 ・ブラウザーでターミナルを表示 ・Shellの起動 ・WebSocketサーバー のソースを順に説明します
1. ブラウザにターミナルを表示(xterm.js) xterm.js と WebSocket を使ってターミナルを実装します xterm.js :ターミナルの機能を持つjsライブラリ ・キー入力 ・画面表示 ・エスケープシーケンスを理解して、 文字色や背景色、カーソル制御を行う ・画面サイズの変更通知 WebSocket :サーバーとの通信(特にPush通知の受信)のために利用 ・WebSocketでサーバーへ接続 ・キー入力イベントをフックして、WebSocket経由で送信 ・WebSocketから通知イベントを受け取り、xterm.jsで表示
1. ブラウザにターミナルを表示(xterm.js)
<body>
<div id="terminal" ></div>
<script src="https://unpkg.com/xterm/lib/xterm.js" ></script>
<script type="module" >
// ターミナル初期化 (@xterm/xterm)
const term = new Terminal ();
// ターミナルを表示
term.open(document .getElementById ('terminal' ));
// WebSocket 接続
const ws = new WebSocket (`ws://localhost:8000/` );
ws.binaryType = 'arraybuffer' ;
// ブラウザ(terminal) のキー入力をサーバへ送信
term.onData((data) => ws.send(data));
// サーバー(PTY)の出力をブラウザ (terminal) に表示
ws.addEventListener ('message' , (ev) => term.write(ev.data));
</script>
</body>
かなり端折ってますが
本質的にはこれだけです
・ターミナル作る
・WSサーバーへ接続
・キー入力をWSへ転送
・WS受信をターミナルへ表
示
2. PTYの作成と、子プロセス(Shell)の紐づけ WebSocketサーバー(となるプロセス)が ①PTY作って(openpty())masterとslaveの ファイルディスクリプタを取得 ②子プロセスを作成して(fork()) (ファイルディスクリプタを継承) ③継承したファイルを標準入出力と 紐づけて(dup2()) ④子プロセスイメージをbashに置き換え(exec()) 上記を行うことで、親のmasterと、子の slave(標準入出力)のin/outがそれぞれつ ながり、会話ができるようになります
2. PTYの作成と、子プロセス(Shell)の紐づけ int spawn_shell (int *master_fd , int *slave_fd ) { // pty を作成 openpty (master_fd , slave_fd , NULL, NULL, NULL); ・PTYを作成 ・子プロセスを生成 pid_t pid = fork(); // 子プロセスを生成 if (pid == 0) { setsid(); // 子プロセスをセッションリーダーにする (親プロセスから切り離す ) ioctl(*slave_fd , TIOCSCTTY , 0); // 制御端末を slave_fd(=/dev/pts/N) に設 定 // slave 側を標準入出力に接続 dup2(*slave_fd , STDIN_FILENO ); dup2(*slave_fd , STDOUT_FILENO ); dup2(*slave_fd , STDERR_FILENO ); ・slaveを標準入出力として 利用できるように接続 // 不要なファイルディスクリプタを閉じる close(*master_fd ); close(*slave_fd ); // 現在のプロセスを bashに置き換える (オープン済みファイルディスクリプタは引き継 ぐ) const char *shell = "/bin/bash" ; char *args[] = {( char *)shell, NULL}; execvp(shell, args); } // 親プロセス close(*slave_fd ); // 親はslaveを閉じる (不要なため ) return pid; } ・プロセスを起動 ※PTY経由で親プロセスと キー入力、ディスプレイ出力 が可能になります
3. WebSocketサーバー ・接続受信時に、PTYとシェルを生成してから通信を開始 ・以後はデータのパイプ役として互いのデータを転送する
3. WebSocketサーバー
int main(int argc, char **argv) {
// WebSocket サーバーの作成
WebSocketServer server(8000);
server.setOnConnectionCallback((ws) => { // 接続コールバックの設定
// 子プロセスを生成してシェルを起動
spawn_shell(&master_fd, &slave_fd);
// リーダースレッド: PTY master(bashの標準出力)を読み取り、WebSocketに転送
thread reader_thread = thread(() => {
vector<char> buf(4096);
// PTY master からの読み取りループ
while (running) {
ssize_t r = read(master_fd, buf.data(), 4096);
// WebSocket 経由で画面(xterm.js)へ送信
string out(buf.data(), (size_t)r);
ws->sendBinary(out);
}
}
}
//xterm.jsからのメッセージを PTY masterに書き込むコールバック
ws->setOnMessageCallback((msg) => {
// テキストメッセージ
const string &s = msg->str;
// 通常の入力データとして PTY master に書き込む
write(master_fd, s.data(), s.size());
});
}
長いので疑似コードです
・接続を受け付けたらPTYと子プ
ロセスを生成
・シェルからのデータ受信用ス
レッドを生成し、無限ループで受
信して画面へ転送
・画面からの入力を受信してシェ
ルへ転送
デモ/結果イメージ 時間があれば実際に ・サーバーを起動 ・ブラウザでターミナルを表 示 ・テトリス.jsを実行 してみます
まとめ ● ● ● ● ● ● ターミナルとシェルは個別に存在するプロセス PTYがプロセス間のデータ入出力の仲介をしている xterm.jsを使えば、ブラウザにも簡単に組み込める(VSCodeも使ってる) Windowsでしか低レベルプログラミングをしたことが無かったので、 fork()やexec()の仕組みに驚いた 十数年ぶりにC++に触れたら、新しい構文や機能が増えてびっくりした 今日を持って、ターミナル(芸人)から卒業します! ターミナルのことは嫌いにならないでください! (来年は何か別のことをやりたい) ご清聴ありがとうございました