Related
Imagine that you have two processes: one sends commands and working in administrative mode, another reads them and executes them.
bool SendMessage(const std::wstring& message, HANDLE pipe)
{
DWORD written = 0;
int bytesToSend = (message.size() + 1) * sizeof(wchar_t);
WriteFile(pipe, message.c_str(), bytesToSend, &written, nullptr); // WinAPI
return written == bytesToSend;
}
std::wstring ReadMessage(HANDLE pipe)
{
std::wstring message;
wchar_t wch;
DWORD bytesRead = 0;
while (true)
{
if (!ReadFile(pipe, &wch, sizeof(wch), &bytesRead, NULL) || !(bytesRead == sizeof(wch))) // WinAPI
{
message.clear();
break;
}
if (wch)
{
message += wch;
}
else
{
break;
}
}
return message;
}
void CommandLoop(HANDLE pipe)
{
DWORD dummy = 0;
WriteFile(pipe, "", 1, &dummy, NULL); //pass the token
while (true)
{
std::wstring command = ReadMessage(pipe);
if (command.empty())
break;
std::wstring file;
std::wstring params;
// Some handling here
// ...
// You are here
if (ParseCommand(command, file, params))
{
INT_PTR shellResult = reinterpret_cast<INT_PTR>(::ShellExecute(nullptr, L"open", file.c_str(), params.c_str(), nullptr, SW_SHOW)); /// freeze here
if (shellResult > HINSTANCE_ERROR)
{
SendMessage(L"ok", pipe);
}
else
{
SendMessage(std::to_wstring(shellResult), pipe);
}
}
}
}
From first process I send message with SendMessage for milliseconds. Second process spinning in CommandLoop.
Then I read this message in second process with ReadMessage for milliseconds. But when I call ShellExecute function in second process, this process freezes for 30 seconds only on Windows XP. On Windows 7, 8, 10 this code works correctly. What's could be wrong?
One common issue is virus scanners. Windows XP of course is long out of support, so performance on such systems is generally considered a non-issue. No makes of virus scanners is going to spend time making them faster on XP.
I have the following code as part of another module that sends messages to the client. This was for IPC. Two dll's are loaded by the exe and these two need to communicate
In DLL-1 I have the following line of code as the server named pipe.
pipe = CreateNamedPipe("\\\\.\\pipe\\S2D8",PIPE_ACCESS_OUTBOUND | FILE_FLAG_OVERLAPPED /**1-way, send only with overlapped IO*/,
PIPE_TYPE_MESSAGE,1,0,0, 0, NULL);
if( INVALID_HANDLE_VALUE != pipe )
{
log("Created Named Pipe as Serverl\n");
}
else
{
log("Cannot create Named Pipe as Server\n");
}
And somewhere else in the DLL-1 I have the following for the server
bool result = ConnectNamedPipe(pipe, NULL);
if (!result)
{
CloseHandle(pipe); // close the pipe
}
else
{
DWORD numWritten;
WriteFile(pipe,KeyBoardBuffer,strlen(KeyBoardBuffer) * sizeof(char),&numWritten,0);
log("Bytes writtern to pipe:%d\n",numWritten);
}
When I look at the logs, I can see the that named pipe. Good so far.
While in DLL-2 I have the following as the client part
log("Connecting to named pipe at client\n");
if(pipe2 == NULL || pipe2 == INVALID_HANDLE_VALUE)
{
pipe2 = CreateFile("\\\\.\\pipe\\S2D8", GENERIC_READ ,
FILE_SHARE_READ | FILE_SHARE_WRITE,NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,NULL);
if (pipe2 == INVALID_HANDLE_VALUE)
{
log("Cannot connect to named pipe at client%x\n", GetLastError());
CloseHandle(pipe2);
}
else
{
log("Connected to named pipe at client! Going to read!!!\n");
char buffer[256] = {'\0'};
DWORD numBytesRead = 0;
BOOL result = ReadFile(
pipe2,
buffer, // the data from the pipe will be put here
sizeof(buffer) * sizeof(char), // number of bytes allocated
&numBytesRead, // this will store number of bytes actually read
NULL // not using overlapped IO
);
if (result)
{
kbBuffer[numBytesRead / sizeof(char)] = '\0'; // null terminate the string
log( "Number of bytes read: %d\n",numBytesRead);
log(kbBuffer );
}
else
{
log("Failed to read data from the pipe.\n");
}
}
}
And in my logs, I can see the line "Connecting to named pipe at client" and then "Connected to named pipe at client! Going to read!!!", after that there is nothing in the log, everything seems stuck.
Is the naming convention of pipe correct? Or is there any security settings I have to define?
I am using VS2010, Win7 x64.
Any guidance is much appreciated.
You're calling the wrong method. The pipe is supposed to pre-exist, so you should be calling OpenFile(), not CreateFile().
Ah, I found the answer to the hang, I had to do a PeekNamedPipe(pipe2, NULL, 0, NULL, &bytesAvailable, NULL); and then check for the bytesAvailable to be greater than zero before I did a ReadFile()
I'm having a serious problem here. I need to execute a CMD command line via C++ without the console window displaying. Therefore I cannot use system(cmd), since the window will display.
I have tried winExec(cmd, SW_HIDE), but this does not work either. CreateProcess is another one I tried. However, this is for running programs or batch files.
I have ended up trying ShellExecute:
ShellExecute( NULL, "open",
"cmd.exe",
"ipconfig > myfile.txt",
"c:\projects\b",
SW_SHOWNORMAL
);
Can anyone see anything wrong with the above code? I have used SW_SHOWNORMAL until I know this works.
I really need some help with this. Nothing has come to light, and I have been trying for quite a while. Any advice anyone could give would be great :)
Redirecting the output to your own pipe is a tidier solution because it avoids creating the output file, but this works fine:
ShellExecute(0, "open", "cmd.exe", "/C ipconfig > out.txt", 0, SW_HIDE);
You don't see the cmd window and the output is redirected as expected.
Your code is probably failing (apart from the /C thing) because you specify the path as "c:\projects\b" rather than "c:\\projects\\b".
Here is my implementation of a DosExec function that allows to (silently) execute any DOS command and retrieve the generated output as a unicode string.
// Convert an OEM string (8-bit) to a UTF-16 string (16-bit)
#define OEMtoUNICODE(str) CHARtoWCHAR(str, CP_OEMCP)
/* Convert a single/multi-byte string to a UTF-16 string (16-bit).
We take advantage of the MultiByteToWideChar function that allows to specify the charset of the input string.
*/
LPWSTR CHARtoWCHAR(LPSTR str, UINT codePage) {
size_t len = strlen(str) + 1;
int size_needed = MultiByteToWideChar(codePage, 0, str, len, NULL, 0);
LPWSTR wstr = (LPWSTR) LocalAlloc(LPTR, sizeof(WCHAR) * size_needed);
MultiByteToWideChar(codePage, 0, str, len, wstr, size_needed);
return wstr;
}
/* Execute a DOS command.
If the function succeeds, the return value is a non-NULL pointer to the output of the invoked command.
Command will produce a 8-bit characters stream using OEM code-page.
As charset depends on OS config (ex: CP437 [OEM-US/latin-US], CP850 [OEM 850/latin-1]),
before being returned, output is converted to a wide-char string with function OEMtoUNICODE.
Resulting buffer is allocated with LocalAlloc.
It is the caller's responsibility to free the memory used by the argument list when it is no longer needed.
To free the memory, use a single call to LocalFree function.
*/
LPWSTR DosExec(LPWSTR command){
// Allocate 1Mo to store the output (final buffer will be sized to actual output)
// If output exceeds that size, it will be truncated
const SIZE_T RESULT_SIZE = sizeof(char)*1024*1024;
char* output = (char*) LocalAlloc(LPTR, RESULT_SIZE);
HANDLE readPipe, writePipe;
SECURITY_ATTRIBUTES security;
STARTUPINFOA start;
PROCESS_INFORMATION processInfo;
security.nLength = sizeof(SECURITY_ATTRIBUTES);
security.bInheritHandle = true;
security.lpSecurityDescriptor = NULL;
if ( CreatePipe(
&readPipe, // address of variable for read handle
&writePipe, // address of variable for write handle
&security, // pointer to security attributes
0 // number of bytes reserved for pipe
) ){
GetStartupInfoA(&start);
start.hStdOutput = writePipe;
start.hStdError = writePipe;
start.hStdInput = readPipe;
start.dwFlags = STARTF_USESTDHANDLES + STARTF_USESHOWWINDOW;
start.wShowWindow = SW_HIDE;
// We have to start the DOS app the same way cmd.exe does (using the current Win32 ANSI code-page).
// So, we use the "ANSI" version of createProcess, to be able to pass a LPSTR (single/multi-byte character string)
// instead of a LPWSTR (wide-character string) and we use the UNICODEtoANSI function to convert the given command
if (CreateProcessA(NULL, // pointer to name of executable module
UNICODEtoANSI(command), // pointer to command line string
&security, // pointer to process security attributes
&security, // pointer to thread security attributes
TRUE, // handle inheritance flag
NORMAL_PRIORITY_CLASS, // creation flags
NULL, // pointer to new environment block
NULL, // pointer to current directory name
&start, // pointer to STARTUPINFO
&processInfo // pointer to PROCESS_INFORMATION
)){
// wait for the child process to start
for(UINT state = WAIT_TIMEOUT; state == WAIT_TIMEOUT; state = WaitForSingleObject(processInfo.hProcess, 100) );
DWORD bytesRead = 0, count = 0;
const int BUFF_SIZE = 1024;
char* buffer = (char*) malloc(sizeof(char)*BUFF_SIZE+1);
strcpy(output, "");
do {
DWORD dwAvail = 0;
if (!PeekNamedPipe(readPipe, NULL, 0, NULL, &dwAvail, NULL)) {
// error, the child process might have ended
break;
}
if (!dwAvail) {
// no data available in the pipe
break;
}
ReadFile(readPipe, buffer, BUFF_SIZE, &bytesRead, NULL);
buffer[bytesRead] = '\0';
if((count+bytesRead) > RESULT_SIZE) break;
strcat(output, buffer);
count += bytesRead;
} while (bytesRead >= BUFF_SIZE);
free(buffer);
}
}
CloseHandle(processInfo.hThread);
CloseHandle(processInfo.hProcess);
CloseHandle(writePipe);
CloseHandle(readPipe);
// convert result buffer to a wide-character string
LPWSTR result = OEMtoUNICODE(output);
LocalFree(output);
return result;
}
You should use CreateProcess on cmd.exe with the /C parameter to tunnel the ipconfig command. The > does not work per se on the command line. You have to redirect programmatically the stdout.
I have a similar program [windows7 and 10 tested] on github
https://github.com/vlsireddy/remwin/tree/master/remwin
This is server program which
listens on "Local Area Connection" named interface in windows for UDP port (5555) and receives udp packet.
received udp packet content is executed on cmd.exe [please not cmd.exe is NOT closed after running the command and the output string [the output of the executed command] is fedback to the client program over same udp port].
In other words,
command received in udp packet -> parsed udp packet -> executed on cmd.exe -> output sent back on same port to client program
This does not show "console window"
No need for someone to execute manually command on cmd.exe
remwin.exe can be running in background and its a thin server program
To add to #Cédric Françoys answer, I fixed a few things in his code for a Windows build:
Missing function definition:
To make the code compile, add the following function definition:
#define UNICODEtoANSI(str) WCHARtoCHAR(str, CP_OEMCP)
LPSTR WCHARtoCHAR(LPWSTR wstr, UINT codePage) {
int len = (int)wcslen(wstr) + 1;
int size_needed = WideCharToMultiByte(codePage, 0, wstr, len, NULL, 0, NULL, NULL);
LPSTR str = (LPSTR)LocalAlloc(LPTR, sizeof(CHAR) * size_needed);
WideCharToMultiByte(codePage, 0, wstr, len, str, size_needed, NULL, NULL);
return str;
}
Unsafe CRT string function calls:
To make the code compile, replace strcpy and strcat with the following calls
strcpy_s(output, sizeof(output), "");
strcat_s(output, RESULT_SIZE, buffer);
Remove redundant null-termination:
Remove in the do-while loop:
buffer[bytesRead] = '\0';
because strcat_s takes care of that.
You could use
string command = "start /B cmd /c " + myCommand;
system(command.c_str());
Hopefully this works for you
I'm having a serious problem here. I need to execute a CMD command line via C++ without the console window displaying. Therefore I cannot use system(cmd), since the window will display.
I have tried winExec(cmd, SW_HIDE), but this does not work either. CreateProcess is another one I tried. However, this is for running programs or batch files.
I have ended up trying ShellExecute:
ShellExecute( NULL, "open",
"cmd.exe",
"ipconfig > myfile.txt",
"c:\projects\b",
SW_SHOWNORMAL
);
Can anyone see anything wrong with the above code? I have used SW_SHOWNORMAL until I know this works.
I really need some help with this. Nothing has come to light, and I have been trying for quite a while. Any advice anyone could give would be great :)
Redirecting the output to your own pipe is a tidier solution because it avoids creating the output file, but this works fine:
ShellExecute(0, "open", "cmd.exe", "/C ipconfig > out.txt", 0, SW_HIDE);
You don't see the cmd window and the output is redirected as expected.
Your code is probably failing (apart from the /C thing) because you specify the path as "c:\projects\b" rather than "c:\\projects\\b".
Here is my implementation of a DosExec function that allows to (silently) execute any DOS command and retrieve the generated output as a unicode string.
// Convert an OEM string (8-bit) to a UTF-16 string (16-bit)
#define OEMtoUNICODE(str) CHARtoWCHAR(str, CP_OEMCP)
/* Convert a single/multi-byte string to a UTF-16 string (16-bit).
We take advantage of the MultiByteToWideChar function that allows to specify the charset of the input string.
*/
LPWSTR CHARtoWCHAR(LPSTR str, UINT codePage) {
size_t len = strlen(str) + 1;
int size_needed = MultiByteToWideChar(codePage, 0, str, len, NULL, 0);
LPWSTR wstr = (LPWSTR) LocalAlloc(LPTR, sizeof(WCHAR) * size_needed);
MultiByteToWideChar(codePage, 0, str, len, wstr, size_needed);
return wstr;
}
/* Execute a DOS command.
If the function succeeds, the return value is a non-NULL pointer to the output of the invoked command.
Command will produce a 8-bit characters stream using OEM code-page.
As charset depends on OS config (ex: CP437 [OEM-US/latin-US], CP850 [OEM 850/latin-1]),
before being returned, output is converted to a wide-char string with function OEMtoUNICODE.
Resulting buffer is allocated with LocalAlloc.
It is the caller's responsibility to free the memory used by the argument list when it is no longer needed.
To free the memory, use a single call to LocalFree function.
*/
LPWSTR DosExec(LPWSTR command){
// Allocate 1Mo to store the output (final buffer will be sized to actual output)
// If output exceeds that size, it will be truncated
const SIZE_T RESULT_SIZE = sizeof(char)*1024*1024;
char* output = (char*) LocalAlloc(LPTR, RESULT_SIZE);
HANDLE readPipe, writePipe;
SECURITY_ATTRIBUTES security;
STARTUPINFOA start;
PROCESS_INFORMATION processInfo;
security.nLength = sizeof(SECURITY_ATTRIBUTES);
security.bInheritHandle = true;
security.lpSecurityDescriptor = NULL;
if ( CreatePipe(
&readPipe, // address of variable for read handle
&writePipe, // address of variable for write handle
&security, // pointer to security attributes
0 // number of bytes reserved for pipe
) ){
GetStartupInfoA(&start);
start.hStdOutput = writePipe;
start.hStdError = writePipe;
start.hStdInput = readPipe;
start.dwFlags = STARTF_USESTDHANDLES + STARTF_USESHOWWINDOW;
start.wShowWindow = SW_HIDE;
// We have to start the DOS app the same way cmd.exe does (using the current Win32 ANSI code-page).
// So, we use the "ANSI" version of createProcess, to be able to pass a LPSTR (single/multi-byte character string)
// instead of a LPWSTR (wide-character string) and we use the UNICODEtoANSI function to convert the given command
if (CreateProcessA(NULL, // pointer to name of executable module
UNICODEtoANSI(command), // pointer to command line string
&security, // pointer to process security attributes
&security, // pointer to thread security attributes
TRUE, // handle inheritance flag
NORMAL_PRIORITY_CLASS, // creation flags
NULL, // pointer to new environment block
NULL, // pointer to current directory name
&start, // pointer to STARTUPINFO
&processInfo // pointer to PROCESS_INFORMATION
)){
// wait for the child process to start
for(UINT state = WAIT_TIMEOUT; state == WAIT_TIMEOUT; state = WaitForSingleObject(processInfo.hProcess, 100) );
DWORD bytesRead = 0, count = 0;
const int BUFF_SIZE = 1024;
char* buffer = (char*) malloc(sizeof(char)*BUFF_SIZE+1);
strcpy(output, "");
do {
DWORD dwAvail = 0;
if (!PeekNamedPipe(readPipe, NULL, 0, NULL, &dwAvail, NULL)) {
// error, the child process might have ended
break;
}
if (!dwAvail) {
// no data available in the pipe
break;
}
ReadFile(readPipe, buffer, BUFF_SIZE, &bytesRead, NULL);
buffer[bytesRead] = '\0';
if((count+bytesRead) > RESULT_SIZE) break;
strcat(output, buffer);
count += bytesRead;
} while (bytesRead >= BUFF_SIZE);
free(buffer);
}
}
CloseHandle(processInfo.hThread);
CloseHandle(processInfo.hProcess);
CloseHandle(writePipe);
CloseHandle(readPipe);
// convert result buffer to a wide-character string
LPWSTR result = OEMtoUNICODE(output);
LocalFree(output);
return result;
}
You should use CreateProcess on cmd.exe with the /C parameter to tunnel the ipconfig command. The > does not work per se on the command line. You have to redirect programmatically the stdout.
I have a similar program [windows7 and 10 tested] on github
https://github.com/vlsireddy/remwin/tree/master/remwin
This is server program which
listens on "Local Area Connection" named interface in windows for UDP port (5555) and receives udp packet.
received udp packet content is executed on cmd.exe [please not cmd.exe is NOT closed after running the command and the output string [the output of the executed command] is fedback to the client program over same udp port].
In other words,
command received in udp packet -> parsed udp packet -> executed on cmd.exe -> output sent back on same port to client program
This does not show "console window"
No need for someone to execute manually command on cmd.exe
remwin.exe can be running in background and its a thin server program
To add to #Cédric Françoys answer, I fixed a few things in his code for a Windows build:
Missing function definition:
To make the code compile, add the following function definition:
#define UNICODEtoANSI(str) WCHARtoCHAR(str, CP_OEMCP)
LPSTR WCHARtoCHAR(LPWSTR wstr, UINT codePage) {
int len = (int)wcslen(wstr) + 1;
int size_needed = WideCharToMultiByte(codePage, 0, wstr, len, NULL, 0, NULL, NULL);
LPSTR str = (LPSTR)LocalAlloc(LPTR, sizeof(CHAR) * size_needed);
WideCharToMultiByte(codePage, 0, wstr, len, str, size_needed, NULL, NULL);
return str;
}
Unsafe CRT string function calls:
To make the code compile, replace strcpy and strcat with the following calls
strcpy_s(output, sizeof(output), "");
strcat_s(output, RESULT_SIZE, buffer);
Remove redundant null-termination:
Remove in the do-while loop:
buffer[bytesRead] = '\0';
because strcat_s takes care of that.
You could use
string command = "start /B cmd /c " + myCommand;
system(command.c_str());
Hopefully this works for you
I don't have much experience with Serial I/O, but have recently been tasked with fixing some highly flawed serial code, because the original programmer has left the company.
The application is a Windows program that talks to a scientific instrument serially via a virtual COMM port running on USB. Virtual COMM port USB drivers are provided by FTDI, since they manufacture the USB chip we use on the instrument.
The serial code is in an unmanaged C++ DLL, which is shared by both our old C++ software, and our new C# / .Net (WinForms) software.
There are two main problems:
Fails on many XP systems
When the first command is sent to the instrument, there's no response. When you issue the next command, you get the response from the first one.
Here's a typical usage scenario (full source for methods called is included below):
char szBuf [256];
CloseConnection ();
if (OpenConnection ())
{
ClearBuffer ();
// try to get a firmware version number
WriteChar ((char) 'V');
BOOL versionReadStatus1 = ReadString (szBuf, 100);
...
}
On a failing system, the ReadString call will never receive any serial data, and times out. But if we issue another, different command, and call ReadString again, it will return the response from the first command, not the new one!
But this only happens on a large subset of Windows XP systems - and never on Windows 7. As luck would have it, our XP dev machines worked OK, so we did not see the problem until we started beta testing. But I can also reproduce the problem by running an XP VM (VirtualBox) on my XP dev machine. Also, the problem only occurs when using the DLL with the new C# version - works fine with the old C++ app.
This seemed to be resolved when I added a Sleep(21) to the low level BytesInQue method before calling ClearCommError, but this exacerbated the other problem - CPU usage. Sleeping for less than 21 ms would make the failure mode reappear.
High CPU usage
When doing serial I/O CPU use is excessive - often above 90%. This happens with both the new C# app and the old C++ app, but is much worse in the new app. Often makes the UI very non-responsive, but not always.
Here's the code for our Port.cpp class, in all it's terrible glory. Sorry for the length, but this is what I'm working with. Most important methods are probably OpenConnection, ReadString, ReadChar, and BytesInQue.
//
// Port.cpp: Implements the CPort class, which is
// the class that controls the serial port.
//
// Copyright (C) 1997-1998 Microsoft Corporation
// All rights reserved.
//
// This source code is only intended as a supplement to the
// Broadcast Architecture Programmer's Reference.
// For detailed information regarding Broadcast
// Architecture, see the reference.
//
#include <windows.h>
#include <stdio.h>
#include <assert.h>
#include "port.h"
// Construction code to initialize the port handle to null.
CPort::CPort()
{
m_hDevice = (HANDLE)0;
// default parameters
m_uPort = 1;
m_uBaud = 9600;
m_uDataBits = 8;
m_uParity = 0;
m_uStopBits = 0; // = 1 stop bit
m_chTerminator = '\n';
m_bCommportOpen = FALSE;
m_nTimeOut = 50;
m_nBlockSizeMax = 2048;
}
// Destruction code to close the connection if the port
// handle was valid.
CPort::~CPort()
{
if (m_hDevice)
CloseConnection();
}
// Open a serial communication port for writing short
// one-byte commands, that is, overlapped data transfer
// is not necessary.
BOOL CPort::OpenConnection()
{
char szPort[64];
m_bCommportOpen = FALSE;
// Build the COM port string as "COMx" where x is the port.
if (m_uPort > 9)
wsprintf(szPort, "\\\\.\\COM%d", m_uPort);
else
wsprintf(szPort, "COM%d", m_uPort);
// Open the serial port device.
m_hDevice = CreateFile(szPort,
GENERIC_WRITE | GENERIC_READ,
0,
NULL, // No security attributes
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (m_hDevice == INVALID_HANDLE_VALUE)
{
SaveLastError ();
m_hDevice = (HANDLE)0;
return FALSE;
}
return SetupConnection(); // After the port is open, set it up.
} // end of OpenConnection()
// Configure the serial port with the given settings.
// The given settings enable the port to communicate
// with the remote control.
BOOL CPort::SetupConnection(void)
{
DCB dcb; // The DCB structure differs betwwen Win16 and Win32.
dcb.DCBlength = sizeof(DCB);
// Retrieve the DCB of the serial port.
BOOL bStatus = GetCommState(m_hDevice, (LPDCB)&dcb);
if (bStatus == 0)
{
SaveLastError ();
return FALSE;
}
// Assign the values that enable the port to communicate.
dcb.BaudRate = m_uBaud; // Baud rate
dcb.ByteSize = m_uDataBits; // Data bits per byte, 4-8
dcb.Parity = m_uParity; // Parity: 0-4 = no, odd, even, mark, space
dcb.StopBits = m_uStopBits; // 0,1,2 = 1, 1.5, 2
dcb.fBinary = TRUE; // Binary mode, no EOF check : Must use binary mode in NT
dcb.fParity = dcb.Parity == 0 ? FALSE : TRUE; // Enable parity checking
dcb.fOutX = FALSE; // XON/XOFF flow control used
dcb.fInX = FALSE; // XON/XOFF flow control used
dcb.fNull = FALSE; // Disable null stripping - want nulls
dcb.fOutxCtsFlow = FALSE;
dcb.fOutxDsrFlow = FALSE;
dcb.fDsrSensitivity = FALSE;
dcb.fDtrControl = DTR_CONTROL_ENABLE;
dcb.fRtsControl = RTS_CONTROL_DISABLE ;
// Configure the serial port with the assigned settings.
// Return TRUE if the SetCommState call was not equal to zero.
bStatus = SetCommState(m_hDevice, &dcb);
if (bStatus == 0)
{
SaveLastError ();
return FALSE;
}
DWORD dwSize;
COMMPROP *commprop;
DWORD dwError;
dwSize = sizeof(COMMPROP) + sizeof(MODEMDEVCAPS) ;
commprop = (COMMPROP *)malloc(dwSize);
memset(commprop, 0, dwSize);
if (!GetCommProperties(m_hDevice, commprop))
{
dwError = GetLastError();
}
m_bCommportOpen = TRUE;
return TRUE;
}
void CPort::SaveLastError ()
{
DWORD dwLastError = GetLastError ();
LPVOID lpMsgBuf;
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
(LPTSTR) &lpMsgBuf,
0,
NULL);
strcpy (m_szLastError,(LPTSTR)lpMsgBuf);
// Free the buffer.
LocalFree( lpMsgBuf );
}
void CPort::SetTimeOut (int nTimeOut)
{
m_nTimeOut = nTimeOut;
}
// Close the opened serial communication port.
void CPort::CloseConnection(void)
{
if (m_hDevice != NULL &&
m_hDevice != INVALID_HANDLE_VALUE)
{
FlushFileBuffers(m_hDevice);
CloseHandle(m_hDevice); ///that the port has been closed.
}
m_hDevice = (HANDLE)0;
// Set the device handle to NULL to confirm
m_bCommportOpen = FALSE;
}
int CPort::WriteChars(char * psz)
{
int nCharWritten = 0;
while (*psz)
{
nCharWritten +=WriteChar(*psz);
psz++;
}
return nCharWritten;
}
// Write a one-byte value (char) to the serial port.
int CPort::WriteChar(char c)
{
DWORD dwBytesInOutQue = BytesInOutQue ();
if (dwBytesInOutQue > m_dwLargestBytesInOutQue)
m_dwLargestBytesInOutQue = dwBytesInOutQue;
static char szBuf[2];
szBuf[0] = c;
szBuf[1] = '\0';
DWORD dwBytesWritten;
DWORD dwTimeOut = m_nTimeOut; // 500 milli seconds
DWORD start, now;
start = GetTickCount();
do
{
now = GetTickCount();
if ((now - start) > dwTimeOut )
{
strcpy (m_szLastError, "Timed Out");
return 0;
}
WriteFile(m_hDevice, szBuf, 1, &dwBytesWritten, NULL);
}
while (dwBytesWritten == 0);
OutputDebugString(TEXT(strcat(szBuf, "\r\n")));
return dwBytesWritten;
}
int CPort::WriteChars(char * psz, int n)
{
DWORD dwBytesWritten;
WriteFile(m_hDevice, psz, n, &dwBytesWritten, NULL);
return dwBytesWritten;
}
// Return number of bytes in RX queue
DWORD CPort::BytesInQue ()
{
COMSTAT ComStat ;
DWORD dwErrorFlags;
DWORD dwLength;
// check number of bytes in queue
ClearCommError(m_hDevice, &dwErrorFlags, &ComStat ) ;
dwLength = ComStat.cbInQue;
return dwLength;
}
DWORD CPort::BytesInOutQue ()
{
COMSTAT ComStat ;
DWORD dwErrorFlags;
DWORD dwLength;
// check number of bytes in queue
ClearCommError(m_hDevice, &dwErrorFlags, &ComStat );
dwLength = ComStat.cbOutQue ;
return dwLength;
}
int CPort::ReadChars (char* szBuf, int nMaxChars)
{
if (BytesInQue () == 0)
return 0;
DWORD dwBytesRead;
ReadFile(m_hDevice, szBuf, nMaxChars, &dwBytesRead, NULL);
return (dwBytesRead);
}
// Read a one-byte value (char) from the serial port.
int CPort::ReadChar (char& c)
{
static char szBuf[2];
szBuf[0] = '\0';
szBuf[1] = '\0';
if (BytesInQue () == 0)
return 0;
DWORD dwBytesRead;
ReadFile(m_hDevice, szBuf, 1, &dwBytesRead, NULL);
c = *szBuf;
if (dwBytesRead == 0)
return 0;
return dwBytesRead;
}
BOOL CPort::ReadString (char *szStrBuf , int nMaxLength)
{
char str [256];
char str2 [256];
DWORD dwTimeOut = m_nTimeOut;
DWORD start, now;
int nBytesRead;
int nTotalBytesRead = 0;
char c = ' ';
static char szCharBuf [2];
szCharBuf [0]= '\0';
szCharBuf [1]= '\0';
szStrBuf [0] = '\0';
start = GetTickCount();
while (c != m_chTerminator)
{
nBytesRead = ReadChar (c);
nTotalBytesRead += nBytesRead;
if (nBytesRead == 1 && c != '\r' && c != '\n')
{
*szCharBuf = c;
strncat (szStrBuf,szCharBuf,1);
if (strlen (szStrBuf) == nMaxLength)
return TRUE;
// restart timer for next char
start = GetTickCount();
}
// check for time out
now = GetTickCount();
if ((now - start) > dwTimeOut )
{
strcpy (m_szLastError, "Timed Out");
return FALSE;
}
}
return TRUE;
}
int CPort::WaitForQueToFill (int nBytesToWaitFor)
{
DWORD start = GetTickCount();
do
{
if (BytesInQue () >= nBytesToWaitFor)
break;
if (GetTickCount() - start > m_nTimeOut)
return 0;
} while (1);
return BytesInQue ();
}
int CPort::BlockRead (char * pcInputBuffer, int nBytesToRead)
{
int nBytesRead = 0;
int charactersRead;
while (nBytesToRead >= m_nBlockSizeMax)
{
if (WaitForQueToFill (m_nBlockSizeMax) < m_nBlockSizeMax)
return nBytesRead;
charactersRead = ReadChars (pcInputBuffer, m_nBlockSizeMax);
pcInputBuffer += charactersRead;
nBytesRead += charactersRead;
nBytesToRead -= charactersRead;
}
if (nBytesToRead > 0)
{
if (WaitForQueToFill (nBytesToRead) < nBytesToRead)
return nBytesRead;
charactersRead = ReadChars (pcInputBuffer, nBytesToRead);
nBytesRead += charactersRead;
nBytesToRead -= charactersRead;
}
return nBytesRead;
}
Based on my testing and reading, I see several suspicious things in this code:
COMMTIMEOUTS is never set. MS docs say "Unpredictable results can occur if you fail to set the time-out values". But I tried setting this, and it didn't help.
Many methods (e.g. ReadString) will go into a tight loop and hammer the port with repeated reads if they don't get data immediately . This seems to explain the high CPU usage.
Many methods have their own timeout handling, using GetTickCount(). Isn't that what COMMTIMEOUTS is for?
In the new C# (WinForms) program, all these serial routines are called directly from the main thread, from a MultiMediaTimer event. Maybe should be run in a different thread?
BytesInQue method seems to be a bottleneck. If I break to debugger when CPU usage is high, that's usually where the program stops. Also, adding a Sleep(21) to this method before calling ClearCommError seems to resolve the XP problem, but exacerbates the CPU usage problem.
Code just seems unnecessarily complicated.
My Questions
Can anyone explain why this only works with a C# program on a small number of XP systems?
Any suggestions on how to rewrite this? Pointers to good sample code would be most welcome.
There are some serious problems with that class and it makes things even worse that there is a Microsoft copyright on it.
There is nothing special about this class. And it makes me wonder why it even exists except as an Adapter over Create/Read/WriteFile. You wouldnt even need this class if you used the SerialPort class in the .NET Framework.
Your CPU usage is because the code goes into an infinite loop while waiting for the device to have enough available data. The code might as well say while(1); If you must stick with Win32 and C++ you can look into Completion Ports and setting the OVERLAPPED flag when invoking CreateFile. This way you can wait for data in a separate worker thread.
You need to be careful when communicating to multiple COM ports. It has been a long time since I've done C++ but I believe the static buffer szBuff in the Read and Write methods is static for ALL instances of that class. It means if you invoke Read against two different COM ports "at the same time" you will have unexpected results.
As for the problems on some of the XP machines, you will most certainly figure out the problem if you check GetLastError after each Read/Write and log the results. It should be checking GetLastError anyways as it sometimes isn't always an "error" but a request from the subsystem to do something else in order to get the result you want.
You can get rid of the the whole while loop for blocking if you set COMMTIMEOUTS correctly. If there is a specific timeout for a Read operation use SetCommTimeouts before you perform the read.
I set ReadIntervalTimeout to the max timeout to ensure that the Read won't return quicker than m_nTimeOut. This value will cause Read to return if the time elapses between any two bytes. If it was set to 2 milliseconds and the first byte came in at t, and the second came in at t+1, the third at t+4, ReadFile would of only returned the first two bytes since the interval between the bytes was surpassed. ReadTotalTimeoutConstant ensures that you will never wait longer than m_nTimeOut no matter what.
maxWait = BytesToRead * ReadTotalTimeoutMultiplier + ReadTotalTimeoutConstant. Thus (BytesToRead * 0) + m_nTimeout = m_nTimeout
BOOL CPort::SetupConnection(void)
{
// Snip...
COMMTIMEOUTS comTimeOut;
comTimeOut.ReadIntervalTimeout = m_nTimeOut; // Ensure's we wait the max timeout
comTimeOut.ReadTotalTimeoutMultiplier = 0;
comTimeOut.ReadTotalTimeoutConstant = m_nTimeOut;
comTimeOut.WriteTotalTimeoutMultiplier = 0;
comTimeOut.WriteTotalTimeoutConstant = m_nTimeOut;
SetCommTimeouts(m_hDevice,&comTimeOut);
}
// If return value != nBytesToRead check check GetLastError()
// Most likely Read timed out.
int CPort::BlockRead (char * pcInputBuffer, int nBytesToRead)
{
DWORD dwBytesRead;
if (FALSE == ReadFile(
m_hDevice,
pcInputBuffer,
nBytesToRead,
&dwBytesRead,
NULL))
{
// Check GetLastError
return dwBytesRead;
}
return dwBytesRead;
}
I have no idea if this is completely correct but it should give you an idea. Remove the ReadChar and ReadString methods and use this if your program relies on things being synchronous. Be careful about setting high time outs also. Communications are fast, in the milliseconds.
Here's a terminal program I wrote years ago (probably at least 15 years ago, now that I think about it). I just did a quick check, and under Windows 7 x64, it still seems to work reasonably well -- connects to my GPS, read, and displays the data coming from it.
If you look at the code, you can see that I didn't spend much time selecting the comm timeout values. I set them all to 1, intending to experiment with longer timeouts until the CPU usage was tolerable. To make a long story short, it uses so little CPU time I've never bothered. For example, on the Task Manager's CPU usage graph, I can't see any difference between it running and not. I've left it running collecting data from the GPS for a few hours at a time, and the Task Manager still says its total CPU usage is 0:00:00.
Bottom line: I'm pretty sure it could be more efficient -- but sometimes good enough is good enough. Given how heavily I don't use it any more, and the chances of ever adding anything like file transfer protocols, making it more efficient probably won't ever get to the top of the pile of things to do.
#include <stdio.h>
#include <conio.h>
#include <string.h>
#define STRICT
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
void system_error(char *name) {
// Retrieve, format, and print out a message from the last error. The
// `name' that's passed should be in the form of a present tense noun
// (phrase) such as "opening file".
//
char *ptr = NULL;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM,
0,
GetLastError(),
0,
(char *)&ptr,
1024,
NULL);
fprintf(stderr, "\nError %s: %s\n", name, ptr);
LocalFree(ptr);
}
int main(int argc, char **argv) {
int ch;
char buffer[64];
HANDLE file;
COMMTIMEOUTS timeouts;
DWORD read, written;
DCB port;
HANDLE keyboard = GetStdHandle(STD_INPUT_HANDLE);
HANDLE screen = GetStdHandle(STD_OUTPUT_HANDLE);
DWORD mode;
char port_name[128] = "\\\\.\\COM3";
char init[] = "";
if ( argc > 2 )
sprintf(port_name, "\\\\.\\COM%s", argv[1]);
// open the comm port.
file = CreateFile(port_name,
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
0,
NULL);
if ( INVALID_HANDLE_VALUE == file) {
system_error("opening file");
return 1;
}
// get the current DCB, and adjust a few bits to our liking.
memset(&port, 0, sizeof(port));
port.DCBlength = sizeof(port);
if (!GetCommState(file, &port))
system_error("getting comm state");
if (!BuildCommDCB("baud=19200 parity=n data=8 stop=1", &port))
system_error("building comm DCB");
if (!SetCommState(file, &port))
system_error("adjusting port settings");
// set short timeouts on the comm port.
timeouts.ReadIntervalTimeout = 1;
timeouts.ReadTotalTimeoutMultiplier = 1;
timeouts.ReadTotalTimeoutConstant = 1;
timeouts.WriteTotalTimeoutMultiplier = 1;
timeouts.WriteTotalTimeoutConstant = 1;
if (!SetCommTimeouts(file, &timeouts))
system_error("setting port time-outs.");
// set keyboard to raw reading.
if (!GetConsoleMode(keyboard, &mode))
system_error("getting keyboard mode");
mode &= ~ ENABLE_PROCESSED_INPUT;
if (!SetConsoleMode(keyboard, mode))
system_error("setting keyboard mode");
if (!EscapeCommFunction(file, CLRDTR))
system_error("clearing DTR");
Sleep(200);
if (!EscapeCommFunction(file, SETDTR))
system_error("setting DTR");
if (!WriteFile(file, init, sizeof(init), &written, NULL))
system_error("writing data to port");
if (written != sizeof(init))
system_error("not all data written to port");
// basic terminal loop:
do {
// check for data on port and display it on screen.
ReadFile(file, buffer, sizeof(buffer), &read, NULL);
if (read)
WriteFile(screen, buffer, read, &written, NULL);
// check for keypress, and write any out the port.
if ( kbhit() ) {
ch = getch();
WriteFile(file, &ch, 1, &written, NULL);
}
// until user hits ctrl-backspace.
} while ( ch != 127);
// close up and go home.
CloseHandle(keyboard);
CloseHandle(file);
return 0;
}
I would add
Sleep(2);
to the while loop in CPort::WaitForQueToFill()
This will give the OS a chance to actually place some bytes in the queue.