Now that we know how to determine whether another instance of the application is running on the same machine, let's extend the idea a bit further to make it useful. Let's create a way to pass information back and forth between multiple instances of the application.
To do so, we're going to use something called an IPC connection, or Inter-Process Communication. This is essentially a socket that will only communicate with other sockets residing on the same machine. Because all the information is flowing between processes on the same machine, the data transfer is much faster than with a regular socket, and you don't have to worry about firewalls, NAT devices or other networking hardships.
REALbasic has IPC functionality built-in (since 5.x, I believe), so we'll be discussing how to use the IPCSocket class.
IPCSockets on OS X and Linux are based off of a virtual file that lives on the user's file system. Because of this, you use a path instead of an address and port when working with IPC. So the first thing we need to do is pick a path for our socket. This path is generally located in the /tmp/ directory, so we'll use /tmp/MySpiffyApplication as the path for our IPCSocket. Note that on Windows, the path is used to identify the IPCSocket as well, but there's no physical file that resides on the user's file system.
Everything else about an IPCSocket behaves exactly the same as a TCPSocket. One socket does a Listen, and another one does a Connect. Once the connection is established, then both sockets get a Connected event and are able to transfer data between each other using Write, Read and ReadAll.
So let's define a simple protocol to use so that a second instance of an application can tell the initial instance to open a particular file. Here's some rough steps we need to take:
When the app launches, check to see if it's the only instance. If it is the only instance, then make our IPCSocket listen for connections. If it's not the only instance, then make our IPCSocket connect. Once the connection is established, send the file's absolute path over and wait. If we receive data in our IPCSocket, assume it's absolute path data and open that file. Once we've received data, we'll terminate the connection.
So here's what the App class will look like. In the Open event, we construct the objects that we need, and in the OpenDocument event, we send the file name across. We're also going to handle the NewDocument event so that we can bring the other instance to the front before quitting.
[rbcode]
Sub App.Open()
mMutex = new Mutex( "My Spiffy Application" )
mIPCSocketSubclass = new MySpiffyIPCSocket
if mMutex.TryEnter then
mIPCSocketSubclass.Listen
else
mIPCSocketSubclass.Connect
// Force the connection to occur
while not mIPCSocketSubclass.IsConnected and mIPCSocketSubclass.LastErrorCode = 0
mIPCSocketSubclass.Poll
wend
end if
End Sub
Sub App.OpenDocument( f as FolderItem )
if mIPCSocketSubclass.IsConnected and not mMutex.TryEnter then
mIPCSocketSubclass.Write( f.AbsolutePath )
end if
End Sub
Sub App.NewDocument()
if mIPCSocketSubclass.IsConnected and not mMutex.TryEnter then
mIPCSocketSubclass.Write( "?front?" )
end if
End Sub
[/rbcode]
Now we'll take a look at how the MySpiffyIPCSocket class works. First, we'll give it a constructor method, which is responsible for setting the path for the socket. Then we'll implement the DataAvailable event so that we can utilize our protocol.
[rbcode]
Class MySpiffyIPCSocket Inherits IPCSocket
Private Dim mQuitter as Boolean
Sub Constructor()
me.Path = "/tmp/MySpiffyApplication"
End Sub
Sub Error() Handles Event
// We want to go ahead and re-listen here.
if not mQuitter then
me.Listen
end if
End Sub
Sub DataAvailable() Handles Event
dim data as String = me.ReadAll
if data = "?quit?" then
mQuitter = true
Quit
elseif data = "?front?" then
FindTheMainWindow().Show
me.Write( "?quit?" )
else
App.OpenDocument( GetFolderItem( data ) )
me.Write( "?quit?" )
end if
End Sub
End Class
[/rbcode]
As you can see, our protocol is quite stupidly simple. If we get data that says "?quit?" then we'll terminate. Otherwise, the data is treated as a file path. If we get a file path, then we construct a FolderItem from it and pass it to the Application.OpenDocument method (which then passes it along to the OpenDocument event), then we tell the remote application to quit. Furthermore, if we get a "?front?" message, then we'll bring the main application window to the front and tell the other application to quit as well. You'll notice that we re-listen once the connection is terminated. If we don't do this, then the next time the user tries to launch a second copy of the app, there's no IPCSocket listening for the connection. So we flag which end is the quitting end, and if it's a quitter, we just ignore the Error event.
This code is by no means ideal, I might add. There's no error checking, the protocol isn't robust, and I don't think it encapsulates (in the OO sense) the problem we're trying to solve very well. However, I feel that it does show one possible solution to the problem -- elegance, in this case, is left up to the reader. I've uploaded the example described here. You can see how things happen by checking the system's debug string viewer (such as DebugView on Windows, or the Console on the Mac) since I added logging code to make it spit out what happens at each step.
Enjoy!
Great example! And that gives me some insight into solving another problem ;)
You must have been reading my mind (or code) lately (Poor you! :P)
Plus it uses absolute paths--evil! evil! evil! But thanks for the explanation--it helps quite a bit. I'm off to add support for open-file message passing to my app...
Technically, you could pass GetSaveInfo across, but I figured AbsolutePaths are an easier concept to grasp. FWIW, they're not evil, except on platforms that are silly enough to not have unique paths. ;-) Since I work mostly on Windows and Linux, absolute paths are perfectly fine.
Now I see what you meant on the NUG list when saying to use an IPCSocket for this situation :)
But, when I run this example, I *often* get the following error on a Win2K machine:
"An exception of class StackOverflowException was not handled. The application must shut down."
This error happens when I have already an instance running, run a 2nd instance and when selecting the window (of the 1st instance) the error message is shown and the application is shut down.
Any ideas why I'm getting this error?
Anyway, I could see that it passes the file path - I placed an EditField on Window1 to show the passed path.
Since UNIX/Linux/OSX can only have one active / (root) directory I don't think that it is possible to have an absolute path conflict in this case.
I do have a question about IPCSocket and multiple users...
If MySpiffyApp is running in one account and also in another account using the OS X 10.3+ or Windows XP multiple user switching feature is it even possible and/or advisable for the apps to communicate with each other?
@Carlos -- I suppose I should have noted that IPCSocket is rather unstable in the current shipping version on Windows because it's a total hack. However, for r3, I finally found a way to make IPCSocket un-hackish, and so it's gotten extremely stable (just as stable as OS X and Linux now). So I'm not surprised you're getting strange errors.
@Phil -- That's a very good question. In order for RB's mutex object to work, it actually creates a physical file in the tmp directory. This should be the user's tmp directory, so therefore all mutexes are limited to the user's account (note, I haven't actually tested this -- it's simply an inference from the code).
But that's a very good point, a mutex probably should have a way to set whether it's global or not. Sounds like a reasonable feature request.
Both OS X and Windows XP have a "Shared" or "All Users" sections of the drive. Maybe the Mutex could work there.
Correct, it's certainly possible to achieve, it's just not currently coded to do it that way.
Re: absolute paths, I'd add that they are fine in this case since you're trying to imitate behavior (opening documents in a common application instance) that is the default on OS X. If this code isn't needed on Mac, there's no need to avoid absolute paths.
This code looks like it expects only one OpenDocument event, and I'm assuming (coming from the Mac side of things) that multiple OpenDocument events are possible. Which raises the general question of how you know when you've received your last OpenDocument event.
What I've been doing is collecting OpenDocument folderitems in an array and using a very short Timer to signal the end of startup events; at that point all the files get sent to the other instance in one squirt. The trick with this approach is making sure to set App.AutoQuit to false, or the app will quit before the timer ever fires because no windows are open.
Correct, it's assuming there's only one document in OpenDocument, which is an invalid assumption. Personally, I'd change the protocol around so that the client side can send multiple messages and determine when it wants to quit instead of relying on the server to tell it when to quit. Like I said, the current demonstration isn't production-quality code, it's just a jumping-off point.
Hi Aaron,
Nice trick to get singleton apps working on windows. But I've run into a problem with it I hope you can suggest a work-around for.
If your demo app quits unexpectedly, the IPCsocket is not cleaned up, and /tmp/MySpiffyApplication continues to exist. The next time your app runs, it just hangs on the "mIPCSocketSubclass.Listen" statement in the Open event while the IPCSocket subclass produceses errors until there is a stack overflow...
Hello Aaron,
As I understand the Mutex only works in XP inside the same session. I have heart that there is a Global method that you can set so the Mutex is available in any user session. Do you know about this more?
Thomas
REALbasic does not have global mutexes, so if you need something like that, you'll have to delve into declares. The WFS has a Mutex class which already takes care of this for you.