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

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

このコミットは、Go言語の実験的パッケージである exp/ssh において、重複していたターミナル関連のコードを削除し、exp/terminal パッケージのコードを利用するように変更したものです。これにより、コードの重複が解消され、exp/terminal パッケージが提供するより汎用的なターミナル処理機能が活用されるようになります。

コミット

commit dd47a0a2ca62e8c7f275545d3aa810d3b875550f
Author: Adam Langley <agl@golang.org>
Date:   Sun Jan 15 09:59:06 2012 -0500

    exp/ssh: remove duplicated terminal code.
    
    The terminal code in exp/terminal was forked from the code in exp/ssh.
    This change removes the duplicated code from exp/ssh in favour of
    using exp/terminal.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5375064

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/dd47a0a2ca62e8c7f275545d3aa810d3b875550f

元コミット内容

exp/ssh: remove duplicated terminal code.

exp/terminal にあるターミナルコードは、元々 exp/ssh のコードからフォークされたものでした。 この変更は、exp/terminal を使用するために、exp/ssh から重複したコードを削除します。

変更の背景

このコミットの背景には、Go言語の標準ライブラリ開発におけるコードの再利用とモジュール化の原則があります。exp (experimental) パッケージは、Goの標準ライブラリに組み込まれる可能性のある、まだ実験段階のコードを格納するために使用されます。

元々、exp/ssh パッケージ(SSHプロトコルの実装)には、SSHセッション内でシェルやターミナルを扱うための独自のターミナル処理コードが含まれていました。しかし、同様のターミナル処理機能が exp/terminal パッケージとして独立して開発され、より汎用的な用途で利用できるようになりました。

この状況は、同じ機能が異なる場所で重複して実装されていることを意味し、コードの保守性や一貫性の観点から望ましくありませんでした。exp/terminal が独立したパッケージとして成熟したため、exp/ssh 内の重複コードを削除し、exp/terminal を利用するように変更することで、以下のメリットが期待されます。

  • コードの重複排除: 同じロジックが二箇所に存在することによるバグの発生リスクや、変更時の手間を削減します。
  • モジュール性の向上: ターミナル処理という特定の機能が独立したパッケージにカプセル化され、再利用性が高まります。
  • 一貫性の確保: exp/terminal が提供する統一されたインターフェースを通じてターミナル処理を行うことで、将来的な機能拡張や改善が容易になります。

このコミットは、このような背景のもと、exp/sshexp/terminal の機能を活用するように依存関係を整理し、コードベースをクリーンアップすることを目的としています。

前提知識の解説

このコミットを理解するためには、以下の前提知識が必要です。

1. Go言語の exp パッケージ

Go言語の標準ライブラリには、exp というプレフィックスを持つパッケージ群が存在します。これらは「実験的 (experimental)」なパッケージであり、将来的に標準ライブラリに組み込まれる可能性があるものの、まだAPIが安定していない、あるいは広く使われる前にフィードバックを収集している段階のものです。exp パッケージのコードは、通常の標準ライブラリとは異なり、APIの変更や削除が行われる可能性があります。

2. SSH (Secure Shell) プロトコル

SSHは、ネットワークを介して安全にコンピュータを操作するためのプロトコルです。主にリモートログインやファイル転送に使用されます。SSHセッション内では、ユーザーがコマンドを入力し、その結果を受け取るための「シェル」や「ターミナル」の概念が重要になります。

  • SSHサーバー: クライアントからの接続を受け入れ、認証を行い、セッションを確立する側。
  • SSHクライアント: サーバーに接続し、リモート操作を行う側。
  • セッション: SSH接続が確立された後、コマンド実行やシェル操作を行うための論理的なチャネル。
  • 擬似ターミナル (Pseudo-Terminal, PTY): リモートシェルセッションで、ユーザーが対話的にコマンドを入力し、その出力を整形して表示するための仮想的なターミナルデバイス。SSHクライアントは、このPTYを介してサーバーと通信し、キー入力の送信や画面出力の受信を行います。

3. ターミナルエミュレーションとVT100

コンピュータのターミナルは、テキストベースの入出力を扱うためのインターフェースです。現代のターミナルは、物理的な端末ではなく、ソフトウェアでエミュレートされた「ターミナルエミュレータ」が一般的です。

  • VT100: DEC社が開発した初期のビデオ端末で、その制御シーケンス(エスケープシーケンス)が事実上の標準となり、多くのターミナルエミュレータがVT100互換性を持っています。カーソル移動、画面クリア、文字色変更などの操作は、特定のバイトシーケンス(エスケープシーケンス)をターミナルに送信することで行われます。
  • 行編集 (Line Editing): ユーザーがコマンドラインで文字を入力、削除、カーソル移動などを行う機能。これはターミナルエミュレータまたはシェルが提供する機能であり、バックスペースや矢印キーなどの特殊なキー入力が、対応する制御シーケンスとして処理されます。

4. Go言語の io パッケージ

Go言語の io パッケージは、I/Oプリミティブを提供します。特に io.Readerio.Writer インターフェースは、データの読み書きを行うための基本的な抽象化であり、ファイル、ネットワーク接続、メモリバッファなど、様々なデータソース/シンクに対して統一的なインターフェースを提供します。

5. Go言語のインターフェース

Go言語のインターフェースは、メソッドのシグネチャの集合を定義します。型がインターフェースのすべてのメソッドを実装していれば、そのインターフェースを満たすとみなされます。これにより、具体的な実装に依存せずに、抽象的な振る舞いを定義し、ポリモーフィズムを実現できます。

技術的詳細

このコミットの技術的な核心は、exp/ssh パッケージが独自に持っていたターミナル処理ロジック(具体的には ServerShell 構造体とその関連メソッド)を削除し、代わりに exp/terminal パッケージが提供する terminal.Terminal インターフェースを利用するように変更した点です。

変更の具体的な内容は以下の通りです。

  1. src/pkg/exp/ssh/server_shell.go の削除:

    • このファイルには、SSHサーバー側でシェルセッションを管理し、VT100互換のターミナルエミュレーションと行編集機能を提供するための ServerShell 構造体とそのメソッド(ReadLine, handleKey, moveCursorToPos など)が実装されていました。
    • これらの機能は exp/terminal パッケージに既に存在するため、このファイル全体が削除されました。
  2. src/pkg/exp/ssh/server_terminal.go の新規作成:

    • この新しいファイルは、exp/terminal パッケージの Terminal インターフェースを exp/ssh パッケージ内で利用するためのブリッジとして機能します。
    • ServerTerminal 構造体が定義され、exp/terminalTerminal インターフェースとSSHチャネル (Channel) をラップします。
    • ReadLine メソッドは、内部で ss.Term.ReadLine() を呼び出すことで、実際のターミナル処理を exp/terminal に委譲します。
    • pty-reqshell, env といったSSHチャネルリクエストの処理は、ServerTerminalReadLine メソッド内で引き続き行われます。これにより、ターミナルサイズ変更などのSSH固有の制御は exp/ssh 側で処理しつつ、実際のキー入力や画面出力のロジックは exp/terminal に任せるという役割分担が明確になります。
  3. src/pkg/exp/ssh/doc.go の変更:

    • SSHサーバーの例において、NewServerShell の代わりに terminal.NewTerminalssh.ServerTerminal を使用するようにコード例が更新されました。
    • sConn.Handshake のエラーハンドリングがより簡潔な形式に変更されました。
    • channel.Reject の呼び出し元が c から channel に変更されました。
    • クライアント認証に関するコメントが追加され、Run メソッドが Exec メソッドに名称変更されたことが反映されました。
  4. src/pkg/exp/ssh/Makefile の変更:

    • ビルド対象ファイルから server_shell.go が削除され、server_terminal.go が追加されました。
  5. src/pkg/exp/ssh/session_test.go の変更:

    • テストコード内で NewServerShell の代わりに新しいヘルパー関数 newServerShell が導入され、これが terminal.NewTerminalssh.ServerTerminal を使用するように変更されました。これにより、テストも新しいターミナル処理のアーキテクチャに適合します。
    • exp/terminal パッケージがインポートされました。

この変更により、exp/ssh はターミナルエミュレーションの詳細から解放され、より高レベルなSSHプロトコル処理に集中できるようになります。ターミナル処理のロジックは exp/terminal に一元化され、Goエコシステム全体での再利用性が向上します。

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

このコミットのコアとなる変更は、src/pkg/exp/ssh/server_shell.go の削除と、src/pkg/exp/ssh/server_terminal.go の新規作成、そしてそれらを利用するように既存コードが変更された点です。

src/pkg/exp/ssh/server_shell.go (削除)

このファイル全体が削除されました。以前はここに ServerShell 構造体と、キー入力の処理、カーソル移動、行編集などの複雑なターミナルエミュレーションロジックが含まれていました。

src/pkg/exp/ssh/server_terminal.go (新規作成)

// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package ssh

// A Terminal is capable of parsing and generating virtual terminal
// data from an SSH client.
type Terminal interface {
	ReadLine() (line string, err error)
	SetSize(x, y int)
	Write([]byte) (int, error)
}

// ServerTerminal contains the state for running a terminal that is capable of
// reading lines of input.
type ServerTerminal struct {
	Term    Terminal
	Channel Channel
}

// parsePtyRequest parses the payload of the pty-req message and extracts the
// dimensions of the terminal. See RFC 4254, section 6.2.
func parsePtyRequest(s []byte) (width, height int, ok bool) {
	_, s, ok = parseString(s)
	if !ok {
		return
	}
	width32, s, ok := parseUint32(s)
	if !ok {
		return
	}
	height32, _, ok := parseUint32(s)
	width = int(width32)
	height = int(height32)
	if width < 1 {
		ok = false
	}
	if height < 1 {
		ok = false
	}
	return
}

func (ss *ServerTerminal) Write(buf []byte) (n int, err error) {
	return ss.Term.Write(buf)
}

// ReadLine returns a line of input from the terminal.
func (ss *ServerTerminal) ReadLine() (line string, err error) {
	for {
		if line, err = ss.Term.ReadLine(); err == nil {
			return
		}

		req, ok := err.(ChannelRequest)
		if !ok {
			return
		}

		ok = false
		switch req.Request {
		case "pty-req":
			var width, height int
			width, height, ok = parsePtyRequest(req.Payload)
			ss.Term.SetSize(width, height)
		case "shell":
			ok = true
			if len(req.Payload) > 0 {
				// We don't accept any commands, only the default shell.
				ok = false
			}
		case "env":
			ok = true
		}
		if req.WantReply {
			ss.Channel.AckRequest(ok)
		}
	}
	panic("unreachable")
}

src/pkg/exp/ssh/doc.go (関連する変更)

--- a/src/pkg/exp/ssh/doc.go
+++ b/src/pkg/exp/ssh/doc.go
@@ -34,8 +34,7 @@ Once a ServerConfig has been configured, connections can be accepted.
  if err != nil {
  panic("failed to accept incoming connection")
  }
- err = sConn.Handshake(conn)
- if err != nil {
+ if err := sConn.Handshake(conn); err != nil {
  panic("failed to handshake")
  }
 
@@ -60,16 +59,20 @@ the case of a shell, the type is "session" and ServerShell may be used to
 present a simple terminal interface.
 
  if channel.ChannelType() != "session" {
-  c.Reject(UnknownChannelType, "unknown channel type")
+  channel.Reject(UnknownChannelType, "unknown channel type")
  return
  }
  channel.Accept()
 
- shell := NewServerShell(channel, "> ")
+ term := terminal.NewTerminal(channel, "> ")
+ serverTerm := &ssh.ServerTerminal{
+  Term: term,
+  Channel: channel,
+ }
  go func() {
  defer channel.Close()
  for {
-  line, err := shell.ReadLine()
+  line, err := serverTerm.ReadLine()
  if err != nil {
  break
  }
@@ -97,12 +100,12 @@ ClientAuth via the Auth field in ClientConfig.
  }
  }
 
-An SSH client is represented with a ClientConn. 
+An SSH client is represented with a ClientConn. Currently only the "password"
+authentication method is supported.
 
  config := &ClientConfig{
  User: "username",
  }
  client, err := Dial("yourserver.com:22", config)
 
-Each ClientConn can support multiple interactive sessions, represented by a Session. 
-
-Once a Session is created, you can execute a single command on the remote side 
-using the Run method.
+Each ClientConn can support multiple interactive sessions, represented by a Session.
+
+Once a Session is created, you can execute a single command on the remote side
+using the Exec method.

コアとなるコードの解説

server_terminal.go の役割

新しく追加された server_terminal.go は、exp/sshexp/terminal の間の接着剤(アダプター)として機能します。

  • Terminal インターフェース:

    type Terminal interface {
    	ReadLine() (line string, err error)
    	SetSize(x, y int)
    	Write([]byte) (int, error)
    }
    

    これは exp/terminal パッケージが提供する Terminal インターフェースの定義を exp/ssh パッケージ内で再定義したものです。これにより、exp/sshexp/terminal の具体的な実装に直接依存することなく、その抽象的な振る舞いを利用できます。ReadLine は一行の入力を読み取り、SetSize はターミナルのサイズを設定し、Write はターミナルにデータを書き込みます。

  • ServerTerminal 構造体:

    type ServerTerminal struct {
    	Term    Terminal
    	Channel Channel
    }
    

    この構造体は、exp/terminalTerminal インターフェースを実装するオブジェクト (Term) と、SSHチャネル (Channel) を保持します。これにより、SSHプロトコル固有のチャネル操作と、汎用的なターミナル操作を連携させることができます。

  • ServerTerminal.ReadLine() メソッド: このメソッドは、SSHセッションにおける主要な入力ループを処理します。

    1. まず、内部の ss.Term.ReadLine() を呼び出し、exp/terminal パッケージに実際の行読み取り処理を委譲します。
    2. ss.Term.ReadLine() がエラーを返した場合、それが ChannelRequest 型のエラーであるかをチェックします。これは、SSHクライアントから pty-req (擬似ターミナル要求), shell (シェル起動要求), env (環境変数設定要求) などの特殊なリクエストが送られてきた場合に発生します。
    3. pty-req の場合、parsePtyRequest を呼び出してターミナルサイズを解析し、ss.Term.SetSize() を使って exp/terminal にそのサイズを伝えます。
    4. shellenv のリクエストも適切に処理し、クライアントに応答 (AckRequest) します。
    5. これらのリクエスト処理が完了したら、ループを継続して再度 ss.Term.ReadLine() を試みます。 このメカニズムにより、ServerTerminal はSSHプロトコルレベルのリクエストを処理しつつ、実際のキー入力や行編集の複雑なロジックは exp/terminal に任せるという、クリーンな分離を実現しています。

doc.go の変更点

doc.go のコード例の変更は、新しいアーキテクチャへの移行を明確に示しています。

  • 以前は NewServerShell(channel, "> ") を直接呼び出していましたが、これは exp/ssh 内部のターミナル実装に依存していました。
  • 変更後は term := terminal.NewTerminal(channel, "> ") を呼び出し、exp/terminal パッケージから Terminal インターフェースの実装を取得しています。
  • そして、この term オブジェクトと channel を使って ssh.ServerTerminal を初期化しています。
    serverTerm := &ssh.ServerTerminal{
    	Term: term,
    	Channel: channel,
    }
    
    これにより、SSHサーバーは ssh.ServerTerminal を介して exp/terminal の機能を利用するようになります。

全体的な影響

この変更は、exp/ssh パッケージの内部構造を大幅に簡素化し、ターミナル処理の責任を exp/terminal に移管することで、コードベースの健全性を向上させています。exp/ssh はSSHプロトコルに特化し、ターミナルエミュレーションの詳細は exp/terminal が担当するという、より良いモジュール設計が実現されました。

関連リンク

  • Go言語の exp/ssh パッケージ (現在のGo標準ライブラリの golang.org/x/crypto/ssh に相当する可能性が高い)
  • Go言語の exp/terminal パッケージ (現在のGo標準ライブラリの golang.org/x/term に相当する可能性が高い)
  • RFC 4254 - The Secure Shell (SSH) Connection Protocol (特にセクション 6.2 "Pseudo-Terminal Allocation")

参考にした情報源リンク

  • Go言語の公式ドキュメント (特に io パッケージやインターフェースに関する記述)
  • SSHプロトコルに関する一般的な情報源 (RFCなど)
  • VT100エスケープシーケンスに関する情報