オープンソース・ソフトウェアの開発とダウンロード

Subversion リポジトリの参照

Contents of /trunk/doc/ja/html/reference/sourcecode.html

Parent Directory Parent Directory | Revision Log Revision Log


Revision 3871 - (show annotations) (download) (as text)
Mon Apr 26 12:06:23 2010 UTC (14 years ago) by yutakapon
File MIME type: text/html
File size: 65863 byte(s)
Windows95サポートについて。

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

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