Related
As discussed in WTSQueryUserToken throws error 1008, even when running under LocalSystem, I'm having trouble getting my Windows service to launch an interactive process on a particular user's desktop as soon as they log in.
The proposed solution there was to handle the SERVICE_CONTROL_SESSIONCHANGE control code and use the passed dwSessionId. Here's all of the code (apologies that it's quite lengthy, but I was told to post it here anyway):
// These headers just contain system header #include's function prototypes
// and global variable declarations. If a variable below seems like it is
// undefined, rest assured that it *is* defined in one of these headers.
#include "events.h"
#include "main.h"
int __cdecl _tmain(int argc, LPTSTR argv[]) {
sysStart = system_clock::now();
LogInit();
// If command-line parameter is "install", install the service.
// Otherwise, the service is probably being started by the SCM
if (lstrcmpi(argv[1], L"install") == 0) {
return SvcInstall();
}
SERVICE_TABLE_ENTRY dispatchTable[] = {
{ &svcName[0], (LPSERVICE_MAIN_FUNCTION)SvcMain },
{ nullptr, nullptr }
};
// This call returns when the service has stopped. The
// process should simply terminate when the call returns
if (!StartServiceCtrlDispatcher(dispatchTable)) {
ReportSvcEvent("StartServiceCtrlDispatcher");
}
return ERROR_SUCCESS;
}
char* WINAPI GetTimestamp(string& buf) {
int ms = (high_resolution_clock::now().
time_since_epoch().count() / 1000000) % 1000;
auto tt = system_clock::to_time_t(
system_clock::now());
tm time;
localtime_s(&time, &tt);
strftime(&buf[0], 21, "[%d-%m-%Y %T", &time);
snprintf(&buf[0], 26, "%s.%03d] ", &buf[0], ms);
buf[25] = ' ';
return &buf[0];
}
bool WINAPI LaunchDebugger(void) {
// Get System directory, typically C:\Windows\System32
wstring systemDir(MAX_PATH + 1, '\0');
UINT nChars = GetSystemDirectory(&systemDir[0], systemDir.length());
if (nChars == 0) {
return false; // failed to get system directory
}
systemDir.resize(nChars);
// Get process ID and create the command line
// wostringstream ss;
// ss << systemDir << L"\\vsjitdebugger.exe -p " << GetCurrentProcessId();
wstring cmdLine = L"";
// Start debugger process
STARTUPINFOW si;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
PROCESS_INFORMATION pi;
ZeroMemory(&pi, sizeof(pi));
if (!CreateProcess(nullptr, &cmdLine[0], nullptr,
nullptr, false, 0, nullptr, nullptr, &si, &pi)) {
return false;
}
// Close debugger process handles to eliminate resource leaks
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
// Wait for the debugger to attach
while (!IsDebuggerPresent()) {
Sleep(100);
}
// Stop execution so the debugger can take over
DebugBreak();
return true;
}
VOID WINAPI LogActiveTime(void) {
// The computer is shutting down - write an entry to logFile to reflect
// this, prefixed with a null byte to mark the current file position
// (used for parsing in the timestamp on the next system boot)
logFile << '\0';
LogMessage("User action", "System shutting down after being "
"active for " + DurationString(system_clock::now() - sysStart));
logFile.close();
// If the log file contains > 40 lines (10 boot/shutdown cycles),
// remove the first 4 lines (the earliest boot/shutdown cycle).
// This stops the file from getting too long to read easily
ifstream inFile(logFilePath);
string line;
auto count = 0;
while (getline(inFile, line)) {
count++;
}
}
DWORD WINAPI LogError(const string& funcName) {
auto err = 0;
LogMessage(funcName, system_category(
).message(err = GetLastError()), true);
return err;
}
DWORD WINAPI LogInactiveTime(void) {
// Create a new log file to be used as the input on the next run
LogInit("temp");
// Open the existing log file for reading and find the last shutdown
// log entry by copying its contents to the new file until a null byte
// or EOF is found (see LogActiveTime() for more info)
ifstream inFile(logFilePath);
if (!inFile) {
return LogError("LogInactiveTime");
}
char ch = inFile.get();
while (ch != '\0' && !inFile.eof()) {
logFile << ch;
ch = inFile.get();
}
if (inFile.eof()) {
// No shutdown log entry was found, i.e. this is probably the first
// time the service has run on the current instance of our log file.
// Close the temp file and re-open the original log file before
// returning, otherwise future messages won't make it to the file!
LogInit();
return ERROR_SUCCESS;
}
// At this point we can be sure that a valid shutdown log entry
// exists, so we now need to parse it into a chrono::time_point.
// Also save the entry's starting position in pos for later use
auto pos = inFile.tellg();
auto tt = system_clock::to_time_t(sysStart);
tm start, end = { 0 };
localtime_s(&start, &tt);
inFile >> get_time(&end, "[%d-%m-%Y %T");
if (inFile.fail() || inFile.bad()) {
return LogError("LogInactiveTime");
}
// Ensure that both time_points refer to
// the correct time, regardless of DST
end.tm_isdst = start.tm_isdst;
sysEnd = system_clock::from_time_t(mktime(&end));
// Go back to the *actual* start of the shutdown
// log entry so we can copy it into the new file
inFile.seekg(pos);
// Finish copying over the rest of our existing log
// file, then close it and replace it with the new one
ch = inFile.get();
while (!inFile.eof()) {
logFile << ch;
ch = inFile.get();
}
inFile.close();
remove(logFilePath.c_str());
logFile.close();
rename("temp", logFilePath.c_str());
// Finally, do what we *actually* came here to do!
LogMessage("User action", "System booting after being "
"inactive for " + DurationString(sysStart - sysEnd));
return ERROR_SUCCESS;
}
VOID WINAPI LogInit(const string& filePath) {
setlocale(LC_ALL, "en_US.UTF8");
if (logFile.is_open()) {
logFile.close();
}
logFile.open(filePath == "" ?
logFilePath : filePath, ios::app);
if (!logFile) {
exit(GetLastError());
}
}
VOID WINAPI LogMessage(const string& funcName,
const string& msg, bool isError) {
if (!logFile.is_open()) {
LogInit();
}
string buf(52, '\0');
snprintf(&buf[0], 52, "%s%-6s %-18s ", GetTimestamp(buf),
isError ? "ERROR:" : "INFO:", &(funcName + ':')[0]);
buf[51] = ' ';
logFile << buf << msg << endl;
}
VOID WINAPI ReportSvcEvent(const string& funcName) {
HANDLE eventSrc = RegisterEventSource(nullptr, &svcName[0]);
if (eventSrc != nullptr) {
LPCSTR errParams[2] = { "WinUtilities" };
char buf[MAX_PATH];
StringCchPrintfA(buf, MAX_PATH, "Function '%s' failed: %s",
funcName.c_str(), system_category().message(GetLastError(
)).c_str());
errParams[1] = buf;
ReportEventA(eventSrc, // event log handle
EVENTLOG_ERROR_TYPE, // event type
0, // event category
SVC_ERROR, // event identifier
nullptr, // no security identifier
2, // size of lpszStrings array
0, // no binary data
errParams, // array of strings
nullptr); // no binary data
DeregisterEventSource(eventSrc);
}
}
VOID WINAPI ReportSvcStatus(DWORD newState,
DWORD exitCode, DWORD waitHint) {
static DWORD dwCheckPoint = 1;
static unordered_map<int, string> svcStates;
if (svcStates.empty()) {
// Initialise mapping from service state codes to readable strings
svcStates.insert({ SERVICE_STOPPED, "Stopped" });
svcStates.insert({ SERVICE_START_PENDING, "Start Pending" });
svcStates.insert({ SERVICE_STOP_PENDING, "Stop Pending" });
svcStates.insert({ SERVICE_RUNNING, "Running" });
svcStates.insert({ SERVICE_CONTINUE_PENDING, "Continue Pending" });
svcStates.insert({ SERVICE_PAUSE_PENDING, "Pause Pending" });
svcStates.insert({ SERVICE_PAUSED, "Paused" });
}
// Update the SERVICE_STATUS structure with the new passed-in values
svcStatus.dwCurrentState = newState;
svcStatus.dwWin32ExitCode = exitCode;
svcStatus.dwWaitHint = waitHint;
if (newState == SERVICE_START_PENDING) {
svcStatus.dwControlsAccepted = 0;
} else {
svcStatus.dwControlsAccepted =
SERVICE_ACCEPT_SESSIONCHANGE |
SERVICE_ACCEPT_STOP |
SERVICE_ACCEPT_PRESHUTDOWN;
}
if (newState == SERVICE_RUNNING ||
newState == SERVICE_STOPPED) {
svcStatus.dwCheckPoint = 0;
} else {
svcStatus.dwCheckPoint = dwCheckPoint++;
}
// Report the status of the service to the SCM and our log file
if (!SetServiceStatus(statusHandle, &svcStatus)) {
LogError("SetServiceStatus");
} else {
LogMessage("SetServiceStatus", "Service status " \
"updated to '" + svcStates[newState] + "'.");
}
}
DWORD WINAPI SvcCtrlHandler(DWORD ctrlCode, DWORD
eventType, LPVOID eventData, LPVOID context) {
switch (ctrlCode) {
case SERVICE_CONTROL_SESSIONCHANGE: {
auto sessionId = ((WTSSESSION_NOTIFICATION*
)eventData)->dwSessionId;
switch (eventType) {
case WTS_SESSION_LOGON: {
string userName;
DWORD size;
WTSQuerySessionInformationA(WTS_CURRENT_SERVER_HANDLE, sessionId,
WTS_INFO_CLASS::WTSUserName, (LPSTR*)&userName[0], &size);
ReportSvcEvent("log on");
// A user has successfully logged on to the PC. Now we can start
// an interactive worker process under that user's account which
// will perform the actual work that we want to do
STARTUPINFO si = { 0 };
si.cb = sizeof(si);
si.wShowWindow = true;
HANDLE hToken;
if (!WTSQueryUserToken(sessionId, &hToken)) {
LogError("WTSQueryUserToken");
return ERROR_CALL_NOT_IMPLEMENTED;
}
wstring cmdLine = L"C:\\Path\\to\\my\\app.exe";
if (!CreateProcessAsUser(hToken, &cmdLine[0], nullptr, nullptr, nullptr,
false, CREATE_NO_WINDOW, nullptr, nullptr, &si, &workerProc)) {
LogError("CreateProcessAsUser");
return ERROR_CALL_NOT_IMPLEMENTED;
}
CloseHandle(hToken);
break;
} default: {
break;
}
}
break;
} case SERVICE_CONTROL_STOP: {
// Signal the service to stop
ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 0);
SetEvent(svcStopEvent);
break;
} case SERVICE_CONTROL_PRESHUTDOWN: {
LogActiveTime();
break;
} default: {
return ERROR_CALL_NOT_IMPLEMENTED;
}
}
return NO_ERROR;
}
VOID WINAPI SvcInit(DWORD argc, LPTSTR argv[]) {
// Get the time at which the last shutdown occurred, and
// log the duration for which the system was inactive
if (LogInactiveTime() > 0) {
return;
}
// Create an event. The control handler function (SvcCtrlHandler)
// signals this event when it receives the stop control code
svcStopEvent = CreateEvent(
nullptr, // default security attributes
TRUE, // manual reset event
FALSE, // not signaled
nullptr); // no name
if (svcStopEvent == nullptr) {
LogError("CreateEvent");
ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
return;
}
// Report running status when initialisation is complete
ReportSvcStatus(SERVICE_RUNNING, NO_ERROR, 0);
// Wait until our stop event has been signalled
WaitForSingleObject(svcStopEvent, INFINITE);
// Code execution won't reach here until the service has been
// fully stopped. Report this to the SCM when it happens, then
// terminate the worker process and clean up its handles
ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
if (workerProc.hProcess) {
TerminateProcess(workerProc.hProcess, 0);
CloseHandle(workerProc.hProcess);
CloseHandle(workerProc.hThread);
}
}
DWORD WINAPI SvcInstall(void) {
TCHAR path[MAX_PATH];
if (!GetModuleFileName(nullptr, path, MAX_PATH)) {
return LogError("GetModuleFileName");
}
// Get a handle to the SCM database
auto scm = OpenSCManager(
nullptr, // local computer
nullptr, // ServicesActive database
SC_MANAGER_ALL_ACCESS); // full access rights
if (scm == nullptr) {
return LogError("OpenSCManager");
}
// Create the service
auto svc = CreateService(
scm, // SCM database
&svcName[0], // name of service
L"Windows Utilities", // service name to display
SERVICE_ALL_ACCESS, // desired access
SERVICE_WIN32_OWN_PROCESS, // service type
SERVICE_AUTO_START, // start type
SERVICE_ERROR_NORMAL, // error control type
path, // path to service's binary
nullptr, // no load ordering group
nullptr, // no tag identifier
nullptr, // no dependencies
nullptr, // LocalSystem account
nullptr); // no password
if (svc == nullptr) {
CloseServiceHandle(scm);
return LogError("CreateService");
}
SERVICE_DESCRIPTION sd;
sd.lpDescription = const_cast<LPTSTR>(L"Logs system "
"shutdown events to a text file on the desktop. "
"Also creates a system-wide hot key to perform "
"internet searches on any selected text.");
if (!ChangeServiceConfig2(
svc, // handle to service
SERVICE_CONFIG_DESCRIPTION, // change: description
&sd)) // new description
{
CloseServiceHandle(svc);
CloseServiceHandle(scm);
return LogError("ChangeServiceConfig2");
}
CloseServiceHandle(svc);
CloseServiceHandle(scm);
LogMessage("SvcInstall", "Service installed successfully.");
return ERROR_SUCCESS;
}
VOID WINAPI SvcMain(DWORD argc, LPTSTR argv[]) {
// Register the handler function for the service
statusHandle = RegisterServiceCtrlHandlerEx(
&svcName[0], SvcCtrlHandler, 0);
if (!statusHandle) {
ReportSvcEvent("RegisterServiceCtrlHandlerEx");
return;
}
// These SERVICE_STATUS members remain as set here
svcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
svcStatus.dwServiceSpecificExitCode = 0;
// Report initial status to the SCM
ReportSvcStatus(SERVICE_START_PENDING, NO_ERROR, 3000);
// Perform service-specific initialization and work
SvcInit(argc, argv);
}
The part that doesn't work is in the SvcCtrlHandler() function, where I'm trying to catch the aforementioned control code.
I've even gone so far as to rewrite this whole thing in C# (which is the language that I should have used in the first place since my code is soooooooooo much cleaner and clearer now) and guess what? I still have the exact same problem with the OnSessionChange() method!
When I cold boot the computer and allow my PC to autologin to my single user account, nothing happens (i.e. no app.exe started). But if I then log out and back in again, I get the results I'm looking for.
So it seems as though my service is one of the last few to load and this is stopping it from properly catching the SERVICE_CONTROL_SESSIONCHANGE control code. How can I fix this? MTIA! :D
I'm stuck on this since days, i'm goind crazy.
Basically i'm trying to open cmd.exe in a thread and give it input and read output from it, from the parent. Like, assigning a tty in linux, since there's no such thing in windows. I have a good understanding of linux systems but can't say the same thing about windows.
So, here's "my" code:
#undef UNICODE
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
#include <strsafe.h>
//using namespace std;
#define BUFFER_SIZE 99
// handles for cmd thread pipes
HANDLE cmd_in_rd = NULL;
HANDLE cmd_in_wr = NULL;
HANDLE cmd_out_rd = NULL;
HANDLE cmd_out_wr = NULL;
HANDLE cmd_thread_handle;
void PrintError(char *text, int err) {
DWORD retSize;
LPTSTR pTemp = NULL;
if (!err) return;
retSize = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_ARGUMENT_ARRAY,
NULL,
err,
LANG_NEUTRAL,
(LPTSTR)&pTemp,
0,
NULL);
if (pTemp) printf("%s: %s\n", text, pTemp);
LocalFree((HLOCAL)pTemp);
return;
}
int pipewrite(char *command) {
DWORD dwRead, dwWritten;
BOOL bSuccess = FALSE;
SetLastError(0);
WriteFile(cmd_in_wr, command, strlen(command), &dwWritten, NULL);
bSuccess = GetLastError();
PrintError("WriteToPipe", bSuccess);
return (bSuccess == 0) || (bSuccess == ERROR_IO_PENDING);
}
int __stdcall cmd_thread(int arg) {
// this function only prints when data is ready
DWORD dwRead, dwWritten;
CHAR chBuf[BUFFER_SIZE];
BOOL bSuccess = FALSE;
HANDLE hParentStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
int rf_ret, wf_ret;
//CloseHandle(cmd_out_wr); makes readfile fail!!
SetLastError(0);
while (1) { // only executes once!!!!!!!
(rf_ret = ReadFile(cmd_out_rd, chBuf, BUFFER_SIZE, &dwRead, NULL))
&&
(wf_ret = WriteFile(hParentStdOut, chBuf, dwRead, &dwWritten, NULL));
printf("ReadFile returned: %d\nWriteFile returned: %d\n", rf_ret, wf_ret);
bSuccess = GetLastError();
PrintError("ReadingFromPipe", bSuccess);
}
bSuccess = GetLastError();
return (bSuccess == 0) || (bSuccess == ERROR_IO_PENDING);
}
int main(void) {
char buffer[BUFFER_SIZE];
// init the pipes
SECURITY_ATTRIBUTES cmd_sa;
cmd_sa.nLength = sizeof(SECURITY_ATTRIBUTES);
cmd_sa.bInheritHandle = TRUE;
cmd_sa.lpSecurityDescriptor = NULL;
if (!CreatePipe(&cmd_out_rd, &cmd_out_wr, &cmd_sa, 0)) {
printf("%s\n", "Error creating pipes");
return 1;
}
if (!SetHandleInformation(cmd_out_rd, HANDLE_FLAG_INHERIT, 0)) {
printf("%s\n", "Error setting handle infos");
return 1;
}
if (!CreatePipe(&cmd_in_rd, &cmd_in_wr, &cmd_sa, 0)) {
printf("%s\n", "Error creating pipes");
return 1;
}
if (!SetHandleInformation(cmd_in_rd, HANDLE_FLAG_INHERIT, 0)) {
printf("%s\n", "Error setting handle infos");
return 1;
}
// create the cmd thread
PROCESS_INFORMATION cmd_pi;
STARTUPINFO cmd_si;
ZeroMemory(&cmd_pi, sizeof(PROCESS_INFORMATION));
ZeroMemory(&cmd_si, sizeof(STARTUPINFO));
cmd_si.cb = sizeof(STARTUPINFO);
cmd_si.hStdError = cmd_out_wr;
cmd_si.hStdOutput = cmd_out_wr;
cmd_si.hStdInput = cmd_in_rd;
cmd_si.dwFlags |= STARTF_USESTDHANDLES;
TCHAR comm[] = TEXT("cmd.exe");
BOOL th = CreateProcess(NULL,
comm,
NULL,
NULL,
TRUE, // handles are inherited
0,
NULL,
NULL,
&cmd_si,
&cmd_pi);
if (th) {
CloseHandle(cmd_pi.hProcess);
CloseHandle(cmd_pi.hThread);
}
cmd_thread_handle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)cmd_thread, NULL, 0, NULL);
// read commands from shell and send them to cmd
ZeroMemory(&buffer, BUFFER_SIZE);
while (1) {
fgets(buffer, BUFFER_SIZE, stdin);
if (!pipewrite(buffer)) break;
}
printf("Program terminated\n");
return 0;
}
I actually, for testing purposes, copied a lot from another question on stackoverflow and from MSDN since i couldn't get it to work on my main program. The things I don't understand are:
Why the while loop inside cmd_thread gets executed at startup and then hangs there waiting for the end of the world? I tried to close the pipe out_write handle from the parent before reading, but that makes other parts not working.
pipewrite() seems to work, but I can't be sure that the cmd.exe thread receives and works the input... Since i get no output :/
I thought about stracing/ltracing the program or running it into a debugger, but I know no tool for that...
The strange thing is that the original works (the one from where i got the code). I tried to spot the difference between the two, but even when I look to them side by side, they seem to do the exact same things.
The child process is dying as soon as it attempts to read from standard input, because:
if (!SetHandleInformation(cmd_in_rd, HANDLE_FLAG_INHERIT, 0)) {
This should have been:
if (!SetHandleInformation(cmd_in_wr, HANDLE_FLAG_INHERIT, 0)) {
like in the original code.
Also, your error handling is largely incorrect; you don't consistently check for errors and you sometimes call GetLastError() when no error has occurred. (Those problems are in the original code too.)
You also need to put the call to CloseHandle(cmd_out_wr); back in because otherwise you won't be able to tell when the child exits.
Oh, and incidentally, cmd.exe is a process, not a thread.
INTRODUCTION:
I am trying to use ReadDirectoryChangesW asynchronously in a loop.
Below snippet illustrates what I am trying to achieve:
DWORD example()
{
DWORD error = 0;
OVERLAPPED ovl = { 0 };
ovl.hEvent = ::CreateEvent(NULL, TRUE, FALSE, NULL);
if (NULL == ovl.hEvent) return ::GetLastError();
char buffer[1024];
while(1)
{
process_list_of_existing_files();
error = ::ReadDirectoryChangesW(
m_hDirectory, // I have added FILE_FLAG_OVERLAPPED in CreateFile
buffer, sizeof(buffer), FALSE,
FILE_NOTIFY_CHANGE_FILE_NAME,
NULL, &ovl, NULL);
// we have new files, append them to the list
if(error) append_new_files_to_the_list(buffer);
// just continue with the loop
else if(::GetLastError() == ERROR_IO_PENDING) continue;
// RDCW error, this is critical -> exit
else return ::GetLastError();
}
}
PROBLEM:
I do not know how to handle the case when ReadDirectoryChangesW returns FALSE with GetLastError() code being ERROR_IO_PENDING.
In that case I should just continue with the loop and keep looping until ReadDirectoryChangesW returns buffer I can process.
MY EFFORTS TO SOLVE THIS:
I have tried using WaitForSingleObject(ovl.hEvent, 1000) but it crashes with error 1450 ERROR_NO_SYSTEM_RESOURCES. Below is the MVCE that reproduces this behavior:
#include <iostream>
#include <Windows.h>
DWORD processDirectoryChanges(const char *buffer)
{
DWORD offset = 0;
char fileName[MAX_PATH] = "";
FILE_NOTIFY_INFORMATION *fni = NULL;
do
{
fni = (FILE_NOTIFY_INFORMATION*)(&buffer[offset]);
// since we do not use UNICODE,
// we must convert fni->FileName from UNICODE to multibyte
int ret = ::WideCharToMultiByte(CP_ACP, 0, fni->FileName,
fni->FileNameLength / sizeof(WCHAR),
fileName, sizeof(fileName), NULL, NULL);
switch (fni->Action)
{
case FILE_ACTION_ADDED:
{
std::cout << fileName << std::endl;
}
break;
default:
break;
}
::memset(fileName, '\0', sizeof(fileName));
offset += fni->NextEntryOffset;
} while (fni->NextEntryOffset != 0);
return 0;
}
int main()
{
HANDLE hDir = ::CreateFile("C:\\Users\\nenad.smiljkovic\\Desktop\\test",
FILE_LIST_DIRECTORY,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL);
if (INVALID_HANDLE_VALUE == hDir) return ::GetLastError();
OVERLAPPED ovl = { 0 };
ovl.hEvent = ::CreateEvent(NULL, TRUE, FALSE, NULL);
if (NULL == ovl.hEvent) return ::GetLastError();
DWORD error = 0, br;
char buffer[1024];
while (1)
{
error = ::ReadDirectoryChangesW(hDir,
buffer, sizeof(buffer), FALSE,
FILE_NOTIFY_CHANGE_FILE_NAME,
NULL, &ovl, NULL);
if (0 == error)
{
error = ::GetLastError();
if (ERROR_IO_PENDING != error)
{
::CloseHandle(ovl.hEvent);
::CloseHandle(hDir);
return error;
}
}
error = ::WaitForSingleObject(ovl.hEvent, 0);
switch (error)
{
case WAIT_TIMEOUT:
break;
case WAIT_OBJECT_0:
{
error = processDirectoryChanges(buffer);
if (error > 0)
{
::CloseHandle(ovl.hEvent);
::CloseHandle(hDir);
return error;
}
if (0 == ::ResetEvent(ovl.hEvent))
{
error = ::GetLastError();
::CloseHandle(ovl.hEvent);
::CloseHandle(hDir);
return error;
}
}
break;
default:
error = ::GetLastError();
::CloseHandle(ovl.hEvent);
::CloseHandle(hDir);
return error;
break;
}
}
return 0;
}
Reading through the documentation, it seems that I need GetOverlappedResult with last parameter set to FALSE but I do not know how to use this API properly.
QUESTION:
Since the MVCE illustrates very well what I am trying to do (print the names of the newly added files), can you show me what must be fixed in the while loop in order for it to work?
Again, the point is to use ReadDirectoryChangesW asynchronously, in a loop, as shown in the snippet from the INTRODUCTION.
The basic structure of your program looks more or less OK, you're just using the asynchronous I/O calls incorrectly. Whenever there are no new files, the wait on the event handle times out immediately, which is fine, but you then issue a brand new I/O request, which isn't.
That's why you're running out of system resources; you're issuing I/O requests full tilt without waiting for any of them to complete. You should only issue a new request after the existing request has completed.
(Also, you should be calling GetOverlappedResult to check whether the I/O was successful or not.)
So your loop should look more like this:
::ReadDirectoryChangesW(hDir,
buffer, sizeof(buffer), FALSE,
FILE_NOTIFY_CHANGE_FILE_NAME,
NULL, &ovl, NULL);
while (1)
{
DWORD dw;
DWORD result = ::WaitForSingleObject(ovl.hEvent, 0);
switch (result)
{
case WAIT_TIMEOUT:
processBackgroundTasks();
break;
case WAIT_OBJECT_0:
::GetOverlappedResult(hDir, &ovl, &dw, FALSE);
processDirectoryChanges(buffer);
::ResetEvent(ovl.hEvent);
::ReadDirectoryChangesW(hDir,
buffer, sizeof(buffer), FALSE,
FILE_NOTIFY_CHANGE_FILE_NAME,
NULL, &ovl, NULL);
break;
}
}
Notes:
The error handling has been elided for simplicity; I have not done any testing or checked your code for any other problems.
If there might not be any background tasks to perform, you should test for that case and set the timeout to INFINITE rather than 0 when it occurs, otherwise you will be spinning.
I wanted to only show the minimal changes necessary to make it work, but calling WaitForSingleObject followed by GetOverlappedResult is redundant; a single call to GetOverlappedResult can both check whether the I/O is complete and retrieve the results if it is.
As requested, the modified version using only GetOverlappedResult and with minimal error checking. I've also added an example of how you might deal with the case where you've run out of work to do; if whatever processing you're doing on the files really does run forever, you don't need that bit.
::ResetEvent(ovl.hEvent);
if (!::ReadDirectoryChangesW(hDir,
buffer, sizeof(buffer), FALSE,
FILE_NOTIFY_CHANGE_FILE_NAME,
NULL, &ovl, NULL))
{
error = GetLastError();
if (error != ERROR_IO_PENDING) fail();
}
while (1)
{
BOOL wait;
result = process_list_of_existing_files();
if (result == MORE_WORK_PENDING)
{
wait = FALSE;
}
else if (result == NO_MORE_WORK_PENDING)
{
wait = TRUE;
}
if (!::GetOverlappedResult(hDir, &ovl, &dw, wait))
{
error = GetLastError();
if (error == ERROR_IO_INCOMPLETE) continue;
fail();
}
processDirectoryChanges(buffer);
::ResetEvent(ovl.hEvent);
if (!::ReadDirectoryChangesW(hDir,
buffer, sizeof(buffer), FALSE,
FILE_NOTIFY_CHANGE_FILE_NAME,
NULL, &ovl, NULL))
{
error = GetLastError();
if (error != ERROR_IO_PENDING) fail();
}
}
Variant of indirect using IOCP
Create a class/struct inherited (containing) OVERLAPPED (or
IO_STATUS_BLOCK), a reference counter, directory handle and data which
you need
Call BindIoCompletionCallback (RtlSetIoCompletionCallback) for
directory handle, for setup your callback
Have a DoRead() routine, which we'll call first-time from the main thread, and then from the callback
In DoRead(), before every call to ReadDirectoryChangesW call
AddRef(); because we pass reference (across OVERLAPPED) to our
struct to kernel
Main (say GUI thread) can continue to do own task after the initial call
to DoRead(), unlike the APC variant, we do not need to wait in alertable state
In the callback, we got a pointer to our struct from inherited (containing)
OVERLAPPED. Do any tasks (processDirectoryChanges), if need
continue spy - call DoRead() and finally call Release()
If ReadDirectoryChangesW from DoRead() fails (as result will be no callback) - we need direct call callback
with error code
For stopping we can simply close the directory handle - as a result, we got
STATUS_NOTIFY_CLEANUP in callback
==================================
//#define _USE_NT_VERSION_
class SPYDATA :
#ifdef _USE_NT_VERSION_
IO_STATUS_BLOCK
#else
OVERLAPPED
#endif
{
HANDLE _hFile;
LONG _dwRef;
union {
FILE_NOTIFY_INFORMATION _fni;
UCHAR _buf[PAGE_SIZE];
};
void DumpDirectoryChanges()
{
union {
PVOID buf;
PBYTE pb;
PFILE_NOTIFY_INFORMATION pfni;
};
buf = _buf;
for (;;)
{
DbgPrint("%x <%.*S>\n", pfni->Action, pfni->FileNameLength >> 1, pfni->FileName);
ULONG NextEntryOffset = pfni->NextEntryOffset;
if (!NextEntryOffset)
{
break;
}
pb += NextEntryOffset;
}
}
#ifdef _USE_NT_VERSION_
static VOID WINAPI _OvCompRoutine(
_In_ NTSTATUS dwErrorCode,
_In_ ULONG_PTR dwNumberOfBytesTransfered,
_Inout_ PIO_STATUS_BLOCK Iosb
)
{
static_cast<SPYDATA*>(Iosb)->OvCompRoutine(dwErrorCode, (ULONG)dwNumberOfBytesTransfered);
}
#else
static VOID WINAPI _OvCompRoutine(
_In_ DWORD dwErrorCode, // really this is NTSTATUS
_In_ DWORD dwNumberOfBytesTransfered,
_Inout_ LPOVERLAPPED lpOverlapped
)
{
static_cast<SPYDATA*>(lpOverlapped)->OvCompRoutine(dwErrorCode, dwNumberOfBytesTransfered);
}
#endif
VOID OvCompRoutine(NTSTATUS status, DWORD dwNumberOfBytesTransfered)
{
DbgPrint("[%x,%x]\n", status, dwNumberOfBytesTransfered);
if (0 <= status)
{
if (status != STATUS_NOTIFY_CLEANUP)
{
if (dwNumberOfBytesTransfered) DumpDirectoryChanges();
process_list_of_existing_files();// so hard do this here ?!?
DoRead();
}
else
{
DbgPrint("\n---- NOTIFY_CLEANUP -----\n");
}
}
Release();
MyReleaseRundownProtection();
}
~SPYDATA()
{
Cancel();
}
public:
void DoRead()
{
if (MyAcquireRundownProtection())
{
AddRef();
#ifdef _USE_NT_VERSION_
NTSTATUS status = ZwNotifyChangeDirectoryFile(_hFile, 0, 0, this, this, &_fni, sizeof(_buf), FILE_NOTIFY_VALID_MASK, TRUE);
if (NT_ERROR(status))
{
OvCompRoutine(status, 0);
}
#else
if (!ReadDirectoryChangesW(_hFile, _buf, sizeof(_buf), TRUE, FILE_NOTIFY_VALID_MASK, (PDWORD)&InternalHigh, this, 0))
{
OvCompRoutine(RtlGetLastNtStatus(), 0);
}
#endif
}
}
SPYDATA()
{
_hFile = 0;// ! not INVALID_HANDLE_VALUE because use ntapi for open file
_dwRef = 1;
#ifndef _USE_NT_VERSION_
RtlZeroMemory(static_cast<OVERLAPPED*>(this), sizeof(OVERLAPPED));
#endif
}
void AddRef()
{
InterlockedIncrement(&_dwRef);
}
void Release()
{
if (!InterlockedDecrement(&_dwRef))
{
delete this;
}
}
BOOL Create(POBJECT_ATTRIBUTES poa)
{
IO_STATUS_BLOCK iosb;
NTSTATUS status = ZwOpenFile(&_hFile, FILE_GENERIC_READ, poa, &iosb, FILE_SHARE_VALID_FLAGS, FILE_DIRECTORY_FILE);
if (0 <= status)
{
return
#ifdef _USE_NT_VERSION_
0 <= RtlSetIoCompletionCallback(_hFile, _OvCompRoutine, 0);
#else
BindIoCompletionCallback(_hFile, _OvCompRoutine, 0);
#endif
}
return FALSE;
}
void Cancel()
{
if (HANDLE hFile = InterlockedExchangePointer(&_hFile, 0))
{
NtClose(hFile);
}
}
};
void DemoF()
{
if (MyInitializeRundownProtection())
{
STATIC_OBJECT_ATTRIBUTES(oa, "<SOME_DIRECTORY>");
if (SPYDATA* p = new SPYDATA)
{
if (p->Create(&oa))
{
p->DoRead();
}
//++ GUI thread run
MessageBoxW(0, L"wait close program...", L"", MB_OK);
//-- GUI thread end
p->Cancel();
p->Release();
}
MyWaitForRundownProtectionRelease();
}
}
I'm writing an integration test driver for a command-line executable. I control both the driver and the executable, so I can make guarantees about their behaviour- for example, the executable never reads from stdin, it just takes command-line arguments, does its thing, and then writes output to a file and stdout.
I wish to capture both the exit code and the stdout of the process for verification.
Here's the code that I'm using:
#include <Windows.h>
class Pipe {
HANDLE ReadHandle;
HANDLE writehandle;
public:
Pipe() {
SECURITY_ATTRIBUTES saAttr;
saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
saAttr.bInheritHandle = TRUE;
saAttr.lpSecurityDescriptor = NULL;
CreatePipe(&ReadHandle, &writehandle, &saAttr, 0);
}
HANDLE WriteHandle() {
return writehandle;
}
std::string Contents() {
CloseHandle(writehandle);
DWORD dwRead;
CHAR chBuf[1024];
BOOL bSuccess = FALSE;
std::string result;
for (;;)
{
bSuccess = ReadFile(ReadHandle, chBuf, 1024, &dwRead, NULL);
if (!bSuccess) break;
result += std::string(chBuf, chBuf + dwRead);
if (dwRead < 1024)
break;
}
return result;
}
~Pipe() {
CloseHandle(ReadHandle);
}
};
Wide::Driver::ProcessResult Wide::Driver::StartAndWaitForProcess(std::string name, std::vector<std::string> args, Util::optional<unsigned> timeout)
{
ProcessResult result;
Pipe stdoutpipe;
PROCESS_INFORMATION info = { 0 };
STARTUPINFO startinfo = { sizeof(STARTUPINFO) };
std::string final_args = name;
for (auto arg : args)
final_args += " " + arg;
startinfo.hStdOutput = stdoutpipe.WriteHandle();
startinfo.hStdError = INVALID_HANDLE_VALUE;
startinfo.hStdInput = INVALID_HANDLE_VALUE;
startinfo.dwFlags |= STARTF_USESTDHANDLES;
auto proc = CreateProcess(
name.c_str(),
&final_args[0],
nullptr,
nullptr,
TRUE,
NORMAL_PRIORITY_CLASS | CREATE_NO_WINDOW,
nullptr,
nullptr,
&startinfo,
&info
);
if (!proc) {
DWORD dw = GetLastError();
const char* message;
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr, dw, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&message, 0, nullptr);
std::string err = message;
LocalFree((void*)message);
throw std::runtime_error(err);
}
if (timeout == 0)
timeout = INFINITE;
result.std_out = stdoutpipe.Contents();
if (WaitForSingleObject(info.hProcess, timeout ? *timeout : INFINITE) == WAIT_TIMEOUT)
TerminateProcess(info.hProcess, 1);
DWORD exit_code;
GetExitCodeProcess(info.hProcess, &exit_code);
CloseHandle(info.hProcess);
CloseHandle(info.hThread);
result.exitcode = exit_code;
if (exit_code != 0)
return result;
return result;
}
I've got 259 integration tests that I run in this way. Some take longer than others. When I run the suite, about 1-3 will fail - different ones each time. I've looked at the outcome in the debugger, and the stdout is cut off half way through. If I don't try to capture the stdout, all the tests succeed every time, so I know that it's based around the stdout capture.
The timeout is specified, but it's a very generous 60 seconds- much longer than the tests take to run ordinarily. I spawn a new process for each test.
How can I capture stdout in a more reliable way, without getting random failures?
As a final note, it takes a long time to run the suite to capture the failure in the debugger, so it may take a while to service any requests for further information.
I have a theory about this, but I'm not wholly certain. The key is in the loop condition for reading the stdout of the process.
std::string result;
for (;;)
{
bSuccess = ReadFile(ReadHandle, chBuf, 1024, &dwRead, NULL);
if (!bSuccess) break;
result += std::string(chBuf, chBuf + dwRead);
if (dwRead < 1024)
break;
}
return result;
There's actually an implicit assumption here. ReadFile is a blocking API, so we would assume that it keeps blocking until it has the data we asked for or input has ended. But I hypothesize that in fact, ReadFile may return before it has a block as big as we asked for, even if the pipe has not terminated. This would cause the input reading loop to terminate.
Since the parent is no longer reading stdout, the child trying to write stdout may block waiting for somebody to clear the buffer- effectively a deadlock since nobody will. Therefore, the timeout will trigger and terminate the process, recording a failure.
The MSDN docs say this:
The ReadFile function returns when one of the following conditions occur:
The number of bytes requested is read.
A write operation completes on the write end of the pipe.
An asynchronous handle is being used and the read is occurring asynchronously.
An error occurs.
It does not say that it will return when a write operation completes and the number of bytes requested is available. In fact, it makes no comment about the write operation making as many bytes as you requested available. So effectively, it behaves semi-asynchronously, even when called synchronously.
I have rewritten the loop as follows:
std::string result;
for (;;)
{
bSuccess = ReadFile(ReadHandle, chBuf, 1024, &dwRead, NULL);
if (!bSuccess || dwRead == 0) break;
result += std::string(chBuf, chBuf + dwRead);
}
return result;
So far I have been unable to reproduce the failures with this loop (and the tests complete noticably faster).
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.