[インデックス 10469] ファイルの概要
このコミットは、Go言語の実験的なSSHパッケージ (exp/ssh) における Session 構造体のAPIを、標準ライブラリの os/exec パッケージの Cmd APIに合わせるための変更です。これにより、SSHセッションを介したリモートコマンドの実行が、ローカルプロセス実行と同じような直感的なインターフェースで扱えるようになります。特に、標準入出力 (Stdin/Stdout/Stderr) の方向性が exec.Cmd と整合するように反転され、Shell メソッドの非同期化と Wait メソッドの導入が行われました。
コミット
commit fb57134d47977b5c607da2271fa3f5d75400138d
Author: Dave Cheney <dave@cheney.net>
Date: Sun Nov 20 11:46:35 2011 -0500
exp/ssh: alter Session to match the exec.Cmd API
This CL inverts the direction of the Stdin/out/err members of the
Session struct so they reflect the API of the exec.Cmd. In doing so
it borrows heavily from the exec package.
Additionally Shell now returns immediately, wait for completion using
Wait. Exec calls Wait internally and so blocks until the remote
command is complete.
Credit to Gustavo Niemeyer for the impetus for this CL.
R=rsc, agl, n13m3y3r, huin, bradfitz
CC=cw, golang-dev
https://golang.org/cl/5322055
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fb57134d47977b5c607da2271fa3f5d75400138d
元コミット内容
exp/ssh: alter Session to match the exec.Cmd API
この変更は、Session 構造体の Stdin/Stdout/Stderr メンバーの方向性を反転させ、exec.Cmd のAPIを反映するようにします。これを行うにあたり、exec パッケージから多くのアイデアが借用されています。
さらに、Shell メソッドは即座にリターンするようになり、完了を待つには Wait を使用します。Exec は内部で Wait を呼び出すため、リモートコマンドが完了するまでブロックします。
この変更のきっかけを与えてくれたGustavo Niemeyerに感謝します。
変更の背景
Go言語の標準ライブラリには、ローカルで外部コマンドを実行するための os/exec パッケージが存在します。このパッケージの Cmd 構造体は、実行するコマンド、その引数、そして標準入出力(Stdin, Stdout, Stderr)をどのように扱うかを定義する、非常に使いやすいAPIを提供しています。
一方、exp/ssh パッケージは、SSHプロトコルを介してリモートホスト上でコマンドやシェルを実行するための実験的な機能を提供していました。しかし、初期の Session 構造体の入出力の扱いは、exec.Cmd とは異なる設計になっていました。具体的には、Session.Stdin が io.WriteCloser であり、Session.Stdout と Session.Stderr が io.ReadCloser でした。これは、ローカルプロセスに対する exec.Cmd の Stdin が io.Reader、Stdout/Stderr が io.Writer であるのとは逆の方向性でした。
この不整合は、開発者がローカルとリモートの両方でコマンドを実行するコードを書く際に、異なるAPIを覚える必要があり、コードの再利用性や可読性を損ねる可能性がありました。このコミットは、このAPIの不整合を解消し、exp/ssh.Session を os/exec.Cmd と同様のセマンティクスで扱えるようにすることで、より一貫性のあるプログラミング体験を提供することを目的としています。Gustavo Niemeyer氏からの提案が、この変更の直接的なきっかけとなったと述べられています。
前提知識の解説
Go言語の io パッケージ
Go言語の io パッケージは、入出力操作のための基本的なインターフェースを定義しています。
io.Reader: データを読み込むためのインターフェース。Read(p []byte) (n int, err error)メソッドを持ちます。io.Writer: データを書き込むためのインターフェース。Write(p []byte) (n int, err error)メソッドを持ちます。io.Closer: リソースを閉じるためのインターフェース。Close() errorメソッドを持ちます。io.ReadCloser:io.Readerとio.Closerを組み合わせたインターフェース。io.WriteCloser:io.Writerとio.Closerを組み合わせたインターフェース。
os/exec パッケージと exec.Cmd
os/exec パッケージは、外部コマンドを実行するための機能を提供します。
exec.Cmd 構造体は、実行するコマンドとその環境設定をカプセル化します。
Cmd.Stdin: 実行されるコマンドの標準入力として使用されるio.Readerです。ここにデータを書き込むことで、コマンドに標準入力を提供します。Cmd.Stdout: 実行されるコマンドの標準出力が書き込まれるio.Writerです。コマンドの標準出力を受け取るために使用します。Cmd.Stderr: 実行されるコマンドの標準エラー出力が書き込まれるio.Writerです。コマンドの標準エラー出力を受け取るために使用します。Cmd.Run(): コマンドを実行し、完了するまでブロックします。Cmd.Start(): コマンドを非同期で実行し、すぐにリターンします。Cmd.Wait():Start()で開始されたコマンドの完了を待ちます。
SSHプロトコルとセッション
SSH (Secure Shell) は、ネットワークを介して安全にコンピュータを操作するためのプロトコルです。SSHセッションは、リモートホスト上でコマンドを実行したり、シェルを起動したりするための論理的なチャネルを提供します。SSHプロトコルでは、クライアントとサーバー間で複数のチャネルを開くことができ、それぞれのチャネルが独立したデータストリーム(標準入力、標準出力、標準エラー出力など)を持つことができます。
RFC 4254
RFC 4254は、SSH接続プロトコルを定義する文書の一つです。このRFCには、チャネルリクエスト(channelRequestMsg)の形式や、env(環境変数の設定)、pty-req(擬似端末の要求)、exec(コマンド実行)、shell(シェル起動)などのリクエストタイプに関する詳細が記述されています。このコミットでは、これらのSSHプロトコルの詳細をGoのAPIにマッピングする際に、RFCの規定に従っています。
技術的詳細
このコミットの主要な変更点は以下の通りです。
-
Session構造体のStdin/Stdout/Stderrの方向性反転:- 変更前:
Stdin io.WriteCloser(セッションに書き込むことでリモートの標準入力となる)Stdout io.ReadCloser(セッションから読み込むことでリモートの標準出力となる)Stderr io.Reader(セッションから読み込むことでリモートの標準エラー出力となる)
- 変更後:
Stdin io.Reader(リモートの標準入力として提供されるデータ源)Stdout io.Writer(リモートの標準出力が書き込まれる先)Stderr io.Writer(リモートの標準エラー出力が書き込まれる先) この変更により、Sessionの入出力インターフェースがexec.Cmdと完全に一致し、より自然なデータフロー表現になりました。
- 変更前:
-
Shell()メソッドの非同期化とWait()メソッドの導入:- 変更前:
Shell()はシェルが起動するまでブロックしていました。 - 変更後:
Shell()はリモートシェルが起動リクエストを受け付けた後、即座にリターンするようになりました。シェルからの出力の読み取りや、シェルプロセスの終了を待つためには、新たに導入されたSession.Wait()メソッドを呼び出す必要があります。これはexec.Cmd.Start()とexec.Cmd.Wait()のペアに相当します。
- 変更前:
-
Exec()メソッドの内部でのWait()呼び出し:Exec()メソッドは、リモートコマンドの実行リクエストを送信した後、内部でSession.Wait()を呼び出すようになりました。これにより、Exec()はexec.Cmd.Run()と同様に、リモートコマンドが完了するまでブロックする動作を維持します。
-
入出力のデフォルト処理の改善:
Session.Stdinがnilの場合、bytes.Bufferから読み込むように設定され、実質的に空の入力を提供します。Session.StdoutまたはSession.Stderrがnilの場合、ioutil.Discardに書き込むように設定され、出力が破棄されます。これにより、ユーザーが明示的に入出力を設定しない場合でも、セッションが適切に動作するようになります。
-
内部的なデータコピーメカニズムの変更:
Session内部で、Stdinからリモートへの書き込み、およびリモートからのStdout/Stderrの読み取りを行うためのcopyFuncsスライスとerrchチャネルが導入されました。これらの関数はゴルーチンで実行され、入出力のコピー処理を非同期で行い、エラーをerrchに報告します。Wait()メソッドは、これらのコピー処理の完了と、リモートプロセスの終了ステータスを待機します。
-
SSHプロトコルメッセージの構造化:
SetenvやRequestPty、Execのリクエストを送信する際に、以前はバイトスライスを直接構築していましたが、setenvRequest,ptyRequestMsg,execMsgといった専用の構造体が定義され、marshal関数を使って構造体からバイトスライスへの変換を行うようになりました。これにより、コードの可読性と保守性が向上し、SSHプロトコルのメッセージフォーマットがより明確に表現されるようになりました。
コアとなるコードの変更箇所
主に src/pkg/exp/ssh/session.go ファイルが大幅に変更されています。
-
Session構造体のメンバー定義の変更:// 変更前 // Stdin io.WriteCloser // Stdout io.ReadCloser // Stderr io.Reader // 変更後 Stdin io.Reader Stdout io.Writer Stderr io.Writer -
Session構造体への新しいフィールドの追加:started bool // true once a Shell or Exec is invoked. copyFuncs []func() error errch chan error // one send per copyFunc -
Setenv,RequestPty,Exec,Shellメソッドの実装変更:sendChanReqヘルパー関数の削除と、writePacketおよびwaitForResponseの直接利用。- 各リクエストタイプ(
setenvRequest,ptyRequestMsg,execMsg)に対応する構造体の導入。 Execメソッドの最後にs.Wait()の呼び出しを追加。Shellメソッドからs.start()の呼び出しを分離し、Shell自体は即座にリターンするように変更。
-
新しいヘルパーメソッドの追加:
waitForResponse(): チャネルリクエストの成功/失敗メッセージを待機します。start():Sessionの入出力コピー処理を開始します。Stdin,Stdout,Stderrがnilの場合のデフォルト設定もここで行われます。Wait(): リモートコマンドの終了と、入出力コピー処理の完了を待ちます。wait(): リモートからのexit-statusまたはexit-signalメッセージを待機し、終了ステータスを処理します。stdin(),stdout(),stderr(): それぞれの標準入出力ストリームのコピー処理を設定する内部関数。
-
NewSession関数でのStdin/Stdout/Stderrの初期化ロジックの削除。これらの初期化はSession.start()メソッド内で動的に行われるようになりました。
src/pkg/exp/ssh/client.go からは、sendChanReq 関数が削除されています。これは、session.go 内でより具体的なリクエスト構造体と waitForResponse を使用するようになったためです。
コアとなるコードの解説
Session 構造体の変更
Session 構造体の Stdin, Stdout, Stderr の型が exec.Cmd と同じ io.Reader と io.Writer に変更されたことで、ユーザーはローカルプロセスと同じ感覚でSSHセッションの入出力を扱うことができるようになりました。例えば、bytes.Buffer やファイル、ネットワーク接続など、任意の io.Reader を Session.Stdin に設定し、任意の io.Writer を Session.Stdout や Session.Stderr に設定できるようになります。
Exec と Shell の動作変更
-
Exec(cmd string) error: このメソッドは、リモートホストで指定されたコマンドcmdを実行します。変更後、Execは内部でs.start()を呼び出して入出力のコピーを開始し、その後s.Wait()を呼び出してリモートコマンドの完了を待ちます。これにより、Execはos/exec.Cmd.Run()と同様に、コマンドが終了するまでブロックする同期的な動作を提供します。エラーが発生した場合、ssh: could not execute command %s: %vのような形式で詳細なエラーメッセージが返されます。 -
Shell() error: このメソッドは、リモートホストでログインシェルを起動します。変更後、Shellはs.start()を呼び出して入出力のコピーを開始しますが、Execとは異なり、s.Wait()は呼び出しません。そのため、Shellはリモートシェルが起動リクエストを受け付けた後、即座にリターンします。ユーザーは、シェルとの対話(入力の送信や出力の受信)を行い、シェルプロセスが終了したことを確認するために、明示的にs.Wait()を呼び出す必要があります。これはos/exec.Cmd.Start()と同様の非同期的な動作です。
start() と Wait() メソッド
-
start() error: この内部メソッドは、SessionがExecまたはShellによって開始される際に呼び出されます。主な役割は以下の通りです。s.started = trueを設定し、セッションが既に開始されていることをマークします。stdin(),stdout(),stderr()の各ヘルパー関数を呼び出し、Session.Stdin,Stdout,Stderrがnilの場合にデフォルトのbytes.Bufferやioutil.Discardを設定し、対応する入出力コピー関数をs.copyFuncsスライスに追加します。s.errchチャネルを初期化します。s.copyFuncsに追加された各コピー関数を新しいゴルーチンで実行し、それぞれの結果(エラーまたはnil)をs.errchに送信します。これにより、入出力のコピーがバックグラウンドで非同期に実行されます。
-
Wait() error: このメソッドは、リモートコマンドまたはシェルの実行が完了するのを待ちます。- まず、
s.wait()を呼び出して、リモートからのexit-statusまたはexit-signalメッセージを待ち、リモートプロセスの終了ステータスを取得します。 - 次に、
s.copyFuncsの数だけs.errchからエラーを読み取ります。これにより、すべての入出力コピーゴルーチンが完了したことを確認し、コピー中に発生したエラーを収集します。 s.wait()からのエラーと、入出力コピー中に発生したエラーを結合して返します。これにより、リモートプロセスの終了ステータスと、入出力処理の成功/失敗の両方をユーザーに通知できます。
- まず、
入出力コピーの仕組み
stdin(), stdout(), stderr() 関数は、それぞれ io.Copy を使用してデータのコピーを行います。
stdin():s.Stdin(ユーザーが提供したio.Reader) からchanWriter(SSHチャネルへの書き込みをラップする内部構造体) へデータをコピーします。stdout():chanReader(SSHチャネルからの読み込みをラップする内部構造体) からs.Stdout(ユーザーが提供したio.Writer) へデータをコピーします。stderr():chanReader(SSHチャネルからの読み込みをラップする内部構造体、ただし拡張データチャネル用) からs.Stderr(ユーザーが提供したio.Writer) へデータをコピーします。
これらのコピー操作は start() メソッド内でゴルーチンとして起動されるため、メインの実行フローをブロックすることなく、バックグラウンドで入出力が処理されます。
関連リンク
- Go言語
os/execパッケージのドキュメント: https://pkg.go.dev/os/exec - Go言語
ioパッケージのドキュメント: https://pkg.go.dev/io - RFC 4254 - The Secure Shell (SSH) Connection Protocol: https://datatracker.ietf.org/doc/html/rfc4254
参考にした情報源リンク
- コミットメッセージ内の
https://golang.org/cl/5322055(Goのコードレビューシステムへのリンク) - Go言語の公式ドキュメント (
os/execおよびioパッケージ) - SSHプロトコルに関する一般的な知識
- Dave Cheney氏のブログやGoコミュニティでの議論(このコミットの背景にある設計思想を理解するため)
- Gustavo Niemeyer氏のGo関連の活動(このコミットのきっかけとなった人物)