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.
SomethingI 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.
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.
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:
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!
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.
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.
Let's build the server code first. There are six steps involved:
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:
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.
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.
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];
strcpy((LPSTR)pSPP->szMessageReturned,szCallType);
strcat((LPSTR)pSPP->szMessageReturned,
" call
Failed - Reason follows :\n\n");
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM,
NULL,
GetLastError(),
MAKELANGID(LANG_NEUTRAL,
SUBLANG_DEFAULT),
(LPTSTR) szErrMsg,
(DWORD) sizeof(szErrMsg),
NULL
);
strcat((LPSTR)pSPP->szMessageReturned,szErrMsg);
}
Remember to declare this function in the header file RunProcess.h.
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.
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.
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:
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.
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.
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:
#include "stdafx.h"
#include "REFClient.h"
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).
These are the steps you need to take before testing (assuming you're using REFTest and REFTarget in your testing).
Client componentsCopy the following files to the client PC: REFTest.EXE, REFServeps.dll, and REFClient.DLL. Run this command from the directory where REFServeps.dll exists:
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 componentsCopy 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).
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.
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!
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.
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 :
LIBRARY REFClient
DESCRIPTION
"Client Interface to COM Server REFServe"
VERSION 1.00
EXPORTS
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.
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:
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.
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 deniedError 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 registeredYou 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.DLLThis 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:
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.
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;
}