![]() |
RMX-OS2: An In-Depth View (Part 4)Written by Johan Wikman |
IntroductionThis 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 ProblemIn 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 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:
After having entered my password, I get an almost ordinary command-line
prompt.
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:
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.
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 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.
The overall structure of RMXSTRTR.EXE is illustrated in the following
figure.
So, what is going on?
When RMXSTRTR.EXE is started, it initializes itself, which among other
things involves the creation of two semaphores.
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).
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.
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.
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 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.
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.
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.
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.
The connection is created the following way.
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.
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.
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.
So first the entire request is read.
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,
where DISPLAY, APPLICATION and ATTRIBUTES denotes strings (without ending
NULL). That is, two or three catenated ASCIIZ strings followed by an
additional NULL.
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.
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.
Finally when everything is ready, the connection can be disconnected and
closed.
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.
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.
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 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.
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.
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.
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.
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.
Then the name is catenated to a common prefix.
And then the semaphore is created.
If this functions fails it is an indication that another instance of
RMXSTRTR that uses the same communications DLL is running.
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:
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.
The name of the starter is, the same way as in RMXSTRTR, obtained from:
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.
Once the connection has been opened, the request can be sent. If it is
successfully sent we wait for the reply.
Finally the connection is closed.
Opening the Connection
When the connection is opened we must take precautions for the event that
RMXSTRTR is busy.
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.
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.
Then each string is copied to the request buffer.
Finally the double NULL is added.
Now the request can be sent to RMXSTRTR.
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.
Then we can enter the loop for reading the reply.
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 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 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.
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.
If the flag is "-c" we use argument following it as the DLL name. From the
DLL name we build the semaphore name.
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.
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.
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.
Then you simply:
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.:
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.
If you are using named pipes, the commands would be something like:
Remember, these programs only provide a means for starting an application
on another computer. They do not, in themselves, redirect any output
anywhere.
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.
|