Application Recovery and Restart Samples

This topic contains the complete source code for a sample application that demonstrates the Application Restart and Recovery (ARR) features that enhance reliability on Windows Server® 2008. This sample application demonstrates the concepts and programming techniques presented in the companion topics Understanding Application Recovery and Restart and Developing with Application Recovery and Restart.

This code sample is a managed console application that implements a simple data-entry system for creating playlists. Each playlist consists of a name, a description, and a list of song titles. A detailed discussion of the core elements of this sample can be found in the Developing with Application Recovery and Restart topic.

The application user can type "!!" in response to most prompts to force the application to crash for demonstration purposes. Also purely for demonstration purposes, the application uses a timer to beep once after 60 seconds. Windows Error Reporting (WER) will not restart an application if it crashed within 60 seconds of execution, so the beep signals to the -user when a crash will result in the application restarting.

The playlist data-entry system uses two files, one for storing the saved collection of playlists (playlists.txt) and one for persisting data for a playlist that has not yet been saved. This file's naming convention is "current_user_name".pl3work, where current_user_name is the name of the user who launched the application. The files reside in the same directory as the application's executable file.

On start up, the application behaves as follows:

  1. Register for Application Recovery.

  2. Register for Application Restart.

  3. If restarted, check to see if there is an incomplete playlist from the prior session.

  4. If there is a previous playlist, allow the user to view it and resume data entry or discard it.

  5. Display the main menu of end-user actions (see below).

  6. Execute the selected end-user action until application exit or crash.

The application supports the following user actions:

  • Show Recovery settings

  • Show Restart settings

  • Display existing playlists

  • Make a new playlist

  • Exit

The following code declares the ARR functions that the sample uses. The delegate for the recovery method and the RecoveryData class, used to pass information to the recovery method, are also defined.

using System;
using System.Runtime.InteropServices;
using System.Text;

namespace Server2008.DeveloperStory.ARRExamples
{
    [Flags]
    public enum RestartRestrictions
    {
        None = 0,
        NotOnCrash = 1,
        NotOnHang = 2,
        NotOnPatch = 4,
        NotOnReboot = 8
    }

    public delegate int RecoveryDelegate(RecoveryData parameter);

    public static class ArrImports
    {
        [DllImport("kernel32.dll")]
        public static extern void ApplicationRecoveryFinished(
            bool success);

        [DllImport("kernel32.dll")]
        public static extern int ApplicationRecoveryInProgress(
            out bool canceled);

        [DllImport("kernel32.dll")]
        public static extern int GetApplicationRecoveryCallback(
            IntPtr processHandle,
            out RecoveryDelegate recoveryCallback,
            out RecoveryData parameter,
            out uint pingInterval,
            out uint flags);

        [DllImport("KERNEL32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern int GetApplicationRestartSettings(
            IntPtr process,
            IntPtr commandLine,
            ref uint size,
            out uint flags);
        
        [DllImport("kernel32.dll")]
        public static extern int RegisterApplicationRecoveryCallback(
            RecoveryDelegate recoveryCallback,
            RecoveryData parameter,
            uint pingInterval,
            uint flags);
        
        [DllImport("kernel32.dll")]
        public static extern int RegisterApplicationRestart(
            [MarshalAs(UnmanagedType.BStr)] string commandLineArgs,
            int flags);

        [DllImport("kernel32.dll")]
        public static extern int UnregisterApplicationRecoveryCallback();

        [DllImport("kernel32.dll")]
        public static extern int UnregisterApplicationRestart();
    }

    public class RecoveryData
    {
        string currentUser;

        public RecoveryData(string who)
        {
            currentUser = who;
        }
        public string CurrentUser
        {
            get { return currentUser; }
        }
    }
}

The following code implements the console application.

using System;
using System.Timers;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Runtime.InteropServices;

namespace Server2008.DeveloperStory.ARRExamples
{
    class PlaylistManager
    {
        static string fileExtension = ".pl3work";
        static StreamWriter log;
        static bool previousSessionLog;
        static string  playListFileName = "playLists.txt";

        static void Main(string[] args)
        {
            RegisterForRecovery();
            RegisterForRestart();

            // SetupTimerNotifyForRestart will set a timer to beep when 
            // WER will restart the program after a crash.
            SetupTimerNotifyForRestart();

            // If we started with /restart command line argument 
            // then try to resume the previous session.
            if (args.Length > 0 && args[0] == "/restart")
            {
                previousSessionLog = RecoverLastSession(args[0]);
            }
            if (previousSessionLog == true)
            {
                // Resume processing for this playlist.
                MakePlayLists();
                previousSessionLog = false;
            }

            // Main loop of activities that a user can select.
            bool done = false;
            do
            {
                string s = GetUserChoiceFromMenu();
                switch (s)
                {
                    case "1": DisplayRecoverySettings();
                        break;
                    case "2": DisplayRestartSettings();
                        break;
                    case "3": DisplayPlayLists();
                        break;
                    case "4" :
                        MakePlayLists();
                        break;
                    case "q" :
                        done = true;
                        break;
                }
            }
            while (done == false);
        }

        private static void SetupTimerNotifyForRestart()
        {
            // Set up a timer to beep when 60 seconds has elapsed.
            Timer notify = new Timer(60000);
            notify.Elapsed += new ElapsedEventHandler(NotifyUser);
            notify.AutoReset = false; // Only beep once.
            notify.Enabled = true;
        }

        private static void NotifyUser(object source, ElapsedEventArgs e)
        {
            Console.Beep();
        }

        private static string GetUserChoiceFromMenu()
        {
            string banner = "-----------------------------------";

            // Display the main menu and get the user's input.
            Console.WriteLine();
            Console.WriteLine(banner);
            Console.WriteLine("PlayList Editor Main Menu");
            Console.WriteLine(banner);
            Console.WriteLine("1 - Show Recovery Settings");
            Console.WriteLine("2 - Show Restart settings");
            Console.WriteLine("3 - Display existing playlists");
            Console.WriteLine("4 - Make a new playlist");
            Console.WriteLine("q - Exit this program");
            Console.WriteLine(
               "[Type !! at any prompt to force this demo to crash.]");
            Console.WriteLine();
            Console.Write("Enter your option: ");

            string s = Console.ReadLine().ToLower();
            TestCrash(s);
            if (s != "1" && s != "2" &&s != "3" && s != "4" && s != "q")
            {
                Console.WriteLine("Invalid Option: {0}", s);
                return GetUserChoiceFromMenu();
            }
            return s;
        }

        // This method is used to check all inputs to see if the user
        // wants to crash this application.
        private static void TestCrash(string s)
        {
            if (s == "!!")
            {
                Environment.FailFast("ARR Demo intentional crash.");
            }
        }

        private static void MakePlayLists()
        {
            if (previousSessionLog == true)
            {
                string[] lines = File.ReadAllLines(GetWorkLogName());

                // Resume the playlist starting where the previous session ended.
                // We will append the log instead of overwriting it.
                log = new StreamWriter(GetWorkLogName(), true);
                ComposePlayList(lines.Length);
            }
            else
            {
                log = new StreamWriter(GetWorkLogName(), false);
                ComposePlayList(0);
            }
            log.Close();
            string answer;
            do
            {
                Console.Write("Do you want to save the current playlist? [y/n]: ");
                answer = Console.ReadLine().ToLower();
                TestCrash(answer);
            }
            while (answer != "y" && answer != "n");
            if (answer == "y")
            {
                Console.WriteLine("Save in progress...");
                CommitPlayList();
            }
            else
            {
                Console.WriteLine("Playlist discarded.");
                File.Delete(GetWorkLogName());
            }
        }

        private static void CommitPlayList()
        {
            string item = File.ReadAllText(GetWorkLogName());
            File.AppendAllText(playListFileName, "\n\n" + item);

            // Once the playlist is saved, we delete the work log.
            File.Delete(GetWorkLogName());
        }

        private static void ComposePlayList(int level)
        {
           if (level < 1)
                AppendPlayListName();
   
           if (level < 2)
           {
               AppendDateAndOwner();
           }
           if (level < 3)
           {
               AppendDescription();
           }
           AppendSongs(level);
        }

        private static void AppendPlayListName()
        {
            Console.WriteLine();
            string answer;
            do
            {
                Console.Write("Please enter a name for the playlist: ");
                answer = Console.ReadLine();
                TestCrash(answer);
            }
            while (answer == String.Empty);

            // Persist to temporary log.
            log.WriteLine("Name: {0}", answer);
        }

        private static void AppendDateAndOwner()
        {
            // Persist to temporary log.
            log.WriteLine("Date: {0} Owner: {1}", DateTime.Today, Environment.UserName);
        }

        private static void AppendSongs(int level)
        {
            string answer = null;

            Console.WriteLine();
            int i=0;
            do
            {
                Console.Write("Add a song or press enter to finish: ");
                answer = Console.ReadLine();
                TestCrash(answer);

                // If the reply is blank, then they are done entering songs.
                if (answer != String.Empty)
                {
                    // Persist to temporary log.
                    log.WriteLine("Song: {0}", answer);
                    i++;
                }
            } while (answer != String.Empty);
            if (i == 0 && level < 4)
            {
                Console.WriteLine("WARNING: This playlist has no songs.");
            }
        }

        private static void AppendDescription()
        {
            Console.WriteLine();
            Console.Write("Please enter a description for the playlist: ");
            string answer = Console.ReadLine();
            TestCrash(answer);
            if (answer == String.Empty)
                answer = "No description supplied.";

            // Persist to temporary log.
            log.WriteLine("Description: {0}", answer);
        }

        private static void RegisterForRestart()
        {
            // Register for automatic restart if the application was terminated for any reason.
            ArrImports.RegisterApplicationRestart("/restart", 
               (int) RestartRestrictions.None);
        }

        private static void RegisterForRecovery()
        {
            // Create the delegate that will invoke the recovery method.
            RecoveryDelegate recoveryCallback = new RecoveryDelegate(RecoveryProcedure);
            uint pingInterval = 5000, flags = 0;
            RecoveryData parameter = new RecoveryData(Environment.UserName);

            // Register for recovery notification.
            int regReturn = ArrImports.RegisterApplicationRecoveryCallback(
            recoveryCallback,
            parameter,
            pingInterval,
            flags);
        }

        // This method is invoked by WER 
        static int RecoveryProcedure(RecoveryData parameter)
        {
            Console.WriteLine("Recovery in progress for {0}", parameter.CurrentUser);

            // Set up timer to notify WER that recovery work is in progress.
            Timer pinger = new Timer(4000);
            pinger.Elapsed += new ElapsedEventHandler(PingSystem);
            pinger.Enabled = true;

            // Do recovery work here.
            if (log != null)
                log.Close();

            // Simulate long running recovery.
            System.Threading.Thread.Sleep(9000);

            // Indicate that recovery work is done.
            Console.WriteLine("Application shutting down...");
            ArrImports.ApplicationRecoveryFinished(true);
            return 0;
        }

        // This method is called by a timer periodically to ensure
        // that WER knows that recovery is in progress.
        private static void PingSystem(object source, ElapsedEventArgs e)
        {
            bool isCanceled;
            ArrImports.ApplicationRecoveryInProgress(out isCanceled);
            if (isCanceled)
            {
                Console.WriteLine("Recovery has been canceled by user.");
                Environment.Exit(2);
            }
            Console.WriteLine(" / ");
        }

        private static string GetWorkLogName()
        {
            return Environment.UserName + fileExtension;
        }
       
        // When this application is restarted, check to see if there
        // is a worklog still open for the prior session.
        private static bool RecoverLastSession(string command)
        {
            Console.WriteLine("Recovery in progress {0}", command);
            string worklog = GetWorkLogName();

            // Is there an open work log?
            if (!File.Exists(worklog))
            {
                Console.WriteLine("No file found for session recovery.");
                Console.WriteLine();
                return false;
            }

            // There is a work log. If it is empty then there is no session to resume.
            string session = File.ReadAllText(worklog);
            if (session == String.Empty)
            {
                return false;
            }

            // Display the work log for the previous session and 
            // see if user wants to resume.
            Console.Write("There is a play list in progress: ");
            Console.WriteLine();
            Console.WriteLine(session);
            Console.WriteLine();
            string answer = null;
            do
            {
                Console.WriteLine("Please enter 'r' to resume or 'd' to discard."); 
                answer = Console.ReadLine().ToLower();
            } while (answer != "r" && answer != "d");

            if (answer == "d")
            {
                // Delete previous session's work log and exit.
                File.Delete(GetWorkLogName());
                return false;
            }

            // Signal to the Main method that there is a session to resume.
            return true;
        }
      
        private static void DisplayRecoverySettings()
        {
            RecoveryDelegate recoveryCallback;
            RecoveryData parameter = null;
            uint pingInterval, flags;

            ArrImports.GetApplicationRecoveryCallback(
            Process.GetCurrentProcess().Handle,
            out recoveryCallback,
            out parameter,
            out pingInterval,
            out flags);

            string paramValue = parameter.CurrentUser;

            Console.WriteLine("delegate: {0}, parameter: {1}, ping: {2}",
                recoveryCallback.ToString(), paramValue, pingInterval);
            Console.WriteLine();
        }

        private static void DisplayRestartSettings()
        {
            IntPtr cmdptr = IntPtr.Zero;
            uint size=0, flags;

            // Find out how big a buffer to allocate
            // for the command line.
            ArrImports.GetApplicationRestartSettings(
            Process.GetCurrentProcess().Handle,
            IntPtr.Zero,
            ref size,
            out flags);

            // Allocate the buffer on the unmanaged heap.
            cmdptr = Marshal.AllocHGlobal((int) size * sizeof(char));

            // Get the settings using the buffer.
            int ret = ArrImports.GetApplicationRestartSettings(
            Process.GetCurrentProcess().Handle,
            cmdptr,
            ref size,
            out flags);

            // Read the buffer's contents as a unicode string.
            string cmd = Marshal.PtrToStringUni(cmdptr);

            Console.WriteLine("cmdline: {0} size: {1} flags: {2}",
                cmd, size, flags);
            Console.WriteLine();

            // Free the buffer.
            Marshal.FreeHGlobal(cmdptr);
        }

        private static void DisplayPlayLists()
        {
            if (!File.Exists(playListFileName))
            {
                Console.WriteLine("You have no playlists saved.");
                return;
            }
            string list = File.ReadAllText(playListFileName);
            Console.WriteLine();
            Console.WriteLine(list);
            Console.WriteLine();
        }
    }
}

Community Additions

ADD
Show: