Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 10825] ファイルの概要

このコミットは、Go言語の実験的なSSHパッケージ (exp/ssh) におけるセッションの標準入出力(Stdin/Stdout/Stderr)パイプ処理を簡素化するものです。具体的には、StdinPipe, StdoutPipe, StderrPipe メソッドが、io.Copyio.Pipe を介した間接的なデータ転送を避け、基盤となるSSHチャネル (session.clientChan) から直接リーダー/ライターを返すように変更されています。また、StdoutPipeStderrPipe の戻り値の型が 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) の間でデータのコピーを行っていました。

このアプローチにはいくつかの問題がありました。

  1. 冗長なデータコピー: io.Pipeio.Copy を使用することで、データがSSHチャネルから一度パイプにコピーされ、さらにそこからユーザーが提供する io.Writerio.Reader にコピーされるという、二重のコピーが発生していました。これはパフォーマンスのオーバーヘッドにつながります。
  2. リソース管理の複雑さ: io.Pipe で作成されたパイプは、セッション終了時に明示的にクローズする必要があり、Session 構造体内に closeAfterWait のようなフィールドを設けて管理する必要がありました。
  3. io.ReadCloser の不適切性: StdoutPipeStderrPipeio.ReadCloser を返していましたが、SSHプロトコルの性質上、ローカルのリーダーがクローズされたことをリモートプロセスに通知するメカニズムがありません。したがって、Close() メソッドが提供されても、それが期待通りのセマンティクスを持たない可能性があり、誤解を招くインターフェースとなっていました。

これらの問題を解決し、より直接的で効率的、かつ正確なSSHセッションのI/Oパイプ機能を提供するために、今回の変更が導入されました。

前提知識の解説

このコミットを理解するためには、以下の概念について基本的な知識が必要です。

  1. SSHプロトコルとチャネル:

    • SSH (Secure Shell) は、ネットワークを介して安全にコンピュータを操作するためのプロトコルです。
    • SSHセッション内では、複数の「チャネル」を開くことができます。各チャネルは独立した論理的な通信路であり、通常、シェルセッション、ポートフォワーディング、X11転送などに使用されます。
    • シェルセッションチャネルは、標準入力 (stdin)、標準出力 (stdout)、標準エラー出力 (stderr) のストリームをサポートします。これらはリモートプロセスとの間でデータを送受信するために使用されます。
  2. 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.Readerio.Closer の両方を満たすインターフェース。
    • io.WriteCloser: io.Writerio.Closer の両方を満たすインターフェース。
    • io.Pipe(): io.PipeReaderio.PipeWriter のペアを返します。io.PipeWriter に書き込まれたデータは、対応する io.PipeReader から読み込むことができます。これは、異なるゴルーチン間でストリームデータを転送する際によく使用されます。
    • io.Copy(dst Writer, src Reader) (written int64, err error): src から dst へデータをコピーするヘルパー関数です。
  3. Go言語の exp/ssh パッケージ:

    • Go言語の標準ライブラリには、SSHクライアントとサーバーを実装するための golang.org/x/crypto/ssh パッケージがありますが、このコミットが対象としているのは、その前身または実験的なバージョンである exp/ssh です。基本的な概念は共通しています。
    • Session 構造体: SSHクライアントがリモートサーバー上でコマンドを実行したり、シェルを起動したりするためのセッションを表します。このセッションを通じて、標準入出力が扱われます。
    • clientChan 構造体: Session の内部で、実際のSSHチャネルとの通信を管理する役割を担います。この clientChan が、SSHプロトコルレベルでの stdin, stdout, stderr のリーダー/ライターを保持しています。

これらの知識があることで、コミットがなぜ、どのように変更されたのかを深く理解することができます。

技術的詳細

このコミットの技術的詳細は、src/pkg/exp/ssh/session.go ファイル内の Session 構造体とその関連メソッドの変更に集約されます。

  1. Session 構造体の変更:

    • 削除: closeAfterWait []io.Closer フィールドが削除されました。これは、io.Pipe を使用しないことで、セッション終了時に明示的にクローズする必要があるパイプの管理が不要になったためです。
    • 追加: stdinpipe, stdoutpipe, stderrpipe bool の3つのブーリアンフィールドが追加されました。これらのフラグは、それぞれ StdinPipe(), StdoutPipe(), StderrPipe() メソッドが呼び出されたかどうかを示すために使用されます。これにより、セッションのI/O設定がパイプ経由で行われる場合に、従来の io.Copy ベースのI/O処理をスキップする制御が可能になります。
  2. 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 に接続されることで、これらの初期設定段階でのエラーが(少なくともこのレベルでは)発生しなくなったためです。
  3. Wait() メソッドの変更:

    • for _, fd := range s.closeAfterWait { fd.Close() } のループが削除されました。これは closeAfterWait フィールドが削除されたことと直接関連しており、不要になったパイプのクローズ処理が取り除かれました。
  4. stdin(), stdout(), stderr() メソッドの変更:

    • これらのメソッドは、セッションの標準入出力が設定されていない場合にデフォルトの io.Reader/io.Writer を設定し、io.Copy を使用して clientChan との間でデータを転送する役割を担っていました。
    • 変更後、各メソッドの冒頭に if s.stdinpipe { return } (または stdoutpipe, stderrpipe) というガード句が追加されました。これにより、もし StdinPipe() などのメソッドが既に呼び出されていて、直接パイプが設定されている場合は、これらのデフォルトの io.Copy ベースの処理がスキップされるようになりました。
    • 戻り値の型が error から void (何も返さない) に変更されました。これは、上記のガード句により、エラーを返すような状況がなくなったためです。
  5. StdinPipe(), StdoutPipe(), StderrPipe() メソッドの変更:

    • StdinPipe():
      • 以前は io.Pipe() を呼び出して pr, pw := io.Pipe() を作成し、s.Stdin = pr と設定し、pw を返していました。
      • 変更後、s.stdinpipe = true を設定し、直接 s.clientChan.stdin を返します。s.clientChan.stdinio.WriteCloser であり、これはSSHチャネルの標準入力に直接書き込むためのインターフェースです。これにより、中間的な io.Pipeio.Copy の層が完全に削除されました。
    • StdoutPipe()StderrPipe():
      • 以前は io.Pipe() を呼び出して pr, pw := io.Pipe() を作成し、s.Stdout = pw (または s.Stderr = pw) と設定し、prio.ReadCloser として返していました。
      • 変更後、s.stdoutpipe = true (または s.stderrpipe = true) を設定し、直接 s.clientChan.stdout (または s.clientChan.stderr) を返します。
      • 重要な変更点: 戻り値の型が io.ReadCloser から io.Reader に変更されました。これはコミットメッセージにも明記されている通り、「SSHはローカルのリーダーのクローズをリモートプロセスに通知できないため」です。io.ReadCloserClose() メソッドは、通常、リソースの解放やストリームの終了を意味しますが、SSHチャネルの出力ストリームにおいては、ローカルでリーダーをクローズしても、それがリモートプロセスに伝わるわけではないため、Close() メソッドのセマンティクスが曖昧でした。io.Reader を返すことで、この誤解を避けることができます。

これらの変更により、SSHセッションのI/Oパイプ処理はより直接的になり、不要なデータコピーが削減され、リソース管理が簡素化され、インターフェースの正確性が向上しました。

コアとなるコードの変更箇所

変更は src/pkg/exp/ssh/session.go ファイルに集中しています。

  1. 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
     }
    
  2. 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))\
    
  3. 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
     	}
    
  4. 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```
    
    
  5. 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.Pipeio.Copy を用いた間接的な方法から、基盤となるSSHチャネル (clientChan) のI/Oインターフェースを直接公開する方法へと変更した点にあります。

  1. Session 構造体の変更:

    • closeAfterWait []io.Closer の削除は、io.Pipe を使用しなくなったことによる直接的な結果です。これにより、セッション終了時のパイプのクローズ管理が不要になり、コードが簡素化されます。
    • stdinpipe, stdoutpipe, stderrpipe bool の追加は、新しい制御フローの鍵となります。これらのフラグは、ユーザーが StdinPipe(), StdoutPipe(), StderrPipe() メソッドを呼び出して直接チャネルのI/Oインターフェースを取得したかどうかを追跡します。
  2. stdin(), stdout(), stderr() メソッドの変更:

    • これらのメソッドは、セッションが開始される際に呼び出され、デフォルトのI/O設定(例えば、s.Stdinnil の場合は bytes.Buffer を設定するなど)と、clientChan からのデータコピー (io.Copy) を担当していました。
    • 新しい実装では、各メソッドの冒頭に if s.stdinpipe { return } のようなチェックが追加されました。これは、もしユーザーが既に StdinPipe() を呼び出して clientChan.stdin を直接取得している場合、このデフォルトの io.Copy ベースのI/O設定は不要になるため、処理をスキップするという意味です。これにより、二重のデータフローや競合を防ぎます。
    • これらのメソッドがエラーを返さなくなったのは、パイプが直接接続されることで、この段階でのエラー発生の可能性が低くなったため、またはエラーハンドリングがより上位の層に移譲されたためと考えられます。
  3. 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.ReadCloserClose() メソッドは、リソースの解放やストリームの終了を期待させますが、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 (コミットメッセージに記載)