This post is an updated version of the original post, with the code examples below reflecting changes in response to very detailed inputs from user @pacmaninbw. The updated source examples below also include the addition of a function that is designed to accommodate commands that do not return a response. The comment block describes why this function addition is necessary, and its usage.
I am interested in getting feedback with emphasis on the same things as before, so will repeat the preface of the previous post below.
The need:
I needed a method to programmatically send commands to the Windows 7 CMD prompt, and return the response without seeing a console popup in multiple applications.
The design:
The environment in addition to the Windows 7 OS is an ANSI C (C99) compiler from National Instruments, and the Microsoft Windows Driver Kit for Windows 8.1. Among the design goals was to present a very small API, including well documented and straightforward usage instructions. The result is two exported functions. Descriptions for each are provided in their respective comment blocks. In its provided form, it is intended to be built as a DLL. The only header files used in this library are
windows.h
andstdlib.h
.For review consideration:
The code posted is complete, and I have tested it, but I am new to using
pipes
tostdin
andstdout
, as well as using Windows methods forCreateProcess(...)
. Also, because the size requirements of the response buffer cannot be known at compile time, the code includes the ability to grow the response buffer as needed during run-time. For example, I have used this code to recursively read directories usingdir /s
from all locations except thec:\
directory with the following command:cd c:\dev && dir /s // approximately 1.8Mbyte buffer is returned on my system
I would especially appreciate feedback focused on the following:
- Pipe creation and usage
CreateProcess
usage- Method for dynamically growing response buffer (very interested in feedback on this)
- Handling embedded
null
bytes in content returned fromReadFile
function. (Credit to @chux, this is a newly discovered deficiency)
Usage example:
#include <stdio.h> // printf()
#include <stdlib.h> // NULL
#include "cmd_rsp.h"
#define BUF_SIZE 100
int main(void)
{
char *buf = NULL;
/// test cmd_rsp
buf = calloc(BUF_SIZE, 1);
if(!buf)return 0;
if (!cmd_rsp("dir /s", &buf, BUF_SIZE))
{
printf("%s", buf);
}
else
{
printf("failed to send command.\n");
}
free(buf);
/// test cmd_no_rsp
buf = calloc(BUF_SIZE, 1);
if(!buf)return 0;
if (!cmd_no_rsp("dir /s", &buf, BUF_SIZE))
{
printf("success.\n"); // function provides no response
}
else
{
printf("failed to send command.\n");
}
free(buf);
return 0;
}
cmd_rsp.h
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Prototype: int int __declspec(dllexport) cmd_rsp(char *command, char **chunk, size_t size)
//
// Description: Executes any command that can be executed in a Windows cmd prompt and returns
// the response via auto-resizing buffer.
/// Note: this function will hang for executables or processes that run and exit
/// without ever writing to stdout.
/// The hang occurs during the call to the read() function.
//
// Inputs: const char *command - string containing complete command to be sent
// char **chunk - initialized pointer to char array to return results
// size_t size - Initial memory size in bytes char **chunk was initialized to.
//
// Return: 0 for success
// -1 for failure
//
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
int __declspec(dllexport) cmd_rsp(const char *command, char **chunk, unsigned int chunk_size);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Prototype: int int __declspec(dllexport) cmd_no_rsp(char *command)
//
// Description: Variation of cmd_rsp that does not wait for a response. This is useful for
// executables or processes that run and exit without ever sending a response to stdout,
// causing cmd_rsp to hang during the call to the read() function.
//
// Inputs: const char *command - string containing complete command to be sent
//
// Return: 0 for success
// -1 for failure
//
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
int __declspec(dllexport) cmd_no_rsp(const char *command);
#endif
cmd_rsp.c
#include <windows.h>
#include <stdlib.h> // calloc, realloc & free
#include "cmd_rsp.h"
#define BUFSIZE 1000
typedef struct {
/* child process's STDIN is the user input or data entered into the child process - READ */
void * in_pipe_read;
/* child process's STDIN is the user input or data entered into the child process - WRITE */
void * in_pipe_write;
/* child process's STDOUT is the program output or data that child process returns - READ */
void * out_pipe_read;
/* child process's STDOUT is the program output or data that child process returns - WRITE */
void * out_pipe_write;
}IO_PIPES;
// Private prototypes
static int CreateChildProcess(const char *cmd, IO_PIPES *io);
static int CreateChildProcessNoStdOut(const char *cmd, IO_PIPES *io);
static int ReadFromPipe(char **rsp, unsigned int size, IO_PIPES *io);
static char * ReSizeBuffer(char **str, unsigned int size);
static void SetupSecurityAttributes(SECURITY_ATTRIBUTES *saAttr);
static void SetupStartUpInfo(STARTUPINFO *siStartInfo, IO_PIPES *io);
static int SetupChildIoPipes(IO_PIPES *io, SECURITY_ATTRIBUTES *saAttr);
int __stdcall DllMain (HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
/* Respond to DLL loading by initializing the RTE */
if (InitCVIRTE (hinstDLL, 0, 0) == 0) return 0;
break;
case DLL_PROCESS_DETACH:
/* Respond to DLL unloading by closing the RTE for its use */
if (!CVIRTEHasBeenDetached ()) CloseCVIRTE ();
break;
}
/* Return 1 to indicate successful initialization */
return 1;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Prototype: int __declspec(dllexport) cmd_rsp(char *command, char **chunk, size_t chunk_size)
//
// Description: Executes any command that can be executed in a Windows cmd prompt and returns
// the response via auto-resizing buffer.
//
// Inputs: const char *command - string containing complete command to be sent
// char **chunk - initialized pointer to char array to return results
// size_t chunk_size - Initial memory size in bytes of char **chunk.
//
// Return: 0 for success
// -1 for failure
//
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
int __declspec(dllexport) cmd_rsp(const char *command, char **chunk, unsigned int chunk_size)
{
SECURITY_ATTRIBUTES saAttr;
/// All commands that enter here must contain (and start with) the substring: "cmd.exe /c
/// /////////////////////////////////////////////////////////////////////////////////////////////////////////
/// char cmd[] = ("cmd.exe /c \"dir /s\""); /// KEEP this comment until format used for things like
/// directory command (i.e. two parts of syntax) is captured
/// /////////////////////////////////////////////////////////////////////////////////////////////////////////
const char rqdStr[] = {"cmd.exe /c "};
int len = (int)strlen(command);
char *Command = NULL;
int status = 0;
Command = calloc(len + sizeof(rqdStr), 1);
if(!Command) return -1;
strcat(Command, rqdStr);
strcat(Command, command);
SetupSecurityAttributes(&saAttr);
IO_PIPES io;
if(SetupChildIoPipes(&io, &saAttr) < 0) return -1;
//eg: CreateChildProcess("adb");
if(CreateChildProcess(Command, &io) == 0)
{
// Read from pipe that is the standard output for child process.
ReadFromPipe(chunk, chunk_size, &io);
status = 0;
}
else
{
status = -1;
}
free(Command);
return status;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Prototype: int __declspec(dllexport) cmd_no_rsp(char *command)
//
// Description: Variation of cmd_rsp that does not wait for a response. This is useful for
// executables or processes that run and exit without ever sending a response to stdout,
// causing cmd_rsp to hang during the call to the read() function.
//
// Inputs: const char *command - string containing complete command to be sent
//
// Return: 0 for success
// -1 for failure
//
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
int __declspec(dllexport) cmd_no_rsp(const char *command)
{
/// All commands that enter here must contain (and start with) the substring: "cmd.exe /c
/// /////////////////////////////////////////////////////////////////////////////////////////////////////////
/// char cmd[] = ("cmd.exe /c \"dir /s\""); /// KEEP this comment until format used for things like
/// directory command (i.e. two parts of syntax) is captured
/// /////////////////////////////////////////////////////////////////////////////////////////////////////////
SECURITY_ATTRIBUTES saAttr;
const char rqdStr[] = {"cmd.exe /c "};
int len = (int)strlen(command);
char *Command = NULL;
int status = 0;
Command = calloc(len + sizeof(rqdStr), 1);
if(!Command) return -1;
strcat(Command, rqdStr);
strcat(Command, command);
SetupSecurityAttributes(&saAttr);
IO_PIPES io;
if(SetupChildIoPipes(&io, &saAttr) < 0) return -1;
status = CreateChildProcessNoStdOut(Command, &io);
free(Command);
return status;
}
static int SetupChildIoPipes(IO_PIPES *io, SECURITY_ATTRIBUTES *saAttr)
{
//child process's STDOUT is the program output or data that child process returns
// Create a pipe for the child process's STDOUT.
if (!CreatePipe(&io->out_pipe_read, &io->out_pipe_write, saAttr, 0))
{
return -1;
}
// Ensure the read handle to the pipe for STDOUT is not inherited.
if (!SetHandleInformation(io->out_pipe_read, HANDLE_FLAG_INHERIT, 0))
{
return -1;
}
//child process's STDIN is the user input or data entered into the child process
// Create a pipe for the child process's STDIN.
if (!CreatePipe(&io->in_pipe_read, &io->in_pipe_write, saAttr, 0))
{
return -1;
}
// Ensure the write handle to the pipe for STDIN is not inherited.
if (!SetHandleInformation(io->in_pipe_write, HANDLE_FLAG_INHERIT, 0))
{
return -1;
}
return 0;
}
// Create a child process that uses the previously created pipes for STDIN and STDOUT.
static int CreateChildProcess(const char *cmd, IO_PIPES *io)
{
PROCESS_INFORMATION piProcInfo;
STARTUPINFO siStartInfo;
BOOL bSuccess = FALSE;
// Set up members of the PROCESS_INFORMATION structure.
ZeroMemory(&piProcInfo, sizeof(PROCESS_INFORMATION));
// Set up members of the STARTUPINFO structure.
// This structure specifies the STDIN and STDOUT handles for redirection.
ZeroMemory(&siStartInfo, sizeof(STARTUPINFO));
SetupStartUpInfo(&siStartInfo, io);
// Create the child process.
bSuccess = CreateProcess(NULL,
cmd, // command line
NULL, // process security attributes
NULL, // primary thread security attributes
TRUE, // handles are inherited
CREATE_NO_WINDOW, // creation flags
//CREATE_NEW_CONSOLE, // creation flags
NULL, // use parent's environment
NULL, // use parent's current directory
&siStartInfo, // STARTUPINFO pointer
&piProcInfo); // receives PROCESS_INFORMATION
// If an error occurs, exit the application.
if (!bSuccess)
{
return -1;
}
else
{
// Close handles to the child process and its primary thread.
CloseHandle(piProcInfo.hProcess);
CloseHandle(piProcInfo.hThread);
CloseHandle(io->out_pipe_write);
}
return 0;
}
// Create a child process that uses the previously created pipes for STDIN and STDOUT.
static int CreateChildProcessNoStdOut(const char *cmd, IO_PIPES *io)
{
PROCESS_INFORMATION piProcInfo;
STARTUPINFO siStartInfo;
BOOL bSuccess = FALSE;
// Set up members of the PROCESS_INFORMATION structure.
ZeroMemory(&piProcInfo, sizeof(PROCESS_INFORMATION));
// Set up members of the STARTUPINFO structure.
// This structure specifies the STDIN and STDOUT handles for redirection.
ZeroMemory(&siStartInfo, sizeof(STARTUPINFO));
SetupStartUpInfo(&siStartInfo, io);
// Create the child process.
bSuccess = CreateProcess(NULL,
cmd, // command line
NULL, // process security attributes
NULL, // primary thread security attributes
TRUE, // handles are inherited
CREATE_NO_WINDOW, // creation flags
//CREATE_NEW_CONSOLE, // creation flags
NULL, // use parent's environment
NULL, // use parent's current directory
&siStartInfo, // STARTUPINFO pointer
&piProcInfo); // receives PROCESS_INFORMATION
// If an error occurs, exit the application.
if (!bSuccess)
{
return -1;
}
else
{
// Close handles to the child process and its primary thread.
CloseHandle(piProcInfo.hProcess);
CloseHandle(piProcInfo.hThread);
CloseHandle(io->out_pipe_write);
}
return 0;
}
// Read output from the child process's pipe for STDOUT
// Grow the buffer as needed
// Stop when there is no more data.
static int ReadFromPipe(char **rsp, unsigned int size, IO_PIPES *io)
{
COMMTIMEOUTS ct;
int size_recv = 0;
unsigned int total_size = 0;
unsigned long dwRead;
BOOL bSuccess = TRUE;
char *accum;
char *tmp1 = NULL;
char *tmp2 = NULL;
//Set timeouts for stream
ct.ReadIntervalTimeout = 0;
ct.ReadTotalTimeoutMultiplier = 0;
ct.ReadTotalTimeoutConstant = 10;
ct.WriteTotalTimeoutConstant = 0;
ct.WriteTotalTimeoutMultiplier = 0;
SetCommTimeouts(io->out_pipe_read, &ct);
//This accumulates each read into one buffer,
//and copies back into rsp before leaving
accum = (char *)calloc(1, sizeof(char)); //grow buf as needed
if(!accum) return -1;
memset(*rsp, 0, size);
do
{
//Reads stream from child stdout
bSuccess = ReadFile(io->out_pipe_read, *rsp, size-1, &dwRead, NULL);
if (!bSuccess || dwRead == 0)
{
free(accum);
return 0;//successful - reading is done
}
(*rsp)[dwRead] = 0;
size_recv = (int)strlen(*rsp);
if(size_recv == 0)
{
//should not get here for streaming
(*rsp)[total_size]=0;
return total_size;
}
else
{
//New Chunk:
(*rsp)[size_recv]=0;
//capture increased byte count
total_size += size_recv+1;
//increase size of accumulator
tmp1 = ReSizeBuffer(&accum, total_size);
if(!tmp1)
{
free(accum);
strcpy(*rsp, "");
return -1;
}
accum = tmp1;
strcat(accum, *rsp);
if(total_size > (size - 1))
{ //need to grow buffer
tmp2 = ReSizeBuffer(&(*rsp), total_size+1);
if(!tmp2)
{
free(*rsp);
return -1;
}
*rsp = tmp2;
}
strcpy(*rsp, accum);//refresh rsp
}
}while(1);
}
// return '*str' after number of bytes realloc'ed to 'size'
static char * ReSizeBuffer(char **str, unsigned int size)
{
char *tmp=NULL;
if(!(*str)) return NULL;
if(size == 0)
{
free(*str);
return NULL;
}
tmp = (char *)realloc((char *)(*str), size);
if(!tmp)
{
free(*str);
return NULL;
}
*str = tmp;
return *str;
}
static void SetupSecurityAttributes(SECURITY_ATTRIBUTES *saAttr)
{
// Set the bInheritHandle flag so pipe handles are inherited.
saAttr->nLength = sizeof(SECURITY_ATTRIBUTES);
saAttr->bInheritHandle = TRUE;
saAttr->lpSecurityDescriptor = NULL;
}
static void SetupStartUpInfo(STARTUPINFO *siStartInfo, IO_PIPES *io)
{
siStartInfo->cb = sizeof(STARTUPINFO);
siStartInfo->hStdError = io->out_pipe_write;
siStartInfo->hStdOutput = io->out_pipe_write;
siStartInfo->hStdInput = io->in_pipe_read;
siStartInfo->dwFlags |= STARTF_USESTDHANDLES;
}
ReadFile(, lpBuffer,,,)
, AFAIK, may read null characters intolpBuffer
. Should this occur, it looks like much of code's use ofstr...()
would suffer. What is the design goal concerning piped null characters? \$\endgroup\$ReadFile()
and family operate in binary mode, so picking up anull
character could happen as part of normal data transfer, and indeed would affect how the string functions would interpret the data. And it would be a problem if the 1st character wasnull
. The honest answer about design goals for pipednull
characters is that I didn't even consider it. Nice catch. Any ideas? \$\endgroup\$ReadFromPipe
function to exclude the use of any string functions, or handle anynull
character detection in-line as the data is read. Perhaps something like THIS. \$\endgroup\$