Kiosk Applications (part three)

| | Comments (2)

Last time, we walked through the process of creating a new desktop and launching our kiosk application into it. This process involves having a bootstrap application which is responsible for making the new desktop and launching our true kiosk application into it. It's also responsible for cleaning up the desktop once the kiosk application closes.

But that seems like an awfully messy way to go about things. It means that every time you want to write a kiosk application, you have to write two applications. The bootstrapper has to change every time so that it can pick your new kiosk application (aside from that, it stays the same). We're software engineers! We love code reuse and encapsulation and abstraction! The current way is simply no good!

Thankfully, we're smart software engineers and are given all the tools we need to make a general, reusable, single-project for kiosk applications.

Let's discuss our design goals before we dive right in to the the project. What we want to end up with is a single project that combines both our bootstrapper and our kiosk application. The project should be reusable with no modifications, and it should produce only a single executable file. We should end up with a class that we can drop into any project to convert it into a kiosk application.

Phew! Sounds almost impossible given our previous discussion, doesn't it? Well, I'm here to assure you that it's not only possible, but quite simple! We're going to make an Application subclass that does all this for us. By the time we're done, you'll only have to put the KioskApplication class into a new project, and reset the original Application object's super to KioskApplication.

So we'll start by making a new class called KioskApplication and setting its super to Application. In the open event, we'll put the code we discussed last time:[rbcode] Soft Declare Function CreateDesktopW Lib "User32" ( name as WString, device as Integer, devMode as Integer, _
flags as Integer, access as Integer, sec as Integer ) as Integer
Declare Function GetCurrentThreadId Lib "Kernel32" () as Integer
Soft Declare Function GetThreadDesktop Lib "User32" ( id as Integer ) as Integer
Soft Declare Function OpenInputDesktop Lib "User32" ( flags as Integer, inherit as Boolean, access as Integer ) as Integer
Soft Declare Sub SetThreadDesktop Lib "User32" ( desk as Integer )
Soft Declare Sub SwitchDesktop Lib "User32" ( desk as Integer )
Soft Declare Sub CloseDesktop Lib "User32" ( desk as Integer )

Const GENERIC_ALL = &h10000000
Const DESKTOP_SWITCHDESKTOP = &h100

// These local variables will hold all of our desktop references
Dim hDesk, hOldDesk, hOldInputDesk as Integer

// The first thing we need to do is keep track of the current
// desktop so that we can switch back to it when we're done
hOldDesk = GetThreadDesktop( GetCurrentThreadId )

// We also need to get the input desktop
hOldInputDesk = OpenInputDesktop( 0, false, DESKTOP_SWITCHDESKTOP )
if hOldInputDesk = 0 then
return
end if

// Now we're ready to make our new desktop. We're just going to use
// the application name as the desktop name.
hDesk = CreateDesktopW( "Spiffy Desktop", 0, 0, 0, GENERIC_ALL, 0 )
if hDesk = 0 then
return
end if

// Now that we've made a new desktop, let's set it up and
// switch over to it
SetThreadDesktop( hDesk )
SwitchDesktop( hDesk )

// Since we're the first instance, we want to launch another instance of
// this application, and wait for it to complete
theKioskApp.LaunchAndWait( "", "Spiffy Desktop" )

// Now clean everything up
if hDesk <> 0 then
SwitchDesktop( hOldInputDesk )
SetThreadDesktop( hOldDesk )
CloseDesktop( hDesk )
end if[/rbcode]
We'll use this as our jumping-off point. The first problem we want to solve is the issue of the desktop name. We can't have every kiosk application we make use "Spiffy Desktop" because it's conceivable (though unlikely) that you may want two different kiosk applications running at the same time. So everywhere you see "Spiffy Desktop", change it to App.ExecutableFile.Name. In this way, each kiosk application will have its own desktop named after the application itself.

The astute reader, or experienced REALbasic user, may be forming an idea right about now -- App.ExecutableFile is a FolderItem that points to the current application. And we want the current application to be launched as a kiosk application. Therefore, it stands to reason that we may want to make use of this feature.

In fact, this is exactly what we're going to do to ensure that we only need one project and one built executable. Where we have the reference to theKioskApp.LaunchAndWait, we're going to instead use App.ExecutableFile.LaunchAndWait. By doing this, we will launch a second instance of our application which is then placed within the new desktop. This is a good start, but has a flaw in it if you follow the logic in your mind.

First, we launch the program. It makes a new desktop and switches to it. It then launches the same program and waits for it to terminate. The same program (launched in the second desktop) then makes a new desktop and switches to it. It the launches the same program (a third instance of it) into the newly made desktop (the third one so far), and waits for it to terminate. So on and so forth, forever and ever, amen. We've created an infinite loop without ever even using a for or while loop! Eventually the system will run out of resources and the entire mess will come to a crashing halt, probably taking the entire machine with it.

Now, you may be wondering why I've lead you down this recursively futile path. But I wouldn't lead you here unless I had a solution. Have you ever wanted to see if there's another instance of your application already running on the system? This would certainly qualify as one of those times. If there's already an instance of your application running, then you don't want to create a new desktop and so on. Instead, you just want to run as normal (since the first instance already put up the new desktop). By using a Mutex, we can determine whether an instance of the application is the first, or second instance. If it's the first instance, we want to do our magic desktop creation dance. And if it's not the first instance, we want to just run as normal.

Make a new private property called mAppTester as Mutex. Then, in the open event, try to enter the mutex. If the call to TryEnter returns true, then it's the first run of the application. Otherwise, it's a subsequent run. So your Open event should now look something like this:[rbcode] mAppTester = new Mutex( App.ExecutableFile.Name )
if mAppTester.TryEnter then
// Do all the new desktop stuff

// Quit the app because the kiosk app we were watching is done
Quit
else
// Do the kiosk app stuff
end if[/rbcode]
As you can see, we're using the application name again for the name of the mutex. This code easily allows us to differentiate between the bootstrap version of the application, and the kiosk version of the application. The only thing left to make our reusable KioskApplication class complete is provide the user with the Open event again. So make a new Event Definition called Open, and in the else clause above, put a call to Open in it. Ta da, we're done!

Your final KioskApplication.Open event should look like this (note that I put in some extra error checking, you should download the project if you'd like to see it in action):[rbcode]Sub Open()
mAppTester = new Mutex( App.ExecutableFile.Name )
if mAppTester.TryEnter then
Soft Declare Function CreateDesktopW Lib "User32" ( name as WString, device as Integer, devMode as Integer, _
flags as Integer, access as Integer, sec as Integer ) as Integer
Declare Function GetCurrentThreadId Lib "Kernel32" () as Integer
Soft Declare Function GetThreadDesktop Lib "User32" ( id as Integer ) as Integer
Soft Declare Function OpenInputDesktop Lib "User32" ( flags as Integer, inherit as Boolean, access as Integer ) as Integer
Soft Declare Sub SetThreadDesktop Lib "User32" ( desk as Integer )
Soft Declare Sub SwitchDesktop Lib "User32" ( desk as Integer )
Soft Declare Sub CloseDesktop Lib "User32" ( desk as Integer )

Const GENERIC_ALL = &H10000000
Const DESKTOP_SWITCHDESKTOP = &H100

// These local variables will hold all of our desktop references
Dim hDesk, hOldDesk, hOldInputDesk as Integer

// The first thing we need to do is keep track of the current
// desktop so that we can switch back to it when we're done
hOldDesk = GetThreadDesktop( GetCurrentThreadId )

// We also need to get the input desktop
hOldInputDesk = OpenInputDesktop( 0, false, DESKTOP_SWITCHDESKTOP )
if hOldInputDesk = 0 then
KioskModeException.Create( "Could not open the input desktop for switching" )
return
end if

// Now we're ready to make our new desktop. We're just going to use
// the application name as the desktop name.
hDesk = CreateDesktopW( App.ExecutableFile.Name, 0, 0, 0, GENERIC_ALL, 0 )
if hDesk = 0 then
KioskModeException.Create( "Could not create a new desktop" )
return
end if

// Now that we've made a new desktop, let's set it up and
// switch over to it
SetThreadDesktop( hDesk )
SwitchDesktop( hDesk )

// Since we're the first instance, we want to launch another instance of
// this application, and wait for it to complete
App.ExecutableFile.LaunchAndWait( "", App.ExecutableFile.Name )

// Now clean everything up
if hDesk <> 0 then
SwitchDesktop( hOldInputDesk )
SetThreadDesktop( hOldDesk )
CloseDesktop( hDesk )
end if

// We're done too
Quit

return
else
// Call the user's Open event since this is a "normal" instance
// of the kiosk application
Open
end if

Exception err as FunctionNotFoundException
// Cannot run in true kiosk mode, so do something about it
KioskModeException.Create( "Kiosk mode not supported on this operating system" )
End Sub
[/rbcode]
Now that we have a totally reusable KioskApplication class, let's discuss some options on where you can go with this, and what you can do.

If you remember, it's the window station which holds onto the atom table and the clipboard. Since we're always creating desktops on the same window station (since we want interactive processes), we can share things like the clipboard. Don't believe me? In our kiosk application, put the following code in a PushButton.Action event:[rbcode]
dim c as new Clipboard
if c.TextAvailable then MsgBox c.Text[/rbcode]
Before you run the kiosk application, be sure to select some text in the default desktop. Now run the kiosk application and notice that you can get access to the clipboard text from the default desktop. It's helpful to remember this sort of thing because it means that you can still share data and communicate with the default desktop, if you so desire. I would recommend using an IPCSocket for any serious communications (so that you don't clutter the clipboard with stuff).

Another thing to keep in mind is that you're not required to have your bootstrap application switch back into the default desktop when the kiosk application quits. For instance, you could have the bootstrapper simply launch another instance of the kiosk application! In this way, if the kiosk application quits, or terminates for any reason (such as a crash), the bootstrapper will launch a new instance of the kiosk application (like a watchdog application). Since kiosk applications are a security feature, it's entirely possible that you don't want the machine to ever switch back to the default desktop. To do this, just put the call to FolderItem.LaunchAndWait in a loop -- then any time the call returns, it just loops back and does it all over again.

Join us next time to discuss more options for getting the most out of your kiosk application.

2 Comments

Trying not to spoil your clinchers, but will you you be addressing the question of debugging kiosk apps in a later article?
This is a slightly bigger issue here than with a separate bootstrap app.

I hadn't planned on covering how to debug kiosk apps -- the process would be the same a debugging a service application. Build it in non-kiosk form and debug (you could easily put the desktop-creation code into a #if not DebugBuild block) as normal. The differences between a new desktop and the default desktop should be fairly minimal in terms of programming differences.

Leave a comment

Disclaimer

I'm currently an employee of REAL Software. My blog is mine. The opinions represented in this blog are mine as well and may not represent my employer's opinions. All original material is copyrighted and property of the author.

REALbasic® is a registered trademark of REAL Software, Inc. REAL SQL Server™ and Lingua™ are pending trademarks of REAL Software, Inc. All rights reserved.