[インデックス 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/ssh
が exp/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.Reader
と io.Writer
インターフェースは、データの読み書きを行うための基本的な抽象化であり、ファイル、ネットワーク接続、メモリバッファなど、様々なデータソース/シンクに対して統一的なインターフェースを提供します。
5. Go言語のインターフェース
Go言語のインターフェースは、メソッドのシグネチャの集合を定義します。型がインターフェースのすべてのメソッドを実装していれば、そのインターフェースを満たすとみなされます。これにより、具体的な実装に依存せずに、抽象的な振る舞いを定義し、ポリモーフィズムを実現できます。
技術的詳細
このコミットの技術的な核心は、exp/ssh
パッケージが独自に持っていたターミナル処理ロジック(具体的には ServerShell
構造体とその関連メソッド)を削除し、代わりに exp/terminal
パッケージが提供する terminal.Terminal
インターフェースを利用するように変更した点です。
変更の具体的な内容は以下の通りです。
-
src/pkg/exp/ssh/server_shell.go
の削除:- このファイルには、SSHサーバー側でシェルセッションを管理し、VT100互換のターミナルエミュレーションと行編集機能を提供するための
ServerShell
構造体とそのメソッド(ReadLine
,handleKey
,moveCursorToPos
など)が実装されていました。 - これらの機能は
exp/terminal
パッケージに既に存在するため、このファイル全体が削除されました。
- このファイルには、SSHサーバー側でシェルセッションを管理し、VT100互換のターミナルエミュレーションと行編集機能を提供するための
-
src/pkg/exp/ssh/server_terminal.go
の新規作成:- この新しいファイルは、
exp/terminal
パッケージのTerminal
インターフェースをexp/ssh
パッケージ内で利用するためのブリッジとして機能します。 ServerTerminal
構造体が定義され、exp/terminal
のTerminal
インターフェースとSSHチャネル (Channel
) をラップします。ReadLine
メソッドは、内部でss.Term.ReadLine()
を呼び出すことで、実際のターミナル処理をexp/terminal
に委譲します。pty-req
やshell
,env
といったSSHチャネルリクエストの処理は、ServerTerminal
のReadLine
メソッド内で引き続き行われます。これにより、ターミナルサイズ変更などのSSH固有の制御はexp/ssh
側で処理しつつ、実際のキー入力や画面出力のロジックはexp/terminal
に任せるという役割分担が明確になります。
- この新しいファイルは、
-
src/pkg/exp/ssh/doc.go
の変更:- SSHサーバーの例において、
NewServerShell
の代わりにterminal.NewTerminal
とssh.ServerTerminal
を使用するようにコード例が更新されました。 sConn.Handshake
のエラーハンドリングがより簡潔な形式に変更されました。channel.Reject
の呼び出し元がc
からchannel
に変更されました。- クライアント認証に関するコメントが追加され、
Run
メソッドがExec
メソッドに名称変更されたことが反映されました。
- SSHサーバーの例において、
-
src/pkg/exp/ssh/Makefile
の変更:- ビルド対象ファイルから
server_shell.go
が削除され、server_terminal.go
が追加されました。
- ビルド対象ファイルから
-
src/pkg/exp/ssh/session_test.go
の変更:- テストコード内で
NewServerShell
の代わりに新しいヘルパー関数newServerShell
が導入され、これがterminal.NewTerminal
とssh.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/ssh
と exp/terminal
の間の接着剤(アダプター)として機能します。
-
Terminal
インターフェース:type Terminal interface { ReadLine() (line string, err error) SetSize(x, y int) Write([]byte) (int, error) }
これは
exp/terminal
パッケージが提供するTerminal
インターフェースの定義をexp/ssh
パッケージ内で再定義したものです。これにより、exp/ssh
はexp/terminal
の具体的な実装に直接依存することなく、その抽象的な振る舞いを利用できます。ReadLine
は一行の入力を読み取り、SetSize
はターミナルのサイズを設定し、Write
はターミナルにデータを書き込みます。 -
ServerTerminal
構造体:type ServerTerminal struct { Term Terminal Channel Channel }
この構造体は、
exp/terminal
のTerminal
インターフェースを実装するオブジェクト (Term
) と、SSHチャネル (Channel
) を保持します。これにより、SSHプロトコル固有のチャネル操作と、汎用的なターミナル操作を連携させることができます。 -
ServerTerminal.ReadLine()
メソッド: このメソッドは、SSHセッションにおける主要な入力ループを処理します。- まず、内部の
ss.Term.ReadLine()
を呼び出し、exp/terminal
パッケージに実際の行読み取り処理を委譲します。 ss.Term.ReadLine()
がエラーを返した場合、それがChannelRequest
型のエラーであるかをチェックします。これは、SSHクライアントからpty-req
(擬似ターミナル要求),shell
(シェル起動要求),env
(環境変数設定要求) などの特殊なリクエストが送られてきた場合に発生します。pty-req
の場合、parsePtyRequest
を呼び出してターミナルサイズを解析し、ss.Term.SetSize()
を使ってexp/terminal
にそのサイズを伝えます。shell
やenv
のリクエストも適切に処理し、クライアントに応答 (AckRequest
) します。- これらのリクエスト処理が完了したら、ループを継続して再度
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
を初期化しています。
これにより、SSHサーバーはserverTerm := &ssh.ServerTerminal{ Term: term, Channel: channel, }
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エスケープシーケンスに関する情報