Feedback Search Top Backward
EDM/2

RMX-OS2: An In-Depth View (Part 4)

Written by Johan Wikman

 
Part1 Part2 Part3 Part4

Introduction

This is the fourth article in a series where I am describing RMX. RMX is a system that allows OS/2 PM applications to run remotely, that is, to run on one computer and be used on another. Now, supposing you have an application that you want to run remotely, how do you start it on the remote computer? In this article I will describe how that can be done.

I assume you have read the previous articles, especially the one dealing with the mechanism RMX uses for the communication between different processes.

The Problem

In the normal case, when applications are run locally, starting an application is not a problem. If the application has an program-object it is enough to double-click on that, or you can simply start it from the command line. Starting an application from within another application is almost as trivial. Using one of the spawn() functions from the C-library or DosExecPgm() directly it is easy to start other applications.

The documentation of DosExecPgm() states that it cannot be used for starting an application that is of different type (fullscreen, windowed, PM application) than the starting application, but DosStartSession() must be used instead. I don't know what the situation actually is, because I have at least not experienced any problems when starting PM applications (using DosExecPgm()) from non-PM applications.

Anyway, when running remote applications the situation is quite different. Obviously DosExecPgm() is not capable of starting an application on another computer. The conclusion is that we need some mechanism on the other computer that starts the application that is to appear on the local computer. So, what is this mechanism? Well, the mechanism is another remote application that is prepared to accept commands from the local computer and start other remote applications.

This is almost a CATCH-22 situation . To run remote applications we need another remote application (actually mechanism as it need not be a single application) that starts them for us.

Standard Solutions

Depending on what network is being used, there are certain standard solutions for running applications remotely. TCP/IP Apart from being a protocol, the TCP/IP concept includes a lot of different applications. Provided the right daemons are running (I won't go into that now) it is possible to use TELNET for logging into a computer and running programs remotely. Currently I have two computers at home, the hostname of one of them is odin, and the hostname of the other is loke. While sitting at odin, I can log into loke the following way:


     [C:\]telnet loke

After having entered my password, I get an almost ordinary command-line prompt.


     [<loke>-C:\]

Although I sit at odin, whatever I do is executed in the context of loke. Typing dir shows me the context of the C-drive of loke. If I run command-line programs they also run on loke, but I can interract with them on odin I am sitting at. However, If I start a PM application then it will appear on loke, and not on odin. That is, TELNET as such does not allow PM application to be used remotely. If you remember from previous articels, all that is required in order to run an RMX application is that the environment variable RMXDISPLAY has been set. So, if I want to run a PM application on loke, yet be able to use it on odin I would:


     [<loke>-C:\]set RMXDISPLAY=odin[<loke>-C:\]start
     pulse.exe

This works, provided PULSE.EXE has been patched the way I described in a previous article.

Named Pipes

LAN Server (perhaps some other products as well) provides the possibility of running programs on another computer using the command NETRUN. I have never tried it out, but supposedly it would be possible to set the RMXDISPLAY variable and start an application (patched for RMX) using it.

Custom Solutions

Even if the TELNET option works and even if TELNET as such is very useful, the option is rather limited. In practice it would be, I can imagine, quite difficult to create a program object that automatically would start a remote application. Also, the TELNET option is present only on TCP/IP networks. For these reasons, but also because it was a nice problem, I developed a custom solution that is independent from any applications provided by the network software. The solution consists of three programs: RMXSTRTR.EXE, RMXSTART.EXE and RMXSTOP.EXE.

RMXSTRTR.EXE and RMXSTART.EXE runs on different computers. RMXSTRTR.EXE is a daemon application that usually is started when the computer is booted. It sits there idle, waiting for start requests from RMXSTART.EXE that is run on some other computer. RMXSTOP.EXE provides a graceful way of stopping a (possibly) detached RMXSTRTR.EXE.

RMXSTRTR.EXE

The overall structure of RMXSTRTR.EXE is illustrated in the following figure.

rmx.gif

So, what is going on?

When RMXSTRTR.EXE is started, it initializes itself, which among other things involves the creation of two semaphores.

  1. If it succeeds in the initializing, it spawns a second thread.
  2. The startup thread then blocks on one of the semaphores.
  3. The started thread creates a connection, which it subsequently blocks on.
  4. When some instance of RMXSTART.EXE somewhere on the network opens the connection, the thread
  5. immediately spawns a new thread and passes the connection handle along. The current thread (thread 2) then loops back to step 4.
  6. The new thread reads the start request from RMXSTART,
  7. starts the requested application, and
  8. sends a response telling whether the starting succeeded. Finally it just dies.

Having a separate thread for each client ensures that the likelihood that RMXSTRTR would be busy when an instance of RMXSTART attempts to open a connection is quite small. Nevertheless, under heavy load the task of spawning a new thread might take a while, so a better approach would perhaps be to have a number of threads ready for immediate dispatch.

Main function of RMXSTRTR

Let's look then at the main function of RMXSTRTR in greater detail (the actual code is available in a zip-file you should have got along with this issue).


     set_new_handler(OutOfMem);
     ios::sync_with_stdio();

The first call establishes a function that is called when the C++ operator new fails to allocate memory. It is unlikely that an out-of-memory situation ever occurs, but it doesn't hurt. The function specified simply writes a message and terminates the application. The following call synchronizes the C++ output mechanism (cout) with the C output mechanism (printf). This is necessary because both are being used within RMXSTRTR.


     ULONG  ulFunctionOrder = MAKEUSHORT(EXLST_ADD, 0xFF);
     DosExitList(ulFunctionOrder, CleanUp);

Then we register a cleanup-function. The function registered - CleanUp() - will be called by the system regardless of how RMXSTRTR is terminated. The first argument of DosExitList() specifies what we want to do. The high-order word should be 0, and the low-order word contains the actual information as two one-byte fields. The lower of the bytes specifies the action: EXLST_ADD indicates that we are adding a function to the cleanup-functions, EXLST_REMOVE indicates that we want to remove an existing function, and EXLST_EXIT indicates that we have done our processing and the system should call the next function. EXLST_EXIT is used only from within the actual cleanup-function. The higher of the bytes specifies where the registered function should be placed in the list of cleanup- functions. Functions with an order code of 0x0 are invoked first, and functions with an order code of 0xFF are invoked last.


     hev = CreateEventSemaphore();
     hmtx = CreateMutexSemaphore();

Two semaphores - one of them an event-semaphore and the other a mutex-semaphore - are created. The main thread blocks on the event-semaphore when it has initialized everything and has started the thread that does the actual job. As I will show later, the name used for the semaphore is constructed from the value of the environment variable RMXCOMMS. Doing this gives us two benefits:

  • It effectively means that there can only be one RMXSTRTR running using a specific RMXCOMMS DLL. This is what we want, as it does not make sense to have several instances of RMXSTRTR that uses the same communications DLL. On the other hand it allows several instances of RMXSTRTR to run, provided they use a different DLL.
  • As the name of the semaphore is constructed from the value of RMXCOMMS it means that we later can open the semaphore and post it, thus causing the (possibly detached) RMXSTRTR to exit.

It is here that RMXSTRTR exits after having printed an error message if the creation of the event semaphore fails, as that indicates that an RMXSTRTR using the same RMXCOMMS DLL already is running. The mutex semaphore is used for synchronizing the output of different threads.


     PSZ  pszName = GetStarterName();

The next this to do is to obtain the name RMXSTRTR should listen on. GetStarterName() is implemented on top of the RMXCOMMS function RmxGetServiceName(). That means that the contents of the buffer that is returned varies depending on which actual communications DLL is being used. That doesn't matter, however, as we do not care what it contains.


     void (*firstThread)(void*) = (void (*)(void*)) StarterLoop;
     int  tid = _beginthread(firstThread, SIZE_STACK, pszName);

Once the semaphores have been created and the name RMXSTRTR should be listening on has been obtained, the thread that does the actual work can be started. The function _beginthread() is the C RTL function for creating threads. The first argument is a pointer to a function that takes a void* as argument and returns nothing, the second argument is the size of the stack, and the third argument is an argument that should be provided to the thread function (the one given as first argument). This is the Borland format, the prototype is slightly different in IBM and other compilers. The function we want to use as thread function - StarterLoop() - is otherwise ok, except that it takes a PSZ as argument and not a void*. The funny looking variable declaration above the call to _beginthread() is simply there for making StarterLoop() acceptable as argument. This is perfectly safe as PSZ and void* are fully compatible in this context.


	   DosWaitEventSem(hev, SEM_INDEFINITE_WAIT);

Once the new thread has been started, the main thread simply blocks on the created semaphore. There it stays forever if need be.

Starter Loop

Essentially the starter loop - as shown in the figure earlier - is very simple. It is a tight loop where a connection is created, a connecting instance of RMXSTART is waited for, and a handler thread that does the job is spawned.


     while (TRUE)   {
	  create connection
	  wait for RMXSTART
	  spawn handler thread
     }

The connection is created the following way.


     HCONNECTION  hConn;
     ULONG  rc;

     rc = RmxCreate(pcszName, &hConn);

The name of the connection - pcszName - was obtained as an argument to the function. It is the same name as was used in the call to _beginthread() in the main thread. If the call is successful, hConn contains the connection handle.


     RmxConnect(hConn);

This call blocks until somebody opens the connection. The task of actually starting an application is a rather heavy operation, so a separate thread is dedicated for it.


     _beginthread(HandlerThread, SIZE_STACK, (VOID*) hConn);

The connection handle is provided as thread argument. Immediately when the thread has been started a new connection is created and RMXSTRTR is ready for another client.

Handling the Client

The client wants RMXSTRTR to start an application. Exactly which one, the client (RMXSTART) has to tell RMXSTRTR. The first thing to do then, is to read the entire request the client sent. For that purpose a small structure has been defined.


     struct Request{
	 PBYTE pbRequest;
	 ULONG ulSize;
     };

So first the entire request is read.


     Request  request;

     ReadRequest(hConn, &request);

The hConn was received by the thread when it was started. ReadRequest() (which I won't look at in detail) simply reads everything the client has sent and allocates a sufficiently large buffer on behalf of the caller where it stores the data. ulSize is updated to the actual size of the data sent by the client. We need to know the exact size of the request as we cannot trust the client to provide us with data of proper format.

Once the request has been read, we must parse it. A proper request is of one of the following formats,


     DISPLAY0APPLICATION00
     DISPLAY0APPLICATION0ATTRIBUTES00

where DISPLAY, APPLICATION and ATTRIBUTES denotes strings (without ending NULL). That is, two or three catenated ASCIIZ strings followed by an additional NULL.


     PSZ  pszDisplay,  pszApplication,  pszArguments;

     ParseRequest(&request, &pszDisplay, &pszApplication,
     &pszArguments);

ParseRequest() takes a request and sets the three provided PSZ pointers to point at the correct place in the request buffer. Once the request has successfully been parsed, we can call the function that actually starts the application.


     StartApp(pszDisplay, pszApplication, pszArguments);

When the application has been started a return code is sent back to the client. What the return code - rc - actually is depends on whether everything done so far has succeeded.


     SendResponse(hConn, rc);

Finally when everything is ready, the connection can be disconnected and closed.


     RmxDisConnect(hConn);RmxClose(hConn);

Starting the Application

Ok, now we have got so far that the request sent by the client (RMXSTART.EXE) has been read and parsed. Now it's time to start the application. But, we don't yet know for sure that the application the client specified actually exists. The first this to do is to find out if the application path specified by the client can be used directly.


     ULONG  rc = NO_ERROR;CHAR  achPath[CCHMAXPATH];if
     (access(pcszApplication, 0)){

Access() returns zero if the string provided as first argument denotes an application. That is, we end up in the if-branch if the application is not found. The next thing is to look for the application on the path.


	 rc = DosSearchPath(SEARCH_IGNORENETERRS |
			    SEARCH_ENVIRONMENT   |
			    SEARCH_CUR_DIRECTORY,
			    "PATH",
			    pcszApplication,
			    achPath,
			    CCHMAXPATH);
	 if (rc == NO_ERROR)
	   pcszApplication = achPath;

If DosSearchPath() returns NO_ERROR then the application was found. In that case the fully qualified name of the application is copied to achPath. Also if the function succeeds we set pcszApplication to point to achPath. That way, in the code to follow, we need not worry where the final application name is to be found, but can simply use pcszApplication. Now that we know the applications exists, we can finally launch it.


     if (rc == NO_ERROR)
	 LaunchApp(pcszDisplay, pcszApplication,
     pcszArguments);

If you look at the code I've provided, you'll see that I've commented out some code that would be executed before LaunchApp(). As I wrote RMXSTRTR to be used for starting remote application it must first be verified that the application to be started can be run remotely. After all, the application to be started is supposed to turn up on another computer somewhere and if that simply is not possible it doesn't make much sense starting it in the first place.

Launching the Application

The first thing that is done when the application is to be launched is that the environment is cloned.


     PSZ  pszEnv = CloneEnvironment(pcszDisplay),  pszArg = 0;

Why is that done? Well, if you have read the previous articles about RMX you know that the value of the environment variable RMXDISPLAY specifies the computer where the remote application will turn up. So, the value of that variable must be set to the value given by the client. Why not simply set RMXDISPLAY to the value desired and then start the application as it is easy to start a child process so that it inherits the environment. That could be done, provided RMXSTRTR would be single-threaded. Now that RMXSTRTR is multi-threaded that cannot be done, as several threads could simultaneously modify the variable. Synchronizing the launching of applications using semaphores would be a viable alternative, but launching the application is the most heavily-used operation in RMXSTRTR and it would be a pity not to use threads there. So, CloneEnvironment() clones the current environment and sets/replaces the value of RMXDISPLAY with the value specified by the client. Finally it returns the copy.

If the application has arguments we must build a proper argument string.


     if (pcszArguments){
       ULONG    ulApplication = strlen(pcszApplication) + 1,
		ulArguments   = strlen(pcszArguments) + 1,
		ulSize        = ulApplication + ulArguments + 1;

       pszArg = new CHAR [ulSize];
       strcpy(pszArg, pcszApplication);
       strcpy(&pszArg[ulApplication], pcszArguments);
       pszArg[ulSize - 1] = 0;
     }

According to the documentation of DosExecPgm(), the argument string should consist of the application name and a NULL, followed by all arguments and a double NULL.


     RESULTCODES  resultCodes;
     ULONG  rc = DosExecPgm(0, 0, EXEC_BACKGROUND,
			    pszArg, pszEnv, &resultCodes,
			    pcszApplication);

Then finally the application is started.

Logging

Throughout RMXSTRTR a function named Print() is used for printing information. It is essentially a thread-safe version of printf(). RMXSTRTR is linked using the MT RTL, but even so I noticed that the printout of different threads occasionally got interleaved.


     static VOID Print(PCSZ pszFormat, ...)
     {
       if (DosRequestMutexSem(hmtx, SEM_INDEFINITE_WAIT))
	 return;
       printf("[%d, %d]: ", pid, CurrentTid());

       va_list         arguments;

       va_start(arguments, pszFormat);
       vprintf(pszFormat, arguments);
       va_end(arguments);
       DosReleaseMutexSem(hmtx);
     }

The mutex semaphore that was created in main() is used for synchronizing different threads. First the current pid and tid is printed out. Then using the va_start, va_list and va_end macros the string with possible additional arguments is printed.

Creating the Event Semaphore

The event semaphore name is created from the name of the used communications DLL. The name of the DLL has to specified in the variable RMXCOMMS. So, first we obtain that name.


     PCSZ  pcszRmxComms = getenv("RMXCOMMS");

Then the name is catenated to a common prefix.


     CHAR      achSemName[CCHMAXPATH];
     strcpy(achSemName, "\\SEM32\\RMX\\STARTER\\");
     strcat(achSemName, pcszRmxComms);

And then the semaphore is created.


     HEV      hev = 0;

     if (DosCreateEventSem(achSemName,
			   &hev,
			   DC_SEM_SHARED,
			   FALSE) != NO_ERROR)  hev = 0;

     return hev;

If this functions fails it is an indication that another instance of RMXSTRTR that uses the same communications DLL is running.

RMXSTART.EXE

RMXSTART is a great deal simpler than RMXSTRTR. It basically makes a request out of the arguments, opens a connection to RMXSTRTR, sends the request, waits for a reply and finally closes the connection. If RMXSTART is started without arguments it prints out:


     usage: rmxstart display cpu application [arguments]

DISPLAY refers to the local computer, that is, the one where the application will appear. CPU refers to the computer where the application will run, that is, in practice a computer where RMXSTRTR is running. Nothing prevents the DISPLAY and CPU from being the same. APPLICATION is, of course, the application to be started, and ARGUMENTS is the arguments that should be provided to the application. If more than one argument is to be provided to the application then they must all be enclosed in quotes.

The main function

When RMXSTART is started we set the arguments to a few variables for easier access.


     int main(int argc, char* argv[])
     {
       PCSZ        pcszDisplay     = argv[1],
		   pcszHost        = argv[2],
		   pcszApplication = argv[3],
		   pcszArguments   = 0;

       if (argc == 5)
	 pcszArguments = argv[4];

The name of the starter is, the same way as in RMXSTRTR, obtained from:


     PSZ  pszName = GetStarterName();

Now that we know the host (the computer where the application is to be run) and the name of the starter we open a connection to the starter.


     HCONNECTION  hConn = OpenConnection(pcszHost, pszName);

Once the connection has been opened, the request can be sent. If it is successfully sent we wait for the reply.


     if (SendRequest(hConn, pcszDisplay, pcszApplication, pcszArguments))
	ReadReply(hConn);

Finally the connection is closed.


     RmxClose(hConn);

Opening the Connection

When the connection is opened we must take precautions for the event that RMXSTRTR is busy.


     HCONNECTION  hConn = 0;
     ULONG        ulAttempts = 1,  rc;

     do{
	 rc = RmxOpen(pcszHost, pcszPort, &hConn);
	 if (rc == ERROR_PIPE_BUSY)
	   DosSleep(ulAttempts * 200);
     } while ((rc == ERROR_PIPE_BUSY) &&
	      (ulAttempts++ < MAX_ATTEMPTS));

Using RmxOpen() we attempt to open the connection. If it fails because RMXSTRTR is busy, then we sleep for a while and try again. Each time the function fails, we sleep a little longer. But not forever; after a specified number of max attempts we just give up.

Sending the Request

In order to start a remote application we must send the name of our local computer, the name of the application and possible arguments. It is simply a question of concatenating the two (possibly three) strings and adding an extra NULL. First we must find out how much memory the entire request needs.


     ULONG lenDisplay     = strlen(pcszDisplay) + 1,
	   lenApplication = strlen(pcszApplication) + 1,
	   lenArguments   = pcszArguments ? strlen(pcszArguments) + 1 : 0,
	   lenRequest     = lenDisplay + lenApplication +
	   lenArguments + 1;

One is added to each sublength as otherwise we wouldn't reserve enough memory for each NULL. Finally an additional byte is added to the length of the entire request. When the length is known, the memory can be allocated.


     PSZ  pszRequest = new CHAR[lenRequest],
	  p          = pszRequest;

Then each string is copied to the request buffer.


     strcpy(p, pcszDisplay);
     p += lenDisplay;
     strcpy(p, pcszApplication);
     p += lenApplication;
     if (pcszArguments){
	strcpy(p, pcszArguments);
	p += lenArguments;
     }

Finally the double NULL is added.


     *p = 0;

Now the request can be sent to RMXSTRTR.


     ULONG  rc = RmxWrite(hConn, pszRequest, lenRequest);

Reading the Reply

Even if it is unlikely that RMXSTRTR would ever return anything but a status code, it is better to make sure that any kind of reply can be handled. We need a few variables for that.


     ULONG      ulBytesRead,  ulSize = SIZE_BUFFER;
     BYTE      *pbReply = 0;
     ULONG      rc;

Then we can enter the loop for reading the reply.


     do{
	 pbReply = new BYTE [ulSize];
	 rc = RmxRead(hConn, pbReply, ulSize, &ulBytesRead);
	 if (rc == RMXERR_ENLARGE_BUFFER)
	 {
	     delete [] pbReply;
	    ulSize = ulBytesRead;
	 }
     }  while (rc == RMXERR_ENLARGE_BUFFER);

We spin in the loop until we have a buffer sufficiently large (shouldn't ever need more than two attempts). The loop is safe as, even if I didn't mention it, a handler for the out-of-memory situation has been set in main().


     if (rc || (ulBytesRead != sizeof(ULONG)))
     {
	 delete [] pbReply;
	 cerr <:< "rmxstart: Failed to read reply from starter." << endl;
	 return;
     }

If RmxRead() returns an error, or if the size of the returned reply is something other than 4 bytes, we give up. The rest of the function is simply a switch on the returned status code along with appropriate messages to the user.

RMXSTOP.EXE

RMXSTOP is the simplest of the three applications. Its task is to gracefully stop a running RMXSTRTR. The RMXSTRTR to stop is either specified by explicitly giving the name of the communications DLL the RMXSTRTR is using, or by allowing RMXSTOP to use the current value of RMXCOMMS. If no arguments has been given RMXSTOP uses the value of RMXCOMMS.


     PCSZ  pcszRmxComms = 0;

     if (argc == 1)
     {
	 pcszRmxComms = getenv(RMXCOMMS);
	 if (pcszRmxComms == 0)
		return EXIT_INIT;
     }

If no arguments have been given and the environment variable RMXCOMMS has not been specified, there is nothing we can do. If arguments have been given we verify that they are of proper format.


     else
     {
	 if (strcmp(argv[1], "-c"))
	   return EXIT_INIT;
	 pcszRmxComms = argv[2];
     }

If the flag is "-c" we use argument following it as the DLL name. From the DLL name we build the semaphore name.


     CHAR   achSemName[CCHMAXPATH];

     strcpy(achSemName, RMXSEMPREFIX);
     strcat(achSemName, pcszRmxComms);

RMXSEMPREFIX is a common prefix (actually "\\SEM32\\RMX\\STARTER\\") that is also used by RMXSTRTR when it creates the event semaphore. Once the complete name is available we can open the semaphore.


     HEV      hev = 0;

     if (DosOpenEventSem(achSemName, &hev) == NO_ERROR)
     {
	 DosPostEventSem(hev);
	 DosCloseEventSem(hev);
     }

If it succeeds we know that the main thread of some RMXSTRTR is blocked on that semaphore. Hence, if we post the semaphore the RMXSTRTR process will terminate. If the opening fails, we know no RMXSTRTR is running using the specified communications DLL.


     else
     {
	 cerr << "rmxstop: No rmxstrtr using " << pcszRmxComms
	      << " seems to be running." << endl;
     }

Building the Applications

Along with the article you should get a zip-file, RMXSTART.ZIP, that contains all source and ready-made applications and DLLs. The makefiles are made for NMAKE.EXE (the make provided with IBM's compiler) and the Borland compiler. Pretty heavy editing is probably needed if you use something else. Before starting the build, you have to set the environment variable RMXROOT to point to the directory where you unzipped the zip-file. E.g.


     [C:\]set RMXROOT=C:\RMX

Then you simply:


     [C:\]cd RMX
     [C:\RMX]nmake

Running the Applications

The first thing you should do is to either include the directory RMX\DLL in your LIBPATH, or move the DLLs to some directory already in your LIBPATH. Remember that RMXPIPE.DLL requires the file RMXPIPE.DAT to be in the same directory where it is and that RMXTCPIP.DLL requires that the file RMX\RMXCOMMS\RMXTCPIP\services is appended to your TCPIP\ETC\services file. To start RMXSTRTR, e.g.:


     [C:\RMX\BIN]set RMXCOMMS=RMXTCPIP
     [C:\RMX\BIN]rmxstrtr

Supposing the name of your computer is odin and the name of the computer where RMXSTRTR is running is loke, you could start pulse.exe on that computer with the following commands.


     [C:\RMX\BIN]set RMXCOMMS=RMXTCPIP
     [C:\RMX\BIN]rmxstart odin loke c:\os2\apps\pulse.exe

If you are using named pipes, the commands would be something like:


     [C:\RMX\BIN]set RMXCOMMS=RMXPIPE
     [C:\RMX\BIN]rmxstart \\odin \\loke c:\os2\apps\pulse.exe

Remember, these programs only provide a means for starting an application on another computer. They do not, in themselves, redirect any output anywhere.

Conclusion

In this article I described the programs that can be used for starting applications on other computers. The programs are slightly modified, compared with the "official" RMX versions, in that they also start applications that have not been patched for RMX. That is, it is possible to start remote applications that you subsequently cannot interact with.

I have uploaded a complete version of RMX to hobbes and the zip resides in the directory network\other. If you install that package, then you actually could start a PM application on a remote computer and use it on your local. But please, keep in mind that the version is early beta. Don't try it out if you expect a finished product <grin>.

If you have any problems with RMX, feel free to send me mail.

Part1 Part2 Part3 Part4