• Support
  • Articles
  • Resources
  • Products

Tera Term Source Code Overview

8. Compatibility with Obsolete Windows Versions

Dynamic Loading

The same Microsoft Windows application executable file can work fine under different Windows versions if certain programming techniques are applied. For example, if we call API function SetLayeredWindowAttributes() that was introduced in Windows 2000, our application will fail under older Windows versions such as Windows NT4.0, or Windows 98. To be able to use newer API-s we need to dynamically load them with LoadLibrary() function.

  1. static BOOL MySetLayeredWindowAttributes(HWND hwnd, COLORREF crKey, BYTE bAlpha, DWORD dwFlags)
  2. {
  3. typedef BOOL (WINAPI *func)(HWND,COLORREF,BYTE,DWORD);
  4. static HMODULE g_hmodUser32 = NULL;
  5. static func g_pSetLayeredWindowAttributes = NULL;
  6.  
  7. if (g_hmodUser32 == NULL) {
  8. g_hmodUser32 = LoadLibrary("user32.dll");
  9. if (g_hmodUser32 == NULL)
  10. return FALSE;
  11.  
  12. g_pSetLayeredWindowAttributes =
  13. (func)GetProcAddress(g_hmodUser32, "SetLayeredWindowAttributes");
  14. }
  15.  
  16. if (g_pSetLayeredWindowAttributes == NULL)
  17. return FALSE;
  18.  
  19. return g_pSetLayeredWindowAttributes(hwnd, crKey,
  20. bAlpha, dwFlags);
  21. }

The downside of this approach is - too much work creating prototypes for every function that will be used. The easier way to achieve the same result is to use a mechanism called "lazy loading DLL". If the function you want to use is not supported by older Windows version, you can specify it in Visual Studio project settings; mark the function as lazy loaded DLL.

Windows 95

Visual Studio 2005 and later VS versions no longer support Microsoft Windows 95. Binary executable files built by Visual Studio 2005 won't run on Windows 95. Visual Studio 2008 and 2010 no longer support Windows 98, NT4.0 and 2000. Windows XP support will also end in near future.

Currently Tera Term can run on Windows 95 despite the fact that it is compiled using Visual Studio 2005. The binary program built by Visual Studio 2005 by default contains link to IsDebuggerPresent function. This causes program to fail under Windows 95 because this function was first introduced in Windows 98. To resolve this issue dummy symbol replacing IsDebuggerPresent function was defined. This is certainly "unofficial" method not supported by Microsoft. For more details please check the header file comapt_w95.h.


9. Debugging Methods

Debug printf

In general, Windows applications cannot use printf() function for debugging, because in these applications standard output is not defined. However, application can use printf() together with AllocConsole() and freopen().

Applications can also display messages in debug console of Visual Studio by using OutputDebugString() API calls. When debugger starts, such debug message can be shown regardless of "Debug build" or "Release build" setting. Furthermore, if external debugger is used, like for example DBCon, debug messages generated by OutputDebugString() will still be displayed.

Tera Term contains wrapper function allowing to send variable length arguments to OutputDebugString().

  1. void OutputDebugPrintf(char *fmt, ...) {
  2. char tmp[1024];
  3. va_list arg;
  4. va_start(arg, fmt);
  5. _vsnprintf(tmp, sizeof(tmp), fmt, arg);
  6. OutputDebugString(tmp);
  7. }
Memory Leaks

Memory leaks occur when developer uses malloc() or similar function to allocate heap memory and then forgets to free it. Visual Studio has mechanism to automatically detect memory leaks. You just need to add below code to the beginning of your program. If any part of heap memory remains unreleased when the program terminates, Visual Studio will show it in the output window.

  1. #ifdef _DEBUG
  2. _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
  3. #endif

It should be noted that Windows allocates separate virtual memory region for each running process. If program ends and leaves unreleased memory, operating system will still release it.


10. Multithreading

Vast majority of modern Windows application use multithreading, however back in the days of Windows 3.1 and Windows 95 when Tera Term was born, this technique was considered a gimmick. Generally speaking Tera Term is singlethreaded application. As it can be seen from the source code, Tera Term uses lots of global variables and most of the procedures are not thread-safe.

However, there are few procedures that utilize _beginthreadex() API function to implement multithreading. The cases where multithreading is used are listed in the tables below.

Tera Term
Generating point Source file
Serial connection CommStart()#commlib.c
TELNET keep-alive TelStartKeepAliveThread()#telnet.c
IPv4/v6 socket creation WSAAsyncGetAddrInfo()#WSAAsyncGetAddrInfo.c

TTSSH
Generating point Source file
SSH keep-alive start_ssh_heartbeat_thread()#ssh.c
SCP sending SSH2_scp_tolocal()#ssh.c
SCP receiving SSH2_scp_fromremote()#ssh.c

As mentioned above, Tera Term and TTSSH are not thread-safe and problems may occur when new thread is created, or when send and receive procedures are interacting with the thread. However, there are cases when multithreading cannot be avoided. For example, a packet needs to be transmitted periodically in order to implement keep-alive (heartbeat) mechanism for TELNET and SSH protocols. Another example - while a file is being sent or received via SCP, user should be able to continue working in terminal window.

When Tera Term uses multithread model, hidden modeless window is created and a thread is generated by using the _beginthreadex() API function. Then the actual procedure works with this modeless window. While this method uses multithreading, it allows to keep the thread safe. The sample code is shown below.

  1. #define WM_SEND_HEARTBEAT (WM_USER + 1)
  2.  
  3. static LRESULT CALLBACK telnet_heartbeat_dlg_proc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
  4. {
  5.  
  6. switch (msg) {
  7. case WM_INITDIALOG:
  8. return FALSE;
  9.  
  10. case WM_SEND_HEARTBEAT:
  11. TelSendNOP();
  12. return TRUE;
  13. break;
  14.  
  15. case WM_COMMAND:
  16. break;
  17.  
  18. case WM_CLOSE:
  19. return TRUE;
  20.  
  21. case WM_DESTROY:
  22. return TRUE;
  23.  
  24. default:
  25. return FALSE;
  26. }
  27. return TRUE;
  28. }
  29.  
  30. static unsigned _stdcall TelKeepAliveThread(void *dummy) {
  31. static int instance = 0;
  32.  
  33. if (instance > 0)
  34. return 0;
  35. instance++;
  36.  
  37. while (cv.Open && nop_interval > 0) {
  38. if (time(NULL) >= cv.LastSendTime + nop_interval) {
  39. SendMessage(keepalive_dialog, WM_SEND_HEARTBEAT, 0, 0);
  40. }
  41.  
  42. Sleep(100);
  43. }
  44. instance--;
  45. return 0;
  46. }
  47.  
  48. void TelStartKeepAliveThread() {
  49. unsigned tid;
  50.  
  51. if (ts.TelKeepAliveInterval > 0) {
  52. nop_interval = ts.TelKeepAliveInterval;
  53.  
  54. keepalive_dialog = CreateDialog(hInst, MAKEINTRESOURCE(IDD_BROADCAST_DIALOG),
  55. HVTWin, (DLGPROC)telnet_heartbeat_dlg_proc);
  56.  
  57. keepalive_thread = (HANDLE)_beginthreadex(NULL, 0, TelKeepAliveThread, NULL, 0, &tid);
  58. if (keepalive_thread == (HANDLE)-1) {
  59. nop_interval = 0;
  60. }
  61. }
  62. }

11. DDE Communication

Overview

Dynamic Data Exchange (DDE) mechanism was introduced in 1987 in Windows 2.0. While DDE allows to establish communication between processes, it is currently considered as legacy technique and is rarely used. Modern methods of inter-process communication are mailslot, named pipes and OLE.

Earlier versions of Microsoft Visual Studio contained DDE spy tool (DDESPY.EXE) that could capture DDE traffic, however this tool is not part of VS anymore.

More information about DDE is available from MSDN Library on Microsoft web site:


DDE communication is similar to TCP - both protocols allow to open peer-to-peer, client-server connection. Applications can establish DDE connectivity by calling functions from Dynamic Data Exchange Management Library (DDEML). Calls to DDEML functions are done the same way as the calls to Win32 API functions.

During DDE communication one process acts as a server and another acts as a client. Each DDE connection must have system-wide unique session identifier. TCP uses IP address and port number combination for this purpose, while DDE uses service name and topic name. DDE service name for Tera Term is constant string "TERATERM". Topic name is generated dynamically by translating the handle (HVTWin) of Tera Term core process into hexadecimal string.

Due to the nature of DDE, macro application can only communicate with one instance of Tera Term.

DDE Communication

Tera Term core (ttermpro.exe) acts as DDE server and macro program (ttpmacro.exe) acts as DDE client as shown on the drawing above. DDE payload is called "transaction". Transactions can be of several different types that are listed in the following table. They are defined as macros in file "ddeml.h".

Type Description
XTYP_ADVREQ Informs the server that an advise transaction is outstanding for the specific topic name. The system sends this transaction to DDE callback function DdeCallback, then the server calls the DdePostAdvise function.
XTYP_POKE DDE client uses XTYP_POKE transaction to send unsolicited data to the server.
XTYP_ADVSTART Advise loop starts on the DDE server.
XTYP_ADVDATA Periodically informs the client about data item value change.
XTYP_EXECUTE DDE client uses XTYP_EXECUTE transaction to send a command string to the server.

DDE has a feature called "advise loop". When DDE server enters advise loop, client will start receiving data periodically. Tera Term uses advise loop to repeatedly send data from remote host to macro application.

DDEML Library

DDEML Library functions used by Tera Term are listed below.

Function Description
DdeInitialize Initializes DDE and registers callback function. If the function succeeds, it returns DMLERR_NO_ERROR.
DdeCreateStringHandle Creates a handle for a string. The handle is used for communication between the server and the client.
DdeNameService Registers or unregisters the service name "TERATERM" in DDE server. If the registration succeeds, the XTYP_REGISTER transaction is send to the DDE client.
DdeCmpStringHandles Compares two string handles.
DdeClientTransaction Sends a transaction from the client to the server. Type of transactions can be specified. Examples of transaction types are: XTYP_REQUEST, XTYP_EXECUTE, XTYP_ADVSTART, XTYP_POKE. Timeout value can be set to define the maximum amount of time in milliseconds that the client will wait for a response (ACK) from the server. Tera Term uses timeout value of 5000 milliseconds (5 seconds).
DdeAccessData Provides access to the data in the specified DDE object. An application must call the DdeUnaccessData function when it has finished accessing the data in the object.
DdeCreateDataHandle Creates the DDE object and returns the handle. The handle is used for the DDE server's advise loop, also to send data to DDE client when XTYP_REQUEST transaction is received.
DdeGetData Copies data from the specified DDE object to the specified local buffer.
DdeDisconnect Terminates DDE communication.
DdePostAdvise Causes the system to send XTYP_ADVREQ transaction to the calling (server) application's DDE callback function for each client with an active advise loop on the specified topic and item. A server application should call this function whenever the data associated with the topic name or item name pair changes.

DDE Server Implementation

During DDE communication Tera Term core (ttermpro.exe) acts as DDE server that's why it needs to be launched the first.

When macro program (ttpmacro.exe), which is DDE client, processes a macro script, it will start DDE communication with DDE server only when it reaches "connect" macro command.

If a macro is executed from Tera Term's menu "Control">"Macro", RunMacro()#ttdde.c function will be called. Then topic name (8 bytes) is created from window handle (HVTWin), DDE is initialized and the server is registered. At the same time DDE buffer (1KB) is created. Finally, topic name is passed to "ttpmacro.exe" in /D argument and "ttpmacro.exe" is launched.

  1. SetTopic();
  2. if (! InitDDE()) return;
  3. strncpy_s(Cmnd, sizeof(Cmnd),"TTPMACRO /D=", _TRUNCATE);
  4. strncat_s(Cmnd,sizeof(Cmnd),TopicName,_TRUNCATE);

Every time a transaction is sent from DDE client to DDE sever DdeCallbackProc callback function is being executed. The callback function is registered during DDE initialization with DdeInitialize().

DDE Client Implementation

Function InitDDE()#ttmdde.c called on macro program (ttpmacro.exe) startup initializes DDE client. DdeInitialize() function initializes DDE and registers DdeCallbackProc callback function. The transactions received from DDE server are processed by callback function.
When DDE communication starts, DdeConnect() must be called to connect to DDE server. Then XTYP_EXECUTE transaction is sent to "ttermpro.exe" window handle (HWin) as notification. Finally, XTYP_ADVSTART transaction is sent to the server that starts the advise loop.

  1. ConvH = DdeConnect(Inst, Service, Topic, NULL);
  2. if (ConvH == 0) return FALSE;
  3. Linked = TRUE;
  4.  
  5. Cmd[0] = CmdSetHWnd;
  6. w = HIWORD(HWin);
  7. Word2HexStr(w,&(Cmd[1]));
  8. w = LOWORD(HWin);
  9. Word2HexStr(w,&(Cmd[5]));
  10.  
  11. DdeClientTransaction(Cmd,strlen(Cmd)+1,ConvH,0,
  12. CF_OEMTEXT,XTYP_EXECUTE,1000,NULL);
  13.  
  14. DdeClientTransaction(NULL,0,ConvH,Item,
  15. CF_OEMTEXT,XTYP_ADVSTART,1000,NULL);

Buffer Management

Certain macro commands (like "wait" and few others) need to analyze the data received from the remote host to make the decision on how to proceed. To implement this functionality both Tera Term core and the macro program need to have buffers.

DDE Flow Control

Tera Term core sends the data from the remote host to macro program via DDE communication (DDE transaction). Initially TCP packet received from a remote host will be detected by OnIdle()#teraterm.cpp loop. OnIdle() then calls CommReceive()#commlib.c which places TCP packet into the buffer (cv->InBuff[]). The size of this buffer is 1KB. It is not a ring buffer and once it fills up, it won't be able to store more packets. If this happens Windows kernel will start accumulating received TCP packets, but if the buffer remains full for a long time, eventually we will start loosing packets from the remote host.

Log collection, escape sequence analysis and macro execution related operations require data buffering. In all these cases Tera Term uses LogPut1() function to place data into DDE buffer cv.LogBuf[]. In other words, the logging and macro executing functions share the same buffer. The size of this buffer is 1KB and this is ring buffer, i.e the oldest data will be overwritten if the buffer is full.

Additionally, when logging mode is binary, the data is stored in another buffer cv.BinBuf[], which is not related to DDE. So, the binary data cannot be passed to DDE client. This means "wait" and similar macro commands cannot be used to wait for a binary data or escape sequences.

When the escape sequence analysis is complete, DDEAdv()#ttdde.c function is called and XTYP_ADVREQ type transaction is sent by DDE server to itself. When server receives XTYP_ADVREQ, it will call DdeCallbackProc() function to pass the data to macro program. Advise loop is being used during this process.

DDE Buffer Management

DDE server's advise loop sends the data in XTYP_ADVDATA transaction type. DDE client (ttpmacro.exe) receives this data and processed it in DdeCallbackProc()#ttmdde.c function.
As it has been mentioned earlier DDE communication buffer and log buffer in Tera Term core are the same (cv.LogBuf[]). For DDE communication the buffer start index is stored in "DStart" and buffer length is "Dcount". For logging corresponding variables are "LStart" and "Lcount". Since the buffer is shared, to avoid data loss, these variables must always be synchronized, i.e. DStart=LStart and Dcount=Lcount.


Copyright © 2008-2015 Tera Term Project