コンテンツにスキップ

入力リプレイの記録と再生

リプレイ機能が記録・保存するのは、端末の画面出力そのものではなく、人間がキーボードから行った入力操作です。 記録した操作を再生する際は、指定のコマンドを PTY(擬似端末)経由で新たに起動し、予定された時刻に合わせてキー入力を送り込むことで、その結果生じる画面出力を改めてキャプチャします。 画面の出力結果をそのまま再生する asciicast とは異なり、コマンド自体の実行を完全に再現できる点が特徴です。

プラットフォームに依存しないキー意味の保存

Section titled “プラットフォームに依存しないキー意味の保存”

キー入力の記録データ(リプレイファイル)には、Windows のコンソールイベント構造体や Unix の端末制御フラグといった OS 固有の内部表現を直接記録しません。 OS やキーボード配列の差異を超えて再生できるよう、キーの意味を抽象化した共通モデルで保存します。

一般的なキーは ArrowUp、Home、Delete、F1〜F12 などの正規化された名称で記録し、Shift、Alt、Ctrl、Meta(Windows キーや Command キー)は独立した修飾キーフラグとして保持します。 入力された印字可能文字は、スキャンコードではなく Unicode 文字列として保存し、絵文字などの サロゲートペア(2 つの 16 ビット値で 1 つの文字を表す UTF-16 表現)も 1 つの論理キーとして扱います。 通常のキーモデルに収まらない特殊なバイト列に対しては raw イベントを用意し、バイトデータを直接格納できるようにしています。

時刻表現のタイムライン正規化

Section titled “時刻表現のタイムライン正規化”

リプレイファイル内の各イベントは、録画開始からの絶対秒数を表す time、または直前のイベントからの差分秒数を表す tick のいずれかを持ちます。

ファイルの読み込み時には、time が指定されているイベントはその値をそのまま採用し、tick のみのイベントは直前の累積時刻に加算することで、すべてのイベントを単一の絶対時刻タイムラインへ正規化します。 また、メタデータには全体の総所要時間(duration)を記録します。 最後のキー入力を送信し終えた後も子プロセスが終了せずプロンプト待ちを続けるケースに備え、総所要時間に 1 秒を加えた時点を全体のタイムアウト上限として安全にプロセスを終了させます。

入力ストリームの分割耐性とエスケープ判定

Section titled “入力ストリームの分割耐性とエスケープ判定”

対話実行の最中に入力をリアルタイム記録する際、ホスト側から受け取ったバイト列は即座に子プロセスの PTY へ書き込みます。 その一方で、記録モデルへ変換する解析パスではステートフルな UTF-8 デコーダーを用います。

キーボードから送信されるエスケープシーケンスは、ストリームの読み取り境界によって途中で切断されて届くことがあります(CSI や OSC の先頭の ESC だけが届き、残りのパラメータが次回の read に届くなど)。 このような場合、未完了のエスケープシーケンスを内部バッファへ保持し、次回届いたバイト列と結合して解析します。 読み取り境界で不完全なエスケープシーケンスを誤った単独のキー(ESC キー単体など)として誤認しないためです。

キーイベントから VT バイト列への復元

Section titled “キーイベントから VT バイト列への復元”

再生処理を実行する際は、保存されたキー名と修飾キーの組み合わせを、端末アプリケーションが解釈可能な VT バイト列(エスケープシーケンス)へと逆変換します。

Enter や Tab、矢印キー、ファンクションキーは標準的な VT シーケンスを生成します。 Ctrl キーとアルファベットの組み合わせは対応する制御文字(0x01〜0x1A)へ変換し、Alt キーの組み合わせには先頭に ESC プレフィックスを付与し、印字可能文字は UTF-8 バイト列へエンコードします。 生成されたバイト列は、対話実行時と全く同一の PTY 書き込みストリームへ流し込まれるため、プログラム側からは人間がキーボードを叩いているのと全く区別がつきません。

タイミング制御と遅延の自動回復

Section titled “タイミング制御と遅延の自動回復”

リプレイストリームは、各イベントの送信予定時刻に到達するまで非同期で待機してからバイト列を PTY へ書き込みます。

ただし、実行マシンの負荷やコマンド側の処理待ちによって処理時刻が予定よりも遅延している場合は、追加の待機時間を挟まずに即座に次のイベントを送信します。 イベントの実行順序を厳格に維持したまま、可能な限り最短で本来のタイムラインに追いつくためです。 また、1 つのキーイベントから生成されたバイト列が読み取り側のバッファサイズより大きい場合でも、ストリーム内のオフセットを維持して分割送信し、入力データが欠損するのを防ぎます。