2012年7月3日 星期二

Use System Events To Protect Your Application Data


Introduction

If you’ve used the new MS Office products recently, you’ve noticed that Outlook, for instance, does not let you log off unless the application is closed. This is because Outlook does its data finalization when it closes and does not want cached data to be corrupted by a user logging off.
I recently worked on an application where we had to log streaming data which was cached for performance reasons, and then dumped the data to a file after certain criteria was met. The importance of the data mandated that we protect the collected data at all costs. It then struck me that it would be necessary to prevent the user from closing the application or shutting down the system while volatile data was being processed and resident in the cache.
The sample program presented here is not anywhere close to being a real production application, rather a simple program coded for the sole purpose of illustrating the concepts in this article. It presents a simple application that logs some random data (in a separate thread) and periodically writes cached data to a log file. If the user tries to log off, the application prompts the user with a choice to:
  • Save data and log off.
  • Don't save data and log off.
  • Cancel the user's logout or shutdown request, and continue.

About System Events

SystemEvents are events raised by, well…, the system. These are events that are raised in response to actions by the user that affect the operating environment. SystemEvents are not to be confused with Win32 system events that were kernel level events accessible to all programs. The events we are referring to here are those raised by theSystemEvents class in the Microsoft.Win32 namespace.
Events raised by the SystemEvents class are as follows:
  • DisplaySettingsChanged
    Occurs when the user changes the display settings.
  • EventsThreadShutdown
    Occurs before the thread that listens for system events is terminated. Delegates will be invoked on the events thread.
  • InstalledFontsChanged
    Occurs when the user adds fonts to or removes fonts from the system.
  • LowMemory
    Occurs when the system is running out of available RAM.
  • PaletteChanged
    Occurs when the user switches to an application that uses a different palette.
  • PowerModeChanged
    Occurs when the user suspends or resumes the system.
  • SessionEnded
    Occurs when the user is logging off or shutting down the system.
  • SessionEnding
    Occurs when the user is trying to log off or shutdown the system.
  • TimeChanged
    Occurs when the user changes the time on the system clock.
  • TimerElapsed
    Occurs when a Windows timer interval has expired.
  • UserPreferenceChanged
    Occurs when a user preference has changed.
  • UserPreferenceChanging
    Occurs when a user preference is changing.
Of the provided system events, the following are particularly useful to our application:
  • SessionEnding – Want to stop user from closing app with cached data.
  • LowMemory – Want to write cached data in hopes of reducing working set.
  • PowerModeChanged – Write cached data before going into standby mode.
In addition to these system events, we also want to trap the ApplicationExit event of the application we are running to make sure we clean up even when the user closes the window.

The Sample Program

Sample screenshot

Registering for events:
In the main form’s Load event handler, we register for the desired events. Here we make sure that we also handle the Close event for the form in addition to the system events to make sure the cached data gets written to the file.
private void Form1_Load(object sender, System.EventArgs e) {
  Application.ApplicationExit +=new EventHandler(Application_ApplicationExit);

  /* Register for system events to detect user trying 
     to log off or low memory condition */
  SystemEvents.SessionEnding +=new 
        SessionEndingEventHandler(SystemEvents_SessionEnding);
  SystemEvents.LowMemory +=new EventHandler(SystemEvents_LowMemory);
  SystemEvents.PowerModeChanged += new 
        PowerModeChangedEventHandler(SystemEvents_PowerModeChanged);

  /* can't stop what hasn't been started */
  button2.Enabled = false;

  /* The local log file */
  this.fileName = 
   Environment.GetFolderPath( Environment.SpecialFolder.LocalApplicationData ) 
   + "\\DumLogFile.bin";
  this.label1.Text = this.fileName;
}
When handling the SessionEnding event, we prompt the user with three choices. If the user chooses to cancel the logout, we simply set the Cancel flag of the SessionEndingEventArgs argument to false to cancel the logout. If the user chooses to logout, then we decide whether to write our cached data to the log or not and let the logout proceed.
private void SystemEvents_SessionEnding(object sender, SessionEndingEventArgs e) {
  /* Don't care if user logs out while no logging going on */
  if( logThread == null ) return;
  /* User is trying to log out. Prompt the user with choices */
  DialogResult dr = MessageBox.Show( "Critical Data In Cache!\n"+
    "Click Yes to save data and log out\n"+ 
    "Click No to logout without saving data\n"+ 
    "Click Cancel to cancel logout and manually stop the application", 
    "Data Logging In Progress", 
    MessageBoxButtons.YesNoCancel, MessageBoxIcon.Exclamation );

  /* User promises to be good and manually stop the app from now on(yeah right) */
  /* Cancel the logout request, app continues */
  if( dr == DialogResult.Cancel ){
    e.Cancel = true;
  }

  /* Good user! Santa will bring lots of data this year */
  /* Save data and logout */
  else if( dr == DialogResult.Yes ){
    /* Write data and tell the event handler to not cancel the logout */
    this.WriteCacheToFile();
    e.Cancel = false;
  }
  /* Bad user! doesn't care about poor data */
  else if( dr == DialogResult.No ){
    e.Cancel = false;
    return;
  } 
}
In a real world application, the data cache might actually be fairly large if disk IO is a latency concern. Hence, we handle the LowMemory event as well. To handle this event, we simply write our cached data to the disk, in hopes of alleviating the burden on the process working set and therefore the system RAM.
private void SystemEvents_LowMemory(object sender, EventArgs e) {
  /* Don't care if user logs out while no logging going on */
  if( logThread == null ) return; 
  /* System is low on memory, write to file. */
  this.WriteCacheToFile(); 
}
Another concern to an application developer is that of the system in standby or suspend mode. In this mode, the system state is saved and the computer is placed in a power save mode. This is IMHO the most dangerous scenario, for people often forget whether the system is in standby or off. This poses a potential for unsaved data to be lost. To handle this case, we handle the PowerModeChanged event. This event provides information whether the power mode is being resumed, suspended, or simply changed (as in laptop battery etc.). Since this event provides no means for cancellation, we simply write out our cache data in case the system should fail to recover from standby properly.
private void SystemEvents_PowerModeChanged(object sender, 
                        PowerModeChangedEventArgs e) {
  /* User is putting the system into standby */
  /* Cannot cancel the operation, write cached data */
  if( e.Mode == PowerModes.Suspend ){
    this.WriteCacheToFile();
  } 
}
The final concern is that the user closes the application while it's running. Though this has been handled by many in the Form's Closing event, it has been highly unreliable and problematic. I hence use the ApplicationExit event of the application class that signals that the application is about to terminate. (See Notes below.)
private void Application_ApplicationExit(object sender, EventArgs e) {
  /* Application is about to exit, cleanup and write data */
  this.Cleanup(); 
}

Note 1:

When handling the SessionEnding event, though the user is prompted with a choice, there is a time limit. The operating system will give the application time to die or kill it forcefully. The example presented here was just to suggest the options available. However, in a real world application, one would most likely cancel the logout, or write data and proceed without giving the spoiled user a clue.

Note 2:

Windows XP supports multiple users logged into the same machine. In this scenario, user A can switch users while programs are still running. The SessionEnding event is not fired when a user remains logged on and temporarily switches to another user. This is important! If user A switches to user B and then user B shuts the system down, user A will not have the ability to react to any message boxes running in user A's process space.

Note 3:

There is nothing a process can do to prevent itself from being killed by the task manager or lower level API. This signal is low level and cannot be handled by any .NET mechanism. When you think about it, this makes sense, though it is a power struggle between the developer and the OS.

Note 4:

I tried looking into the exact purpose of the EventsThreadShutdown event. It would seem to me that it was implemented to prevent the application from terminating a thread waiting for the SessionEnding event. I tried to get this event to fire but couldn’t. I also could not locate any documentation on MSDN or otherwise that would provide more info. If anyone has too much spare time to figure this out, I'd love feedback on this issue.

沒有留言:

張貼留言