[インデックス 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関連の活動(このコミットのきっかけとなった人物)