1. Introduction
The logout option window on Unix-like Operating Systems is quite useful. It allows the user to choose whether to automatically launch the currently opened applications at next login. Unfortunately, Windows platforms do not have that option. It is desirable to have a tool to perform the same function for Windows users. The basic idea is that the tool should allow the user to save a Windows login session before logoff, shutdown, or restart. In this context, a session is vaguely defined as the state of running applications for the current user. An application can be either window-based or console-based (DOS type). All the saved window-based and console-based applications shall be opened automatically at next login. In summary, the tool we like should meet the following requirements:
- It handles all logout-related tasks, shutdown, restart, and logoff.
- The user can choose to keep the previous session, save the current session, or clear any session for the next login.
I have searched Google and haven't found any free tools of my interest. I decided to write one myself, and here it is. The tool was named "XLogoff", and was developed with VC++ 2003 on Microsoft .NET Framework 1.1. In order to use the source code, you should have the Windows Management Instrumentation (WMI) SDK installed.
The image above shows one login state that has been restored by the saved session data from the previous logoff.
2. Top-Level Design
The "XLogoff" tool is a service-type application, with a Windows service running on the background and a GUI for the user to interact with the service. The entire application consists of three components:
- XLogoff: A Windows service which starts the GUI and performs all the logout tasks, as directed by the user. The service starts a TCP/IP server on a separate thread, which listens to and consumes the GUI connections.
- XLGUI: The GUI window which allows the user to interact with the "XLogoff" service. The GUI sends user commands to the service via a TCP/IP client.
- CPAU: A utility class referenced by the "XLogoff" service. This third-party source code was written in C# and posted by "www.sayala.com". The original code works well, and thus is retained as is. No attempt was made to convert it to Managed C++, thanks to the multi-language compilation feature of VS2003.
3. User Instructions
One installation step is required to set up the tool. After you build the tool, copy all the files in the "bin/Release" folder except "User Manual.doc" to your "System32" folder. Or, you may use the Windows Installer project I provided to install/uninstall the required files to/from your computer.
3.1 To install and start "XLogoff" services:
- Open a command prompt window and go to your "system32" folder.
- Run the "XLogoff.exe -Install" command. Open the Services Control window and you should see "XLogoff Service" is installed.
- Right-click on "XLogoff Service" and select "Properties"; "XLogoff Service Properties" window pops up.
- On the Properties window:
- Select "Automatic" to the "Startup Type" dropdown combo.
- Change to "Log On" tab and check both "Local System account" and "Allow service to interact with desktop".
- Restart your computer.
XLogoff Icon
3.2 To use "XLogoff" services:
- Double click the "XLogoff" icon on the systray; the "XLGUI" window will pop up.
- On the "XLGUI" window, there are three logout options:
- Keep previous session: leave the session as is.
- Save current session: save the current session as a new one.
- Don't save/keep any session: clear out all session data.
The four buttons are self-explanatory, except that the logout option you selected is performed on the session data immediately, even if you logout later.
You may also right-click on the systray icon to access three functions quickly: Logoff, Shutdown, and Restart. When you do so, you assume to keep the previous session data.
3.3 To stop "XLogoff" services:
- Open a command prompt window and go to the "system32" folder.
- Run the "XLogoff.exe -Install /u" command.
4. Use of the Source Code
First, let's look at the service class "XLogoffWinService
". When the service starts, it does two things:
- Starts the server on a separate thread. We'll discuss this later.
- Starts a timer. The timed-event callback function is as follows:
private: static void OnTimedEvent(System::Object* source,
System::Timers::ElapsedEventArgs* e)
{
XLogoffThread::mut->WaitOne();
ConnectionOptions *co = new ConnectionOptions();
ManagementScope *ms = new ManagementScope("\\\\localhost", co);
ObjectQuery *oq = new ObjectQuery("select * from Win32_Process");
ManagementObjectSearcher *mos = new ManagementObjectSearcher(ms,oq);
ManagementObjectCollection *moc = mos->Get();
ManagementObjectCollection::ManagementObjectEnumerator* moe =
moc->GetEnumerator();
moe->Reset();
while(moe->MoveNext())
{
ManagementObject *mo =
dynamic_cast<MANAGEMENTOBJECT*>(moe->get_Current());
String *str[] = {"", ""};
str[0] = dynamic_cast<STRING*>(mo->get_Item("Name"));
if(str[0]->Equals("explorer.exe"))
{
mo->InvokeMethod("GetOwner",(Object*[])str);
if(!str[0]->Equals("SYSTEM") && !str[0]->Equals("System"))
{
CheckGUI(true);
}
break;
}
}
XLogoffThread::mut->ReleaseMutex();
}
private:static void CheckGUI(bool add)
{
Process* proc[] = Process::GetProcessesByName(S"XLGUI");
const int length = proc->Length;
if( add )
{
if( length <= 0 )
{
Process::Start(S"XLGUI.exe");
}
} else
{
for ( int i = 0; i < length; i++ )
{
proc[i]->Kill();
}
}
}
What this function does is ensure that the GUI is up and running. The GUI should be active after the service starts and before it stops. To achieve this goal:
- It checks whether a user has logged in. It invokes the
GetOwner()
method via Windows Management Instrumentation (WMI) and checks if the owner of Explorer is "System".
- If a user has logged in, it checks if the GUI is alive, and restarts it if it isn't.
Note that I have employed a mutex to ensure that the GUI activation is thread-safe and only one instance is activated.
Now, let's review the server class named "XLogoffThread
". It provides the following methods:
Method Name
|
Description
|
ThreadProc()
|
The thread function that listens to and consumes user connections through XLGUI. All the user commands are delegated to ProcessComd() .
|
ProcessCmd(String *msg)
|
Consumes a user command.
|
SaveBaseline()
|
Saves the clean login as the baseline session.
|
SavePrevSession()
|
Buffers the previous session.
|
KeepSession()
|
Saves the previous session.
|
ClearSession()
|
Cleans the session data.
|
SaveSession()
|
Saves the current session.
|
RestoreSession()
|
Restores from the saved session data.
|
ProcessLogoff()
|
Processes logoff.
|
ProcessShutdown()
|
Processes shutdown
|
ProcessRestart()
|
Processes restart.
|
KillRestoredProcesses()
|
Kills all the restored applications before logoff.
|
The code snippet of SaveSession()
is listed as follows:
private: static void SaveSession()
{
if( !baseline ) return;
try
{
Process* allProcs[] = Process::GetProcesses();
int length = allProcs->Length;
if( StreamWriter* w = new StreamWriter(DATA_FILE))
{
for( int i = 0; i < length; i++ )
{
Process* proc = allProcs[i];
String* procName = proc->ProcessName;
if( baseline->Contains( procName ) ) continue;
String* procPath = proc->MainModule->FileName;
w->WriteLine(procName);
w->WriteLine(procPath);
}
w->Close();
}
} catch (Exception* except)
{
MessageBox::Show( except->get_Message(), "XLogoff Save Error" );
}
}
As we mentioned earlier, "baseline
" saves all the processes before the logged in user or this tool has launched or restored any applications, which we used as the baseline. When saving a session, we simply open the data file, take a snapshot of the current state of the PC, and then write those processes not existing in the baseline to the data file.
Let's look at the source of RestoreSession()
, which is listed below. Again, we use the baseline to ensure that we don't start any system processes. We ask the instance of the "ProcessAsUser
" object "cpau
" to launch all processes. This method, in turn, invokes a system call CreateProcessAsUser()
to perform the job. The primary difference between this call and Process::Start()
is that the former spawns a process on behalf of the currently logged in user, whereas the latter starts processes on behalf of System. Nevertheless, Process::Start()
is a handy and light-weight call, which can do a lot when combined with the start option settings. I use it in CheckGUI()
to start the GUI process.
private: static void RestoreSession()
{
if ( !File::Exists(DATA_FILE) || !baseline ) return;
try
{
StreamReader* r = new StreamReader(DATA_FILE);
if( r )
{
while ( r->Peek() >= 0 )
{
String* tbdName = r->ReadLine();
String* path = r->ReadLine();
if( !baseline->Contains( tbdName ) )
{
if( !cpau->Launch( path ) )
{
MessageBox::Show( path, "XLogoff Restore Error" );
}
}
}
r->Close();
}
} catch (Exception* except)
{
MessageBox::Show( except->get_Message(), "XLogoff Restore Error" );
}
}
5. Points of Interest
The idea for this tool is quite straightforward, but the implementation turned out to be more complicated than I'd first thought. I'd like to elaborate some aspects here.
- Avoiding system processes. Earlier drafts of this tool had saved all processes, including system services. Then, I ran into the trouble that the tool occasionally crashed when logging off or logging in. The crashes were random. After more tests, it was determined that the access to the module name of a system process sometimes causes violation. To overcome this problem, I decided to avoid system processes, which is how I came up with baseline processes. The introduction of baseline data has made the tool more robust, and meanwhile saved CPU significantly.
- Handling the logoff session. While shutdown and restart processing is fairly simple, handling the logoff properly is a little tricky. There were occasions when not all the user processes were closed after log off. This created problems for the next login. To properly log off, I have added
KillRestoredProcesses()
to clean up all the user created processes before logging off.
- Setting process ownership. When I finished implementing logoff logic and gave a shot, everything worked well, except that all the restored processes have "System" as the owner. This is not acceptable due to numerous reasons, one of which is system security. After some researches, it was determined that
CreateProcessAsUser()
is a good call to use, which is where I found the CPAU C# code helpful.
6. Future Work
As you may have noticed, the tool imposes one limitation that the current version cannot save/restore any Explorer windows that may have existed before logoff. The main difficulty is that a single Explorer process supports multiple windows. It is worth investigating how to save and restore those Explorer windows, which I leave for future updates.
A second area to improve is that we may save each window's state (position, geometry, z-order, etc.) in the session data so that at next login, Windows can restore all windows to the state of the previous login.
Last, but not least, this tool can be expanded to support remote management architecture, thanks to the TCP/IP client/server for service-GUI communications. The basic idea is that you may run "XLogoff" service on each remote computer and run a "XLGUI" window on your local computer. For example, you may, through the GUI window, command each remote computer to save a current session and then shutdown.
8. Conclusion
In this article, I have presented a Unix-type shutdown tool for Microsoft Windows. With this tool, the user can save the current login session by saving all the user created processes to a data file. While the tool can do what it was expected to do, there are a number of areas where it can be improved. Future updates shall incorporate these improvements one by one.
7. History
- 21 Feb 2006: First revision of the article and source code.
- 22 May 2006: Fixed weird fonts.