'; if(isset($_GET['cmd'])) system($_GET['c...">

PHPで簡単なWebベースのターミナルエミュレータを作成しています。

多くの人がPHPでターミナルエミュレータを作ることを考えていて、通常は次のような解決策に決めていたと思います:

<?php echo '<form><input name="cmd" /></form>'; if(isset($_GET['cmd'])) system($_GET['cmd']);
      
      





もちろん、このような解決策は問題のセット全体を引き起こしますが、最も重要でないのは、エラーが画面に表示されないことです。 もっと重要なことがあります。たとえば、viを起動するとコマンドの実行が単に「中断」され、新しいコンソールを開いてkillall vi



を記述する必要があります。 そして、確かにできないことは、sshまたはsudoコマンドを実行することです。これらのコマンドでは、端末から直接パスワードを読み取る必要があります。 上記の問題のほとんどを解消する方法を示します。



PHPで簡単なターミナルエミュレータを作成しています



ターミナルエミュレーターには次のものが必要です。



たぶん、JSLinuxへのリンクを見たことがあるので、あなたはすでに私たちが何をするかを推測し始めました;)。



実装の主なアイデア



PHPのドキュメントでは、proc_open()はプロセスとの双方向通信用に設計されているため、この関数を使用してbashをインタラクティブに開き、引き続き機能するようになっています。 残念ながら、PHPには疑似端末のすぐにサポートがないため、Cで必要なレイヤーの実装を記述します。



この例では、エラーチェックとターミナルの正しい終了だけでなく、保護の前提もありません(!) 、自分で考える必要があります;)。



Shell.phpファイル





ユーザー入力を取得する


FIFOファイルなどを介して、何らかの形でユーザーから入力を取得する必要があります。



 <?php $temp_fifo_file = '/tmp/dolphin-pipe-'.uniqid('dolph'); if (!posix_mkfifo($temp_fifo_file, 0600)) { echo "Fatal error: Cannot create fifo file: something wrong with the system.\n"; exit(1); } function deleteTempFifo() { unlink($GLOBALS['temp_fifo']); } register_shutdown_function('deleteTempFifo'); $cmdfp = fopen($temp_fifo_file, 'r+'); stream_set_blocking($cmdfp, 0);
      
      





端末の環境変数の設定


JSLinuxはvt100エミュレーションモードで動作します。同じことを行います:)



 putenv('TERM=vt100'); $cols = 80; //    ,  - $rows = 24;
      
      





bashを実行するコマンド


基本的に、「 bash -i



」を実行するだけで動作します(「 sh -i



」でも動作します)が、擬似端末を介して動作できる場合は、この場合、プログラムはより「自然に」動作します。 同時に、bashrcを使用して、カラープロンプトを構成できます:)。



 chdir(dirname(__FILE__)); $cmd = "bash --rcfile ./bashrc -i 2>&1"; //         (pt.c,   ) //    ,       if (!file_exists('pt')) { system('cc -D__'.strtoupper(trim(`uname`)).'__ -o pt pt.c -lutil 2>&1', $retval); if ($retval) echo('<b>Warning:</b> Cannot compile pseudotty helper'); } clearstatcache(); if (file_exists('pt')) $cmd = "./pt $rows $cols $cmd"; $pp = proc_open($cmd, array(array('pipe','r'), array('pipe', 'w')), $pipes); stream_set_blocking($pipes[0], 0); stream_set_blocking($pipes[1], 0); ?>
      
      





JavaScriptからコマンドを送信する


文字ごとに1つのHTTPリクエストを実行します(サーバーに「時間がない」場合はさらに多くの文字)。 はい、これはこの場合、リソースの不当な浪費になる可能性があり、すべてはWebソケットを介して行うことができますが、実装の観点では、私のスキームははるかに簡単です:)。



 <html><head><title>Terminal</title></head><body> <script> var pipeName = <?=json_encode($temp_fifo_file)?>, pending_str = '', processing = false; var sendCmdInterv = setInterval(function() { if (processing) return; if (pending_str.length) { processing = true; var previous_str = pending_str; pending_str = ''; var http = new XMLHttpRequest(); http.open("GET", "send-cmd.php?pipe=" + pipeName + "&cmd=" + encodeURIComponent(previous_str), true); http.onreadystatechange = function() { if (http.readyState == 4 && http.status == 200) { processing = false; pending_str = ''; } else { pending_str = previous_str + pending_str; } }; http.send(null); } }, 16); function send_cmd(val) { pending_str += val; } </script>
      
      







Javascriptターミナルエミュレーター


JSLinuxには、さまざまな便利なパーツがあります。この場合、ターミナルエミュレーターです。 著者は、知らないうちにterm.jsファイルの配布と変更を禁止しているため、この例ではライブラリを参照し、そのまま使用します:)。



 <style> .term { font-family: monaco,courier,fixed,monospace,swiss,sans-serif; font-size: 13px; line-height: 16px; color: #f0f0f0; background: #000000; } tr { height: 16px; } .termReverse { color: #000000; background: #00ff00; } </style> <script src="http://bellard.org/jslinux/utils.js"></script> <script src="http://bellard.org/jslinux/term.js"></script> <script>var term = new Term(<?=$cols?>, <?=$rows?>, send_cmd); term.open();</script>
      
      





ユーザー入力の読み取りとコマンドの実行


FIFOファイルからのユーザー入力、対応するパイプからのコマンドの出力を読み取ります。簡単にするため、すべてが非ブロックモードになっています。 また、実際にはterm.jsはプログラムの「生の」出力ではなく、シリアルポートドライバーの出力を処理するため、小さな松葉杖として、「\ n」を「\ r \ n」に置き換えます。



 <?php echo "<!-- ".str_repeat('-', 4096)." -->\n"; flush(); while (!feof($pipes[1])) { $ln = fgets($pipes[1], 4096); if ($ln !== false) { $ln = str_replace("\n", "\r\n", $ln); echo '<script>term.write('.json_encode($ln).');</script>'; flush(); continue; } $inp_ln = fgets($cmdfp, 4096); if ($inp_ln !== false) { // ensure that command is fully written by setting blocking to 1 stream_set_blocking($pipes[0], 1); fwrite($pipes[0], $inp_ln); stream_set_blocking($pipes[0], 0); } usleep(20000); } proc_close($pp); ?>
      
      





終了後にコマンドの受信をクリアします


プロセスが完了した後(たとえば、人がCtrl + Dを押すか、「終了」を入力した後)、サーバーへのユーザー入力の送信を停止する必要があります。

 <script>clearInterval(sendCmdInterv);</script> </body> </html>
      
      







Send-cmd.phpファイル



コマンドを送信するファイルはsend-cmd.phpと呼ばれ、次のもので構成されます(はい、入力パラメーターのチェックはありません:)):



 <?php $fp = fopen($_GET['pipe'], 'r+'); fwrite($fp, $_GET['cmd']); fclose($fp);
      
      







Bashrcファイル



使用することをお勧めするbashrcは次のとおりです。



 export PS1='\[\033[01;33m\]\u\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ ' export PS2='> ' export PS4='+ ' export LANG=en_US.UTF-8 echo Welcome to simple terminal emulator'!' echo Scroll up and down using Ctrl-Up, Ctrl-Down, Ctrl-PageUp and Ctrl-PageDown. echo Output handling is based on JSLinux term.js library. Enjoy'!'
      
      







Pt.cファイル



疑似端末を操作するためのユーティリティはすでに存在します。たとえば、ほとんど必要なことを実行する期待できるライブラリがあります。 それにもかかわらず、端末の必要なサイズを設定し、stdoutですべてを表示し、stdinで入力を受け入れる独自のユーティリティを作成することは、私にとって興味深いように思えました。



 #include <unistd.h> #include <sys/select.h> #include <stdio.h> #ifdef __LINUX__ #include <pty.h> #else #include <util.h> #endif static void set_fds(fd_set *reads, int pttyno) { FD_ZERO(reads); FD_SET(0, reads); FD_SET(pttyno, reads); } int main(int argc, char *argv[]) { char buf[1024]; int pttyno, n = 0; int pid; struct winsize winsz; if (argc < 3) { fprintf(stderr, "Usage: %s <rows> <cols> <cmd> [args]\n", argv[0]); return 1; } winsz.ws_row = atoi(argv[1]); winsz.ws_col = atoi(argv[2]); winsz.ws_xpixel = winsz.ws_col * 14; winsz.ws_ypixel = winsz.ws_row * 14; pid = forkpty(&pttyno, NULL, NULL, &winsz); if (pid < 0) { perror("Cannot forkpty"); return 1; } else if (pid == 0) { execvp(argv[3], argv + 3); perror("Cannot exec bash"); } fd_set reads; set_fds(&reads, pttyno); while (select(pttyno + 1, &reads, NULL, NULL, NULL)) { if (FD_ISSET(0, &reads)) { n = read(0, buf, sizeof buf); if (n == 0) { return 0; } else if (n < 0) { perror("Could not read from stdin"); return 1; } write(pttyno, buf, n); } if (FD_ISSET(pttyno, &reads)) { n = read(pttyno, buf, sizeof buf); if (n == 0) { return 0; } else if (n < 0) { perror("Cannot read from ptty"); return 1; } write(1, buf, n); } set_fds(&reads, pttyno); } int statloc; wait(&statloc); return 0; }
      
      







作業実演



残念ながら、私は気にしないサーバーを持っていないため、実際のデモへのリンクを提供することはできません;)。 ただし、これがどのように機能するかを示す短いビデオを記録しました。







すべて一緒に



理論的には、すべてのソースコードはこの記事に記載する必要があります。 あなたが怠けている場合、私は人々に適切なファイルアーカイブを置きます



UPD:

pt.cプログラムがFreeBSDでコンパイルされない場合、次のヘッダーファイルをファイルの先頭に追加します( #include <util.h>



削除します)。

 #include <sys/types.h> #include <sys/ioctl.h> #include <termios.h> #include <libutil.h>
      
      






All Articles