Kiosk Applications (part two)

| | Comments (19)

Last time, we familiarized ourselves with the concept of what a kiosk application is, as well as the idea of desktop objects on Windows. This time we're going to take a look at the process behind making our own desktop and running our application on it.

Before we dive into code, I want to discuss the basic ideas behind what we're going to do and what to expect. When you create a new desktop and switch to it, what you're given isn't very much. You have the desktop background, and no processes active on that desktop. Not even Windows Explorer. So what you see visually is the user's desktop wallpaper, and that's it. No start bar. No icons. Nothing. What's more, the system doesn't trap Ctrl+Alt+Del for non-default desktops, so hitting Ctrl+Alt+Del does nothing. So you've got the most bare-bones user interface possible.

The little red flag that should be going up in your head right about now is that this means you have no way to shut anything down. You can't end-task the application. You can't go to Start->Shut Down. You can't do anything aside from power the machine down. So that brings up the chicken and egg question: how do you first launch an application inside of a desktop which doesn't currently exist, and when the application terminates, what do you do?

The most common answers to those questions are: you bootstrap your application so that it first creates a new desktop, then it launches within that desktop. When the application terminates, you can do anything you'd like, but eventually you need to go back to the original desktop to be able to do anything (like shut the computer down).

Let's talk about bootstrapping, since it seems to be the start of our journey. The concept sounds scary, but it's really quite easy. Before you can launch your application in kiosk mode, you must first have a desktop object which you can launch on. Processes belong to a desktop, not windows. So that means once your application has launched, it's already on the desktop. You can't move a process from one desktop to another (for many technical reasons which I won't speculate on), so that means you need to bootstrap. The thought behind bootstrapping is that you launch one application which is responsible for launching another application. In our case, the first application is responsible for creating the new desktop, and then launching our application on it. But because of bookkeeping purposes, and security, the first app (the bootstrapper) is also responsible for some bookkeeping -- it needs to keep track of what the current desktop is before making the new one, and once the kiosk app is done running, it usually is responsible for switching back to the default desktop and closing the new one.

So what actions does the bootstrapper need to take? Well, first, it needs to get some information about what desktop is currently active. It does this by calling the GetThreadDesktop API for the current thread and stores that value. Then it needs to "open" the input desktop with the ability to switch desktops using the OpenInputDesktop API. Note that if the user doesn't have the proper access rights to do this, then they will not be able to switch from one desktop to another, and this API will return nil instead of the old input desktop. So now that you're storing the current thread's desktop and the current input desktop, it's time to make a new one using a call to CreateDesktop. If the user has the proper access rights, this call will return a non-nil value for the newly made desktop. At this point, there's a new desktop object on the system, but it's not currently active -- calling SetThreadDesktop and SwitchDesktop takes care of that. The first call assigns the new desktop to the current thread so that it can interact with the new desktop. The call to SwitchDesktop is what physically changes your view from the default desktop to the new one. When this happens, you're presented with the spartan view described earlier.

The next step the bootstrapper needs to take is to launch the kiosk application and wait for it to terminate. Once the kiosk application terminates, the bootstrapper is then able to restore the default desktop switching back to the old input desktop (the value returned to us from OpenInputDesktop), and calling SetThreadDesktop again with the value returned to us from our earlier call to GetThreadDesktop. Then it can destroy the kiosk's desktop with a call to CloseDesktop.

Now that we know the general process we need to follow, let's take a look at some of the declares needed to make it happen.[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 )[/rbcode]
As you can see, we're using soft declares for the desktop functions. We need to do this because older versions of Windows (such as 98 and ME) do not support multiple desktops.

Following our pseduocode from above, to store our desktop information, we'd do:[rbcode] Const DESKTOP_SWITCHDESKTOP = &h100

// 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
mOldDesk = GetThreadDesktop( GetCurrentThreadId )

// We also need to get the input desktop
mOldInputDesk = OpenInputDesktop( 0, false, DESKTOP_SWITCHDESKTOP )[/rbcode]
That's takes care of the initial bookkeeping. The call to GetThreadDesktop should be self-explanatory (we're simply getting a handle to the current thread's desktop object). The call to OpenInputDesktop will return to us our current input desktop object. We're specifying no flags, that we don't want to inherit any handles, and that we want the ability to switch desktops.

Now we're ready to create the new desktop.[rbcode]Const GENERIC_ALL = &h10000000
// Now we're ready to make our new desktop. We're just going to use
// the application name as the desktop name.
mDesk = CreateDesktopW( "Spiffy Desktop", 0, 0, 0, GENERIC_ALL, 0 )[/rbcode]
This call creates a desktop named "Spiffy Desktop" (note that desktop names are case insensitive and cannot contain a backslash [\] character). The second and third parameters are reserved and must be zero. The fourth parameter specifies the flags, and we don't have any to set. The fifth parameter determines our desktop's access rights, and we want to allow them all. The final parameter is a security attribute, which we can safely ignore (it'll inherit the security from the current application). As you can see, making a new desktop object really isn't that hard!

The next step we have to accomplish is switching to our new desktop, which we do like this:[rbcode] // Now that we've made a new desktop, let's set it up and
// switch over to it
SetThreadDesktop( mDesk )
SwitchDesktop( mDesk )[/rbcode]
If you run this code right now, as-is, you'll see what I mean by security. However, you should save any open work you have, because you will be shutting down your computer. When your application runs, you'll see the screen go to a blank-looking desktop with no icons. No amount of monkeying around will get you back to your original desktop. Go ahead, give it a spin -- I'll just wait here for you to reboot. :-)

Ok, now that you're back, let's continue on. At this point, your bootstrapper will be launching the kiosk application onto the new desktop, and it should wait for the application to terminate. I am going to gloss-over the details of how this is accomplished a bit. You'll use more declares, which are found in the Windows Functionality Suite. Specifically, you'll use CreateProcess to launch the application and WaitForSingleObject to wait for the application to terminate. Just assume you have access to a function called FolderItem.LaunchAndWait (I'll provide the code below).[rbcode]// Launch the kiosk application into the new desktop
theKioskAppItem.LaunchAndWait( "", "Spiffy Desktop" )[/rbcode]
This causes the kiosk application to launch, and the bootstrapper to pause execution. After the kiosk application has finished executing, the bootstrapper will resume executing and should clean things up.[rbcode] // Now clean everything up
SwitchDesktop( mOldInputDesk )
SetThreadDesktop( mOldDesk )
CloseDesktop( mDesk )[/rbcode]
That's all there is to it! When you put it all together, you suddenly have a bootstrapping application which can launch a kiosk app into a new desktop. However, we're not done with this topic yet. Tune in next time for a discussion of how to improve upon our knowledge of how kiosk applications work.

Oh, and as promised, here's the LaunchAndWait function.[rbcode]Sub LaunchAndWait(extends f as FolderItem, params as String = "", deskName as String = "")
// We want to launch the application and wait for
// it to finish executing before we return.
#if TargetWin32
Soft Declare Function CreateProcessW Lib "Kernel32" ( appName as WString, params as WString, _
procAttribs as Integer, threadAttribs as Integer, inheritHandles as Boolean, flags as Integer, _
env as Integer, curDir as Integer, startupInfo as Ptr, procInfo as Ptr ) as Boolean

Soft Declare Function CreateProcessA Lib "Kernel32" ( appName as CString, params as CString, _
procAttribs as Integer, threadAttribs as Integer, inheritHandles as Boolean, flags as Integer, _
env as Integer, curDir as Integer, startupInfo as Ptr, procInfo as Ptr ) as Boolean

dim startupInfo, procInfo as MemoryBlock

startupInfo = new MemoryBlock( 17 * 4 )
procInfo = new MemoryBlock( 16 )

dim unicodeSavvy as Boolean = System.IsFunctionAvailable( "CreateProcessW", "Kernel32" )

startupInfo.Long( 0 ) = startupInfo.Size

dim deskStr, deskPtr as MemoryBlock
if deskName <> "" then
if unicodeSavvy then
deskStr = ConvertEncoding( deskName + Chr( 0 ), Encodings.UTF16 )
else
deskStr = deskName + Chr( 0 )
end if

startupInfo.Ptr( 8 ) = deskStr
end if

dim ret as Boolean
if unicodeSavvy then
ret = CreateProcessW( f.AbsolutePath, params, 0, 0, false, 0, 0, 0, startupInfo, procInfo )
else
ret = CreateProcessA( f.AbsolutePath, params, 0, 0, false, 0, 0, 0, startupInfo, procInfo )
end if

if not ret then return

Declare Function WaitForSingleObject Lib "Kernel32" ( handle as Integer, howLong as Integer ) as Integer
Const INFINITE = -1
Const WAIT_TIMEOUT = &h00000102
Const WAIT_OBJECT_0 = &h0

// We want to loop here so that we can yield time back
// to other threads. This means threaded applications
// will "just work", but non-threaded ones will still appear hung.
while WaitForSingleObject( procInfo.Long( 0 ), 1 ) = WAIT_TIMEOUT
App.SleepCurrentThread( 10 )
wend

Declare Sub CloseHandle Lib "Kernel32" ( handle as Integer )
CloseHandle( procInfo.Long( 0 ) )
CloseHandle( procInfo.Long( 4 ) )
#endif
End Sub[/rbcode]

19 Comments

......Uh, I smell article here.

Well it works (tested on XPsp2) except it doesn't disable Ctrl+Alt+Del.
I can get the initial CAD dialog, but not the Task Mangler dialog (which opens on the main desktop)
I can, however, get the ShutDown dialog - Not ideal in a kiosk app.

@Steve -- that's news to me, I don't get any CAD dialog. Are you running as admin, or a limited user account?

After doing some checking, I'm really confused Steve -- every resource I found said that CreateDesktop and switching into the new desktop will disable the CAD dialog. There are some other hacks you can do to disable it as well, but I wouldn't trust them.

Are you sure that it's switching you into a new desktop? You should see nothing on the screen aside from your kiosk application (if that, depending on what step you're trying from) -- no task bar, nothing.

I'm running as Admin. I've reset to the default XP theme so you recognise it and run a restored app that normally cohabits with the task-bar quite happily.
I definitely get a new desktop that looks like this PrtScrn capture: http://rb.sgarman.net/desktop.jpg
When I CAD, I get the main CAD dialog but the app window disappears (it's not hiding behind the dialog). PrtScrn key doesn't capture that dialog but I think it captures the desktop with the app again.
When I dismiss the CAD, the app reappears.
If I press the "Task Manager" button on the CAD screen, nothing seems to happen but when I dismiss that and close the app, the main desktop reappears with the Task manager showing.

To save you trying to second-guess all the stupid things I might have done, there's a copy of my source at http://rb.sgarman.net/Kiosk.rbp
It's entirely up to you if you think it's worth downloading.

This is truly bizarre. When I run your application, I get the expected behavior with no CAD dialog. In fact, what happens when I hit CAD is that the dialog appears in the default desktop (as expected), but not in the new desktop -- that stays put.

I'm running XP SP 2, and running as admin as well.

Here's a data point though; when I hit CAD, I don't switch into the winlogon desktop because I'm set to auto login. I wonder what happens if I enable winlogon. Off to go test -- thanks for bringing this up Steve!

Aha! That's the ticket!

When you go to the User Accounts control panel, if you change the way users logon and tell it to use the welcome screen, you get my behavior. When you tell it to not use the welcome screen, you get your behavior. So the issue seems to be with the way users logon. Interesting!

Well, I think I've got something else to ponder for a while. I know there is a way for the admin to disable certain items on the task window (or the task window entirely), so I may have to explore that for another part of the talk (remember, the series isn't over yet).

I'd suggest that if someone wanted a kiosk app, it was reasonable to insist they configure welcome-screen login.
But I think if an XP machine is a member of a domain (rather than a workgroup) it's difficult to configure it without CAD login.

I'm starting to think this is why all the kiosks I've seen have specialized setups, such as touch screens, trackballs, or specialized keyboards.

@Aaron: I'm hoping you'll still figure out some way to have things behave as expected.

Does turn on/off "Fast User Switching" make any difference? (I don't have RB here to give it a shot)

IIRC, “Fast User Switching” isn't an option unless you use a welcome screen.

@Steve and Scott -- patience, my friends. Part Three goes into some more about how to really make our kiosk application launcher shine, and part four goes into locking things down tight. There are a lot more issues to worry about than just the CAD dialog. It just so happens that the CAD dialog was a surprise to me since most fingers I could find pointed towards it being disabled by default. I appreciate the head's up on it, since it gave me another bullet point to talk about during part four.

@Aaron: I'm just glad you can follow their documentation :) I spent the last hour or so trying to read it and have the headache to prove it :P Would be nice if Microsoft did more "How To" and DIY-type articles.

As a side thought, I seem to remember that at some point in the past it was possible to replace Explorer with a different application as the "shell". If that's still possible, would terminating the application log out the current user? And, can you set this shell on a per-user basis? :) Just wondering

@Scott -- yup, you can replace the shell, but I'm not certain that I'd advise it since the explorer shell does a lot for you. But I've not looked very deeply into what replacing the shell actually involves and what you have to do. I may look more into that (perhaps a different series of blog postings are a-brewing here).

Understood... and... Swwweeeeeett :)

Nice article series...

But if you're using Windows 2000 or XP, shouldn't you use a Group Policy to deal with the security aspects? Group Policies allows you to configure things like: disable the Task Manager, disable some Windows applications from running, run an application at logon, disable log off and to configure many, many other things!

Group policy can be setup on standalone machines or on machines that belongs to an Active Directory.

@Carlos -- :: coughs :: wait till part four :: coughs ::

Man, everyone keeps spoiling my clinchers. ;-)

[...] Steve brought up the first good point (which I happened to miss) in the second installment of the series: Ctrl+Alt+Del behavior is erratic. It turns out that if you have your logon settings set a specific way, you can get to the Ctrl+Alt+Del desktop even from your kiosk desktop. Since we don’t want to rely on something as spurious as logon settings when it comes to security, let’s talk about how to solve this issue. [...]

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.