This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.
Borrow the D from DCOM-Remote Execution of Any Old EXE
Tony Smith
Running code on a remote server usually implies that it was
designed and built to be run that way. What if you want to run existing console
applications remotely without altering them, and integrate the launching and
error checking into a client application? In this article, Tony Smith
demonstrates how to build a generic solution to do just that, taking advantage
of NT's built-in distributed processing.
Something I find I have to do during my work is learn
stuff fast, in less-than-ideal
circumstances (that is, from the online Help or manuals, and by trial and
error, rather than in a training course!). This article describes the result of
one such learning experience. Wherever possible, I try to build generic
solutions, and this is a case in point-the code outlined here can be used to
run any program or command on a remote computer running NT.
The requirement
I was developing a client-server system: the client in
PowerBuilder and the server side in SQL Server stored procedures for use by
online clients. C++ was used for the large server processes where performance
was important, and no client connectivity was required.
A new business
requirement meant that one of the large C++ processes was to be executed at the
request of an online user. The process was too large to run on the client PC (a
big memory user) and too complex to redevelop as a stored procedure. We needed
a way to execute the server process in its existing form at the request of a
client computer, and make that execution appear synchronous-that is, return
control to the user only when the process had completed.
The options
Some server-side C++ was obviously required to launch and
monitor the existing process using standard Win32 API calls. The main question
was how to handle the communication with the client. There were several
options:
- Write the controlling process as an extended
stored procedure in C++
- Write some "middleware" to perform the
communication over named pipes
- Write some "middleware" to use Remote
Procedure Calls
- Use NT's built-in DCOM to provide the remote
execution capability
I opted for DCOM
as the best solution, mainly because I believed it could be developed in the
shortest time. I then had two choices-learn a lot about COM and redesign my
application, or build a wrapper for the minimum DCOM functionality required to
provide a generic solution for running remote processes. It was a no-brainer-I
had only one week to produce a working solution!
Windows NT and DCOM
DCOM is the mechanism that enables COM servers and COM
clients to reside on physically separate computers. In Windows NT 4.0, DCOM is
built into the operating system and is implemented "under the covers" using
Remote Procedure Calls. The great thing is that you don't need to know anything
about the RPC mechanism to use DCOM in an application. The combination of NT's
native support for it and Visual Studio's support via the ATL COM AppWizard
means that you can create a COM server in a few simple steps, and distribute it
in a few more.
Introducing REFServe and REFClient
This article describes the creation of a generic mechanism
for executing a program on a remote NT computer-I've called the whole thing REF
(Remote Execution Facility). Everything has to have a TLA, right? The target
machine can be running NT Server 4.0 or NT Workstation 4.0. The client can be
either flavor of NT, or Windows 95/98.
So here it is: the
Remote Execution Facility, featuring REFServe.exe and REFClient.dll. There are
two other components required for testing-an application to run at the server
end (REFTarget) and a test client application to request that it be run
(REFTest). Figure 1 shows the overall architecture.
REFServe is a COM
server that exposes an interface called IRunProcess. That interface has a
single function: RunSyncProcess(). You pass in a command to be executed on the
server, and the name of the computer on which to run the command. The command
is used to launch a new process, and the server function waits for the process
to complete while trapping its output. Upon completion of the process, the
output from the process and its exit code are returned to the calling program
via REFClient.
REFClient.DLL
simply wraps the call to the function in REFServe. It exports a single
function, ExecuteRemote(), which hides the COM object creation and destruction
so that the call can be easily embedded in a VB, C++, or PowerBuilder
application.
Creating the REFServe project
Let's build the server code first. There are six steps
involved:
- Create the project.
- Add a custom interface.
- Define parameter data.
- Add the method to interface.
- Add the code to launch and monitor the target
process (that is, RunSyncProcess()).
- Build and deploy REFserve.EXE.
First, use AppWizard
to create an ATL EXE project called REFServe. This COM server is a candidate
for implementing as an NT service, but for now we'll make it a "normal" EXE
since it will be easier to test.
The next step is
to define the interface that our server code will expose. We're going to create
a custom interface, IRunProcess, with a single function (RunSyncProcess()).
This function appears to run the process synchronously-control is only returned
once the process has completed. In reality, it launches another asynchronous process on the same NT
computer and monitors that process until it has completed.
Select class view
in the left pane and do the following:
- Right-click "REFServe classes" and
select "New ATL Object..."
- Under the "objects" category, select "simple
object"
- Enter the short name "RunProcess"
- Before closing the dialog box, go to the
Attributes tab and select the Custom interface radio button.
Now you must
define the parameters that will be passed to RunSyncProcess(). The data being
passed back and forth between client and server will be managed by the
proxy/stub library. This code is generated automatically from the interface
definition, and it's in the IDL file that we define our data structure. The
MIDL compiler copies it from there to the header file so that it only needs to
be coded in one place.
The parameters
passed to the function are defined in a single structure, ServerProcessParams,
for convenience. The downside of this approach is that the interface can't be
used by scripting clients (it would need to be defined as a dual interface),
which isn't a problem here. This warning message from the MIDL compiler can
therefore be ignored:
warning MIDL2039 : interface does not conform to
[oleautomation] attribute : [Parameter 'pSPP' of
Procedure 'RunSyncProcess' (Interface'IRunProcess')]
For a discussion
on passing data (and in particular, arrays) via DCOM, see Charles Steinhardt's
June 1999 column in Visual C++ Developer
("Dear Charles...: Passing Arrays in COM").
The ServerProcessParams
structure contains the command to be executed on the server as well as the
message returned from it. Add the following definition at the top of the
REFServe.idl file:
typedef struct ServerProcessParams
{
char szClientComputerName[32];
char szClientUserID[20];
char szServerComputerName[32];
char szCommand[200];
char szMessageReturned[500];
}
ServerProcessParams;
Having defined the
interface and the parameter data, add the method that will launch and monitor
the target process. In the class view, right-click the interface IRunProcess.
From the pop-up menu, select "Add Method" and add a method called
RunSyncProcess. Enter the method name and parameter details as shown in Figure
2.
Note the [in,out]
specification on the parameter line. This ensures that the data is copied both
ways by the marshaling code, which is necessary to return the status message
from the target server command.
What's in CRunProcess::RunSyncProcess?
Okay, down to business. The function CRunProcess::RunSyncProcess()
contains the code to launch the target process and wait for it to complete.
Remember that the CRunProcess object is instantiated on the target server
machine, so that as far as the RunSyncProcess() is concerned, the target
process is being run locally.
Reporting API call errors
The first code to add is the simple function shown here,
which converts a system error code to a meaningful message. It makes use of the
API call FormatMessage(), which is combined with GetLastError() to return a
useful message following an unsuccessful system call. During testing, this is
far preferable to displaying a number and then scanning winerror.h to find out
what it means-the extra few minutes spent adding functions like these is a good
investment. If you're unfamiliar with these API calls, take a look at Jim
Marshall's piece in the May 1999 edition of Visual
C++ Developer ("Getting Human Readable Error Messages from Windows") or use
the online Help.
void CRunProcess::ReportAPIError(LPSTR szCallType,
ServerProcessParams * pSPP)
{
char szErrMsg[200];
<![if !supportEmptyParas]> <![endif]>
strcpy((LPSTR)pSPP->szMessageReturned,szCallType);
strcat((LPSTR)pSPP->szMessageReturned,
" call Failed - Reason follows :\n\n");
<![if !supportEmptyParas]> >![endif]>
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM,NULL,
GetLastError(),
MAKELANGID(LANG_NEUTRAL,SUBLANG_DEFAULT),
(LPTSTR) szErrMsg, (DWORD) sizeof(szErrMsg), NULL
);
strcat((LPSTR)pSPP->szMessageReturned,szErrMsg);
<![if !supportEmptyParas]> <![endif]>
}
<![if !supportEmptyParas]> <![endif]>
strcat((LPSTR)pSPP->szMessageReturned,szErrMsg);
<![if !supportEmptyParas]> <![endif]>
}
<![if !supportEmptyParas]> <![endif]>
Remember to
declare this function in the header file RunProcess.h.
The pipe with no name
Now to code the main function, RunSyncProcess(), which is
shown in Listing 1. Two assumptions are made about the target process:
that it communicates with the outside world via standard output (that is, it
reports success or failure there) and that it returns a zero exit code if it
completes successfully, non-zero if it doesn't. If either of these is not true
(for example, a process using STDERR to report failure), then you can change
the code in RunSyncProcess accordingly.
The mechanism used
to capture the output from the new process is an anonymous pipe. The procedure
works like this: a pipe is created and two handles are obtained to it, one for
reading and one for writing. The "write handle" is passed to the new process by
modifying the STARTUPINFO structure prior to calling CreateProcess(). The "read
handle" is retained and used to read from the pipe. The process goes into a
loop, reading from the pipe until ERROR_BROKEN_PIPE is returned-this happens
when the called process ends. The data read from the pipe is copied into the
message area that forms part of the parameter block received from the caller, checking
that there's room for it. The application that I built this mechanism to run
passes back a simple message-either an "Okay, I've worked" message or "I
haven't worked, and here's why
" The 500-byte message area was sufficient.
Change this if more data needs to be returned.
The CreatePipe()
API call takes four parameters. The first two are the addresses of the
locations where the read and write handles are to be placed. The third
parameter is the address of a SECURITY_ATTRIBUTES structure. We need one of
these to tell the function that the handles it creates should be inheritable
(because the "write handle" is inherited by the new process). The last
parameter is the buffer size-specifying zero means to use the system default
value.
The ReadFile()
function is used to read data from the pipe. It requires the "read handle" to
the pipe, the address of a buffer to place the data in, and the number of bytes
to be read. The location of a return field is also specified-dwBytesRead is
where the function returns the number of bytes in the buffer. The last
parameter is set to NULL to indicate that overlapped I/O processing isn't being
used.
Desktops and Windows stations
I mentioned the STARTUPINFO structure, which is used to pass
details about the new process to the CreateProcess() call. Another field that
needs to be set is lpDesktop. It should contain the address of a
null-terminated string containing the name of the desktop and Windows station
that the new process should use.
When the value is
set to "Winsta0\\Default," the new process is passed the default desktop of the
NT system. This means any calls to write to the screen will work and will be
visible on the screen. You can get user input, assuming someone is present at
the time the process is running. However, you can have problems if the user
that you sign on to run the process doesn't have access to the desktop.
When you configure
the DCOM server using DCOMCNFG (which I'll cover later), you need to specify
which user should be signed on to run the process. The easiest solution is to
specify "the interactive user." However, this might be seen as a security risk.
An alternative is to create a user id specifically for running REFServe.
Returning to the
code in RunSyncProcess(), the new process is launched with CreateProcess(), and
the initiating process loops around reading data from the anonymous pipe. When
the pipe is "broken" (that is, the called process has completed), a check is
made to ensure that it has finished, using WaitForSingleObject(). This function
is passed the process handle that was set up in the PROCESS_INFORMATION
structure passed to CreateProcess().
Finally, the
GetExitCodeProcess() function is used to retrieve the process's exit code,
which is returned to the calling function.
Building the proxy/stub DLL
The code for the proxy/stub library is generated
automatically, but the library is not built
automatically. A make file is generated by the AppWizard, and you simply need
to add it as a post-build step, so that when you rebuild the project you also
rebuild the proxy/stub DLL. Here's how:
- Under Project...Settings
, select the
Post-build step tab (see Figure 3).
- Ensure that "All configurations" is selected
in the left-hand pane.
- Add a post-build description (for example,
"Building Proxy/Stub Library").
- Add this command under "Post-build
command(s)": nmake -f REFServeps.mk.
Building the client project REFClient
So you've successfully built the REFServe.EXE and
REFServeps.dll. Now you must create the client-side code that instantiates the
REFServe object and calls RunSyncProcess().
This project is a
Win32 DLL that exports a function, ExecuteRemote(), to allow any client process
written in any language to launch a remote process. In this example, the caller
of this function is written in C++-some differences apply if you want to call
it from Visual Basic or PowerBuilder (for example), and I'll cover those later
in this article.
Create a Win32 DLL
project ("a DLL that exports some symbols"). Edit the header and source files
to remove the exported class and variable from the sample code. Use the
exported function as the basis for ExecuteRemote(). In REFClient.cpp, add the
following:
#include <string.h>
#include <stdio.h>
#include <windows.h>
#include "RefServe.h"
Add to stdafx.h :
#define _WIN32_WINNT 0x0400
#include<atlbase.h>
Listing
2 shows the code for ExecuteRemote().
The processing
consists of a call to CoInitialize() to initialize the COM library, followed by
a call to CreateInstanceEx() to create the REFServe instance on the server. The
CLSID of the class is specified, along with a data structure of type
COSERVERINFO which specifies the name of the target machine.
Notice the
alternative call, which is useful during testing-when a target computer name of
"LOCAL" is received, then the server object is created locally, on the client
computer.
There are two
approaches that can be used to specify the location of the target process: It
can be set in the Registry using DCOMCNFG or passed to CoCreateInstanceEx() in
the COSERVERINFO structure. I've opted for the latter because I don't want to
reconfigure all of the client machines simply to change the location of the
server components. I might also wish to run REFServe in more than one location
from the same client machine, depending on the name of the target process. In
the production system that uses this mechanism, the server name is held in the
system database, making it a simple matter to change it.
Once the instance
of the object is created, the interface pointer is retrieved and used to call
the function RunSyncProcess(). When this call has completed (that is, the
target server process has been started and
has ended), then the server object is released. In fact, multiple clients can
connect to the same instance of the server. The server object (that is, the
executing program) will physically disappear after five seconds have elapsed
with no client connected. This can be changed by altering the value of
dwTimeOut at the top of the REFServe.cpp file.
Linking the projects together
Before compiling the REFClient project, consider this next
approach to make life easier. This step isn't necessary but saves having to
copy files around between the client and server projects, such as the C code
generated by the MIDL compiler. By placing the server project as a
sub-directory of the client one, and compiling the server project first, no
copying is needed. Also, the whole lot can be moved elsewhere without requiring
any changes.
- Move REFServe directory as sub-directory of
REFClient.
- Add this to the project:
/REFServe/REFServe_i.c.
- Put REFServePS.lib in the linker options.
- Specify "not using pre-compiled headers"
for REFServe_i.c.
- Under the Project
Settings
menu option, Link
tab, Preprocessor category, add .\REFServe in the "Additional library path"
field.
- Under the Project...Settings... menu option,
C++ tab, Input category, add .\REFServe in the "Additional include directories"
field.
Trying it out
In order to test the whole mechanism, I created a program
(REFTest) to call the RunProcess() function in REFServe.dll. This is a Win32
Console application that does nothing except call the required function, like
this:
<![if !supportEmptyParas]> <![endif]>
#include "stdafx.h"
#include "REFClient.h"
<![if !supportEmptyParas]> <![endif]>
int main(int argc, char* argv[])
{
#define MESSAGE_SIZE 500
long rc;
char szMessageReturned[MESSAGE_SIZE];
strcpy(szMessageReturned,"No Message Returned from REFClient/REFServe");
rc = ExecuteRemote("server1"
// Use "LOCAL" to run here
,"C:\\WHEREVER\\REFTARGET.EXE"
,szMessageReturned
,MESSAGE_SIZE
);
printf("Output string returned from server: %s",szMessageReturned);
printf("\nPress Enter to exit...");
getchar();
return 0;
}
You should alter
this to reflect the name of your server and the location of the target program
on it. If you're testing on one computer (not a brilliant way to test a
distributed system, but useful for getting the basics working!) you can specify
"LOCAL" as the computer name, and run the client and server components on the
same machine. Before you build the REFTest project, use Project Settings to
ensure that you're getting refclient.h and refclient.lib from their location on
your system. On the C/C++ tab, set the Category dropdown box to Preprocessor
and set the Additional Include Directories. On the Link tab, set the
Object/Library Modules.
Finally, a target
process to run on the server is required. You can actually test by sending a
command such as "DIR," but I created a test target program, REFTarget. This is
another console app that simply reports to STDOUT the name of the computer on
which it's running. This output is intercepted by REFServe and returned to the
client application. The target process can be any EXE, and it has no knowledge
of any of the other components involved (or DCOM, for that matter).
Deploying the client and server components
These are the steps you need to take before testing (assuming
you're using REFTest and REFTarget in your testing).
Client components Copy the following files to the client PC: REFTest.EXE,
REFServeps.dll, and REFClient.DLL. Run this command from the directory where
REFServeps.dll exists:
<![if !supportEmptyParas]> <![endif]>
regsvr32 REFServeps.dll
This registers the
proxy/stub DLL by calling its internal registration function. If you're testing
on the same machine that you compiled the programs on, this will already have
been done.
Server components Copy the following files to the server: REFServe.exe,
REFServeps.dll, and REFTarget.exe. Enter this command in the directory where
REFServe.exe exists:
REFServe /RegServer
This registers the
COM server by calling its internal registration function.
Register the
proxy/stub dll as for the client (in other words, run this command from the directory
where REFServeps.dll exists):
regsvr32 REFServeps.dll
Use the DCOMCNFG
utility to set the location and security details (as described later).
Using DCOMCNFG
DCOMCNFG is supplied with NT and is a utility to help
administer Registry entries for DCOM components. When you launch it, you're
presented with a list of registered applications. Locate REFServe and
double-click it. Under the "Location" tab, enter where the process runs. You
could set this on the client machine to point to the specific server machine,
but for REFServe there's no need. The name of the server is set at run time
from the parameters supplied to the ExecuteRemote() function in REFClient.dll.
The "Security" tab
enables you to set custom security settings for "access," "launch," and
"configuration." During testing, I allowed everyone full access using the
custom settings, and in production I created a specific user for running the
process.
Finally, the
"Identity" tab enables you to set the user that the server process should run
under. The options are "the interactive user," "the launching user," or a
specified user and password. For testing, it's easiest to use the interactive
user. An approach to security needs to be devised if you're deploying REFServe
at a large installation, and this would normally have to be agreed upon by
whoever administers security at your site.
Testing the whole configuration
After all that work, it's time to try it! On the client
machine, open a command prompt window and run REFTest.EXE. You should see the
output displayed indicating that the Target program has been successfully run
on the server, as in *. That's all there is to it. Okay, so it doesn't do
much yet, but think of the possibilities!
Calling from PowerBuilder or VB
If you intend to call ExecuteRemote() from a PowerBuilder
application (as I did), there are some differences in the way you need to build
REFClient.dll. I believe the same changes apply if you want to call from VB,
although I haven't tried it.
- In REFClient.h, change REFServePS_API
__dclspec(dllexport) to REFServePS_API __dclspec(dllexport) _stdcall.
- Do the same on the import definitions. This
will use the PASCAL convention expected by PowerBuilder.
A DEF file is also
required if the ExecuteRemote() is to be called from a language that expects
non-mangled names, which PowerBuilder does. The function names need to be
explicitly exported as shown here. Create REFCLient.DEF and add it to the
project:
REFClient.DEF :</code></pre>
<pre><code><![if !supportEmptyParas]> <![endif]></code></pre>
<pre><code>LIBRARY REFClient</code></pre>
<pre><code>DESCRIPTION "Client Interface to COM Server REFServe"</code></pre>
<pre><code>VERSION 1.00</code></pre>
<pre><code>EXPORTS</code></pre>
<pre><code>ExecuteRemote
In theory, the
non-mangled name should be followed by the mangled name generated by the
compiler, but luckily it works okay without. This means you don't have to delve
into the created DLL (using Quickview from Explorer) to see what the mangled
name is.
Renaming the proxy/stub
In the projects described in this article, I accepted the
default proxy/stub name REFServeps. If you wish to change the name to fit in
with your site's naming standards you can do so:
- Edit REFServe.CPP (change "REFServeps' to
"NEWNAME").
- Edit REFServe.DEF (change "REFServeps" to
"NEWNAME").
- Edit REFServeps.MK (change "REFServeps" to
"NEWNAME")
- Rename REFServeps.DEF to NEWNAME.DEF.
- Rename REFServeps.MK to NEWNAME.MAK.
Platforms used for testing
The server must be NT, but it can be Server or Workstation. The client was tested on
NT Workstation 4.0 SP3, but it should work with NT Server, Windows 95, or
Windows 98. DCOM support for Windows 95 has to be installed separately-see the
Microsoft site for details. Another factor that shouldn't make any difference
(but you never know) is that I compiled the code under NT, not 95/98. The
server end could be implemented on
95/98, except that REFServe would have to be launched manually, so what's the
point?
The code makes an
assumption that the called application returns -1 if it fails. If any other
error is returned, it's assumed to be a system error and is interpreted using
FormatMessage(). If the application has a range of possible errors, then code
them in the ExecuteRemote() function.
Common problems
DCOM is complex! While building and testing the software I've
described here, I encountered several problems. These are summarized in
comprobs.txt in the Subscriber Download file available at www.pinpub.com/vcd
and in this section.
Access denied Error 5 (access denied) errors can occur when the initiating
user doesn't have authority to launch the desired process (that is, permission
to create an instance of the class on the remote server). This is remedied
using DCOMCNFG. The exact settings to use are a matter for the security
administrators on any given site-allowing "everyone" launch permission is a way
to get it working during testing. The rest is up to you!
Class not registered You run "REFServe /RegServer," which doesn't report when it
doesn't work, but the class doesn't appear in DCOMCNFG. It could be that you
didn't compile the "minimum dependency" version, and you're installing on a
server that doesn't have ATL.DLL installed. Either copy ATL.DLL to that machine
(and register it) or recompile the "minimum dependency" version.
DCOM error-"Bad variable type" This error was received from a call to the remote COM Server.
In this case, the server app was REFServe.EXE and the proxy/stub DLL was
REFServeps.DLL. The two steps required to register the server components are:
REFServe /RegServer
regsvr32 REFServeps
You'll get this
error if you do them in the wrong order, or if you're testing on a local
machine and think you don't have to register the dll again.
DLL initialization error in KERNEL32.DLL or USER32.DLL This occurred on the server when calling a function via DCOM.
The cause is the remote application (REFTarget) trying to issue API calls in
its initialization, which requires desktop access. You have three options:
- Replace CreateProcess with CreateProcessAsUser
and use a load of MS-supplied code to set up correct security and log the user
on (!). This isn't really the best thing security-wise, but see Q165194 in the
MS Knowledge Base if you're interested.
- Run the process as administrator or interactive
user (not allowed at my client's site).
- Instead of passing the default desktop:
>![if !supportEmptyParas]< >![endif]<
si.lpDesktop =
"WinSta0\\Default"
(where si is a STARTUPINFO structure), send
an empty string instead (si.lpDesktop = "").
This causes a new
non-visible desktop to be provided for the launched process. This is what I
chose.
Conclusion
The built-in support for DCOM provides the basis for
sophisticated distributed systems based on the Windows NT operating system.
Better still, with minimal knowledge of COM/DCOM, you can borrow the
distributed-ness of DCOM and use it to distribute processing that was never
designed to be run that way.
You could use the
techniques described here in many ways. One example is writing a front-end that
allows remote diagnosis of servers by allowing commands and diagnostic
utilities to be run remotely. Don't forget that the "server" machine can be
running Windows NT Workstation, so it could also be used to administer desktop
machines in your organization. And don't forget to think about the security
implications of anything you do!
Download ref.exe
Tony Smith is a freelance developer currently working for a large
retailer in the U.K. He designs and builds high-performance NT Server-based
systems using ERwin, Visual C++, and SQLServer. tony.smith@tesco.net.
Listing 1. RunSyncProcess(). STDMETHODIMP CRunProcess::RunSyncProcess(ServerProcessParams *pSPP)
{
char szHostName[MAX_COMPUTERNAME_LENGTH + 1];
DWORD dMaxLen = MAX_COMPUTERNAME_LENGTH + 1;
DWORD dwExitCode;
STARTUPINFO StartupInfo;
PROCESS_INFORMATION ProcessInformation;
HANDLE hPipeRead;
HANDLE hPipeWrite;
SECURITY_ATTRIBUTES sa;
int nSpaceLeft;
int nBytesToMove;
int nCopiedSoFar;
#define BUFFER_SIZE 4096
char Buffer[BUFFER_SIZE + 1];
DWORD dwBytesRead;
DWORD dwMaxUserNameLen = 32;
memset(pSPP->szMessageReturned,0,sizeof(pSPP->szMessageReturned));
strcpy(szHostName,"Unknown");
GetComputerName((LPSTR)szHostName,&dMaxLen);
memset(&ProcessInformation,0,sizeof(ProcessInformation));
// Create an anonymous pipe and let standard output
// of new process write to it.
memset(&sa,0,sizeof(sa));
sa.nLength=sizeof(sa);
sa.bInheritHandle = TRUE;
CreatePipe(&hPipeRead,&hPipeWrite,&sa,0); // Create anonymous pipe and get both ends
memset(&StartupInfo,0,sizeof(StartupInfo)); // Initialize structure
StartupInfo.lpDesktop = "WinSta0\\Default"; // Default desktop
StartupInfo.hStdOutput = hPipeWrite; // Point child process at write end of pipe
StartupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE); // Standard handles for input
StartupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE); // and error
StartupInfo.dwFlags = STARTF_USESTDHANDLES;
if (!CreateProcess( NULL // Application Name
,(LPSTR)pSPP->szCommand // Command to execute
,NULL // Process security attributes
,NULL // Thread security attributes
,TRUE // Inherit handles
,NORMAL_PRIORITY_CLASS // Creation flags
,NULL // Environment
,NULL // Current Directory
,&StartupInfo // How to start the process
,&ProcessInformation // Filled in by WIN32 call
)
)
{
ReportAPIError("Create Process",pSPP);
return -1;
}
else
{
// Close the write handle in this process or ReadFile will hang
CloseHandle(hPipeWrite);
// Read the pipe until we get an error (including ERROR_BROKEN_PIPE,
// which is okay because it happens when child process ends).
// If callers buffer fills up, discard further messages.
// If error is ERROR_MORE_DATA then keep going.
nSpaceLeft = ((sizeof(pSPP->szMessageReturned) - 1));
nCopiedSoFar = 0;
DWORD rc;
while (TRUE)
{
dwBytesRead = 0;
if (!ReadFile(hPipeRead,&Buffer,BUFFER_SIZE,&dwBytesRead,NULL))
{
rc = GetLastError();
if (rc != ERROR_MORE_DATA)
break;
}
if (dwBytesRead > 0 && nSpaceLeft > 0)
{
nBytesToMove = (int)dwBytesRead;
if (nBytesToMove > nSpaceLeft)
nBytesToMove = nSpaceLeft;
nSpaceLeft -= nBytesToMove;
memmove((LPSTR)pSPP->szMessageReturned+nCopiedSoFar,Buffer,nBytesToMove);
nCopiedSoFar += nBytesToMove;
}
}
if (rc != ERROR_BROKEN_PIPE)
{
ReportAPIError("ReadPipe()",pSPP);
return -1;
}
// Everything okay - called process has closed the pipe.
// For safety, check that process ended, then pick up its exit code
// and return it. If it failed the error details have already been
// piped into the callers message buffer using the code above.
WaitForSingleObject(ProcessInformation.hProcess,INFINITE);
if (!GetExitCodeProcess(ProcessInformation.hProcess,&dwExitCode))
{
ReportAPIError("GetExitCodeProcess()",pSPP);
return -1;
}
else
{
return dwExitCode;
}
}
}
Listing 2. ExecuteRemote(). long REFCLIENT_API ExecuteRemote( LPSTR szServerComputerName
,LPSTR szCommand
,LPSTR szMessageReturned
,long lMessageBufferSize
)
{
long lRetCode = 0;
DWORD dMaxLen;
char szErrMsg[200];
IRunProcess * pIRunProcess;
HRESULT hr;
if (!SUCCEEDED(CoInitialize(NULL)))
{
strcpy(szMessageReturned,"Error returned from REFClient.DLL - Details follow :");
strcat(szMessageReturned,"\n");
strcat(szMessageReturned,"COM Library Initialization failed");
lRetCode = -1;
}
else
{
USES_CONVERSION; // defines some local variables for T2OLE macro
COSERVERINFO csi;
memset(&csi,0,sizeof(csi));
csi.pwszName = T2OLE(szServerComputerName);
MULTI_QI mqi[1];
mqi[0].pIID = &IID_IRunProcess;
mqi[0].pItf = NULL;
mqi[0].hr = S_OK;
if (strcmp(szServerComputerName,"LOCAL")==0)
{
hr = CoCreateInstanceEx( CLSID_RunProcess
,NULL
,CLSCTX_LOCAL_SERVER
,NULL
,1
,mqi
);
}
else
{
hr = CoCreateInstanceEx( CLSID_RunProcess
,NULL
,CLSCTX_REMOTE_SERVER
,&csi
,1
,mqi
);
}
if (hr != S_OK)
{
strcpy(szMessageReturned
,"Error returned from REFClient.DLL - Details follow :\n");
strcat(szMessageReturned,
"Failed to create instance of Server object RunProcess (REFServe.EXE).\n");
strcat(szMessageReturned,"System error message : ");
FormatMessage( FORMAT_MESSAGE_FROM_SYSTEM,
NULL,
hr,
MAKELANGID(LANG_NEUTRAL,SUBLANG_DEFAULT),
(LPTSTR) szErrMsg,
(DWORD) sizeof(szErrMsg),
NULL
);
strcat(szMessageReturned,szErrMsg);
lRetCode = -1;
}
else
{
pIRunProcess = (struct IRunProcess *)mqi[0].pItf;
ServerProcessParams parms;
strcpy((LPSTR)parms.szCommand,szCommand);
strcpy((LPSTR)parms.szMessageReturned,"No message text returned from Server");
strcpy((LPSTR)parms.szServerComputerName,szServerComputerName);
dMaxLen = sizeof(parms.szClientComputerName);
GetComputerName((LPSTR)parms.szClientComputerName,&dMaxLen);
dMaxLen = sizeof(parms.szClientUserID);
GetUserName((LPSTR)parms.szClientUserID,&dMaxLen);
// Call the function to launch and wait for the remote process
hr = pIRunProcess->RunSyncProcess(&parms);
// Extract the returned text and return to caller
strcpy(szMessageReturned,"REFClient Saw : ");
strcat(szMessageReturned,(const LPSTR)parms.szMessageReturned);
// Free up the instance of the server object
pIRunProcess->Release();
lRetCode = hr;
// Error code -1 is an application error. A related message should
// already have been placed in the message buffer.
// If another error has occurred (e.g., a DCOM problem)
// then get the system message text.
// If using this to execute programs that have error codes other
// than -1, modify this code accordingly.
if (hr != S_OK && hr != -1)
{
strcpy(szMessageReturned
,"Error returned from DCOM via REFClient.DLL - Details follow :");
strcat(szMessageReturned,"\n");
strcat(szMessageReturned
,"Failed to execute function RunSyncProcess() of Server object RunProcess (in REFServe.EXE).");
strcat(szMessageReturned,"\n");
strcat(szMessageReturned,"System error message : ");
FormatMessage( FORMAT_MESSAGE_FROM_SYSTEM,
NULL,
hr,
MAKELANGID(LANG_NEUTRAL,SUBLANG_DEFAULT),
(LPTSTR) szErrMsg,
(DWORD) sizeof(szErrMsg),
NULL
);
strcat(szMessageReturned,szErrMsg);
}
}
}
return lRetCode;
}
To find out more about Visual C++ Developer and Pinnacle Publishing, visit their website at
http://www.pinpub.com/
Note: This is not a Microsoft Corporation website.Microsoft is not responsible for its content.
This article is reproduced from the January 2000 issue of Visual C++ Developer. Copyright 2000, by Pinnacle Publishing, Inc., unless otherwise noted. All rights are reserved. Visual C++ Developer is an independently produced publication of Pinnacle Publishing, Inc. No part of this article may be used or reproduced in any fashion (except in brief quotations used in critical articles and reviews) without prior consent of Pinnacle Publishing, Inc. To contact Pinnacle Publishing, Inc., please call 1-800-788-1900.