[インデックス 10825] ファイルの概要
このコミットは、Go言語の実験的なSSHパッケージ (exp/ssh
) におけるセッションの標準入出力(Stdin/Stdout/Stderr)パイプ処理を簡素化するものです。具体的には、StdinPipe
, StdoutPipe
, StderrPipe
メソッドが、io.Copy
や io.Pipe
を介した間接的なデータ転送を避け、基盤となるSSHチャネル (session.clientChan
) から直接リーダー/ライターを返すように変更されています。また、StdoutPipe
と StderrPipe
の戻り値の型が io.ReadCloser
から io.Reader
に変更され、SSHプロトコルの特性に合わせたより正確なインターフェースが提供されています。
コミット
commit 52c8107a3c68245bccc836a0003fea1dcead450a
Author: Dave Cheney <dave@cheney.net>
Date: Thu Dec 15 16:50:41 2011 -0500
exp/ssh: simplify Stdin/out/errPipe methods
If a Pipe method is called, return the underlying
reader/writer from session.clientChan, bypassing the
io.Copy and io.Pipe harness.
StdoutPipe and StderrPipe now return an io.Reader not
an io.ReadCloser as SSH cannot signal the close of the
local reader to the remote process.
R=rsc, agl, gustav.paul, cw
CC=golang-dev
https://golang.org/cl/5493047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/52c8107a3c68245bccc836a0003fea1dcead450a
元コミット内容
exp/ssh: simplify Stdin/out/errPipe methods
If a Pipe method is called, return the underlying
reader/writer from session.clientChan, bypassing the
io.Copy and io.Pipe harness.
StdoutPipe and StderrPipe now return an io.Reader not
an io.ReadCloser as SSH cannot signal the close of the
local reader to the remote process.
R=rsc, agl, gustav.paul, cw
CC=golang-dev
https://golang.org/cl/5493047
変更の背景
このコミットの背景には、Go言語の実験的なSSHライブラリ (exp/ssh
) におけるセッションの標準入出力パイプ処理の効率化と、インターフェースの正確性の向上が挙げられます。
以前の実装では、StdinPipe
, StdoutPipe
, StderrPipe
メソッドが呼び出されると、io.Pipe()
を使用して新しいパイプを作成し、そのパイプの片側をセッションの Stdin
, Stdout
, Stderr
フィールドに接続し、もう片側を呼び出し元に返していました。そして、セッションの開始時に io.Copy
を用いて、これらのパイプと実際のSSHチャネル (session.clientChan
) の間でデータのコピーを行っていました。
このアプローチにはいくつかの問題がありました。
- 冗長なデータコピー:
io.Pipe
とio.Copy
を使用することで、データがSSHチャネルから一度パイプにコピーされ、さらにそこからユーザーが提供するio.Writer
やio.Reader
にコピーされるという、二重のコピーが発生していました。これはパフォーマンスのオーバーヘッドにつながります。 - リソース管理の複雑さ:
io.Pipe
で作成されたパイプは、セッション終了時に明示的にクローズする必要があり、Session
構造体内にcloseAfterWait
のようなフィールドを設けて管理する必要がありました。 io.ReadCloser
の不適切性:StdoutPipe
とStderrPipe
がio.ReadCloser
を返していましたが、SSHプロトコルの性質上、ローカルのリーダーがクローズされたことをリモートプロセスに通知するメカニズムがありません。したがって、Close()
メソッドが提供されても、それが期待通りのセマンティクスを持たない可能性があり、誤解を招くインターフェースとなっていました。
これらの問題を解決し、より直接的で効率的、かつ正確なSSHセッションのI/Oパイプ機能を提供するために、今回の変更が導入されました。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識が必要です。
-
SSHプロトコルとチャネル:
- SSH (Secure Shell) は、ネットワークを介して安全にコンピュータを操作するためのプロトコルです。
- SSHセッション内では、複数の「チャネル」を開くことができます。各チャネルは独立した論理的な通信路であり、通常、シェルセッション、ポートフォワーディング、X11転送などに使用されます。
- シェルセッションチャネルは、標準入力 (stdin)、標準出力 (stdout)、標準エラー出力 (stderr) のストリームをサポートします。これらはリモートプロセスとの間でデータを送受信するために使用されます。
-
Go言語の
io
パッケージ:- Go言語の
io
パッケージは、I/Oプリミティブ(入出力の基本的な操作)を定義するインターフェース群を提供します。 io.Reader
: データを読み込むためのインターフェース。Read([]byte) (n int, err error)
メソッドを持ちます。io.Writer
: データを書き込むためのインターフェース。Write([]byte) (n int, err error)
メソッドを持ちます。io.Closer
: リソースをクローズするためのインターフェース。Close() error
メソッドを持ちます。io.ReadCloser
:io.Reader
とio.Closer
の両方を満たすインターフェース。io.WriteCloser
:io.Writer
とio.Closer
の両方を満たすインターフェース。io.Pipe()
:io.PipeReader
とio.PipeWriter
のペアを返します。io.PipeWriter
に書き込まれたデータは、対応するio.PipeReader
から読み込むことができます。これは、異なるゴルーチン間でストリームデータを転送する際によく使用されます。io.Copy(dst Writer, src Reader) (written int64, err error)
:src
からdst
へデータをコピーするヘルパー関数です。
- Go言語の
-
Go言語の
exp/ssh
パッケージ:- Go言語の標準ライブラリには、SSHクライアントとサーバーを実装するための
golang.org/x/crypto/ssh
パッケージがありますが、このコミットが対象としているのは、その前身または実験的なバージョンであるexp/ssh
です。基本的な概念は共通しています。 Session
構造体: SSHクライアントがリモートサーバー上でコマンドを実行したり、シェルを起動したりするためのセッションを表します。このセッションを通じて、標準入出力が扱われます。clientChan
構造体:Session
の内部で、実際のSSHチャネルとの通信を管理する役割を担います。このclientChan
が、SSHプロトコルレベルでのstdin
,stdout
,stderr
のリーダー/ライターを保持しています。
- Go言語の標準ライブラリには、SSHクライアントとサーバーを実装するための
これらの知識があることで、コミットがなぜ、どのように変更されたのかを深く理解することができます。
技術的詳細
このコミットの技術的詳細は、src/pkg/exp/ssh/session.go
ファイル内の Session
構造体とその関連メソッドの変更に集約されます。
-
Session
構造体の変更:- 削除:
closeAfterWait []io.Closer
フィールドが削除されました。これは、io.Pipe
を使用しないことで、セッション終了時に明示的にクローズする必要があるパイプの管理が不要になったためです。 - 追加:
stdinpipe, stdoutpipe, stderrpipe bool
の3つのブーリアンフィールドが追加されました。これらのフラグは、それぞれStdinPipe()
,StdoutPipe()
,StderrPipe()
メソッドが呼び出されたかどうかを示すために使用されます。これにより、セッションのI/O設定がパイプ経由で行われる場合に、従来のio.Copy
ベースのI/O処理をスキップする制御が可能になります。
- 削除:
-
start()
メソッドの変更:type F func(*Session) error
からtype F func(*Session)
へと、関数シグネチャが変更されました。これは、stdin()
,stdout()
,stderr()
メソッドがエラーを返さなくなったためです。setupFd(s)
の呼び出しにおけるエラーハンドリング (if err := setupFd(s); err != nil { return err }
) が削除されました。これは、stdin()
,stdout()
,stderr()
がエラーを返さなくなったことと、パイプが直接clientChan
に接続されることで、これらの初期設定段階でのエラーが(少なくともこのレベルでは)発生しなくなったためです。
-
Wait()
メソッドの変更:for _, fd := range s.closeAfterWait { fd.Close() }
のループが削除されました。これはcloseAfterWait
フィールドが削除されたことと直接関連しており、不要になったパイプのクローズ処理が取り除かれました。
-
stdin()
,stdout()
,stderr()
メソッドの変更:- これらのメソッドは、セッションの標準入出力が設定されていない場合にデフォルトの
io.Reader
/io.Writer
を設定し、io.Copy
を使用してclientChan
との間でデータを転送する役割を担っていました。 - 変更後、各メソッドの冒頭に
if s.stdinpipe { return }
(またはstdoutpipe
,stderrpipe
) というガード句が追加されました。これにより、もしStdinPipe()
などのメソッドが既に呼び出されていて、直接パイプが設定されている場合は、これらのデフォルトのio.Copy
ベースの処理がスキップされるようになりました。 - 戻り値の型が
error
からvoid
(何も返さない) に変更されました。これは、上記のガード句により、エラーを返すような状況がなくなったためです。
- これらのメソッドは、セッションの標準入出力が設定されていない場合にデフォルトの
-
StdinPipe()
,StdoutPipe()
,StderrPipe()
メソッドの変更:StdinPipe()
:- 以前は
io.Pipe()
を呼び出してpr, pw := io.Pipe()
を作成し、s.Stdin = pr
と設定し、pw
を返していました。 - 変更後、
s.stdinpipe = true
を設定し、直接s.clientChan.stdin
を返します。s.clientChan.stdin
はio.WriteCloser
であり、これはSSHチャネルの標準入力に直接書き込むためのインターフェースです。これにより、中間的なio.Pipe
とio.Copy
の層が完全に削除されました。
- 以前は
StdoutPipe()
とStderrPipe()
:- 以前は
io.Pipe()
を呼び出してpr, pw := io.Pipe()
を作成し、s.Stdout = pw
(またはs.Stderr = pw
) と設定し、pr
をio.ReadCloser
として返していました。 - 変更後、
s.stdoutpipe = true
(またはs.stderrpipe = true
) を設定し、直接s.clientChan.stdout
(またはs.clientChan.stderr
) を返します。 - 重要な変更点: 戻り値の型が
io.ReadCloser
からio.Reader
に変更されました。これはコミットメッセージにも明記されている通り、「SSHはローカルのリーダーのクローズをリモートプロセスに通知できないため」です。io.ReadCloser
のClose()
メソッドは、通常、リソースの解放やストリームの終了を意味しますが、SSHチャネルの出力ストリームにおいては、ローカルでリーダーをクローズしても、それがリモートプロセスに伝わるわけではないため、Close()
メソッドのセマンティクスが曖昧でした。io.Reader
を返すことで、この誤解を避けることができます。
- 以前は
これらの変更により、SSHセッションのI/Oパイプ処理はより直接的になり、不要なデータコピーが削減され、リソース管理が簡素化され、インターフェースの正確性が向上しました。
コアとなるコードの変更箇所
変更は src/pkg/exp/ssh/session.go
ファイルに集中しています。
-
Session
構造体の定義変更:--- a/src/pkg/exp/ssh/session.go +++ b/src/pkg/exp/ssh/session.go @@ -68,10 +68,12 @@ type Session struct { *clientChan // the channel backing this session - started bool // true once Start, Run or Shell is invoked. - closeAfterWait []io.Closer - copyFuncs []func() error - errch chan error // one send per copyFunc + started bool // true once Start, Run or Shell is invoked. + copyFuncs []func() error + errch chan error // one send per copyFunc + + // true if pipe method is active + stdinpipe, stdoutpipe, stderrpipe bool }
-
start()
メソッドのシグネチャと呼び出しの変更:--- a/src/pkg/exp/ssh/session.go +++ b/src/pkg/exp/ssh/session.go @@ -237,11 +239,9 @@ func (s *Session) waitForResponse() error { func (s *Session) start() error { s.started = true - type F func(*Session) error + type F func(*Session) for _, setupFd := range []F{(*Session).stdin, (*Session).stdout, (*Session).stderr} { -\t\tif err := setupFd(s); err != nil {\n-\t\t\treturn err\n-\t\t}\n+\t\tsetupFd(s) } s.errch = make(chan error, len(s.copyFuncs))\
-
Wait()
メソッドからのcloseAfterWait
処理の削除:--- a/src/pkg/exp/ssh/session.go +++ b/src/pkg/exp/ssh/session.go @@ -274,9 +274,6 @@ func (s *Session) Wait() error { \tcopyError = err } } -\tfor _, fd := s.closeAfterWait {\n-\t\tfd.Close()\n-\t}\n if waitErr != nil { return waitErr }
-
stdin()
,stdout()
,stderr()
メソッドの変更:--- a/src/pkg/exp/ssh/session.go +++ b/src/pkg/exp/ssh/session.go @@ -341,7 +338,10 @@ func (s *Session) wait() error { return &ExitError{wm} } -func (s *Session) stdin() error { +func (s *Session) stdin() { +\tif s.stdinpipe {\n+\t\treturn\n+\t}\n if s.Stdin == nil { s.Stdin = new(bytes.Buffer) } @@ -352,10 +352,12 @@ func (s *Session) stdin() error { } return err })\n-\treturn nil\n }\n \n-func (s *Session) stdout() error { +func (s *Session) stdout() { +\tif s.stdoutpipe {\n+\t\treturn\n+\t}\n if s.Stdout == nil { s.Stdout = ioutil.Discard } @@ -363,10 +365,12 @@ func (s *Session) stdout() error { _, err := io.Copy(s.Stdout, s.clientChan.stdout) return err })\n-\treturn nil\n }\n \n-func (s *Session) stderr() error { +func (s *Session) stderr() { +\tif s.stderrpipe {\n+\t\treturn\n+\t}\n if s.Stderr == nil { s.Stderr = ioutil.Discard } @@ -374,7 +378,6 @@ func (s *Session) stderr() error { _, err := io.Copy(s.Stderr, s.clientChan.stderr) return err })\n-\treturn nil\n }\n \n```
-
StdinPipe()
,StdoutPipe()
,StderrPipe()
メソッドの変更:--- a/src/pkg/exp/ssh/session.go +++ b/src/pkg/exp/ssh/session.go @@ -386,10 +389,8 @@ func (s *Session) StdinPipe() (io.WriteCloser, error) { if s.started { return nil, errors.New("ssh: StdinPipe after process started") } -\tpr, pw := io.Pipe()\n-\ts.Stdin = pr\n-\ts.closeAfterWait = append(s.closeAfterWait, pr)\n-\treturn pw, nil\n+\ts.stdinpipe = true\n+\treturn s.clientChan.stdin, nil } // StdoutPipe returns a pipe that will be connected to the @@ -398,17 +399,15 @@ func (s *Session) StdinPipe() (io.WriteCloser, error) { // stdout and stderr streams. If the StdoutPipe reader is // not serviced fast enought it may eventually cause the // remote command to block.\n-func (s *Session) StdoutPipe() (io.ReadCloser, error) { +func (s *Session) StdoutPipe() (io.Reader, error) { if s.Stdout != nil { return nil, errors.New("ssh: Stdout already set") } if s.started { return nil, errors.New("ssh: StdoutPipe after process started") } -\tpr, pw := io.Pipe()\n-\ts.Stdout = pw\n-\ts.closeAfterWait = append(s.closeAfterWait, pw)\n-\treturn pr, nil\n+\ts.stdoutpipe = true\n+\treturn s.clientChan.stdout, nil } // StderrPipe returns a pipe that will be connected to the @@ -417,17 +416,15 @@ func (s *Session) StdoutPipe() (io.ReadCloser, error) { // stdout and stderr streams. If the StderrPipe reader is // not serviced fast enought it may eventually cause the // remote command to block.\n-func (s *Session) StderrPipe() (io.ReadCloser, error) { +func (s *Session) StderrPipe() (io.Reader, error) { if s.Stderr != nil { return nil, errors.New("ssh: Stderr already set") } if s.started { return nil, errors.New("ssh: StderrPipe after process started") } -\tpr, pw := io.Pipe()\n-\ts.Stderr = pw\n-\ts.closeAfterWait = append(s.closeAfterWait, pw)\n-\treturn pr, nil\n+\ts.stderrpipe = true\n+\treturn s.clientChan.stderr, nil } // TODO(dfc) add Output and CombinedOutput helpers
コアとなるコードの解説
このコミットの核心は、Session
構造体のI/Oパイプ関連のロジックを、io.Pipe
と io.Copy
を用いた間接的な方法から、基盤となるSSHチャネル (clientChan
) のI/Oインターフェースを直接公開する方法へと変更した点にあります。
-
Session
構造体の変更:closeAfterWait []io.Closer
の削除は、io.Pipe
を使用しなくなったことによる直接的な結果です。これにより、セッション終了時のパイプのクローズ管理が不要になり、コードが簡素化されます。stdinpipe, stdoutpipe, stderrpipe bool
の追加は、新しい制御フローの鍵となります。これらのフラグは、ユーザーがStdinPipe()
,StdoutPipe()
,StderrPipe()
メソッドを呼び出して直接チャネルのI/Oインターフェースを取得したかどうかを追跡します。
-
stdin()
,stdout()
,stderr()
メソッドの変更:- これらのメソッドは、セッションが開始される際に呼び出され、デフォルトのI/O設定(例えば、
s.Stdin
がnil
の場合はbytes.Buffer
を設定するなど)と、clientChan
からのデータコピー (io.Copy
) を担当していました。 - 新しい実装では、各メソッドの冒頭に
if s.stdinpipe { return }
のようなチェックが追加されました。これは、もしユーザーが既にStdinPipe()
を呼び出してclientChan.stdin
を直接取得している場合、このデフォルトのio.Copy
ベースのI/O設定は不要になるため、処理をスキップするという意味です。これにより、二重のデータフローや競合を防ぎます。 - これらのメソッドがエラーを返さなくなったのは、パイプが直接接続されることで、この段階でのエラー発生の可能性が低くなったため、またはエラーハンドリングがより上位の層に移譲されたためと考えられます。
- これらのメソッドは、セッションが開始される際に呼び出され、デフォルトのI/O設定(例えば、
-
StdinPipe()
,StdoutPipe()
,StderrPipe()
メソッドの変更:StdinPipe()
: 以前はio.Pipe()
を使って新しいパイプを作成し、その書き込み側をユーザーに、読み込み側をセッションのStdin
に接続していました。これにより、ユーザーがパイプに書き込んだデータがio.Copy
を介してSSHチャネルに転送される仕組みでした。 変更後は、s.stdinpipe = true
を設定し、直接s.clientChan.stdin
を返します。s.clientChan.stdin
はSSHチャネルの標準入力に直接対応するio.WriteCloser
です。これにより、ユーザーはSSHチャネルに直接データを書き込むことができ、中間的なパイプとコピーのオーバーヘッドがなくなります。StdoutPipe()
とStderrPipe()
: 以前は同様にio.Pipe()
を使ってパイプを作成し、その読み込み側をユーザーに、書き込み側をセッションのStdout
/Stderr
に接続していました。ユーザーがパイプから読み込んだデータは、io.Copy
を介してSSHチャネルから転送されていました。 変更後は、s.stdoutpipe = true
(またはs.stderrpipe = true
) を設定し、直接s.clientChan.stdout
(またはs.clientChan.stderr
) を返します。これらはSSHチャネルの標準出力/標準エラー出力に直接対応するio.Reader
です。 最も重要な変更点は、戻り値の型がio.ReadCloser
からio.Reader
になったことです。これは、SSHプロトコルでは、ローカルで出力ストリームのリーダーをクローズしたことをリモートプロセスに通知する標準的なメカニズムがないためです。io.ReadCloser
のClose()
メソッドは、リソースの解放やストリームの終了を期待させますが、SSHの文脈ではそのセマンティクスが保証されません。io.Reader
を返すことで、この誤解を避け、より正確なインターフェースを提供します。ユーザーは引き続きリーダーを読み込むことができますが、Close()
を呼び出すことによるリモートへの影響は期待できません。
これらの変更により、exp/ssh
パッケージはより効率的で、SSHプロトコルの特性に合致したI/Oパイプ機能を提供するようになりました。
関連リンク
- Go言語の
io
パッケージのドキュメント: https://pkg.go.dev/io - Go言語の
golang.org/x/crypto/ssh
パッケージのドキュメント (現在のSSHパッケージ): https://pkg.go.dev/golang.org/x/crypto/ssh - RFC 4254 (SSH Connection Protocol): 特にセクション 6.2 "Channel Open" および 6.3 "Channel Data" が関連します。
参考にした情報源リンク
- Go言語の公式ドキュメント
- SSHプロトコルに関する一般的な情報源 (RFC 4254など)
- Go言語の
io
パッケージのソースコードとドキュメンテーション golang.org/x/crypto/ssh
パッケージのソースコード (現在の実装との比較のため)- コミットメッセージと差分情報 (diff)
- Go言語のコードレビューシステム (Gerrit) の変更リスト (CL): https://golang.org/cl/5493047 (コミットメッセージに記載)