Open-Source-Software-Entwicklung und Downloads

Browse Subversion Repository

Diff of /trunk/doc/jp/html/reference/sourcecode.html

Parent Directory Parent Directory | Revision Log Revision Log | View Patch Patch

revision 3226 by maya, Tue Mar 24 09:37:20 2009 UTC revision 3227 by maya, Tue Mar 24 15:10:33 2009 UTC
# Line 1  Line 1 
1  <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"  <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
2    "http://www.w3.org/TR/html4/strict.dtd">    "http://www.w3.org/TR/html4/strict.dtd">
3  <HTML>  <HTML>
4  <HEAD>  <HEAD>
5  <meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS">  <meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS">
6  <TITLE>Tera Termソースコード解説</TITLE>  <TITLE>Tera Termソースコード解説</TITLE>
7  <META http-equiv="Content-Style-Type" content="text/css">  <META http-equiv="Content-Style-Type" content="text/css">
8  <link rel="stylesheet" href="../style.css" type="text/css">  <link rel="stylesheet" href="../style.css" type="text/css">
9  </HEAD>  </HEAD>
10  <BODY>  <BODY>
11    
12  <h1 class="center">Tera Termソースコード解説</h1>  <h1 class="center">Tera Termソースコード解説</h1>
13    
14  <hr width=80% align=center>  <hr width=80% align=center>
15    
16  <ol>  <ol>
17    <li><a href="#foreword">はじめに</a></li>    <li><a href="#foreword">はじめに</a></li>
18    <li><a href="#skillset">必要スキル</a></li>    <li><a href="#skillset">必要スキル</a></li>
19    <li><a href="#module">モジュール構成</a></li>    <li><a href="#module">モジュール構成</a></li>
20    <li><a href="#library">ライブラリ構成</a></li>    <li><a href="#library">ライブラリ構成</a></li>
21    <li><a href="#plugin">プラグインサポート</a></li>    <li><a href="#plugin">プラグインサポート</a></li>
22    <li><a href="#configuration">設定ファイルの読み書き</a></li>    <li><a href="#configuration">設定ファイルの読み書き</a></li>
23    <li><a href="#secure">セキュアプログラミング</a></li>    <li><a href="#secure">セキュアプログラミング</a></li>
24    <li><a href="#compatibility">古いバージョンのWindowsとの互換性維持</a></li>    <li><a href="#compatibility">古いバージョンのWindowsとの互換性維持</a></li>
25    <li><a href="#debug">デバッグ手法</a></li>    <li><a href="#debug">デバッグ手法</a></li>
26    <li><a href="#thread">マルチスレッド</a></li>    <li><a href="#thread">マルチスレッド</a></li>
27    <li><a href="#dde">DDEによるプロセス間通信</a></li>    <li><a href="#dde">DDEによるプロセス間通信</a></li>
28    <li><a href="#ttssh">TTSSHによるSSHの設計と実装</a></li>    <li><a href="#ttssh">TTSSHによるSSHの設計と実装</a></li>
29    <li><a href="#macro">マクロ言語の設計と実装</a></li>    <li><a href="#macro">マクロ言語の設計と実装</a></li>
30    <li><a href="#caret">キャレット制御</a></li>    <li><a href="#caret">キャレット制御</a></li>
31  </ol>  </ol>
32    
33  <hr width=80% align=center>  <hr width=80% align=center>
34    
35  <h2><a name="foreword">はじめに</a></h2>  <h2><a name="foreword">はじめに</a></h2>
36   本文書では、Tera Termのソースコードについて解説をします。解説対象とするソースコードはバージョン"4.58"(2008年2月現在)のものをベースとしています。   本文書では、Tera Termのソースコードについて解説をします。解説対象とするソースコードはバージョン"4.58"(2008年2月現在)のものをベースとしています。
37  <hr>  <hr>
38    
39    
40  <h2><a name="skillset">必要スキル</a></h2>  <h2><a name="skillset">必要スキル</a></h2>
41   Tera Termのパッケージに含まれるほとんどのプログラムは、C言語で記述されています。一部のコードはC++言語で、MFC(Microsoft Foundation Class)が利用されています。Windows特有の処理を行うために、Win32 APIが多用されているため、APIの知識が必要となってきます。<br>   Tera Termのパッケージに含まれるほとんどのプログラムは、C言語で記述されています。一部のコードはC++言語で、MFC(Microsoft Foundation Class)が利用されています。Windows特有の処理を行うために、Win32 APIが多用されているため、APIの知識が必要となってきます。<br>
42   ソースコードをビルドするためには、Microsoft Visual Studio 2005 Standard Edition以上が必要です。Express EditionではMFCが利用できないため、ビルドができません。また、C++BuilderやTurbo C++ Explorer、gccなどのコンパイラにおいても、ビルドすることはできません。<br>   ソースコードをビルドするためには、Microsoft Visual Studio 2005 Standard Edition以上が必要です。Express EditionではMFCが利用できないため、ビルドができません。また、C++BuilderやTurbo C++ Explorer、gccなどのコンパイラにおいても、ビルドすることはできません。<br>
43   Windowsプログラミングに関する情報の源は、Microsoftが提供する「MSDNライブラリ」にあります。開発を行う際は、MSDNライブラリを頻繁に参照することになります。<br>   Windowsプログラミングに関する情報の源は、Microsoftが提供する「MSDNライブラリ」にあります。開発を行う際は、MSDNライブラリを頻繁に参照することになります。<br>
44      
45  <ul>  <ul>
46    <li><a href="http://msdn2.microsoft.com/en-us/library/default.aspx">MSDNライブラリ</a></li>    <li><a href="http://msdn2.microsoft.com/en-us/library/default.aspx">MSDNライブラリ</a></li>
47    <li><a href="http://msdn2.microsoft.com/ja-jp/library/default.aspx">MSDNライブラリ(日本語版)</a></li>    <li><a href="http://msdn2.microsoft.com/ja-jp/library/default.aspx">MSDNライブラリ(日本語版)</a></li>
48  </ul>  </ul>
49    
50  <p>  <p>
51   ただし、CygTermのみはCygwinのgccでコンパイルをします。ゆえに、CygTermはgccの機能を使った実装になっています。言語はC++です。   ただし、CygTermのみはCygwinのgccでコンパイルをします。ゆえに、CygTermはgccの機能を使った実装になっています。言語はC++です。
52  </p>  </p>
53    
54   Tera TermのメインエンジンはC++で実装されていますが、C言語的なコーディングがなされているため、Tera Termのソースコードを読み解くには、C言語に関する基礎知識があれば問題ないと言えます。ただし、Microsoft Visual C++(VC++)はANSI C準拠(C89)とはいえ、C99には未対応であるために、本来のC99相当の機能が独自に拡張されている部分もあります。そうした独自拡張された関数には、頭文字にアンダースコア(_)が付いているために、区別が付けやすくなっています。たとえば、VC++の_snprintf()は、ANSI C(C99)のsnprintf()とは似て非なるものです。<br>   Tera TermのメインエンジンはC++で実装されていますが、C言語的なコーディングがなされているため、Tera Termのソースコードを読み解くには、C言語に関する基礎知識があれば問題ないと言えます。ただし、Microsoft Visual C++(VC++)はANSI C準拠(C89)とはいえ、C99には未対応であるために、本来のC99相当の機能が独自に拡張されている部分もあります。そうした独自拡張された関数には、頭文字にアンダースコア(_)が付いているために、区別が付けやすくなっています。たとえば、VC++の_snprintf()は、ANSI C(C99)のsnprintf()とは似て非なるものです。<br>
55    
56  <hr>  <hr>
57    
58    
59  <h2><a name="module">モジュール構成</a></h2>  <h2><a name="module">モジュール構成</a></h2>
60   Tera Termパッケージに含まれる実行モジュール(.exeと.dll)の関連図を以下に示します。実行ファイルの拡張子は".exe"になっており、必要に応じてDLLが動的リンクされます。いずれも32ビットプログラム(x86)であるために、x86-64やIA-64といった64ビット環境ではそのまま動作するかどうかは評価されていません。   Tera Termパッケージに含まれる実行モジュール(.exeと.dll)の関連図を以下に示します。実行ファイルの拡張子は".exe"になっており、必要に応じてDLLが動的リンクされます。いずれも32ビットプログラム(x86)であるために、x86-64やIA-64といった64ビット環境ではそのまま動作するかどうかは評価されていません。
61      
62  <div align="center">  <div align="center">
63  <img src="image/module_relation.png" width=720 height=540>  <img src="image/module_relation.png" width=720 height=540>
64  </div>  </div>
65    
66   通常、ユーザがデスクトップやスタートメニューからアプリケーションを起動するときに、呼び出される実行ファイルは"ttermpro.exe"になります。実行ファイルはさらに5つのDLLとダイナミックリンクしています。静的リンクを行い、単一のEXEファイルにしていないのは、1つのプロセスのメモリ占有率を抑えるためです。Tera Termでは多数の起動が行われることが想定されるため、初期設計段階からDLLに分割されています。一度読み込まれたDLLは、複数のプロセス間で共有することができます。<br>   通常、ユーザがデスクトップやスタートメニューからアプリケーションを起動するときに、呼び出される実行ファイルは"ttermpro.exe"になります。実行ファイルはさらに5つのDLLとダイナミックリンクしています。静的リンクを行い、単一のEXEファイルにしていないのは、1つのプロセスのメモリ占有率を抑えるためです。Tera Termでは多数の起動が行われることが想定されるため、初期設計段階からDLLに分割されています。一度読み込まれたDLLは、複数のプロセス間で共有することができます。<br>
67   <br>   <br>
68      
69   マクロスクリプトを実行する際は、"ttpmacro.exe"というまったく別のプロセスが呼び出されます。"ttermpro.exe"とプロセス単位で分けられているのは、マクロを単体で実行できるようにするためです。両プロセス間で、データのやりとりを行うためには、プロセス間通信が必要です。Tera Termでは、DDE(Dynamic Data Exchange)と呼ばれる現在ではレガシーとなってしまったしくみが採用されています。将来のWindowsではDDEがサポートされなくなる可能性があり、その場合Tera Term上でマクロを実行することは一切できなくなります。<br>   マクロスクリプトを実行する際は、"ttpmacro.exe"というまったく別のプロセスが呼び出されます。"ttermpro.exe"とプロセス単位で分けられているのは、マクロを単体で実行できるようにするためです。両プロセス間で、データのやりとりを行うためには、プロセス間通信が必要です。Tera Termでは、DDE(Dynamic Data Exchange)と呼ばれる現在ではレガシーとなってしまったしくみが採用されています。将来のWindowsではDDEがサポートされなくなる可能性があり、その場合Tera Term上でマクロを実行することは一切できなくなります。<br>
70   <br>   <br>
71      
72   TTSSHやTTProxy、TTXKanjiMenuといったプラグイン形式のDLLは、Tera Termの起動時に明示的に LoadLibrary() を使ってダイナミックロードされます。ロード対象となるDLLのファイル名は、TTXInit()#ttplug.c において、"TTX*.DLL"というパターンにマッチしたものとなります。<br>   TTSSHやTTProxy、TTXKanjiMenuといったプラグイン形式のDLLは、Tera Termの起動時に明示的に LoadLibrary() を使ってダイナミックロードされます。ロード対象となるDLLのファイル名は、TTXInit()#ttplug.c において、"TTX*.DLL"というパターンにマッチしたものとなります。<br>
73   <br>   <br>
74      
75   "keycode.exe"と"ttpmenu.exe"、"LogMeTT.exe"は単体アプリケーションです。<br>   "keycode.exe"と"ttpmenu.exe"、"LogMeTT.exe"は単体アプリケーションです。<br>
76   <br>   <br>
77      
78   Cygwin接続のしくみについては、別の節で説明します。   Cygwin接続のしくみについては、別の節で説明します。
79    
80  <hr>  <hr>
81    
82    
83  <h2><a name="library">ライブラリ構成</a></h2>  <h2><a name="library">ライブラリ構成</a></h2>
84   高度な機能を実現するために、フルスクラッチで実装することは効率がいいとは言えません。Tera Termでは開発効率化を図るために、オープンソースのライブラリを積極的に利用しています。ただし、オープンソース製品のライセンスによる競合には注意を払う必要があります(特にGPL)。<br>   高度な機能を実現するために、フルスクラッチで実装することは効率がいいとは言えません。Tera Termでは開発効率化を図るために、オープンソースのライブラリを積極的に利用しています。ただし、オープンソース製品のライセンスによる競合には注意を払う必要があります(特にGPL)。<br>
85   下図に、オープンソースのライブラリをリンクしているモジュールと、そのリンク状況を示します。Tera Termマクロプログラムにおいて、"waitregex"や"sprintf"コマンドにおいて正規表現を利用するために、Onigurumaと呼ばれる正規表現ライブラリをリンクしています。Tera Term本体では、バージョンダイアログにOnigurumaのバージョンを表示するためだけにリンクをしています。   下図に、オープンソースのライブラリをリンクしているモジュールと、そのリンク状況を示します。Tera Termマクロプログラムにおいて、"waitregex"や"sprintf"コマンドにおいて正規表現を利用するために、Onigurumaと呼ばれる正規表現ライブラリをリンクしています。Tera Term本体では、バージョンダイアログにOnigurumaのバージョンを表示するためだけにリンクをしています。
86      
87  <p>  <p>
88   SSHモジュールである"TTSSH"は、暗号処理を行うためにOpenSSLを利用しています。"OpenSSL"というネーミングからWebアクセスに使われるSSL(Secure Socket Layer)プロトコル専用のライブラリかと思われがちですが、基本的な暗号アルゴリズムをサポートしていることから、TTSSHではOpenSSLに含まれる低レイヤのルーチンを利用するだけに留まっています。このことは、すなわちOpenSSLライブラリにセキュリティホールが発見されたとしても、TTSSHへの影響は極めて低いということです。<br>   SSHモジュールである"TTSSH"は、暗号処理を行うためにOpenSSLを利用しています。"OpenSSL"というネーミングからWebアクセスに使われるSSL(Secure Socket Layer)プロトコル専用のライブラリかと思われがちですが、基本的な暗号アルゴリズムをサポートしていることから、TTSSHではOpenSSLに含まれる低レイヤのルーチンを利用するだけに留まっています。このことは、すなわちOpenSSLライブラリにセキュリティホールが発見されたとしても、TTSSHへの影響は極めて低いということです。<br>
89   zlibライブラリは、SSHパケットの圧縮を行うために利用しています。ただし、ダイヤルアップ回線などの低速度なネットワークにおいては、パケット圧縮は有効ですが、昨今の高速回線ではむしろ速度低下を招く足かせとなります。ゆえに、デフォルトではパケット圧縮機能は無効化されています。   zlibライブラリは、SSHパケットの圧縮を行うために利用しています。ただし、ダイヤルアップ回線などの低速度なネットワークにおいては、パケット圧縮は有効ですが、昨今の高速回線ではむしろ速度低下を招く足かせとなります。ゆえに、デフォルトではパケット圧縮機能は無効化されています。
90   PuTTYは世界標準であるフリーのターミナルエミュレータです。PuTTYに含まれるPageantと呼ばれるSSH認証エージェントがあるのですが、TTSSHでPageantによる公開鍵認証をサポートするために、PuTTYのソースコードを利用しています。   PuTTYは世界標準であるフリーのターミナルエミュレータです。PuTTYに含まれるPageantと呼ばれるSSH認証エージェントがあるのですが、TTSSHでPageantによる公開鍵認証をサポートするために、PuTTYのソースコードを利用しています。
91  </p>  </p>
92      
93   なお、いずれのライブラリも静的リンク(static link)としています。ライブラリのコンパイルオプションには"/MT"を付加しています。動的リンク(dynamic link)を行うと、一部のユーザ環境でTera Termが起動できないという現象が発生したために、現在では動的リンクは行っていません。   なお、いずれのライブラリも静的リンク(static link)としています。ライブラリのコンパイルオプションには"/MT"を付加しています。動的リンク(dynamic link)を行うと、一部のユーザ環境でTera Termが起動できないという現象が発生したために、現在では動的リンクは行っていません。
94      
95    
96  <div align="center">  <div align="center">
97  <img src="image/library_relation.png" width=720 height=540>  <img src="image/library_relation.png" width=720 height=540>
98  </div>  </div>
99    
100  <hr>  <hr>
101    
102    
103  <h2><a name="plugin">プラグインサポート</a></h2>  <h2><a name="plugin">プラグインサポート</a></h2>
104   Tera Termでは、DLLという形式でプラグインのしくみをサポートしています。プラグイン形式のDLLファイルを、Tera Termがインストールされているディレクトリへ設置するだけで、Tera Termのソースコードを修正することなく、機能追加を行うことができます。代表的なプラグインとして、TTSSHがあります。<br>   Tera Termでは、DLLという形式でプラグインのしくみをサポートしています。プラグイン形式のDLLファイルを、Tera Termがインストールされているディレクトリへ設置するだけで、Tera Termのソースコードを修正することなく、機能追加を行うことができます。代表的なプラグインとして、TTSSHがあります。<br>
105   プラグインを作成するためのサンプルコードとして、TTXSamples\ttxtest\ttxtest.c というソースファイルが用意されています。プラグインを開発するときは、このソースファイルをひな形とするとよいでしょう。実践的なプラグインとして、"TTX KanjiMenu"のソースコード(TTXKanjiMenu\配下)がシンプルで分かりやすいです。<br><br>   プラグインを作成するためのサンプルコードとして、TTXSamples\ttxtest\ttxtest.c というソースファイルが用意されています。プラグインを開発するときは、このソースファイルをひな形とするとよいでしょう。実践的なプラグインとして、"TTX KanjiMenu"のソースコード(TTXKanjiMenu\配下)がシンプルで分かりやすいです。<br><br>
106    
107   プラグインは、Tera Term("ttermpro.exe")の起動時に読み込まれます。TTXInit()#ttplug.c が読み込みを行う関数で、カレントディレクトリから"TTX*.DLL"というワイルドカードに合致するDLLファイルが読み込み対象となります。<br>   プラグインは、Tera Term("ttermpro.exe")の起動時に読み込まれます。TTXInit()#ttplug.c が読み込みを行う関数で、カレントディレクトリから"TTX*.DLL"というワイルドカードに合致するDLLファイルが読み込み対象となります。<br>
108   複数のDLLが存在する場合は、Tera Term本体からチェインするような形で、各DLLのエクスポート関数が連結されます。連結される順番は、それぞれのDLLが定義するオーダー値(TTXExports構造体のloadOrderメンバ)で決定され、現状下記の通りとなっています。    複数のDLLが存在する場合は、Tera Term本体からチェインするような形で、各DLLのエクスポート関数が連結されます。連結される順番は、それぞれのDLLが定義するオーダー値(TTXExports構造体のloadOrderメンバ)で決定され、現状下記の通りとなっています。 
109  <p>  <p>
110  <table border=1 align=center>  <table border=1 align=center>
111  <tr>  <tr>
112    <th>モジュール</th>    <th>モジュール</th>
113    <th>オーダー</th>    <th>オーダー</th>
114  </tr>  </tr>
115    
116  <tr>  <tr>
117    <td>TTProxy</td>    <td>TTProxy</td>
118    <td>0</td>    <td>0</td>
119  </tr>  </tr>
120    
121  <tr>  <tr>
122    <td>TTSSH</td>    <td>TTSSH</td>
123    <td>2500</td>    <td>2500</td>
124  </tr>  </tr>
125    
126  <tr>  <tr>
127    <td>TTX Kanji Menu</td>    <td>TTX Kanji Menu</td>
128    <td>5000</td>    <td>5000</td>
129  </tr>  </tr>
130  </table>  </table>
131   </p>   </p>
132    
133  オーダー値が小さいほど、Tera Term本体側に近くなります。たとえば、Tera Term本体からTTXModifyMenu()が呼び出された場合、  オーダー値が小さいほど、Tera Term本体側に近くなります。たとえば、Tera Term本体からTTXModifyMenu()が呼び出された場合、
134      
135   <ul>   <ul>
136    <li>TTXModifyMenu()#ttplug.c → TTProxyのTTXModifyMenu()  → TTSSHのTTXModifyMenu()  → TTX Kanji MenuのTTXModifyMenu()</li>    <li>TTXModifyMenu()#ttplug.c → TTProxyのTTXModifyMenu()  → TTSSHのTTXModifyMenu()  → TTX Kanji MenuのTTXModifyMenu()</li>
137   </ul><br>   </ul><br>
138      
139  という順番で、各DLLの関数が呼び出されていくことになります。  という順番で、各DLLの関数が呼び出されていくことになります。
140   <br>   <br>
141    
142   各DLLが、Tera Term本体側から呼び出してもらうためにエクスポートする関数群は、TTXExports構造体で定義し、TTXBind()で渡します。たとえば、TTX Kanji Menuのエクスポート関数は以下のとおりです。不要な関数は NULL で定義してあります。   各DLLが、Tera Term本体側から呼び出してもらうためにエクスポートする関数群は、TTXExports構造体で定義し、TTXBind()で渡します。たとえば、TTX Kanji Menuのエクスポート関数は以下のとおりです。不要な関数は NULL で定義してあります。
143    
144  <pre class=code>  <pre class=code>
145  static TTXExports Exports = {  static TTXExports Exports = {
146  /* This must contain the size of the structure. See below for its usage. */  /* This must contain the size of the structure. See below for its usage. */
147          sizeof(TTXExports),          sizeof(TTXExports),
148    
149  /* This is the load order number of this DLL. */  /* This is the load order number of this DLL. */
150          ORDER,          ORDER,
151    
152  /* Now we just list the functions that we've implemented. */  /* Now we just list the functions that we've implemented. */
153          TTXInit,          TTXInit,
154          NULL, /* TTXGetUIHooks */          NULL, /* TTXGetUIHooks */
155          NULL, /* TTXGetSetupHooks */          NULL, /* TTXGetSetupHooks */
156          NULL, /* TTXOpenTCP */          NULL, /* TTXOpenTCP */
157          NULL, /* TTXCloseTCP */          NULL, /* TTXCloseTCP */
158          NULL, /* TTXSetWinSize */          NULL, /* TTXSetWinSize */
159          TTXModifyMenu,          TTXModifyMenu,
160          TTXModifyPopupMenu,          TTXModifyPopupMenu,
161          TTXProcessCommand,          TTXProcessCommand,
162          NULL, /* TTXEnd */          NULL, /* TTXEnd */
163          NULL  /* TTXSetCommandLine */          NULL  /* TTXSetCommandLine */
164  };  };
165  </pre>  </pre>
166    
167   原則、プラグインのエクスポート関数は、他のプラグインと干渉しないように設計をするべきです。また、Tera Term本体側からの呼び出しが、自分宛てであるかどうかを判断する必要がある場合もあります。<br>   原則、プラグインのエクスポート関数は、他のプラグインと干渉しないように設計をするべきです。また、Tera Term本体側からの呼び出しが、自分宛てであるかどうかを判断する必要がある場合もあります。<br>
168   プラグインがエクスポートする関数について、以下に示します。   プラグインがエクスポートする関数について、以下に示します。
169      
170  <p>  <p>
171  <table border=1 align=center>  <table border=1 align=center>
172  <tr>  <tr>
173    <th>関数</th>    <th>関数</th>
174    <th>意味</th>    <th>意味</th>
175  </tr>  </tr>
176    
177  <tr>  <tr>
178    <td>TTXBind</td>    <td>TTXBind</td>
179    <td>一番始めに呼び出される関数であり、エクスポート関数のテーブルを渡す。</td>    <td>一番始めに呼び出される関数であり、エクスポート関数のテーブルを渡す。</td>
180  </tr>  </tr>
181    
182  <tr>  <tr>
183    <td>TTXInit</td>    <td>TTXInit</td>
184    <td>TTXBind()の呼び出し後にすぐに実行される関数で、Tera Term本体のグローバル変数(ts, cv)を受け取り、プラグインの初期化を行う。</td>    <td>TTXBind()の呼び出し後にすぐに実行される関数で、Tera Term本体のグローバル変数(ts, cv)を受け取り、プラグインの初期化を行う。</td>
185  </tr>  </tr>
186    
187  <tr>  <tr>
188    <td>TTXGetUIHooks</td>    <td>TTXGetUIHooks</td>
189    <td>ダイアログのハンドルをフックするための関数。Tera Term本体のダイアログインターフェイスを変更したい場合に使う。フック対象の関数は以下のとおり。<br>    <td>ダイアログのハンドルをフックするための関数。Tera Term本体のダイアログインターフェイスを変更したい場合に使う。フック対象の関数は以下のとおり。<br>
190        &SetupTerminal, &SetupWin, &SetupKeyboard, &SetupSerialPort,        &SetupTerminal, &SetupWin, &SetupKeyboard, &SetupSerialPort,
191        &SetupTCPIP, &GetHostName, &ChangeDirectory, &AboutDialog,          &SetupTCPIP, &GetHostName, &ChangeDirectory, &AboutDialog,  
192        &ChooseFontDlg, &SetupGeneral, &WindowWindow                        &ChooseFontDlg, &SetupGeneral, &WindowWindow                
193    </td>    </td>
194  </tr>  </tr>
195    
196  <tr>  <tr>
197    <td>TTXGetSetupHooks</td>    <td>TTXGetSetupHooks</td>
198    <td>セットアップルーチンをフックするための関数。フックした側は、元の関数も責任を持って呼び出す必要がある。複数のプラグインが存在する場合、関数がチェインされていく。フック対象の関数は以下のとおり。<br>    <td>セットアップルーチンをフックするための関数。フックした側は、元の関数も責任を持って呼び出す必要がある。複数のプラグインが存在する場合、関数がチェインされていく。フック対象の関数は以下のとおり。<br>
199        &ReadIniFile, &WriteIniFile, &ReadKeyboardCnf, &CopyHostList,          &ReadIniFile, &WriteIniFile, &ReadKeyboardCnf, &CopyHostList,  
200        &AddHostToList, &ParseParam                                            &AddHostToList, &ParseParam                                    
201    </td>    </td>
202  </tr>  </tr>
203    
204  <tr>  <tr>
205    <td>TTXOpenTCP</td>    <td>TTXOpenTCP</td>
206    <td>TCP接続を行うときに呼び出される関数。シリアル接続のときは呼び出されない。また、以下のソケットインターフェイスをフックすることもできる。<br>    <td>TCP接続を行うときに呼び出される関数。シリアル接続のときは呼び出されない。また、以下のソケットインターフェイスをフックすることもできる。<br>
207        &Pclosesocket, &Pconnect, &Phtonl, &Phtons, &Pinet_addr,          &Pclosesocket, &Pconnect, &Phtonl, &Phtons, &Pinet_addr,  
208        &Pioctlsocket, &Precv, &Pselect, &Psend, &Psetsockopt,            &Pioctlsocket, &Precv, &Pselect, &Psend, &Psetsockopt,    
209        &Psocket, &PWSAAsyncSelect, &PWSAAsyncGetHostByName,              &Psocket, &PWSAAsyncSelect, &PWSAAsyncGetHostByName,      
210        &PWSACancelAsyncRequest, &PWSAGetLastError                        &PWSACancelAsyncRequest, &PWSAGetLastError                
211    </td>    </td>
212  </tr>  </tr>
213    
214  <tr>  <tr>
215    <td>TTXCloseTCP</td>    <td>TTXCloseTCP</td>
216    <td>TCPコネクションが切断されるときに呼び出される関数。シリアル接続のときは呼び出されない。下記のうちフックしたインターフェイスがあるならば、元に戻す必要がある。<br>    <td>TCPコネクションが切断されるときに呼び出される関数。シリアル接続のときは呼び出されない。下記のうちフックしたインターフェイスがあるならば、元に戻す必要がある。<br>
217        &Pclosesocket, &Pconnect, &Phtonl, &Phtons, &Pinet_addr,          &Pclosesocket, &Pconnect, &Phtonl, &Phtons, &Pinet_addr,  
218        &Pioctlsocket, &Precv, &Pselect, &Psend, &Psetsockopt,            &Pioctlsocket, &Precv, &Pselect, &Psend, &Psetsockopt,    
219        &Psocket, &PWSAAsyncSelect, &PWSAAsyncGetHostByName,              &Psocket, &PWSAAsyncSelect, &PWSAAsyncGetHostByName,      
220        &PWSACancelAsyncRequest, &PWSAGetLastError                        &PWSACancelAsyncRequest, &PWSAGetLastError                
221    </td>    </td>
222  </tr>  </tr>
223    
224  <tr>  <tr>
225    <td>TTXSetWinSize</td>    <td>TTXSetWinSize</td>
226    <td>Tera Termウィンドウの画面サイズが変更されたときに呼び出される関数。</td>    <td>Tera Termウィンドウの画面サイズが変更されたときに呼び出される関数。</td>
227  </tr>  </tr>
228    
229  <tr>  <tr>
230    <td>TTXModifyMenu</td>    <td>TTXModifyMenu</td>
231    <td>Tera Termのメニューが初期化されるときに呼び出される関数。プラグイン用のメニューを挿入したい場合に使われる。    <td>Tera Termのメニューが初期化されるときに呼び出される関数。プラグイン用のメニューを挿入したい場合に使われる。
232    </td>    </td>
233  </tr>  </tr>
234    
235  <tr>  <tr>
236    <td>TTXModifyPopupMenu</td>    <td>TTXModifyPopupMenu</td>
237    <td>Tera Termのポップアップメニューが初期化されるときに呼び出される関数。プラグイン用のポップアップメニューを挿入したい場合に使われる。</td>    <td>Tera Termのポップアップメニューが初期化されるときに呼び出される関数。プラグイン用のポップアップメニューを挿入したい場合に使われる。</td>
238  </tr>  </tr>
239    
240  <tr>  <tr>
241    <td>TTXProcessCommand</td>    <td>TTXProcessCommand</td>
242    <td>メニューが呼び出されたときに実行される関数。プラグイン用のメニューを処理したいときに使われる。    <td>メニューが呼び出されたときに実行される関数。プラグイン用のメニューを処理したいときに使われる。
243    </td>    </td>
244  </tr>  </tr>
245    
246  <tr>  <tr>
247    <td>TTXEnd</td>    <td>TTXEnd</td>
248    <td>Tera Term本体が終了するときに呼び出される関数。</td>    <td>Tera Term本体が終了するときに呼び出される関数。</td>
249  </tr>  </tr>
250    
251  <tr>  <tr>
252    <td>TTXSetCommandLine</td>    <td>TTXSetCommandLine</td>
253    <td>新規接続やセッションの複製を行うときに、コマンドラインパラメータの処理を行うときに呼び出される関数。プラグイン独自のオプションを追加したときは、ここで処理される。    <td>新規接続やセッションの複製を行うときに、コマンドラインパラメータの処理を行うときに呼び出される関数。プラグイン独自のオプションを追加したときは、ここで処理される。
254    </td>    </td>
255  </tr>  </tr>
256    
257  </table>  </table>
258   </p>   </p>
259      
260    
261  <hr>  <hr>
262    
263    
264    
265  <h2><a name="configuration">設定ファイルの読み書き</a></h2>  <h2><a name="configuration">設定ファイルの読み書き</a></h2>
266   Windowsではアプリケーションのデータ保存のために、レジストリが伝統的に利用されていますが、Tera Termではその誕生がWindows 3.1までに遡るために、.iniファイルによるローカルディレクトリへの保存方法が標準となっています。<br>   Windowsではアプリケーションのデータ保存のために、レジストリが伝統的に利用されていますが、Tera Termではその誕生がWindows 3.1までに遡るために、.iniファイルによるローカルディレクトリへの保存方法が標準となっています。<br>
267   パッケージに同梱されるCollectorやLogMeTT、CygTermに関してもローカルディレクトリへデータが保存されます。<br>   パッケージに同梱されるCollectorやLogMeTT、CygTermに関してもローカルディレクトリへデータが保存されます。<br>
268   例外として、TeraTerm Menuはデフォルトでレジストリへ保存をします。カレントディレクトリに"ttpmenu.ini"(0バイトで可)を設置することで、レジストリの代わりに.iniファイルを使うようにすることもできます。<br>   例外として、TeraTerm Menuはデフォルトでレジストリへ保存をします。カレントディレクトリに"ttpmenu.ini"(0バイトで可)を設置することで、レジストリの代わりに.iniファイルを使うようにすることもできます。<br>
269   <br>   <br>
270      
271   teraterm.iniファイルにエントリを追加した場合は、ReadIniFile()#ttset.cに設定を読み込みするようにします。   teraterm.iniファイルにエントリを追加した場合は、ReadIniFile()#ttset.cに設定を読み込みするようにします。
272    
273  <pre class=code>  <pre class=code>
274          ts->ConfirmChangePaste =          ts->ConfirmChangePaste =
275                  GetOnOff(Section, "ConfirmChangePaste", FName, TRUE);                  GetOnOff(Section, "ConfirmChangePaste", FName, TRUE);
276  </pre>  </pre>
277    
278   WriteIniFile()#ttset.c に設定を書き込みするようにします。   WriteIniFile()#ttset.c に設定を書き込みするようにします。
279    
280  <pre class=code>  <pre class=code>
281          WriteOnOff(Section, "ConfirmChangePaste", FName,          WriteOnOff(Section, "ConfirmChangePaste", FName,
282                  ts->ConfirmChangePaste);                  ts->ConfirmChangePaste);
283  </pre>  </pre>
284    
285   エントリに文字列を設定する場合は、Win32APIのGetPrivateProfileString()とWritePrivateProfileString()を使います。数値を扱いたい場合は、GetPrivateProfileInt()とWriteInt()を使います。   エントリに文字列を設定する場合は、Win32APIのGetPrivateProfileString()とWritePrivateProfileString()を使います。数値を扱いたい場合は、GetPrivateProfileInt()とWriteInt()を使います。
286    
287  <hr>  <hr>
288    
289    
290    
291  <h2><a name="secure">セキュアプログラミング</a></h2>  <h2><a name="secure">セキュアプログラミング</a></h2>
292   WindowsのデフォルトアカウントはAdministrator権限を保持するために(ただし、Windows Vistaには当てはまらない)、アプリケーションにバッファオーバーフローの不具合があると、管理者権限を第三者に奪取されてしまう危険性があります。<br>   WindowsのデフォルトアカウントはAdministrator権限を保持するために(ただし、Windows Vistaには当てはまらない)、アプリケーションにバッファオーバーフローの不具合があると、管理者権限を第三者に奪取されてしまう危険性があります。<br>
293   従来、C言語の文字列処理は開発者のミスにより、バッファオーバーフローが発生しやすいという状況にありました。そこで、MicrosoftはVisual Studio 2005から文字列処理関数のセキュリティ強化バージョンを提供するようになりました。<br>   従来、C言語の文字列処理は開発者のミスにより、バッファオーバーフローが発生しやすいという状況にありました。そこで、MicrosoftはVisual Studio 2005から文字列処理関数のセキュリティ強化バージョンを提供するようになりました。<br>
294   <br>   <br>
295    
296  <ul>  <ul>
297    <li><a href="http://msdn2.microsoft.com/ja-jp/library/8ef0s5kh(VS.80).aspx">CRT のセキュリティ強化(MSDNライブラリ)</a></li>    <li><a href="http://msdn2.microsoft.com/ja-jp/library/8ef0s5kh(VS.80).aspx">CRT のセキュリティ強化(MSDNライブラリ)</a></li>
298  </ul>  </ul>
299  <br>  <br>
300    
301   Tera Termではセキュリティ強化を図るため、文字列操作のほとんどをセキュリティ強化バージョンに置き換えています。以下に代替関数を示します。<br>   Tera Termではセキュリティ強化を図るため、文字列操作のほとんどをセキュリティ強化バージョンに置き換えています。以下に代替関数を示します。<br>
302   <br>   <br>
303    
304  <table border=1 align=center>  <table border=1 align=center>
305  <tr>  <tr>
306    <th>旧</th>    <th>旧</th>
307    <th>新</th>    <th>新</th>
308  </tr>  </tr>
309    
310  <tr>  <tr>
311    <td>sprintf(), _snprintf()</td>    <td>sprintf(), _snprintf()</td>
312    <td>_snprintf_s()</td>    <td>_snprintf_s()</td>
313  </tr>  </tr>
314    
315  <tr>  <tr>
316    <td>strcat(), strncat()</td>    <td>strcat(), strncat()</td>
317    <td>strncat_s()</td>    <td>strncat_s()</td>
318  </tr>  </tr>
319    
320  <tr>  <tr>
321    <td>strcpy(), strncpy()</td>    <td>strcpy(), strncpy()</td>
322    <td>strncpy_s()</td>    <td>strncpy_s()</td>
323  </tr>  </tr>
324  </table>  </table>
325   <br>   <br>
326      
327   デフォルトのロケールが適用されると、期待する動作とならないケースにおいては、_snprintf_s_l()を使用しています。<br>   デフォルトのロケールが適用されると、期待する動作とならないケースにおいては、_snprintf_s_l()を使用しています。<br>
328   いずれの関数においても、_s("secure")という接尾辞が付くため、見た目に区別が付きやすくなっています。当然のことながら、これらの関数はANSI C非互換です。<br>   いずれの関数においても、_s("secure")という接尾辞が付くため、見た目に区別が付きやすくなっています。当然のことながら、これらの関数はANSI C非互換です。<br>
329   <br>   <br>
330   なお、これらの関数を利用する際、Count引数(格納する最大文字数)には"_TRUNCATE"マクロを指定しており、バッファオーバーフローが発生する場合は、強制的にバッファの切り詰めを行っています。   なお、これらの関数を利用する際、Count引数(格納する最大文字数)には"_TRUNCATE"マクロを指定しており、バッファオーバーフローが発生する場合は、強制的にバッファの切り詰めを行っています。
331    
332  <hr>  <hr>
333    
334    
335    
336  <h2><a name="compatibility">古いバージョンのWindowsとの互換性維持</a></h2>  <h2><a name="compatibility">古いバージョンのWindowsとの互換性維持</a></h2>
337   Windowsのアプリケーションプログラムは、単一のバイナリファイルを変更することなく、新旧のバージョンのWindows上で起動できるようにするためには、アプリケーションプログラム側での工夫が必要です。<br>   Windowsのアプリケーションプログラムは、単一のバイナリファイルを変更することなく、新旧のバージョンのWindows上で起動できるようにするためには、アプリケーションプログラム側での工夫が必要です。<br>
338   たとえば、Windows2000で導入された SetLayeredWindowAttributes() APIを直接呼び出すと、WindowsNT4.0や98などではアプリケーションの起動に失敗するようになります。そのため、新しいAPIを呼び出すときは、LoadLibrary()を使って動的ロードするようにします。<br>   たとえば、Windows2000で導入された SetLayeredWindowAttributes() APIを直接呼び出すと、WindowsNT4.0や98などではアプリケーションの起動に失敗するようになります。そのため、新しいAPIを呼び出すときは、LoadLibrary()を使って動的ロードするようにします。<br>
339    
340  <pre class=code>  <pre class=code>
341  static BOOL MySetLayeredWindowAttributes(HWND hwnd, COLORREF crKey, BYTE bAlpha, DWORD dwFlags)  static BOOL MySetLayeredWindowAttributes(HWND hwnd, COLORREF crKey, BYTE bAlpha, DWORD dwFlags)
342  {  {
343          typedef BOOL (WINAPI *func)(HWND,COLORREF,BYTE,DWORD);          typedef BOOL (WINAPI *func)(HWND,COLORREF,BYTE,DWORD);
344          static HMODULE g_hmodUser32 = NULL;          static HMODULE g_hmodUser32 = NULL;
345          static func g_pSetLayeredWindowAttributes = NULL;          static func g_pSetLayeredWindowAttributes = NULL;
346    
347          if (g_hmodUser32 == NULL) {          if (g_hmodUser32 == NULL) {
348                  g_hmodUser32 = LoadLibrary("user32.dll");                  g_hmodUser32 = LoadLibrary("user32.dll");
349                  if (g_hmodUser32 == NULL)                  if (g_hmodUser32 == NULL)
350                          return FALSE;                          return FALSE;
351    
352                  g_pSetLayeredWindowAttributes =                  g_pSetLayeredWindowAttributes =
353                          (func)GetProcAddress(g_hmodUser32, "SetLayeredWindowAttributes");                          (func)GetProcAddress(g_hmodUser32, "SetLayeredWindowAttributes");
354          }          }
355    
356          if (g_pSetLayeredWindowAttributes == NULL)          if (g_pSetLayeredWindowAttributes == NULL)
357                  return FALSE;                  return FALSE;
358    
359          return g_pSetLayeredWindowAttributes(hwnd, crKey,          return g_pSetLayeredWindowAttributes(hwnd, crKey,
360                                               bAlpha, dwFlags);                                               bAlpha, dwFlags);
361  }  }
362  </pre>  </pre>
363    
364   いちいち、手で関数プロトタイプを書いていくのは面倒である場合は、「DLLの遅延読み込み」というしくみを利用すると、上記のような手順は不要です。いきなり、関数を呼び出すことができます。ダイレクトに呼び出したい関数がある場合、それが古いWindowsではサポートされていないものであるならば、Visual Studioのプロジェクト設定で、「DLLの遅延読み込み」に該当するDLLを指定しておきます。   いちいち、手で関数プロトタイプを書いていくのは面倒である場合は、「DLLの遅延読み込み」というしくみを利用すると、上記のような手順は不要です。いきなり、関数を呼び出すことができます。ダイレクトに呼び出したい関数がある場合、それが古いWindowsではサポートされていないものであるならば、Visual Studioのプロジェクト設定で、「DLLの遅延読み込み」に該当するDLLを指定しておきます。
365    
366  <hr>  <hr>
367    
368    
369  <h2><a name="debug">デバッグ手法</a></h2>  <h2><a name="debug">デバッグ手法</a></h2>
370  <h3>debug printf</h3>  <h3>debug printf</h3>
371   Windowsアプリケーションでは printf() が使えません。標準出力がどこにも割り当てられていないからです。AllocConsole()とfreopen()を使えば、Windowsアプリケーションにおいても printf() を利用することができます。<br>   Windowsアプリケーションでは printf() が使えません。標準出力がどこにも割り当てられていないからです。AllocConsole()とfreopen()を使えば、Windowsアプリケーションにおいても printf() を利用することができます。<br>
372   OutputDebugString()というAPIがあります。これは Visual Studio のデバッグコンソールにメッセージ出力することができる関数です。当該APIは、"Debug build"および"Release build"に関係なく、デバッガが存在すれば、メッセージを送信します。ゆえに、 Visual Studioがなくとも、<a href="http://www.vector.co.jp/soft/win95/prog/se046776.html">DBCon</a>のようなツールを使えば、アプリケーションの単体起動においても、OutputDebugString()によるメッセージを拾うことができます。<br>   OutputDebugString()というAPIがあります。これは Visual Studio のデバッグコンソールにメッセージ出力することができる関数です。当該APIは、"Debug build"および"Release build"に関係なく、デバッガが存在すれば、メッセージを送信します。ゆえに、 Visual Studioがなくとも、<a href="http://www.vector.co.jp/soft/win95/prog/se046776.html">DBCon</a>のようなツールを使えば、アプリケーションの単体起動においても、OutputDebugString()によるメッセージを拾うことができます。<br>
373   Tera Termでは、可変長引数を扱えるようにラッパー関数を用意しています。   Tera Termでは、可変長引数を扱えるようにラッパー関数を用意しています。
374      
375  <pre class=code>  <pre class=code>
376  void OutputDebugPrintf(char *fmt, ...) {  void OutputDebugPrintf(char *fmt, ...) {
377          char tmp[1024];          char tmp[1024];
378          va_list arg;          va_list arg;
379          va_start(arg, fmt);          va_start(arg, fmt);
380          _vsnprintf(tmp, sizeof(tmp), fmt, arg);          _vsnprintf(tmp, sizeof(tmp), fmt, arg);
381          OutputDebugString(tmp);          OutputDebugString(tmp);
382  }  }
383  </pre>  </pre>
384    
385  <h3>memory leak</h3>  <h3>memory leak</h3>
386   malloc()等による確保したヒープメモリの解放し忘れによる「メモリリーク」を、自動で検出するしくみが Visual Studio には用意されています。プログラムの起動時に、以下のコードを挿入するだけです。プログラムの終了時に、解放していないヒープメモリがあれば、 Visual Studio の「出力」ウィンドウにリストアップされます。   malloc()等による確保したヒープメモリの解放し忘れによる「メモリリーク」を、自動で検出するしくみが Visual Studio には用意されています。プログラムの起動時に、以下のコードを挿入するだけです。プログラムの終了時に、解放していないヒープメモリがあれば、 Visual Studio の「出力」ウィンドウにリストアップされます。
387    
388  <pre class=code>  <pre class=code>
389  #ifdef _DEBUG  #ifdef _DEBUG
390    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
391  #endif  #endif
392  </pre>  </pre>
393    
394   なお、Windowsのように仮想記憶で動くアプリケーションプログラムに関しては、プログラムの終了時に解放されていないメモリが存在した場合、OSが面倒を見て、メモリが解放されるようになっています。   なお、Windowsのように仮想記憶で動くアプリケーションプログラムに関しては、プログラムの終了時に解放されていないメモリが存在した場合、OSが面倒を見て、メモリが解放されるようになっています。
395    
396  <hr>  <hr>
397    
398    
399  <h2><a name="thread">マルチスレッド</a></h2>  <h2><a name="thread">マルチスレッド</a></h2>
400   Windowsのアプリケーションはマルチスレッドで設計されることがほとんどですが、Windows 3.1から95の時代ではあまり一般的ではありませんでした。そのため、元々Tera Termはマルチスレッド化されていません。ソースコードを見ると分かるように、グローバル変数が多用されているため、ほとんどの処理がスレッドセーフではありません。<br>   Windowsのアプリケーションはマルチスレッドで設計されることがほとんどですが、Windows 3.1から95の時代ではあまり一般的ではありませんでした。そのため、元々Tera Termはマルチスレッド化されていません。ソースコードを見ると分かるように、グローバル変数が多用されているため、ほとんどの処理がスレッドセーフではありません。<br>
401   ただし、一部の処理においては _beginthreadex() API を使ってスレッドが生成されています。以下にスレッド生成箇所を示します。   ただし、一部の処理においては _beginthreadex() API を使ってスレッドが生成されています。以下にスレッド生成箇所を示します。
402    
403  <p>  <p>
404  <div align=center><b>Tera Term</b></div>  <div align=center><b>Tera Term</b></div>
405  <table border=1 align=center>  <table border=1 align=center>
406  <tr>  <tr>
407    <th>生成箇所</th>    <th>生成箇所</th>
408    <th>ソースファイル</th>    <th>ソースファイル</th>
409  </tr>  </tr>
410    
411  <tr>  <tr>
412    <td>シリアル接続</td>    <td>シリアル接続</td>
413    <td>CommStart()#commlib.c</td>    <td>CommStart()#commlib.c</td>
414  </tr>  </tr>
415    
416  <tr>  <tr>
417    <td>TELNETキープアライブ</td>    <td>TELNETキープアライブ</td>
418    <td>TelStartKeepAliveThread()#telnet.c</td>    <td>TelStartKeepAliveThread()#telnet.c</td>
419  </tr>  </tr>
420    
421  <tr>  <tr>
422    <td>IPv4/v6ソケットの生成</td>    <td>IPv4/v6ソケットの生成</td>
423    <td>WSAAsyncGetAddrInfo()#WSAAsyncGetAddrInfo.c</td>    <td>WSAAsyncGetAddrInfo()#WSAAsyncGetAddrInfo.c</td>
424  </tr>  </tr>
425  </table>  </table>
426    
427  <br>  <br>
428    
429  <div align=center><b>TTSSH</b></div>  <div align=center><b>TTSSH</b></div>
430  <table border=1 align=center>  <table border=1 align=center>
431  <tr>  <tr>
432    <th>生成箇所</th>    <th>生成箇所</th>
433    <th>ソースファイル</th>    <th>ソースファイル</th>
434  </tr>  </tr>
435    
436  <tr>  <tr>
437    <td>SSHキープアライブ</td>    <td>SSHキープアライブ</td>
438    <td>start_ssh_heartbeat_thread()#ssh.c</td>    <td>start_ssh_heartbeat_thread()#ssh.c</td>
439  </tr>  </tr>
440    
441  <tr>  <tr>
442    <td>SCP送信処理</td>    <td>SCP送信処理</td>
443    <td>SSH2_scp_tolocal()#ssh.c</td>    <td>SSH2_scp_tolocal()#ssh.c</td>
444  </tr>  </tr>
445    
446  <tr>  <tr>
447    <td>SCP受信処理</td>    <td>SCP受信処理</td>
448    <td>SSH2_scp_fromremote()#ssh.c</td>    <td>SSH2_scp_fromremote()#ssh.c</td>
449  </tr>  </tr>
450  </table>  </table>
451  </p>  </p>
452    
453   すでに説明したとおり、Tera Term(TTSSH含む)の内部処理はスレッドセーフではないため、シンプルにスレッドを生成し、スレッド内から送受信処理等を行おうとすると、不具合が発生してしまいます。<br>   すでに説明したとおり、Tera Term(TTSSH含む)の内部処理はスレッドセーフではないため、シンプルにスレッドを生成し、スレッド内から送受信処理等を行おうとすると、不具合が発生してしまいます。<br>
454   TELNETやSSHのキープアライブ(ハートビート)処理を実現するためには、定期的にパケットの送信処理を行う必要があります。また、SCPによるファイル送受信を行う際においても、ファイルの送信処理中に、ユーザの端末操作のレスポンスを落とさないために、スレッドの使用が不可欠です。<br>   TELNETやSSHのキープアライブ(ハートビート)処理を実現するためには、定期的にパケットの送信処理を行う必要があります。また、SCPによるファイル送受信を行う際においても、ファイルの送信処理中に、ユーザの端末操作のレスポンスを落とさないために、スレッドの使用が不可欠です。<br>
455   そこで、マルチスレッドを使う場合は、モードレスダイアログを非表示で作成したあとに、_beginthreadex() APIでスレッドを生成し、実際の処理はモードレスダイアログに行わせるという手段を使用しています。このしくみにより、マルチスレッドを使いながら、スレッドセーフを保つことができます。以下に、コード例を示します。<br>   そこで、マルチスレッドを使う場合は、モードレスダイアログを非表示で作成したあとに、_beginthreadex() APIでスレッドを生成し、実際の処理はモードレスダイアログに行わせるという手段を使用しています。このしくみにより、マルチスレッドを使いながら、スレッドセーフを保つことができます。以下に、コード例を示します。<br>
456    
457  <pre class=code>  <pre class=code>
458  #define WM_SEND_HEARTBEAT (WM_USER + 1)  #define WM_SEND_HEARTBEAT (WM_USER + 1)
459    
460  static LRESULT CALLBACK telnet_heartbeat_dlg_proc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)  static LRESULT CALLBACK telnet_heartbeat_dlg_proc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
461  {  {
462    
463          switch (msg) {          switch (msg) {
464                  case WM_INITDIALOG:                  case WM_INITDIALOG:
465                          return FALSE;                          return FALSE;
466    
467                  case WM_SEND_HEARTBEAT:                  case WM_SEND_HEARTBEAT:
468                          TelSendNOP();                          TelSendNOP();
469                          return TRUE;                          return TRUE;
470                          break;                          break;
471    
472                  case WM_COMMAND:                  case WM_COMMAND:
473                          break;                          break;
474    
475                  case WM_CLOSE:                  case WM_CLOSE:
476                          return TRUE;                          return TRUE;
477    
478                  case WM_DESTROY:                  case WM_DESTROY:
479                          return TRUE;                          return TRUE;
480    
481                  default:                  default:
482                          return FALSE;                          return FALSE;
483          }          }
484          return TRUE;          return TRUE;
485  }  }
486    
487  static unsigned _stdcall TelKeepAliveThread(void *dummy) {  static unsigned _stdcall TelKeepAliveThread(void *dummy) {
488    static int instance = 0;    static int instance = 0;
489    
490    if (instance > 0)    if (instance > 0)
491      return 0;      return 0;
492    instance++;    instance++;
493    
494    while (cv.Open && nop_interval > 0) {    while (cv.Open && nop_interval > 0) {
495      if (time(NULL) >= cv.LastSendTime + nop_interval) {      if (time(NULL) >= cv.LastSendTime + nop_interval) {
496                  SendMessage(keepalive_dialog, WM_SEND_HEARTBEAT, 0, 0);                  SendMessage(keepalive_dialog, WM_SEND_HEARTBEAT, 0, 0);
497      }      }
498    
499      Sleep(100);      Sleep(100);
500    }    }
501    instance--;    instance--;
502    return 0;    return 0;
503  }  }
504    
505  void TelStartKeepAliveThread() {  void TelStartKeepAliveThread() {
506    unsigned tid;    unsigned tid;
507    
508    if (ts.TelKeepAliveInterval > 0) {    if (ts.TelKeepAliveInterval > 0) {
509      nop_interval = ts.TelKeepAliveInterval;      nop_interval = ts.TelKeepAliveInterval;
510    
511          keepalive_dialog = CreateDialog(hInst, MAKEINTRESOURCE(IDD_BROADCAST_DIALOG),          keepalive_dialog = CreateDialog(hInst, MAKEINTRESOURCE(IDD_BROADCAST_DIALOG),
512                 HVTWin, (DLGPROC)telnet_heartbeat_dlg_proc);                 HVTWin, (DLGPROC)telnet_heartbeat_dlg_proc);
513    
514      keepalive_thread = (HANDLE)_beginthreadex(NULL, 0, TelKeepAliveThread, NULL, 0, &tid);      keepalive_thread = (HANDLE)_beginthreadex(NULL, 0, TelKeepAliveThread, NULL, 0, &tid);
515      if (keepalive_thread == (HANDLE)-1) {      if (keepalive_thread == (HANDLE)-1) {
516        nop_interval = 0;        nop_interval = 0;
517      }      }
518    }    }
519  }  }
520  </pre>  </pre>
521    
522    
523    
524  <hr>  <hr>
525    
526    
527  <h2><a name="dde">DDEによるプロセス間通信</a></h2>  <h2><a name="dde">DDEによるプロセス間通信</a></h2>
528    <h3>概要</h3>    <h3>概要</h3>
529   DDE(Dynamic Data Exchange)の誕生は、1987年のWindows 2.0までに遡ります。DDEはプロセス間通信を行うためのしくみですが、現在ではレガシーな方式であり、ほとんどのアプリケーションでは利用されていません。Windowsにおけるプロセス間通信といえば、メールスロットや名前付きパイプ、OLEなどが定番です。<br>   DDE(Dynamic Data Exchange)の誕生は、1987年のWindows 2.0までに遡ります。DDEはプロセス間通信を行うためのしくみですが、現在ではレガシーな方式であり、ほとんどのアプリケーションでは利用されていません。Windowsにおけるプロセス間通信といえば、メールスロットや名前付きパイプ、OLEなどが定番です。<br>
530   かつては、DDEによるプロセス間の通信データをキャプチャすることができる「DDEスパイ」(DDESPY.EXE)というツールがVisual Studioに付属していましたが、現在のVisual Studioにはもはや含まれていません。<br>   かつては、DDEによるプロセス間の通信データをキャプチャすることができる「DDEスパイ」(DDESPY.EXE)というツールがVisual Studioに付属していましたが、現在のVisual Studioにはもはや含まれていません。<br>
531   DDEに関するリファレンスはMSDNライブラリから参照することができます。<br>   DDEに関するリファレンスはMSDNライブラリから参照することができます。<br>
532    
533  <p>  <p>
534  <ul>  <ul>
535    <li><a href="http://msdn2.microsoft.com/en-us/library/ms648711(VS.85).aspx">Dynamic Data Exchange(MSDNライブラリ)</a></li>    <li><a href="http://msdn2.microsoft.com/en-us/library/ms648711(VS.85).aspx">Dynamic Data Exchange(MSDNライブラリ)</a></li>
536    <li><a href="http://msdn2.microsoft.com/en-us/library/ms648712(VS.85).aspx">Dynamic Data Exchange Management Library(MSDNライブラリ)</a></li>    <li><a href="http://msdn2.microsoft.com/en-us/library/ms648712(VS.85).aspx">Dynamic Data Exchange Management Library(MSDNライブラリ)</a></li>
537  </ul>  </ul>
538  </p>  </p>
539    
540   DDEは、TCPによるネットワーク通信と似ており、サーバとクライアント間を一対一で接続し、通信を行います。アプリケーションがDDEによる通信を行うために、DDEML(Dynamic Data Exchange Management Library)と呼ばれるライブラリをWin32 APIとして提供されています。<br>   DDEは、TCPによるネットワーク通信と似ており、サーバとクライアント間を一対一で接続し、通信を行います。アプリケーションがDDEによる通信を行うために、DDEML(Dynamic Data Exchange Management Library)と呼ばれるライブラリをWin32 APIとして提供されています。<br>
541   DDE通信を行うために、一方がサーバとなり、他方がクライアントになる必要があります。また、通信のセッションをシステム全体でユニークとするために、識別情報が必要です。TCP通信ではIPアドレスとポート番号が使われますが、DDE通信では「サービス名」と「トピック名」の組み合わせが使われます。Tera Termではサービス名は"TERATERM"という文字列が使われ、トピック名はTera Term本体のウィンドウハンドル(HVTWin)の16進数値を文字列化したものが使われています。<br>   DDE通信を行うために、一方がサーバとなり、他方がクライアントになる必要があります。また、通信のセッションをシステム全体でユニークとするために、識別情報が必要です。TCP通信ではIPアドレスとポート番号が使われますが、DDE通信では「サービス名」と「トピック名」の組み合わせが使われます。Tera Termではサービス名は"TERATERM"という文字列が使われ、トピック名はTera Term本体のウィンドウハンドル(HVTWin)の16進数値を文字列化したものが使われています。<br>
542   このようなしくみになっているために、マクロスクリプトからまったく別のTera Termへコマンドを送ることはできません。<br>   このようなしくみになっているために、マクロスクリプトからまったく別のTera Termへコマンドを送ることはできません。<br>
543    
544  <div align="center">  <div align="center">
545  <img src="image/dde.png" width=720 height=540>  <img src="image/dde.png" width=720 height=540>
546  </div>  </div>
547    
548   上図に示すように、Tera Term本体("ttermpro.exe")がDDEサーバとなり、マクロプログラム("ttpmacro.exe")がDDEクライアントとなります。DDEでは、やりとりするデータの塊のことを「トランザクション」と呼びます。トランザクションには以下に示すような何種類かがあります。タイプは"ddeml.h"でマクロ定義されています。<br>   上図に示すように、Tera Term本体("ttermpro.exe")がDDEサーバとなり、マクロプログラム("ttpmacro.exe")がDDEクライアントとなります。DDEでは、やりとりするデータの塊のことを「トランザクション」と呼びます。トランザクションには以下に示すような何種類かがあります。タイプは"ddeml.h"でマクロ定義されています。<br>
549    
550  <p>  <p>
551  <table border=1 align=center>  <table border=1 align=center>
552  <tr>  <tr>
553    <th>タイプ</th>    <th>タイプ</th>
554    <th>意味</th>    <th>意味</th>
555  </tr>  </tr>
556    
557  <tr>  <tr>
558    <td>XTYP_ADVREQ</td>    <td>XTYP_ADVREQ</td>
559    <td>DDEサーバがクライアントへデータを送るために、DDEサーバが自分自身に送るメッセージ。</td>    <td>DDEサーバがクライアントへデータを送るために、DDEサーバが自分自身に送るメッセージ。</td>
560  </tr>  </tr>
561    
562  <tr>  <tr>
563    <td>XTYP_POKE</td>    <td>XTYP_POKE</td>
564    <td>DDEクライアントからサーバへデータを送る。</td>    <td>DDEクライアントからサーバへデータを送る。</td>
565  </tr>  </tr>
566    
567  <tr>  <tr>
568    <td>XTYP_ADVSTART</td>    <td>XTYP_ADVSTART</td>
569    <td>DDEサーバに対してアドバイズループの開始を指示する。</td>    <td>DDEサーバに対してアドバイズループの開始を指示する。</td>
570  </tr>  </tr>
571    
572  <tr>  <tr>
573    <td>XTYP_ADVDATA</td>    <td>XTYP_ADVDATA</td>
574    <td>DDEクライアントにデータを定期的に送る。</td>    <td>DDEクライアントにデータを定期的に送る。</td>
575  </tr>  </tr>
576    
577  <tr>  <tr>
578    <td>XTYP_EXECUTE</td>    <td>XTYP_EXECUTE</td>
579    <td>DDEサーバに文字列を送り、何らかの処理をサーバに指示する。</td>    <td>DDEサーバに文字列を送り、何らかの処理をサーバに指示する。</td>
580  </tr>  </tr>
581    
582  </table>  </table>
583  </p>  </p>
584    
585   DDE通信の特徴として、アドバイズループ(advise loop)という概念があります。DDEサーバがアドバイズループに入ると、クライアントはサーバから定期的にデータを受け取り続けることができます。Tera Termでは、リモートホストからの受信データを、マクロプログラムへ渡すために、アドバイズループが使われています。<br>   DDE通信の特徴として、アドバイズループ(advise loop)という概念があります。DDEサーバがアドバイズループに入ると、クライアントはサーバから定期的にデータを受け取り続けることができます。Tera Termでは、リモートホストからの受信データを、マクロプログラムへ渡すために、アドバイズループが使われています。<br>
586    
587    <h3>ライブラリ</h3>    <h3>ライブラリ</h3>
588   Tera Termで使われているDDEMLについて、以下に示します。   Tera Termで使われているDDEMLについて、以下に示します。
589      
590      
591  <p>  <p>
592  <table border=1 align=center>  <table border=1 align=center>
593  <tr>  <tr>
594    <th>関数名</th>    <th>関数名</th>
595    <th>機能</th>    <th>機能</th>
596  </tr>  </tr>
597    
598  <tr>  <tr>
599    <td>DdeInitialize</td>    <td>DdeInitialize</td>
600    <td>DDEを初期化し、コールバック関数を登録する。初期化できるとインスタンスを返す。</td>    <td>DDEを初期化し、コールバック関数を登録する。初期化できるとインスタンスを返す。</td>
601  </tr>  </tr>
602    
603  <tr>  <tr>
604    <td>DdeCreateStringHandle</td>    <td>DdeCreateStringHandle</td>
605    <td>文字列リテラルからハンドルを作成する。ハンドルはサーバとクライアントの通信用に使われる。</td>    <td>文字列リテラルからハンドルを作成する。ハンドルはサーバとクライアントの通信用に使われる。</td>
606  </tr>  </tr>
607    
608  <tr>  <tr>
609    <td>DdeNameService</td>    <td>DdeNameService</td>
610    <td>インスタンスとサービス名("TERATERM")をサーバに登録する。登録後、XTYP_REGISTERトランザクションがクライアントへ送られる。登録解除する際にも使われる。</td>    <td>インスタンスとサービス名("TERATERM")をサーバに登録する。登録後、XTYP_REGISTERトランザクションがクライアントへ送られる。登録解除する際にも使われる。</td>
611  </tr>  </tr>
612    
613  <tr>  <tr>
614    <td>DdeCmpStringHandles</td>    <td>DdeCmpStringHandles</td>
615    <td>2つの文字列ハンドルを比較する。</td>    <td>2つの文字列ハンドルを比較する。</td>
616  </tr>  </tr>
617    
618  <tr>  <tr>
619    <td>DdeClientTransaction</td>    <td>DdeClientTransaction</td>
620    <td>クライアントからサーバへトランザクションを送ることができる。トランザクションタイプとして、XTYP_REQUEST・XTYP_EXECUTE・XTYP_ADVSTART・XTYP_POKEなどが指定できる。サーバからのACKを待つまでのタイムアウト時間を指定することができ、Tera Termではほとんど"1000ミリ秒(1秒)"が指定されている。ただし、ACKを確認するケースにおいては"5000ミリ秒(5秒)"が指定されている。</td>    <td>クライアントからサーバへトランザクションを送ることができる。トランザクションタイプとして、XTYP_REQUEST・XTYP_EXECUTE・XTYP_ADVSTART・XTYP_POKEなどが指定できる。サーバからのACKを待つまでのタイムアウト時間を指定することができ、Tera Termではほとんど"1000ミリ秒(1秒)"が指定されている。ただし、ACKを確認するケースにおいては"5000ミリ秒(5秒)"が指定されている。</td>
621  </tr>  </tr>
622    
623  <tr>  <tr>
624    <td>DdeAccessData</td>    <td>DdeAccessData</td>
625    <td>DDEハンドルから実際のデータへのポインタを取得する。データの取り出しが終わったら、DdeUnaccessData()を呼び出すこと。</td>    <td>DDEハンドルから実際のデータへのポインタを取得する。データの取り出しが終わったら、DdeUnaccessData()を呼び出すこと。</td>
626  </tr>  </tr>
627    
628  <tr>  <tr>
629    <td>DdeCreateDataHandle</td>    <td>DdeCreateDataHandle</td>
630    <td>DDEオブジェクトを作成し、ハンドルを返す。DDEサーバのアドバイズループや、XTYP_REQUESTトランザクション受信時に、DDEクライアントへデータを送るために使われている。</td>    <td>DDEオブジェクトを作成し、ハンドルを返す。DDEサーバのアドバイズループや、XTYP_REQUESTトランザクション受信時に、DDEクライアントへデータを送るために使われている。</td>
631  </tr>  </tr>
632    
633  <tr>  <tr>
634    <td>DdeGetData</td>    <td>DdeGetData</td>
635    <td>DDEオブジェクトからバッファへコピーする。</td>    <td>DDEオブジェクトからバッファへコピーする。</td>
636  </tr>  </tr>
637    
638  <tr>  <tr>
639    <td>DdeDisconnect</td>    <td>DdeDisconnect</td>
640    <td>DDE通信を終了する</td>    <td>DDE通信を終了する</td>
641  </tr>  </tr>
642    
643  <tr>  <tr>
644    <td>DdePostAdvise</td>    <td>DdePostAdvise</td>
645    <td>DDEサーバ側で使われる関数で、自分自身に XTYP_ADVREQ トランザクションを送る。</td>    <td>DDEサーバ側で使われる関数で、自分自身に XTYP_ADVREQ トランザクションを送る。</td>
646  </tr>  </tr>
647    
648  </table>  </table>
649  </p>  </p>
650    
651    
652    
653    <h3>実装</h3>    <h3>実装</h3>
654   DDEサーバ側の実装について見ていきます。Tera Term本体("ttermpro.exe")がDDEサーバとなり、かならずDDEサーバから起動されます。マクロプログラム("ttpmacro.exe")から直接マクロスクリプトが実行されるケースにおいても、"connect"マクロによりDDE接続をしないと、通信が開始できません。<br>   DDEサーバ側の実装について見ていきます。Tera Term本体("ttermpro.exe")がDDEサーバとなり、かならずDDEサーバから起動されます。マクロプログラム("ttpmacro.exe")から直接マクロスクリプトが実行されるケースにおいても、"connect"マクロによりDDE接続をしないと、通信が開始できません。<br>
655   Tera TermのControlメニューからMacroを呼び出した場合、RunMacro()#ttdde.c がコールされます。<br>   Tera TermのControlメニューからMacroを呼び出した場合、RunMacro()#ttdde.c がコールされます。<br>
656   HVTWinウィンドウハンドルからトピック名(8バイト)を作成し、DDEの初期化とサーバの登録を行います。また、このタイミングでDDEバッファ(1KB)を作成しています。その後、"ttpmacro.exe"を /D= オプションでトピック名を渡しつつ、起動をします。<br>   HVTWinウィンドウハンドルからトピック名(8バイト)を作成し、DDEの初期化とサーバの登録を行います。また、このタイミングでDDEバッファ(1KB)を作成しています。その後、"ttpmacro.exe"を /D= オプションでトピック名を渡しつつ、起動をします。<br>
657      
658  <pre class=code>  <pre class=code>
659          SetTopic();          SetTopic();
660          if (! InitDDE()) return;          if (! InitDDE()) return;
661          strncpy_s(Cmnd, sizeof(Cmnd),"TTPMACRO /D=", _TRUNCATE);          strncpy_s(Cmnd, sizeof(Cmnd),"TTPMACRO /D=", _TRUNCATE);
662          strncat_s(Cmnd,sizeof(Cmnd),TopicName,_TRUNCATE);          strncat_s(Cmnd,sizeof(Cmnd),TopicName,_TRUNCATE);
663  </pre>  </pre>
664    
665   DDEサーバに、DDEクライアントからトランザクションが送られてきたときは、DdeCallbackProcコールバック関数が呼び出されます。コールバック関数は、DdeInitialize()でDDEの初期化を行うときに登録されます。<br><br>   DDEサーバに、DDEクライアントからトランザクションが送られてきたときは、DdeCallbackProcコールバック関数が呼び出されます。コールバック関数は、DdeInitialize()でDDEの初期化を行うときに登録されます。<br><br>
666      
667   次に、DDEクライアントについて見てみましょう。マクロプログラムの起動時、InitDDE()#ttmdde.c が呼び出され、DDEクライアントとして初期化が行われます。DDEの初期化は、DdeInitialize()で行われ、同時にDdeCallbackProcコールバック関数が登録されます。DDEサーバから届いたトランザクションは、コールバック関数で処理されます。<br>   次に、DDEクライアントについて見てみましょう。マクロプログラムの起動時、InitDDE()#ttmdde.c が呼び出され、DDEクライアントとして初期化が行われます。DDEの初期化は、DdeInitialize()で行われ、同時にDdeCallbackProcコールバック関数が登録されます。DDEサーバから届いたトランザクションは、コールバック関数で処理されます。<br>
668   DDE通信を始めるためには、DdeConnect()を呼び出し、サーバと接続する必要があります。次に、"ttpmacro.exe"のウィンドウハンドル(HWin)をサーバへ通知するために、XTYP_EXECUTEトランザクションで送ります。最後に、XTYP_ADVSTARTトランザクションをサーバへ送り、アドバイズループを開始します。<br>   DDE通信を始めるためには、DdeConnect()を呼び出し、サーバと接続する必要があります。次に、"ttpmacro.exe"のウィンドウハンドル(HWin)をサーバへ通知するために、XTYP_EXECUTEトランザクションで送ります。最後に、XTYP_ADVSTARTトランザクションをサーバへ送り、アドバイズループを開始します。<br>
669    
670  <pre class=code>  <pre class=code>
671    ConvH = DdeConnect(Inst, Service, Topic, NULL);    ConvH = DdeConnect(Inst, Service, Topic, NULL);
672    if (ConvH == 0) return FALSE;    if (ConvH == 0) return FALSE;
673    Linked = TRUE;    Linked = TRUE;
674    
675    Cmd[0] = CmdSetHWnd;    Cmd[0] = CmdSetHWnd;
676    w = HIWORD(HWin);    w = HIWORD(HWin);
677    Word2HexStr(w,&(Cmd[1]));    Word2HexStr(w,&(Cmd[1]));
678    w = LOWORD(HWin);    w = LOWORD(HWin);
679    Word2HexStr(w,&(Cmd[5]));    Word2HexStr(w,&(Cmd[5]));
680    
681    DdeClientTransaction(Cmd,strlen(Cmd)+1,ConvH,0,    DdeClientTransaction(Cmd,strlen(Cmd)+1,ConvH,0,
682      CF_OEMTEXT,XTYP_EXECUTE,1000,NULL);      CF_OEMTEXT,XTYP_EXECUTE,1000,NULL);
683    
684    DdeClientTransaction(NULL,0,ConvH,Item,    DdeClientTransaction(NULL,0,ConvH,Item,
685      CF_OEMTEXT,XTYP_ADVSTART,1000,NULL);      CF_OEMTEXT,XTYP_ADVSTART,1000,NULL);
686  </pre>  </pre>
687    
688    
689    <h3>バッファの管理</h3>    <h3>バッファの管理</h3>
690   マクロプログラムでは"wait"コマンド等で、リモートホストから送られてきたデータを監視するための機能が用意されています。この機能を実現するためには、Tera Term本体とマクロプログラムのそれぞれにおいて、バッファを用意する必要があり、プロセス間通信(DDEトランザクション)により、Tera Term本体からマクロプログラムへリモートホストからの受信データを送らなければなりません。<br>   マクロプログラムでは"wait"コマンド等で、リモートホストから送られてきたデータを監視するための機能が用意されています。この機能を実現するためには、Tera Term本体とマクロプログラムのそれぞれにおいて、バッファを用意する必要があり、プロセス間通信(DDEトランザクション)により、Tera Term本体からマクロプログラムへリモートホストからの受信データを送らなければなりません。<br>
691    
692  <div align="center">  <div align="center">
693  <img src="image/dde_flowcontrol.png" width=720 height=540>  <img src="image/dde_flowcontrol.png" width=720 height=540>
694  </div>  </div>
695    
696   まず、Tera Term本体におけるリモートホストからのTCPパケット受信は、アイドルループ OnIdle()#teraterm.cpp にて行われます。OnIdle()から呼び出される CommReceive()#commlib.c において、TCPパケットデータをバッファ(cv->InBuff[])に格納します。このバッファは 1KB の大きさを持ちます。また、リングバッファではないため、バッファフルになった場合は、TCPパケットの受信をしません。ただし、バッファフル状態が長く続くと、Windowsカーネル内にTCPパケットが溜まっていき、いずれはリモートホストからのパケットを受信できなくなる可能性があります。<br>   まず、Tera Term本体におけるリモートホストからのTCPパケット受信は、アイドルループ OnIdle()#teraterm.cpp にて行われます。OnIdle()から呼び出される CommReceive()#commlib.c において、TCPパケットデータをバッファ(cv->InBuff[])に格納します。このバッファは 1KB の大きさを持ちます。また、リングバッファではないため、バッファフルになった場合は、TCPパケットの受信をしません。ただし、バッファフル状態が長く続くと、Windowsカーネル内にTCPパケットが溜まっていき、いずれはリモートホストからのパケットを受信できなくなる可能性があります。<br>
697   エスケープシーケンスの解析処理を行う過程で、「ログ採取」か「マクロ実行」を行っている場合は、LogPut1()が呼び出され、DDEバッファ(cv.LogBuf[])へ受信データが格納されます。このバッファは1KBの大きさを持つリングバッファであり、バッファフルになった場合は、最古のデータから上書きされてゆきます。<br>   エスケープシーケンスの解析処理を行う過程で、「ログ採取」か「マクロ実行」を行っている場合は、LogPut1()が呼び出され、DDEバッファ(cv.LogBuf[])へ受信データが格納されます。このバッファは1KBの大きさを持つリングバッファであり、バッファフルになった場合は、最古のデータから上書きされてゆきます。<br>
698   Tera Term本体のDDEバッファのデータは、エスケープシーケンスの解析処理が完了後、DDEAdv()#ttdde.c がすぐに呼び出され、自分自身(DDEサーバ)へ XTYP_ADVREQ トランザクションを送ります。XTYP_ADVREQを受け取ったら、DDEコールバック関数 DdeCallbackProc() が呼び出され、マクロプログラムへのデータ送信を行います。ここでアドバイズループが使われています。<br>   Tera Term本体のDDEバッファのデータは、エスケープシーケンスの解析処理が完了後、DDEAdv()#ttdde.c がすぐに呼び出され、自分自身(DDEサーバ)へ XTYP_ADVREQ トランザクションを送ります。XTYP_ADVREQを受け取ったら、DDEコールバック関数 DdeCallbackProc() が呼び出され、マクロプログラムへのデータ送信を行います。ここでアドバイズループが使われています。<br>
699    
700  <div align="center">  <div align="center">
701  <img src="image/dde_buffer.png" width=720 height=540>  <img src="image/dde_buffer.png" width=720 height=540>
702  </div>  </div>
703    
704   アドバイズループによりDDEサーバよりデータが送られてくると、DDEクライアントであるマクロプログラムにおいては、XTYP_ADVDATAトランザクションがDDEコールバック関数 DdeCallbackProc()#ttmdde.c により処理されます。<br>   アドバイズループによりDDEサーバよりデータが送られてくると、DDEクライアントであるマクロプログラムにおいては、XTYP_ADVDATAトランザクションがDDEコールバック関数 DdeCallbackProc()#ttmdde.c により処理されます。<br>
705      
706   なお、Tera Term本体において、DDE通信用のバッファと、ログ採取用のバッファは cv.LogBuf[] で共有されています。バッファの先頭とデータサイズを表すインデックスは、DDE通信の場合は"DStart"と"Dcount"、ログ採取の場合は"LStart"と"Lcount"と区別されています。実際には、1つのバッファを共有しているわけなので、それぞれのインデックスが食い違うと、誤動作する原因となるため、常に同期を取っておくことになります。<br>   なお、Tera Term本体において、DDE通信用のバッファと、ログ採取用のバッファは cv.LogBuf[] で共有されています。バッファの先頭とデータサイズを表すインデックスは、DDE通信の場合は"DStart"と"Dcount"、ログ採取の場合は"LStart"と"Lcount"と区別されています。実際には、1つのバッファを共有しているわけなので、それぞれのインデックスが食い違うと、誤動作する原因となるため、常に同期を取っておくことになります。<br>
707  <hr>  <hr>
708    
709    
710  <h2><a name="ttssh">TTSSHによるSSHの設計と実装</a></h2>  <h2><a name="ttssh">TTSSHによるSSHの設計と実装</a></h2>
711    <h3>概要</h3>    <h3>概要</h3>
712   オリジナルのTTSSHは<a href="http://www.cs.cmu.edu/People/roc/">Robert O'Callahan</a>氏(現在は<a href="http://weblogs.mozillazine.org/roc/">Mozilla hacker</a>として活躍)により開発されたプラグインです。SSH1へ対応しており、ポートフォワーディングやzlibによるパケット圧縮もサポートしていました。TTSSHは、Tera Termをセキュア通信に対応させるためのプラグインであったために、SCPやSFTP等には未対応でした。オリジナルTera Termが1998年に開発凍結後も、2001年ごろまでメンテナンスが続けられていました。<br>   オリジナルのTTSSHは<a href="http://www.cs.cmu.edu/People/roc/">Robert O'Callahan</a>氏(現在は<a href="http://weblogs.mozillazine.org/roc/">Mozilla hacker</a>として活躍)により開発されたプラグインです。SSH1へ対応しており、ポートフォワーディングやzlibによるパケット圧縮もサポートしていました。TTSSHは、Tera Termをセキュア通信に対応させるためのプラグインであったために、SCPやSFTP等には未対応でした。オリジナルTera Termが1998年に開発凍結後も、2001年ごろまでメンテナンスが続けられていました。<br>
713   TTSSHのSSH2対応を実現するために、TeraTerm Projectにより2004年から設計と実装が始められました。3年の歳月をかけて、ほぼSSH2プロトコルのフルサポートを実現しました。現在ではSCPへも対応しています。将来的にはSFTPへも対応されるかもしれません。<br>   TTSSHのSSH2対応を実現するために、TeraTerm Projectにより2004年から設計と実装が始められました。3年の歳月をかけて、ほぼSSH2プロトコルのフルサポートを実現しました。現在ではSCPへも対応しています。将来的にはSFTPへも対応されるかもしれません。<br>
714   原則、TTSSHの実装は<a href="http://www.openssh.com/">OpenSSH</a>を参考にしています。一部、コードをそのまま流用しているところもあります。ただし、OpenSSHはUNIXのコマンドライン向けに設計されているため、Tera TermのようなWindowsアプリケーションにはそのまま適合しない箇所も多く、フレームワークとしてはOpenSSHと大きく異なったものとなっています。<br>   原則、TTSSHの実装は<a href="http://www.openssh.com/">OpenSSH</a>を参考にしています。一部、コードをそのまま流用しているところもあります。ただし、OpenSSHはUNIXのコマンドライン向けに設計されているため、Tera TermのようなWindowsアプリケーションにはそのまま適合しない箇所も多く、フレームワークとしてはOpenSSHと大きく異なったものとなっています。<br>
715    
716    
717    <h3>SSHプロトコル</h3>    <h3>SSHプロトコル</h3>
718   SSH(Secure Shell)は、バージョン1(厳密には1.5)とバージョン2が存在し、略して"SSH1"および"SSH2"と呼ばれます。それらのバージョン間にはプロトコル仕様としての互換性はありません。SSH1にはセキュリティ上の問題があるために、現在はほとんど利用されません。<br>   SSH(Secure Shell)は、バージョン1(厳密には1.5)とバージョン2が存在し、略して"SSH1"および"SSH2"と呼ばれます。それらのバージョン間にはプロトコル仕様としての互換性はありません。SSH1にはセキュリティ上の問題があるために、現在はほとんど利用されません。<br>
719   SSH2プロトコルの仕様に関しては、RFC化されています。   SSH2プロトコルの仕様に関しては、RFC化されています。
720      
721  <p>  <p>
722  <ul>  <ul>
723    <li><a href="http://www.ietf.org/rfc/rfc4250.txt">RFC4250: The Secure Shell (SSH) Protocol Assigned Numbers</a></li>    <li><a href="http://www.ietf.org/rfc/rfc4250.txt">RFC4250: The Secure Shell (SSH) Protocol Assigned Numbers</a></li>
724    <li><a href="http://www.ietf.org/rfc/rfc4251.txt">RFC4251: The Secure Shell (SSH) Protocol Architecture</a></li>    <li><a href="http://www.ietf.org/rfc/rfc4251.txt">RFC4251: The Secure Shell (SSH) Protocol Architecture</a></li>
725    <li><a href="http://www.ietf.org/rfc/rfc4252.txt">RFC4252: The Secure Shell (SSH) Authentication Protocol</a></li>    <li><a href="http://www.ietf.org/rfc/rfc4252.txt">RFC4252: The Secure Shell (SSH) Authentication Protocol</a></li>
726    <li><a href="http://www.ietf.org/rfc/rfc4253.txt">RFC4253: The Secure Shell (SSH) Transport Layer Protocol</a></li>    <li><a href="http://www.ietf.org/rfc/rfc4253.txt">RFC4253: The Secure Shell (SSH) Transport Layer Protocol</a></li>
727    <li><a href="http://www.ietf.org/rfc/rfc4254.txt">RFC4254: The Secure Shell (SSH) Connection Protocol</a></li>    <li><a href="http://www.ietf.org/rfc/rfc4254.txt">RFC4254: The Secure Shell (SSH) Connection Protocol</a></li>
728    <li><a href="http://www.ietf.org/rfc/rfc4255.txt">RFC4255: Using DNS to Securely Publish Secure Shell (SSH) Key Fingerprints</a></li>    <li><a href="http://www.ietf.org/rfc/rfc4255.txt">RFC4255: Using DNS to Securely Publish Secure Shell (SSH) Key Fingerprints</a></li>
729    <li><a href="http://www.ietf.org/rfc/rfc4256.txt">RFC4256: Generic Message Exchange Authentication for the Secure Shell Protocol (SSH)</a></li>    <li><a href="http://www.ietf.org/rfc/rfc4256.txt">RFC4256: Generic Message Exchange Authentication for the Secure Shell Protocol (SSH)</a></li>
730    <li><a href="http://www.ietf.org/rfc/rfc4344.txt">RFC4344: The Secure Shell (SSH) Transport Layer Encryption Modes</a></li>    <li><a href="http://www.ietf.org/rfc/rfc4344.txt">RFC4344: The Secure Shell (SSH) Transport Layer Encryption Modes</a></li>
731    
732  </ul>  </ul>
733  </p>  </p>
734    
735    
736    <h3>接続処理</h3>    <h3>接続処理</h3>
737   TTSSHは、Tera Termの一部のコードでもあるため、ネットワーク接続処理はTera TermとTTSSHの間を行き来することになり、処理の流れが複雑になっています。また、SSHプロトコルそのもののフローを熟知していないと、TTSSHのシーケンスを追っていくのが難しくなっています。以下に、リモートホストへの接続を行うまでのフローを示します。<br>   TTSSHは、Tera Termの一部のコードでもあるため、ネットワーク接続処理はTera TermとTTSSHの間を行き来することになり、処理の流れが複雑になっています。また、SSHプロトコルそのもののフローを熟知していないと、TTSSHのシーケンスを追っていくのが難しくなっています。以下に、リモートホストへの接続を行うまでのフローを示します。<br>
738    
739  <div align="center">  <div align="center">
740  <img src="image/ssh.png" width=720 height=540>  <img src="image/ssh.png" width=720 height=540>
741  </div>  </div>
742    
743    
744    <h3>送信パケット処理</h3>    <h3>送信パケット処理</h3>
745    SSH2プロトコルに載せて、パケットをサーバへ送るときのコードは以下のような書き方となります。begin_send_packet()の呼び出しで、「pvar->ssh_state.outbuf + 12」が返り値となり、それがペイロードを表します。ペイロードは純粋にサーバへ送りたいデータのことで、サイズやパディング等を含みません。<br>    SSH2プロトコルに載せて、パケットをサーバへ送るときのコードは以下のような書き方となります。begin_send_packet()の呼び出しで、「pvar->ssh_state.outbuf + 12」が返り値となり、それがペイロードを表します。ペイロードは純粋にサーバへ送りたいデータのことで、サイズやパディング等を含みません。<br>
746        
747  <pre class=code>  <pre class=code>
748          buffer_t *msg;          buffer_t *msg;
749          int len;          int len;
750          char *s;          char *s;
751          unsigned char *outmsg;          unsigned char *outmsg;
752                    
753          msg = buffer_init();          msg = buffer_init();
754          if (msg != NULL) {          if (msg != NULL) {
755                  buffer_put_int(msg, SSH2_DISCONNECT_PROTOCOL_ERROR);                  buffer_put_int(msg, SSH2_DISCONNECT_PROTOCOL_ERROR);
756                  s = "disconnected by server request";                  s = "disconnected by server request";
757                  buffer_put_string(msg, s, strlen(s));                  buffer_put_string(msg, s, strlen(s));
758                  s = "";                  s = "";
759                  buffer_put_string(msg, s, strlen(s));                  buffer_put_string(msg, s, strlen(s));
760    
761                  len = buffer_len(msg);                  len = buffer_len(msg);
762                  outmsg = begin_send_packet(pvar, SSH2_MSG_DISCONNECT, len);                  outmsg = begin_send_packet(pvar, SSH2_MSG_DISCONNECT, len);
763                  memcpy(outmsg, buffer_ptr(msg), len);                  memcpy(outmsg, buffer_ptr(msg), len);
764                  finish_send_packet(pvar);                  finish_send_packet(pvar);
765                  buffer_free(msg);                  buffer_free(msg);
766          }          }
767  </pre>  </pre>
768    
769   SSH通信に載せられて、実際にパケットが送出されるのは、finish_send_packet()から呼び出される finish_send_packet_special() です。パケットを送信するときのフォーマットについて、以下に示します。共通鍵暗号でパケットデータを暗号化する前に、ヘッダとフッタを付ける必要があります。<br>   SSH通信に載せられて、実際にパケットが送出されるのは、finish_send_packet()から呼び出される finish_send_packet_special() です。パケットを送信するときのフォーマットについて、以下に示します。共通鍵暗号でパケットデータを暗号化する前に、ヘッダとフッタを付ける必要があります。<br>
770   パケットサイズはHMACを除く長さです。パケットサイズそのものはビッグエンディアン形式で、4バイト分格納しますが、その"4"バイトは含まれません。ペイロードの直後にパディングを埋めるのは、共通鍵暗号で暗号化するときに「ブロックサイズ単位」になっていなければ、アルゴリズム的に暗号化できないからです。ブロックサイズは暗号アルゴリズムにより異なり、たとえば3DES-CBCならば24バイト、AES128ならば16バイトです。<br>   パケットサイズはHMACを除く長さです。パケットサイズそのものはビッグエンディアン形式で、4バイト分格納しますが、その"4"バイトは含まれません。ペイロードの直後にパディングを埋めるのは、共通鍵暗号で暗号化するときに「ブロックサイズ単位」になっていなければ、アルゴリズム的に暗号化できないからです。ブロックサイズは暗号アルゴリズムにより異なり、たとえば3DES-CBCならば24バイト、AES128ならば16バイトです。<br>
771   HMAC(Keyed-Hashing for Message Authentication)は、暗号化本文に対するハッシュです。ハッシュのアルゴリズムは選択可能であり、"MD5"や"SHA-1"がよく使われています。HMACを付加することにより、「第三者によるデータの改ざん」を検出することができます。HMACは、暗号化対象となる本文を秘密鍵とシーケンス番号を加え、ハッシュ値を計算します。秘密鍵とシーケンス番号を加えることにより、第三者がデータをまるごと差し替えたとしても、送信者が生成したハッシュ値を復元することは理論上できません。<br>   HMAC(Keyed-Hashing for Message Authentication)は、暗号化本文に対するハッシュです。ハッシュのアルゴリズムは選択可能であり、"MD5"や"SHA-1"がよく使われています。HMACを付加することにより、「第三者によるデータの改ざん」を検出することができます。HMACは、暗号化対象となる本文を秘密鍵とシーケンス番号を加え、ハッシュ値を計算します。秘密鍵とシーケンス番号を加えることにより、第三者がデータをまるごと差し替えたとしても、送信者が生成したハッシュ値を復元することは理論上できません。<br>
772      
773    
774  <div align="center">  <div align="center">
775  <img src="image/ssh_packet_format1.png" width=720 height=540>  <img src="image/ssh_packet_format1.png" width=720 height=540>
776  </div>  </div>
777    
778   zlibによるパケット圧縮を行う場合における、パケットを送信するときのフォーマットについて、以下に示します。パケット圧縮を行うのは、「ペイロード」の部分のみで、残りは通常の送信パケットとフォーマットは同じです。なお、パケットを圧縮したとしても、かならずしも元のサイズよりも小さくなるとは限らないので、そのことを考慮したバッファ管理が必要です。<br>   zlibによるパケット圧縮を行う場合における、パケットを送信するときのフォーマットについて、以下に示します。パケット圧縮を行うのは、「ペイロード」の部分のみで、残りは通常の送信パケットとフォーマットは同じです。なお、パケットを圧縮したとしても、かならずしも元のサイズよりも小さくなるとは限らないので、そのことを考慮したバッファ管理が必要です。<br>
779   パケット圧縮送信で難しいのは、圧縮を開始するタイミングです。ローカルホストからリモートホストへのSSH接続を開始すると、実にたくさんのネゴシエーションが行われますが、パケットを圧縮してよいのは決められたタイミングであり、このタイミングを間違えると、サーバとまったく通信ができなくなります。<br>   パケット圧縮送信で難しいのは、圧縮を開始するタイミングです。ローカルホストからリモートホストへのSSH接続を開始すると、実にたくさんのネゴシエーションが行われますが、パケットを圧縮してよいのは決められたタイミングであり、このタイミングを間違えると、サーバとまったく通信ができなくなります。<br>
780   通常のパケット圧縮の場合は、"SSH2_MSG_KEXINIT"を受信したタイミングです。遅延パケット圧縮の場合は、ユーザ認証が成功したタイミング("SSH2_MSG_USERAUTH_SUCCESS"を受信した時)です。遅延パケット圧縮というのは、それまで"SSH2_MSG_KEXINIT"を受信したタイミングで圧縮を開始していたのを、ユーザ認証が完了するまで延長する方式です。遅延パケット圧縮は、zlibライブラリのセキュリティホールにより、不正なSSHサーバへ接続しただけで、クライアント側に影響が出るのを回避するためのしくみです。   通常のパケット圧縮の場合は、"SSH2_MSG_KEXINIT"を受信したタイミングです。遅延パケット圧縮の場合は、ユーザ認証が成功したタイミング("SSH2_MSG_USERAUTH_SUCCESS"を受信した時)です。遅延パケット圧縮というのは、それまで"SSH2_MSG_KEXINIT"を受信したタイミングで圧縮を開始していたのを、ユーザ認証が完了するまで延長する方式です。遅延パケット圧縮は、zlibライブラリのセキュリティホールにより、不正なSSHサーバへ接続しただけで、クライアント側に影響が出るのを回避するためのしくみです。
781    
782    
783  <div align="center">  <div align="center">
784  <img src="image/ssh_packet_format2.png" width=720 height=540>  <img src="image/ssh_packet_format2.png" width=720 height=540>
785  </div>  </div>
786    
787    
788    <h3>受信パケット処理</h3>    <h3>受信パケット処理</h3>
789   パケットの受信は、TeraTerm本体側からは recv ソケット関数を呼び出した場合に、それがTELNETなのかSSHなのかを意識させないような設計になっていることと、 recv ソケット関数の呼び出しでは、かならずしも十分なバッファサイズが指定されてくるとは限らないため、少々実装が複雑になっています。<br>   パケットの受信は、TeraTerm本体側からは recv ソケット関数を呼び出した場合に、それがTELNETなのかSSHなのかを意識させないような設計になっていることと、 recv ソケット関数の呼び出しでは、かならずしも十分なバッファサイズが指定されてくるとは限らないため、少々実装が複雑になっています。<br>
790    
791  <div align="center">  <div align="center">
792  <img src="image/ssh_recv_packet.png" width=720 height=540>  <img src="image/ssh_recv_packet.png" width=720 height=540>
793  </div>  </div>
794    
795   TeraTerm本体側は OnIdle()#teraterm.cpp というアイドルループにおいて、常時パケットの受信がないかをポーリングしています。それが CommReceive() で、recv()を呼び出します。recv()はTTSSHによりフックされているので、ソケット関数ではなく、TTXrecv()#ttxssh.c が呼び出されます。<br>   TeraTerm本体側は OnIdle()#teraterm.cpp というアイドルループにおいて、常時パケットの受信がないかをポーリングしています。それが CommReceive() で、recv()を呼び出します。recv()はTTSSHによりフックされているので、ソケット関数ではなく、TTXrecv()#ttxssh.c が呼び出されます。<br>
796   CommReceive()は recv() を呼び出す際に、バッファ(cv->InBuff[])の空きポインタとサイズを引数に渡します。バッファサイズは 1KB です。つまり、TTXrecv()のサイズには、1〜1024 までの数値が渡される可能性があるということです。<br>   CommReceive()は recv() を呼び出す際に、バッファ(cv->InBuff[])の空きポインタとサイズを引数に渡します。バッファサイズは 1KB です。つまり、TTXrecv()のサイズには、1〜1024 までの数値が渡される可能性があるということです。<br>
797   TTXrecv()から呼び出される PKT_recv() は、少々複雑なループ処理となっています。SSH接続を初めて行うときのシーケンスを以下に示します。   TTXrecv()から呼び出される PKT_recv() は、少々複雑なループ処理となっています。SSH接続を初めて行うときのシーケンスを以下に示します。
798      
799  <ol>  <ol>
800   <li>recv_data() で本当の recv() を呼び出し、サーバからの受信パケットをカーネルから受け取る。pvar->pkt_state.datalenが更新される。 </li>   <li>recv_data() で本当の recv() を呼び出し、サーバからの受信パケットをカーネルから受け取る。pvar->pkt_state.datalenが更新される。 </li>
801   <li>SSH_handle_server_ID() でSSHサーバのバージョンチェックが行われる。pvar->pkt_state.datastart と pvar->pkt_state.datalen を更新する。</li>   <li>SSH_handle_server_ID() でSSHサーバのバージョンチェックが行われる。pvar->pkt_state.datastart と pvar->pkt_state.datalen を更新する。</li>
802   <li>再度、recv_data() が呼ばれるが、サーバからの受信データがもうないので、connection_closed=TRUE として while ループを抜ける。</li>   <li>再度、recv_data() が呼ばれるが、サーバからの受信データがもうないので、connection_closed=TRUE として while ループを抜ける。</li>
803   <li>TeraTermの recv() は"0"で返ってくる。すなわち、受信データなし。</li>   <li>TeraTermの recv() は"0"で返ってくる。すなわち、受信データなし。</li>
804  </ol>  </ol>
805    
806   次に、SSH通信のための共通鍵生成までのシーケンスを以下に示します。   次に、SSH通信のための共通鍵生成までのシーケンスを以下に示します。
807    
808  <ol>  <ol>
809   <li>recv_data() で本当の recv() を呼び出し、サーバからの受信パケットをカーネルから受け取る。pvar->pkt_state.datalenが更新される。 </li>   <li>recv_data() で本当の recv() を呼び出し、サーバからの受信パケットをカーネルから受け取る。pvar->pkt_state.datalenが更新される。 </li>
810   <li>SSH_predecrpyt_packet() で、受信パケットの先頭ブロックのみを復号化する。SSHパケットのサイズを取得する。</li>   <li>SSH_predecrpyt_packet() で、受信パケットの先頭ブロックのみを復号化する。SSHパケットのサイズを取得する。</li>
811   <li>妥当なSSHパケットサイズならば、SSH_handle_packet() を呼び出し、メッセージタイプに応じたハンドラを呼び出す。pvar->ssh_state.payload と pvar->ssh_state.payloadlen を設定する。</li>   <li>妥当なSSHパケットサイズならば、SSH_handle_packet() を呼び出し、メッセージタイプに応じたハンドラを呼び出す。pvar->ssh_state.payload と pvar->ssh_state.payloadlen を設定する。</li>
812   <li>pvar->pkt_state.datastart と pvar->pkt_state.datalen を更新する。</li>   <li>pvar->pkt_state.datastart と pvar->pkt_state.datalen を更新する。</li>
813   <li>pvar->pkt_state.datalen がゼロになるまで、SSH_predecrpyt_packet() の処理を繰り返す。</li>   <li>pvar->pkt_state.datalen がゼロになるまで、SSH_predecrpyt_packet() の処理を繰り返す。</li>
814   <li>recv_data() が呼ばれるが、サーバからの受信データがもうないので、connection_closed=TRUE として while ループを抜ける。</li>   <li>recv_data() が呼ばれるが、サーバからの受信データがもうないので、connection_closed=TRUE として while ループを抜ける。</li>
815   <li>TeraTermの recv() は"0"で返ってくる。すなわち、受信データなし。</li>   <li>TeraTermの recv() は"0"で返ってくる。すなわち、受信データなし。</li>
816  </ol>  </ol>
817    
818   次に、端末データ通信のシーケンスを以下に示します。   次に、端末データ通信のシーケンスを以下に示します。
819    
820  <ol>  <ol>
821   <li>recv_data() で本当の recv() を呼び出し、サーバからの受信パケットをカーネルから受け取る。pvar->pkt_state.datalenが更新される。 </li>   <li>recv_data() で本当の recv() を呼び出し、サーバからの受信パケットをカーネルから受け取る。pvar->pkt_state.datalenが更新される。 </li>
822   <li>SSH_predecrpyt_packet() で、受信パケットの先頭ブロックのみを復号化する。SSHパケットのサイズを取得する。</li>   <li>SSH_predecrpyt_packet() で、受信パケットの先頭ブロックのみを復号化する。SSHパケットのサイズを取得する。</li>
823   <li>妥当なSSHパケットサイズならば、SSH_handle_packet() を呼び出し、メッセージタイプに応じたハンドラを呼び出す。pvar->ssh_state.payload と pvar->ssh_state.payloadlen を設定する。</li>   <li>妥当なSSHパケットサイズならば、SSH_handle_packet() を呼び出し、メッセージタイプに応じたハンドラを呼び出す。pvar->ssh_state.payload と pvar->ssh_state.payloadlen を設定する。</li>
824   <li>メッセージタイプがSSH2_MSG_CHANNEL_DATAなので、handle_SSH2_channel_data() を呼び出す。pvar->ssh_state.payload_datalen と pvar->ssh_state.payload_datastart を設定する。</li>   <li>メッセージタイプがSSH2_MSG_CHANNEL_DATAなので、handle_SSH2_channel_data() を呼び出す。pvar->ssh_state.payload_datalen と pvar->ssh_state.payload_datastart を設定する。</li>
825   <li>pvar->pkt_state.datastart と pvar->pkt_state.datalen を更新する。</li>   <li>pvar->pkt_state.datastart と pvar->pkt_state.datalen を更新する。</li>
826   <li>SSH_is_any_payload() が真を返すようになり、PKT_recv()に渡されてきたバッファへデータをコピーする。</li>   <li>SSH_is_any_payload() が真を返すようになり、PKT_recv()に渡されてきたバッファへデータをコピーする。</li>
827   <li>TeraTerm側のバッファサイズがいっぱいになった場合は、SSH端末データが残っていたとしても、PKT_recv()は返る。</li>   <li>TeraTerm側のバッファサイズがいっぱいになった場合は、SSH端末データが残っていたとしても、PKT_recv()は返る。</li>
828   <li>TeraTerm側のバッファサイズに余裕がある場合は、recv_data()を呼び出し、サーバからの受信データを取得する。</li>   <li>TeraTerm側のバッファサイズに余裕がある場合は、recv_data()を呼び出し、サーバからの受信データを取得する。</li>
829   <li>TeraTermの recv() は「受信データサイズ」で返ってくる。</li>   <li>TeraTermの recv() は「受信データサイズ」で返ってくる。</li>
830  </ol>  </ol>
831    
832    
833    <h3>シーケンス制御</h3>    <h3>シーケンス制御</h3>
834   SSH2接続を行うことで、通信経路を暗号化することができるのが特徴ですが、パケットの暗号化を行うためには、「鍵」が必要です。通信経路の暗号化には、共通鍵による共通鍵暗号が利用されます。公開鍵暗号のほうがセキュリティ強度は高いのですが、暗号処理に多大な時間がかかるため、SSHのような通信性能が要求されるしくみでは採用されません。SSH2では、共通鍵暗号アルゴリズムとして、AES(Advanced Encryption Standard:Rijndaelアルゴリズム)や3DES(Triple DES)などが利用されます。<br>   SSH2接続を行うことで、通信経路を暗号化することができるのが特徴ですが、パケットの暗号化を行うためには、「鍵」が必要です。通信経路の暗号化には、共通鍵による共通鍵暗号が利用されます。公開鍵暗号のほうがセキュリティ強度は高いのですが、暗号処理に多大な時間がかかるため、SSHのような通信性能が要求されるしくみでは採用されません。SSH2では、共通鍵暗号アルゴリズムとして、AES(Advanced Encryption Standard:Rijndaelアルゴリズム)や3DES(Triple DES)などが利用されます。<br>
835   共通鍵は通信を行う二者間でのみに共有される情報であり、第三者に知られてはなりません。SSH2では、クライアントがリモートホスト(SSHサーバ)へTCP接続した時に、"Diffie-Hellman"アルゴリズムをベースとした独自の方式により、クライアントとサーバでしか知り得ないDH(Diffie-Hellman)鍵を生成します。DH鍵生成までの過程は、ネットワーク上をパケットが平文で流れるため、第三者によるパケットキャプチャが可能となっていますが、パケットを覗かれても、DH鍵は理論上第三者には分からないようになっています。<br>   共通鍵は通信を行う二者間でのみに共有される情報であり、第三者に知られてはなりません。SSH2では、クライアントがリモートホスト(SSHサーバ)へTCP接続した時に、"Diffie-Hellman"アルゴリズムをベースとした独自の方式により、クライアントとサーバでしか知り得ないDH(Diffie-Hellman)鍵を生成します。DH鍵生成までの過程は、ネットワーク上をパケットが平文で流れるため、第三者によるパケットキャプチャが可能となっていますが、パケットを覗かれても、DH鍵は理論上第三者には分からないようになっています。<br>
836   共通鍵が生成できたあとは、その鍵を使ってパケットを暗号化します。SSH2では、送受信されるパケットは種類があるため、それぞれに「メッセージ番号」を割り振っています。RFC4250にメッセージ番号の一覧があります。メッセージ名は"SSH2_MSG_xxxx"というネーミングになっており、TTSSH内部でも同じ名前でマクロ定義しています。<br>   共通鍵が生成できたあとは、その鍵を使ってパケットを暗号化します。SSH2では、送受信されるパケットは種類があるため、それぞれに「メッセージ番号」を割り振っています。RFC4250にメッセージ番号の一覧があります。メッセージ名は"SSH2_MSG_xxxx"というネーミングになっており、TTSSH内部でも同じ名前でマクロ定義しています。<br>
837   以下に、クライアントからサーバへTCP接続(ポート22番)してから、パスワード認証でユーザ認証されるまでの流れを示します。<br>   以下に、クライアントからサーバへTCP接続(ポート22番)してから、パスワード認証でユーザ認証されるまでの流れを示します。<br>
838    
839    
840  <div align="center">  <div align="center">
841  <img src="image/ssh2_sequence1.png" width=720 height=540>  <img src="image/ssh2_sequence1.png" width=720 height=540>
842  </div>  </div>
843    
844  <div align="center">  <div align="center">
845  <img src="image/ssh2_sequence2.png" width=720 height=540>  <img src="image/ssh2_sequence2.png" width=720 height=540>
846  </div>  </div>
847    
848   以下は、リモートホストのシェル上で"exit"や"logout"として、クライアントから明示的にシェルをクローズするときの、パケットの流れを示しています。<br>   以下は、リモートホストのシェル上で"exit"や"logout"として、クライアントから明示的にシェルをクローズするときの、パケットの流れを示しています。<br>
849    
850  <div align="center">  <div align="center">
851  <img src="image/ssh2_sequence3.png" width=720 height=540>  <img src="image/ssh2_sequence3.png" width=720 height=540>
852  </div>  </div>
853    
854   TTSSHは、SSH2でパスワード認証のほかにkeyboard-interactive認証、publickey認証、Pageantを利用したpublickey認証をサポートしています。それぞれの認証方式でどのようなシーケンスで認証が行われるのか、以下に示します。   TTSSHは、SSH2でパスワード認証のほかにkeyboard-interactive認証、publickey認証、Pageantを利用したpublickey認証をサポートしています。それぞれの認証方式でどのようなシーケンスで認証が行われるのか、以下に示します。
855    
856  <div align="center">  <div align="center">
857  <img src="image/ssh2_auth1.png" width=720 height=540>  <img src="image/ssh2_auth1.png" width=720 height=540>
858  </div>  </div>
859  <div align="center">  <div align="center">
860  <img src="image/ssh2_auth2.png" width=720 height=540>  <img src="image/ssh2_auth2.png" width=720 height=540>
861  </div>  </div>
862    
863    
864    
865    <h3>疑似端末のしくみ</h3>    <h3>疑似端末のしくみ</h3>
866   SSH2では、新しく「フロー制御」という概念が取り込まれています。TCPのウィンドウと同じ考え方で、「ウィンドウサイズ」というしくみを導入しています。この機能により、クライアント(Tera Term)とサーバ(SSHデーモン)間において、フロー制御が働くため、原則データが溢れることはありません。<br>   SSH2では、新しく「フロー制御」という概念が取り込まれています。TCPのウィンドウと同じ考え方で、「ウィンドウサイズ」というしくみを導入しています。この機能により、クライアント(Tera Term)とサーバ(SSHデーモン)間において、フロー制御が働くため、原則データが溢れることはありません。<br>
867   ところで、SSH2におけるフロー制御があるにも関わらず、大量のクリップボードをTeraTermの端末へペーストすると、サーバ側での「データの取りこぼし」が発生することがあります。この現象を理解するためには、UNIXにおける疑似端末(PTY: pseudo-terminal)の動作原理を知る必要があります。   ところで、SSH2におけるフロー制御があるにも関わらず、大量のクリップボードをTeraTermの端末へペーストすると、サーバ側での「データの取りこぼし」が発生することがあります。この現象を理解するためには、UNIXにおける疑似端末(PTY: pseudo-terminal)の動作原理を知る必要があります。
868    
869  <div align="center">  <div align="center">
870  <img src="image/pty.png" width=720 height=540>  <img src="image/pty.png" width=720 height=540>
871  </div>  </div>
872    
873   SSHデーモン(sshd)はクライアントに対して、あたかもサーバ側のシェルが直接接続されているかのように見せる必要があります。逆に、シェル上で動くプログラムは、文字を送りたいときは printf(3) を、文字を受け取りたい場合は scanf(3) といったCライブラリ関数を呼び出すだけでよく、その先がシリアルコンソールなのか、VGAコンソールなのか、SSH接続されているのかは、一切気にしなくてよいようになっています。<br>   SSHデーモン(sshd)はクライアントに対して、あたかもサーバ側のシェルが直接接続されているかのように見せる必要があります。逆に、シェル上で動くプログラムは、文字を送りたいときは printf(3) を、文字を受け取りたい場合は scanf(3) といったCライブラリ関数を呼び出すだけでよく、その先がシリアルコンソールなのか、VGAコンソールなのか、SSH接続されているのかは、一切気にしなくてよいようになっています。<br>
874   sshdは、クライアントからの接続要求があったタイミングで、openpty(3)を使って、疑似端末の初期化を行います。疑似端末では、カーネル空間でクライアントとサーバをつなぐために、「マスターデバイスドライバ」と「スレーブデバイスドライバ」が用意されます。マスターデバイスドライバが担当するデバイスファイルは"/dev/ptyXX"、スレーブデバイスドライバでは"/dev/ttyXX"です。つまり、sshdはマスターデバイスドライバへアクセスすることで、シェルとお話をすることができます。シェルは、sshdからforkされて子プロセスとなり、親プロセス(sshd)が初期化済みのスレーブデバイスドライバとお話をすることになります。この疑似端末のしくみにより、sshdとシェルが接続されます。<br>   sshdは、クライアントからの接続要求があったタイミングで、openpty(3)を使って、疑似端末の初期化を行います。疑似端末では、カーネル空間でクライアントとサーバをつなぐために、「マスターデバイスドライバ」と「スレーブデバイスドライバ」が用意されます。マスターデバイスドライバが担当するデバイスファイルは"/dev/ptyXX"、スレーブデバイスドライバでは"/dev/ttyXX"です。つまり、sshdはマスターデバイスドライバへアクセスすることで、シェルとお話をすることができます。シェルは、sshdからforkされて子プロセスとなり、親プロセス(sshd)が初期化済みのスレーブデバイスドライバとお話をすることになります。この疑似端末のしくみにより、sshdとシェルが接続されます。<br>
875   なお、端末ラインディシプリン(line discipline: 回線規約)というのは、たとえばプログラムが getchar() を呼び出したときに、Enterキーを押下するまで、プログラムに制御が渡りません。端末ラインディシプリンは、プログラム実行中での「行内編集」を可能とするためのモジュールです。Linuxでは、端末ラインディシプリンは /proc/tty/ldiscs で確認できます(N_TTYが標準的に利用される)。   なお、端末ラインディシプリン(line discipline: 回線規約)というのは、たとえばプログラムが getchar() を呼び出したときに、Enterキーを押下するまで、プログラムに制御が渡りません。端末ラインディシプリンは、プログラム実行中での「行内編集」を可能とするためのモジュールです。Linuxでは、端末ラインディシプリンは /proc/tty/ldiscs で確認できます(N_TTYが標準的に利用される)。
876    
877    
878  <hr>  <hr>
879    
880    
881  <h2><a name="macro">マクロ言語の設計と実装</a></h2>  <h2><a name="macro">マクロ言語の設計と実装</a></h2>
882    <h3>概要</h3>    <h3>概要</h3>
883   Tera Termのマクロスクリプトは、BASIC風の言語仕様となっています。BisonやFlexといったしくみは利用しておらず、力業的な独自の構文解析(再帰的下降法)により実装されています。そのため、本格的なスクリプト言語としての記述はできない側面があります。<br>   Tera Termのマクロスクリプトは、BASIC風の言語仕様となっています。BisonやFlexといったしくみは利用しておらず、力業的な独自の構文解析(再帰的下降法)により実装されています。そのため、本格的なスクリプト言語としての記述はできない側面があります。<br>
884      
885    <h3>ファイルの読み込み</h3>    <h3>ファイルの読み込み</h3>
886   ttpmacro.exeの起動時に、マクロファイル(.ttl)が一括してバッファへ読み込まれます。   ttpmacro.exeの起動時に、マクロファイル(.ttl)が一括してバッファへ読み込まれます。
887      
888  <p><ul>  <p><ul>
889    <li>OnInitDialog()#ttmmain.cpp -> InitTTL() -> InitBuff() -> LoadMacroFile()</li>    <li>OnInitDialog()#ttmmain.cpp -> InitTTL() -> InitBuff() -> LoadMacroFile()</li>
890  </ul></p>  </ul></p>
891    
892   初めて読み込まれるマクロファイルの全内容は Buff[0] # ttmbuff.c に格納されます。この時点で、ファイルの内容は一括して読み込まれるため、マクロ実行中はファイルを削除してしまっても問題はありません。ただし、"include"で別のファイルを読み込む場合は、includeを実行する時点で、include対象となるファイルの読み込みが発生します。   初めて読み込まれるマクロファイルの全内容は Buff[0] # ttmbuff.c に格納されます。この時点で、ファイルの内容は一括して読み込まれるため、マクロ実行中はファイルを削除してしまっても問題はありません。ただし、"include"で別のファイルを読み込む場合は、includeを実行する時点で、include対象となるファイルの読み込みが発生します。
893      
894  <pre class=code>  <pre class=code>
895  #define MAXNESTLEVEL 10     /* 扱えるファイル数(includeは9つまで)*/  #define MAXNESTLEVEL 10     /* 扱えるファイル数(includeは9つまで)*/
896    
897  static int INest;     /* 現在のネスト位置 */  static int INest;     /* 現在のネスト位置 */
898  static HANDLE BuffHandle[MAXNESTLEVEL];   /* GlobalAlloc()によるバッファ */  static HANDLE BuffHandle[MAXNESTLEVEL];   /* GlobalAlloc()によるバッファ */
899  static PCHAR Buff[MAXNESTLEVEL];          /* バッファ領域 */  static PCHAR Buff[MAXNESTLEVEL];          /* バッファ領域 */
900  static BINT BuffLen[MAXNESTLEVEL];        /* ファイルサイズ(バッファサイズ) */  static BINT BuffLen[MAXNESTLEVEL];        /* ファイルサイズ(バッファサイズ) */
901  static BINT BuffPtr[MAXNESTLEVEL];        /* バッファのオフセット(読み込み位置)*/  static BINT BuffPtr[MAXNESTLEVEL];        /* バッファのオフセット(読み込み位置)*/
902  </pre>  </pre>
903    
904    
905    <h3>マクロエンジン</h3>    <h3>マクロエンジン</h3>
906   マクロ処理はアイドルループ OnIdle()#ttmmain.cpp で行われます。アイドルループでは TTLStatus 変数により、マクロエンジンの動作を変えています。通常の実行状態は IdTTLRun がセットされています。以下に、動作一覧を示します。   マクロ処理はアイドルループ OnIdle()#ttmmain.cpp で行われます。アイドルループでは TTLStatus 変数により、マクロエンジンの動作を変えています。通常の実行状態は IdTTLRun がセットされています。以下に、動作一覧を示します。
907    
908  <p>  <p>
909  <table border=1 align=center>  <table border=1 align=center>
910  <tr>  <tr>
911    <th>条件</th>    <th>条件</th>
912    <th>処理</th>    <th>処理</th>
913  </tr>  </tr>
914    
915  <tr>  <tr>
916    <td>TTLStatus==IdTTLEnd</td>    <td>TTLStatus==IdTTLEnd</td>
917    <td>マクロプログラムを終了する</td>    <td>マクロプログラムを終了する</td>
918  </tr>  </tr>
919    
920  <tr>  <tr>
921    <td>送信データがある場合(OutLen > 0)</td>    <td>送信データがある場合(OutLen > 0)</td>
922    <td>Tera Term本体へデータを送る</td>    <td>Tera Term本体へデータを送る</td>
923  </tr>  </tr>
924    
925  <tr>  <tr>
926    <td>TTLStatus==IdTTLRun</td>    <td>TTLStatus==IdTTLRun</td>
927    <td>一行ずつマクロを実行する</td>    <td>一行ずつマクロを実行する</td>
928  </tr>  </tr>
929    
930  <tr>  <tr>
931    <td>TTLStatus==IdTTLWait</td>    <td>TTLStatus==IdTTLWait</td>
932    <td>ウェイトする('wait'コマンド)</td>    <td>ウェイトする('wait'コマンド)</td>
933  </tr>  </tr>
934    
935  <tr>  <tr>
936    <td>TTLStatus==IdTTLWaitLn</td>    <td>TTLStatus==IdTTLWaitLn</td>
937    <td>ウェイトする('waitln'コマンド)</td>    <td>ウェイトする('waitln'コマンド)</td>
938  </tr>  </tr>
939    
940  <tr>  <tr>
941    <td>TTLStatus==IdTTLWaitNL</td>    <td>TTLStatus==IdTTLWaitNL</td>
942    <td>一行受信する('recvln'コマンド)</td>    <td>一行受信する('recvln'コマンド)</td>
943  </tr>  </tr>
944    
945  <tr>  <tr>
946    <td>TTLStatus==IdTTLWait2</td>    <td>TTLStatus==IdTTLWait2</td>
947    <td>文字列を待つ('waitrecv'コマンド)</td>    <td>文字列を待つ('waitrecv'コマンド)</td>
948  </tr>  </tr>
949    
950  </table>  </table>
951   </p>   </p>
952    
953    
954    <h3>インタープリタ処理</h3>    <h3>インタープリタ処理</h3>
955   アイドルループから Exec()#ttl.c が定期的に呼び出される度に、マクロファイルが一行ずつ処理されてゆきます。GetNewLine() では、バッファから一行分を取り出し、LineBuff[]#ttmparse.c へ格納します。行の終わりかどうかは、「ASCIIコードが0x20未満で、かつタブ(0x09)以外」のコードが出現したタイミングで判定しています。先頭の空白やタブは無視されます。セミコロン(;)が出現すると、以降の処理をスキップするため、コメントは行の途中でも付けられることになります。<br>   アイドルループから Exec()#ttl.c が定期的に呼び出される度に、マクロファイルが一行ずつ処理されてゆきます。GetNewLine() では、バッファから一行分を取り出し、LineBuff[]#ttmparse.c へ格納します。行の終わりかどうかは、「ASCIIコードが0x20未満で、かつタブ(0x09)以外」のコードが出現したタイミングで判定しています。先頭の空白やタブは無視されます。セミコロン(;)が出現すると、以降の処理をスキップするため、コメントは行の途中でも付けられることになります。<br>
956    
957  <pre class=code>  <pre class=code>
958  char LineBuff[MaxLineLen];      /* 1つの行は500バイトまで格納可能 */  char LineBuff[MaxLineLen];      /* 1つの行は500バイトまで格納可能 */
959  WORD LinePtr;       /* バッファオフセット */  WORD LinePtr;       /* バッファオフセット */
960  WORD LineLen;       /* バッファサイズ */  WORD LineLen;       /* バッファサイズ */
961  </pre>  </pre>
962    
963   Exec()から呼ばれる ExecCmnd() で、字句解析を行います。字句解析は単純な文字列検索であり、LineBuff[]を1バイトずつ参照していきます。大まかな処理の流れは以下のとおりです。   Exec()から呼ばれる ExecCmnd() で、字句解析を行います。字句解析は単純な文字列検索であり、LineBuff[]を1バイトずつ参照していきます。大まかな処理の流れは以下のとおりです。
964    
965  <p><ol>  <p><ol>
966    <li>endwhileの判定</li>    <li>endwhileの判定</li>
967    <li>break処理</li>    <li>break処理</li>
968    <li>endifの判定</li>    <li>endifの判定</li>
969    <li>elseの判定</li>    <li>elseの判定</li>
970    <li>マクロコマンドの実行</li>    <li>マクロコマンドの実行</li>
971    <li>識別子の判定</li>    <li>識別子の判定</li>
972    <li>文法エラー(上記のいずれでもない場合)</li>    <li>文法エラー(上記のいずれでもない場合)</li>
973  </ol></p>  </ol></p>
974    
975   マクロコマンドかどうかは、GetReservedWord()で判別しています。_stricmp()で比較しているので、アルファベットの大文字・小文字は区別されません(case-insensitive)。マクロコマンドの場合は、TTLxxx() の関数を呼び出します。<br>   マクロコマンドかどうかは、GetReservedWord()で判別しています。_stricmp()で比較しているので、アルファベットの大文字・小文字は区別されません(case-insensitive)。マクロコマンドの場合は、TTLxxx() の関数を呼び出します。<br>
976   識別子の判定は、GetIdentifier() で行います。アルファベット(a-z, A-Z)および数値(0-9)、アンダースコア(_)から構成されるトークンを切り出します。トークンは32文字までです。トークンは「変数」として扱われます。左辺値に変数が来る場合は、「変数への代入」しかありえないので、その直後に「イコール(=)」があるかどうかを調べます。<br>   識別子の判定は、GetIdentifier() で行います。アルファベット(a-z, A-Z)および数値(0-9)、アンダースコア(_)から構成されるトークンを切り出します。トークンは32文字までです。トークンは「変数」として扱われます。左辺値に変数が来る場合は、「変数への代入」しかありえないので、その直後に「イコール(=)」があるかどうかを調べます。<br>
977   イコール以降の判定処理は、以下の順番となります。   イコール以降の判定処理は、以下の順番となります。
978      
979  <p><ol>  <p><ol>
980    <li>文字列の判定</li>    <li>文字列の判定</li>
981    <li>計算式の判定</li>    <li>計算式の判定</li>
982  </ol></p>  </ol></p>
983    
984   文字列かどうかは GetString() で判定します。文字列は’か”でクォートされているため、取り出すのは容易です。<br>   文字列かどうかは GetString() で判定します。文字列は’か”でクォートされているため、取り出すのは容易です。<br>
985   計算式の判定は、GetExpression() で行います。ここでは再帰的下降法により、構文解析されます。<br>   計算式の判定は、GetExpression() で行います。ここでは再帰的下降法により、構文解析されます。<br>
986   左辺値が定義済みの変数かどうかは CheckVar() でチェックし、数値もしくは文字列をセットします。そうではない場合は NewStrVar() で、新しい変数として登録します。   左辺値が定義済みの変数かどうかは CheckVar() でチェックし、数値もしくは文字列をセットします。そうではない場合は NewStrVar() で、新しい変数として登録します。
987      
988    
989  <hr>  <hr>
990    
991    
992    
993  <h2><a name="caret">キャレット制御</a></h2>  <h2><a name="caret">キャレット制御</a></h2>
994    <h3>概要</h3>    <h3>概要</h3>
995   ユーザが端末上でキーボード入力を行うと、カーソルが移動しますが、サーバからのエスケープシーケンスにより、キーボード入力なしにカーソルを移動させる必要があります。また、ウィンドウが非アクティブ状態の場合においても、カーソルを表示させることにより、ブロードキャストモードにおいて、複数端末の同時操作性を向上させています。   ユーザが端末上でキーボード入力を行うと、カーソルが移動しますが、サーバからのエスケープシーケンスにより、キーボード入力なしにカーソルを移動させる必要があります。また、ウィンドウが非アクティブ状態の場合においても、カーソルを表示させることにより、ブロードキャストモードにおいて、複数端末の同時操作性を向上させています。
996  <br>  <br>
997    
998    <h3>システムキャレット</h3>    <h3>システムキャレット</h3>
999   Tera Termにおけるカーソル描画には、システムキャレットを利用しています。Tera Termで使用されているシステムキャレットを制御するAPIを以下に示します。   Tera Termにおけるカーソル描画には、システムキャレットを利用しています。Tera Termで使用されているシステムキャレットを制御するAPIを以下に示します。
1000    
1001  <p><ul>  <p><ul>
1002    <li>CreateCaret</li>    <li>CreateCaret</li>
1003    <li>DestroyCaret</li>    <li>DestroyCaret</li>
1004    <li>GetCaretBlinkTime</li>    <li>GetCaretBlinkTime</li>
1005    <li>HideCaret</li>    <li>HideCaret</li>
1006    <li>SetCaretBlinkTime</li>    <li>SetCaretBlinkTime</li>
1007    <li>SetCaretPos</li>    <li>SetCaretPos</li>
1008    <li>ShowCaret</li>    <li>ShowCaret</li>
1009  </ul></p>  </ul></p>
1010    
1011   <a href="http://msdn.microsoft.com/library/ja/default.asp?url=/library/ja/jpwinui/html/_win32_createcaret.asp">CreateCaretのドキュメント</a>によると、   <a href="http://msdn.microsoft.com/library/ja/default.asp?url=/library/ja/jpwinui/html/_win32_createcaret.asp">CreateCaretのドキュメント</a>によると、
1012    
1013  <pre>  <pre>
1014  システムは 1 つのキューにつき 1 つのキャレットを提供します。ウィンドウが  システムは 1 つのキューにつき 1 つのキャレットを提供します。ウィンドウが
1015  キーボードフォーカスを備えているとき、またはアクティブな状態のときにだけ、  キーボードフォーカスを備えているとき、またはアクティブな状態のときにだけ、
1016  キャレットを作成するべきです。また、キーボードフォーカスを失ったり非アク  キャレットを作成するべきです。また、キーボードフォーカスを失ったり非アク
1017  ティブになる前に、キャレットを破棄するべきです。  ティブになる前に、キャレットを破棄するべきです。
1018  </pre>  </pre>
1019    
1020  とあるため、ウィンドウがアクティブになったタイミングで CreateCaret() を呼び出し、フォーカスが外れ、非アクティブになるタイミングで DestroyCaret() を呼び出す必要があることを意味しています。<br>  とあるため、ウィンドウがアクティブになったタイミングで CreateCaret() を呼び出し、フォーカスが外れ、非アクティブになるタイミングで DestroyCaret() を呼び出す必要があることを意味しています。<br>
1021   キャレットの表示は CaretOn()#vtdisp.c で、消去は CaretOff()#vtdisp.c で実装されています。CaretOn()やCaretOff()が呼び出されるタイミングは、エスケープシーケンス処理 VTParse() の箇所以外にも、マウスボタンを押したときやウィンドウのリサイズを行っているときなどがあります。<br>   キャレットの表示は CaretOn()#vtdisp.c で、消去は CaretOff()#vtdisp.c で実装されています。CaretOn()やCaretOff()が呼び出されるタイミングは、エスケープシーケンス処理 VTParse() の箇所以外にも、マウスボタンを押したときやウィンドウのリサイズを行っているときなどがあります。<br>
1022    
1023    
1024    
1025    <h3>非アクティブ時のカーソル表示</h3>    <h3>非アクティブ時のカーソル表示</h3>
1026   ウィンドウが非アクティブの場合は、カーソルが消滅します。Windowsの上ではユーザが操作できうるウィンドウは1つであるため、システムキャレットも1つのみ用意されています。通常のオペレーションにおいては、この動作で問題がありません。<br>   ウィンドウが非アクティブの場合は、カーソルが消滅します。Windowsの上ではユーザが操作できうるウィンドウは1つであるため、システムキャレットも1つのみ用意されています。通常のオペレーションにおいては、この動作で問題がありません。<br>
1027   しかし、ブロードキャストモードを利用する場合、非アクティブのTera Termウィンドウに対して、コマンドを投入することになります。特に、viなどで複数の端末を同時操作するときは、カーソルが消えていると不都合があります。<br>   しかし、ブロードキャストモードを利用する場合、非アクティブのTera Termウィンドウに対して、コマンドを投入することになります。特に、viなどで複数の端末を同時操作するときは、カーソルが消えていると不都合があります。<br>
1028   そこで、ウィンドウが非アクティブの場合においても、カーソルを描画するようにしています。ただし、システムキャレットは使えないので、自前でカーソルを描画する必要があります。Tera Termのウィンドウが非アクティブの場合においても、リモートホストから送られてくるエスケープシーケンスを処理するためにメインエンジンは動いており、常にカーソル位置は更新されています。現在のカーソル位置は、CursorXとCursorYに設定されています。<br>   そこで、ウィンドウが非アクティブの場合においても、カーソルを描画するようにしています。ただし、システムキャレットは使えないので、自前でカーソルを描画する必要があります。Tera Termのウィンドウが非アクティブの場合においても、リモートホストから送られてくるエスケープシーケンスを処理するためにメインエンジンは動いており、常にカーソル位置は更新されています。現在のカーソル位置は、CursorXとCursorYに設定されています。<br>
1029   非アクティブ時のカーソル表示は CaretKillFocus() で行っています。このときに表示されるカーソルを「ポリゴンカーソル」と呼んでいます。ts.VTColor[0] は Text color です。非アクティブ状態でカーソル位置が更新されるときは、以前に描いたカーソルを消す必要があるので、そのときは ts.VTColor[1] で表される Background color で再描画することで、以前のカーソルを消去しています。<br>   非アクティブ時のカーソル表示は CaretKillFocus() で行っています。このときに表示されるカーソルを「ポリゴンカーソル」と呼んでいます。ts.VTColor[0] は Text color です。非アクティブ状態でカーソル位置が更新されるときは、以前に描いたカーソルを消す必要があるので、そのときは ts.VTColor[1] で表される Background color で再描画することで、以前のカーソルを消去しています。<br>
1030   Background colorでポリゴンカーソルを描画すると、ちょうどそのとき背景にあった文字の一部が欠けることがあります。そのため、その文字の再描画を行う必要があり、UpdateCaretKillFocus() で実現しています。当該関数では InvalidateRect() で WM_PAINT を送ることにより、文字の再描画を促しています。<br>   Background colorでポリゴンカーソルを描画すると、ちょうどそのとき背景にあった文字の一部が欠けることがあります。そのため、その文字の再描画を行う必要があり、UpdateCaretKillFocus() で実現しています。当該関数では InvalidateRect() で WM_PAINT を送ることにより、文字の再描画を促しています。<br>
1031    
1032  <pre class=code>  <pre class=code>
1033  void CaretKillFocus(BOOL show)  void CaretKillFocus(BOOL show)
1034  {  {
1035    int CaretX, CaretY;    int CaretX, CaretY;
1036    POINT p[5];    POINT p[5];
1037    HPEN oldpen;    HPEN oldpen;
1038    HDC hdc;    HDC hdc;
1039    
1040    DispInitDC();    DispInitDC();
1041    hdc = VTDC;    hdc = VTDC;
1042    
1043    CaretX = (CursorX-WinOrgX)*FontWidth;    CaretX = (CursorX-WinOrgX)*FontWidth;
1044    CaretY = (CursorY-WinOrgY)*FontHeight;    CaretY = (CursorY-WinOrgY)*FontHeight;
1045    
1046    p[0].x = CaretX;    p[0].x = CaretX;
1047    p[0].y = CaretY;    p[0].y = CaretY;
1048    p[1].x = CaretX;    p[1].x = CaretX;
1049    p[1].y = CaretY + FontHeight - 1;    p[1].y = CaretY + FontHeight - 1;
1050    if (CursorOnDBCS)    if (CursorOnDBCS)
1051          p[2].x = CaretX + FontWidth*2 - 1;          p[2].x = CaretX + FontWidth*2 - 1;
1052    else    else
1053          p[2].x = CaretX + FontWidth - 1;          p[2].x = CaretX + FontWidth - 1;
1054    p[2].y = CaretY + FontHeight - 1;    p[2].y = CaretY + FontHeight - 1;
1055    if (CursorOnDBCS)    if (CursorOnDBCS)
1056          p[3].x = CaretX + FontWidth*2 - 1;          p[3].x = CaretX + FontWidth*2 - 1;
1057    else    else
1058          p[3].x = CaretX + FontWidth - 1;          p[3].x = CaretX + FontWidth - 1;
1059    p[3].y = CaretY;    p[3].y = CaretY;
1060    p[4].x = CaretX;    p[4].x = CaretX;
1061    p[4].y = CaretY;    p[4].y = CaretY;
1062    
1063    if (show) {  // ポリゴンカーソルを表示(非フォーカス時)    if (show) {  // ポリゴンカーソルを表示(非フォーカス時)
1064            oldpen = SelectObject(hdc, CreatePen(PS_SOLID, 0, ts.VTColor[0]));            oldpen = SelectObject(hdc, CreatePen(PS_SOLID, 0, ts.VTColor[0]));
1065    } else {    } else {
1066            oldpen = SelectObject(hdc, CreatePen(PS_SOLID, 0, ts.VTColor[1]));            oldpen = SelectObject(hdc, CreatePen(PS_SOLID, 0, ts.VTColor[1]));
1067    }    }
1068    Polyline(VTDC, p, 5);    Polyline(VTDC, p, 5);
1069    oldpen = SelectObject(hdc, oldpen);    oldpen = SelectObject(hdc, oldpen);
1070    DeleteObject(oldpen);    DeleteObject(oldpen);
1071    
1072    DispReleaseDC();    DispReleaseDC();
1073  }  }
1074  </pre>  </pre>
1075    
1076    
1077    <h3>非アクティブ時のカーソル表示タイミング</h3>    <h3>非アクティブ時のカーソル表示タイミング</h3>
1078   非アクティブ時のカーソル表示のタイミングは、いくつかのパターンがあるため、漏れなく対処しておく必要があります。表示タイミングとしては以下のとおりです。   非アクティブ時のカーソル表示のタイミングは、いくつかのパターンがあるため、漏れなく対処しておく必要があります。表示タイミングとしては以下のとおりです。
1079    
1080  <p>  <p>
1081  <ul>  <ul>
1082    <li>ウィンドウがアクティブ(Active == TRUE)の場合は、ポリゴンキャレット描画関数(CaretKillFocus)を一切呼ばないようにする。</li>    <li>ウィンドウがアクティブ(Active == TRUE)の場合は、ポリゴンキャレット描画関数(CaretKillFocus)を一切呼ばないようにする。</li>
1083    <li>CaretOn()では、非アクティブ(Active == FALSE)の場合、ShowCaret()を呼ぶタイミングで、ポリゴンキャレット描画関数(true)を呼ぶ。</li>    <li>CaretOn()では、非アクティブ(Active == FALSE)の場合、ShowCaret()を呼ぶタイミングで、ポリゴンキャレット描画関数(true)を呼ぶ。</li>
1084    <li>CaretOff()では、非アクティブ(Active == FALSE)の場合、HideCaret()を呼ぶタイミングで、ポリゴンキャレット描画関数(false)を呼ぶ。</li>    <li>CaretOff()では、非アクティブ(Active == FALSE)の場合、HideCaret()を呼ぶタイミングで、ポリゴンキャレット描画関数(false)を呼ぶ。</li>
1085    <li>IsCaretOn()の判定論理に、(!Active && (CaretStatus==0)) のORを追加する。</li>    <li>IsCaretOn()の判定論理に、(!Active && (CaretStatus==0)) のORを追加する。</li>
1086    <li>ChangeCaret()は何もしない</li>    <li>ChangeCaret()は何もしない</li>
1087    <li>WM_KILLFOCUSされるタイミングでは、IsCaretOn()が真であれば、ポリゴンキャレット描画関数(true)を呼ぶ。</li>    <li>WM_KILLFOCUSされるタイミングでは、IsCaretOn()が真であれば、ポリゴンキャレット描画関数(true)を呼ぶ。</li>
1088    <li>WM_ACTIVEされるタイミングでは、IsCaretOn()が真であれば、ポリゴンキャレット描画関数(false)を呼ぶ。</li>    <li>WM_ACTIVEされるタイミングでは、IsCaretOn()が真であれば、ポリゴンキャレット描画関数(false)を呼ぶ。</li>
1089  </ul>  </ul>
1090  </p>  </p>
1091    
1092  <br>  <br>
1093    
1094  <hr>  <hr>
1095    
1096    
1097    
1098  </BODY>  </BODY>
1099  </HTML>  </HTML>

Legend:
Removed from v.3226  
changed lines
  Added in v.3227

Back to OSDN">Back to OSDN
ViewVC Help
Powered by ViewVC 1.1.26