Overview and Background
Microsoft's terminal services client (also called 'Remote Desktop Connection') has one main thing against it. Remote applications do not appear as if they are running on the local desktop, instead they appear in a separate window which represents the server's desktop. This is fine if you just want to work exclusively on the server, but can be a pain if you want to switch between applications on the server and the local desktop or want to run applications on different servers. What is needed is a way to display the remoted applications as 'Seamless Windows' on the client.
Commercial products have been written to achieve this in a Windows environment, the most well known would be Citrix. Citrix uses its own protocol (ICA) to publish applications to the client. Others have used Microsoft's protocol called RDP (Remote Desktop Protocol) with additional software to achieve the same effect (the most notable of these is Tarentalla's Canaveral IQ � I suspect they use a similar, but more sophisticated, method to the one presented in this article).
While these products provide a lot more than just seamless windows, they are also quite expensive. It would be nice to have this feature in a regular RDP client without having to buy a whole application publishing product.
This article provides a possible solution to this problem by extending Microsoft's RDP client using virtual channels to communicate between the server and the client. This option has been chosen over writing or extending an existing open source RDP client (such as rdesktop) because we will still be able to take advantage of all the features in Microsoft's client (and presumably all new features they add in the future). Also, an advantage to using Microsoft's client is that we can get some rudimentary application publishing over a web page since their terminal services client has an ActiveX component to do this.
Concept And Approach
Introduction
The RDP protocol does not provide any information about the open windows, all it does is send a bitmap of the server screen down to the client and route mouse and key presses back to the server. To make it appear that applications are running on the local machine, we need to 'clip' away the server desktop on the client.
To do this, it is quite obvious that we are going to need to know what windows are on the server and where they are. For this, we can use global hooks to find out what is happening in the server session (windows opening, closing, minimizing etc.). Next, we need to communicate this information back to the client, this is where virtual channels come in. They are Microsoft's mechanism for two way communication between a server and client when a terminal services session is open. They are mainly used for transferring files and printing but we can use it to send window information back to the client. Once the client has this information, it can then use it to 'clip' the server desktop to just show the applications running on the server.
Hooking into Window Messages On The Server
At this point, I must give credit to Markus Rollmann. I have used his Code Project article as a basis to write this part of the software. This part of the application runs on the server and opens the server side of the virtual channel. To use global hooks, there is a separate DLL (called 'hookdll.dll'). This DLL monitors what is happening to the windows on the server and sends the appropriate string messages down the virtual channel which can then be interpreted by the client code.
An interesting thing I learnt here is that you can't get WH_SHELL
notifications from global hooks if there is no registered shell. Normally, 'explorer.exe' is running as a shell, but in a terminal services session, you usually run just the application without the shell. To get round this (and it took me a long time to figure out why I wasn't getting these notifications), you need to register your application as the shell to replace 'explorer.exe' (see the code on how to do this).
Our application on the server ('clipper.exe') is now also our shell. When we launch our session from the client, we set the starting application as 'clipper.exe'. This ensures all the global hooks are setup before any applications are launched on the server. One of the parameters to 'clipper.exe' is the path of the application to start, which 'clipper.exe' launches as a new process. In addition, it will monitor this process so that when the process has exited, 'clipper.exe' can also close. This will then result in the session logging off as shell application has closed.
Responding to the messages on the client
Virtual channels work by having an executable on the server sending information back to the client. On the client side, there is a DLL that is loaded by the terminal services client when it loads which can listen for the information coming down the virtual channels ('TswindowClipper.dll' � this is loaded by setting a key ("Name" = "TSWindowClipper.dll"
) in the registry at: HKEY_CURRENT_USER\Software\Microsoft\Terminal Server Client\Default\AddIns\TSWindowClipper
). Our client DLL code maintains a hashtable which contains the location of all the windows on the server and their state. The client DLL code is then able to calculate a region that matches the windows on the server and 'clip' the terminal services client window appropriately (the client DLL gets a handle to the terminal services client window when it starts). When windows are moved, resized, opened, closed, minimized or maximized on the server, a message gets sent to the client DLL which is then able to recalculate this region and clip the window accordingly. Another thing the client does is to create dummy taskbar items that represent the windows open on the server.
Publishing Applications On the Web
It is possible to launch a seamless terminal services session from a web page using the remote desktop ActiveX control. To do this, we need to tell it to use our client side Virtual channel DLL. This is how we do it:
MsRdpClient.SecuredSettings.StartProgram = "c:\tswinclipper\clipper.exe notepad.exe"
MsRdpClient.AdvancedSettings.PluginDlls = "tswindowclipper.dll"
MsRdpClient.FullScreen = TRUE
MsRdpClient.Width = screen.width
MsRdpClient.Height = screen.height
We need to set the 'StartProgram' to the correct path for 'clipper.exe' and the application we want to launch on the server. Here is an example HTML file (your will need to get the 'msrdp.cab' file for the ActiveX control from here and put it in the same directory as this HTML page):
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>Remote Desktop Web Connection</title>
<meta content="JavaScript" name="vs_defaultClientScript">
</head>
<body>
<script language="vbscript">
sub BtnConnect
MsRdpClient.server = "<%YOUR SERVER%>"
MsRdpClient.UserName = "<%YOUR USERNAME%>"
MsRdpClient.AdvancedSettings.ClearTextPassword="<%YOUR PASSWORD%>"
MsRdpClient.Domain = "<%YOUR DOMAIN%>"
MsRdpClient.SecuredSettings.StartProgram = "c:\tswinclipper\clipper.exe notepad.exe"
MsRdpClient.AdvancedSettings.PluginDlls = "tswindowclipper.dll"
MsRdpClient.FullScreen = TRUE
MsRdpClient.Width = screen.width
MsRdpClient.Height = screen.height
'These 2 do not work from the activeX control
'MsRdpClient.AdvancedSettings2.DisplayConnectionBar = FALSE
'MsRdpClient.AdvancedSettings2.PinConnectionBar = TRUE
'Device redirection options
MsRdpClient.AdvancedSettings2.RedirectDrives = FALSE
MsRdpClient.AdvancedSettings2.RedirectPrinters = TRUE
MsRdpClient.AdvancedSettings2.RedirectPorts = FALSE
MsRdpClient.AdvancedSettings2.RedirectSmartCards = FALSE
'Connect
MsRdpClient.Connect
end sub
sub MsRdpClient_OnDisconnected(disconnectCode)
'goback to the page that called this one
history.go(-1)
end sub
</script>
<br>
<object language="vbscript" id="MsRdpClient" onreadystatechange="BtnConnect"
codebase="msrdp.cab#version=5,1,2600,1050"
classid="CLSID:9059f30f-4eb1-4bd2-9fdc-36f43a218f4a">
</object>
<br>
</body>
</html>
How To Install And Run
Here are the main things to do:
- Run 'setup.exe' on the server and client. Then run the terminal services client or remote desktop connection on the client.
- Resolution must be set to full screen and the connection bar must not be displayed.
- The start program shell must be the full path to 'clipper.exe' followed by one parameter which is the application on the server you want to launch seamlessly.
- 'Show contents of window while dragging' must be turned on in the connection setting, otherwise moving or sizing windows will not work.
- It is also important to note that the 'Show contents of window while dragging' also has to be enabled on the server as well.
-
It is also a good idea to turn the desktop background off to save bandwidth, as we will be clipping the desktop anyway.
There is a PDF called 'windowclipper.pdf' that is installed with the client setup. Have a look at that if you have any problems.
Limitations
- Certain types of windows are not clipped (DOS windows and child windows such as the open/save file dialogs are the most obvious). This is because I have not found a way to detect these windows using the global hooks.
- Minimize and maximize hasn't been fully implemented.
- Resolutions on the client screen have to be supported on the server (some resolutions exhibit the black bars on the left/right of the screen).
- Connecting to a disconnected session will not restore the clipping since the data about the open windows was stored in the client's memory and is lost when the remote desktop client is closed.
- Clicking on the dummy taskbar items does nothing (should sent a message back to the server, currently communication is only from the server to the client).
Acknowledgments
- Global Hooks in Windows and system tray: Thanks to Markus Rollmann for this Code Project article and code.
- Hash table in C++: Thanks to Jerry Coffin (hash.h, hash.cpp)
- String help class in C++: Thanks to Joe O'Leary (StdString.h)
- Tokenizer class in C++: Thanks to Eduardo Velasquez (Tokenizer.h, Tokenizer.cpp)
Revision History
- 16/01/2005: Added version 1.0 of article.
- 16/01/2005: Added version 0.3 of the source code and 'setup.exe'.
- 14/04/2005: Added version 0.3.1 of the source code and '/setup.exe/'. Updated code ZIP file to contain empty 'build' folder for post build events (was missing). Modified installer to copy dependant DLLs (msvcr70d.dll and wtsapi32.dll).