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

[インデックス 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.Stdinio.WriteCloser であり、Session.StdoutSession.Stderrio.ReadCloser でした。これは、ローカルプロセスに対する exec.CmdStdinio.ReaderStdout/Stderrio.Writer であるのとは逆の方向性でした。

この不整合は、開発者がローカルとリモートの両方でコマンドを実行するコードを書く際に、異なるAPIを覚える必要があり、コードの再利用性や可読性を損ねる可能性がありました。このコミットは、このAPIの不整合を解消し、exp/ssh.Sessionos/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.Readerio.Closer を組み合わせたインターフェース。
  • io.WriteCloser: io.Writerio.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の規定に従っています。

技術的詳細

このコミットの主要な変更点は以下の通りです。

  1. 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 と完全に一致し、より自然なデータフロー表現になりました。
  2. Shell() メソッドの非同期化と Wait() メソッドの導入:

    • 変更前: Shell() はシェルが起動するまでブロックしていました。
    • 変更後: Shell() はリモートシェルが起動リクエストを受け付けた後、即座にリターンするようになりました。シェルからの出力の読み取りや、シェルプロセスの終了を待つためには、新たに導入された Session.Wait() メソッドを呼び出す必要があります。これは exec.Cmd.Start()exec.Cmd.Wait() のペアに相当します。
  3. Exec() メソッドの内部での Wait() 呼び出し:

    • Exec() メソッドは、リモートコマンドの実行リクエストを送信した後、内部で Session.Wait() を呼び出すようになりました。これにより、Exec()exec.Cmd.Run() と同様に、リモートコマンドが完了するまでブロックする動作を維持します。
  4. 入出力のデフォルト処理の改善:

    • Session.Stdinnil の場合、bytes.Buffer から読み込むように設定され、実質的に空の入力を提供します。
    • Session.Stdout または Session.Stderrnil の場合、ioutil.Discard に書き込むように設定され、出力が破棄されます。これにより、ユーザーが明示的に入出力を設定しない場合でも、セッションが適切に動作するようになります。
  5. 内部的なデータコピーメカニズムの変更:

    • Session 内部で、Stdin からリモートへの書き込み、およびリモートからの Stdout/Stderr の読み取りを行うための copyFuncs スライスと errch チャネルが導入されました。これらの関数はゴルーチンで実行され、入出力のコピー処理を非同期で行い、エラーを errch に報告します。Wait() メソッドは、これらのコピー処理の完了と、リモートプロセスの終了ステータスを待機します。
  6. SSHプロトコルメッセージの構造化:

    • SetenvRequestPtyExec のリクエストを送信する際に、以前はバイトスライスを直接構築していましたが、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, Stderrnil の場合のデフォルト設定もここで行われます。
    • 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.Readerio.Writer に変更されたことで、ユーザーはローカルプロセスと同じ感覚でSSHセッションの入出力を扱うことができるようになりました。例えば、bytes.Buffer やファイル、ネットワーク接続など、任意の io.ReaderSession.Stdin に設定し、任意の io.WriterSession.StdoutSession.Stderr に設定できるようになります。

ExecShell の動作変更

  • Exec(cmd string) error: このメソッドは、リモートホストで指定されたコマンド cmd を実行します。変更後、Exec は内部で s.start() を呼び出して入出力のコピーを開始し、その後 s.Wait() を呼び出してリモートコマンドの完了を待ちます。これにより、Execos/exec.Cmd.Run() と同様に、コマンドが終了するまでブロックする同期的な動作を提供します。エラーが発生した場合、ssh: could not execute command %s: %v のような形式で詳細なエラーメッセージが返されます。

  • Shell() error: このメソッドは、リモートホストでログインシェルを起動します。変更後、Shells.start() を呼び出して入出力のコピーを開始しますが、Exec とは異なり、s.Wait() は呼び出しません。そのため、Shell はリモートシェルが起動リクエストを受け付けた後、即座にリターンします。ユーザーは、シェルとの対話(入力の送信や出力の受信)を行い、シェルプロセスが終了したことを確認するために、明示的に s.Wait() を呼び出す必要があります。これは os/exec.Cmd.Start() と同様の非同期的な動作です。

start()Wait() メソッド

  • start() error: この内部メソッドは、SessionExec または Shell によって開始される際に呼び出されます。主な役割は以下の通りです。

    1. s.started = true を設定し、セッションが既に開始されていることをマークします。
    2. stdin(), stdout(), stderr() の各ヘルパー関数を呼び出し、Session.Stdin, Stdout, Stderrnil の場合にデフォルトの bytes.Bufferioutil.Discard を設定し、対応する入出力コピー関数を s.copyFuncs スライスに追加します。
    3. s.errch チャネルを初期化します。
    4. s.copyFuncs に追加された各コピー関数を新しいゴルーチンで実行し、それぞれの結果(エラーまたは nil)を s.errch に送信します。これにより、入出力のコピーがバックグラウンドで非同期に実行されます。
  • Wait() error: このメソッドは、リモートコマンドまたはシェルの実行が完了するのを待ちます。

    1. まず、s.wait() を呼び出して、リモートからの exit-status または exit-signal メッセージを待ち、リモートプロセスの終了ステータスを取得します。
    2. 次に、s.copyFuncs の数だけ s.errch からエラーを読み取ります。これにより、すべての入出力コピーゴルーチンが完了したことを確認し、コピー中に発生したエラーを収集します。
    3. 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() メソッド内でゴルーチンとして起動されるため、メインの実行フローをブロックすることなく、バックグラウンドで入出力が処理されます。

関連リンク

参考にした情報源リンク

  • コミットメッセージ内の https://golang.org/cl/5322055 (Goのコードレビューシステムへのリンク)
  • Go言語の公式ドキュメント (os/exec および io パッケージ)
  • SSHプロトコルに関する一般的な知識
  • Dave Cheney氏のブログやGoコミュニティでの議論(このコミットの背景にある設計思想を理解するため)
  • Gustavo Niemeyer氏のGo関連の活動(このコミットのきっかけとなった人物)