Click here to Skip to main content
15,868,141 members
Articles / Desktop Programming / MFC
Article

Autosave and Crash Recovery

Rate me:
Please Sign up or sign in to vote.
4.70/5 (7 votes)
24 Feb 2000 148.3K   68   19
How to implement autosave and autorecover features in your application.

Introduction

How to autosave files and then recover these files after a program crashes, is something that is seldom discussed, but adds power and flexibility to a program. This article will describe how to implement an autosave and autorecover method similar to that used by MS Office.

How it works

Autosaving is quite simple. After a specific amount of time, your program must serialize the loaded documents to the disk at a specific location. These serializations must not overwrite the files that are currently in use by the user, because this would eliminate the user's choice of whether or not they would like to save the file on exit from your program. Each time the program loads up, it must search for autosaved files and restore them as needed.

As with Office, this implementation saves these temporary files to a temporary directory inside your Windows directory and searches this directory at load time for autosaved files. If any are found, it will then begin its recovery process.

Assigning an Autosave Directory

First, you must choose where you are going to save the files. This directory must remain constant, or else you would have to search the entire hard disk for autosaved files... and that is out of the question. For practical purposes, your program should place them inside a folder in the most constant directory on the HD, the Windows directory. This directory should be stored in the environment variable WINDIR. If for some reason, the user's system does not contain a WINDIR environment variable, or the environment variable is incorrectly set, your program needs to detect this and choose another directory. You can easily check to see if the stored directory exists, by using the CFileFind class to search for it. The following code will do just that (gm_autosave directory should be a string, either global, as with this example, or accessible through a member function in your application). It is best preformed within the InitInstance function of your application.

::GetEnvironmentVariable("WINDIR",buffer,512);
CFileFind CFF;
if(CFF.FindFile(buffer,0))
{
    base=buffer;
}
else base = "C:\\TEMP";

gm_autosaveDirectory = base+"\\TEMP";

Optionally, you could use GetTempPath(DWORD nBufferLength, LPTSTR lpBuffer); to retrieve a temporary path. However, the above implementation will allow you to specify a specific directory for the files to occupy, such as "Temporary Autosaved Files".

Autosaving files

Now that the path is determined, your program will know where to save its files. The next part is, therefore, implementing the save routine in your application. To avoid soaking up system resources, use the OnTimer function in your Main Frame window. On initialization, your program needs to call the SetTimer(...) function with the appropriate values. SetTimer(0,m_autosavetime * 60 * 1000,NULL) will install the timer for you (assuming m_autosavetime is in minutes). The OnTimer() function of your Main Frame will now be called each time the timer has expired. Within this function, you will need to send autosave messages to all the views contained by your app. Use EnumChildWindows to call a procedure with each child window.

::EnumChildWindows(m_hWnd,AutosaveTimerChildProc,NULL);

AutosaveTimerChildProc needs to verify that the given child is a view for the document before sending the message to the window. DYNAMIC_DOWNCAST the pointer passed to your procedure to verify that it is a view, and then use PostMessage to notify your view of the change:

BOOL CALLBACK AutosaveTimerChildProc( HWND hwnd,  LPARAM lParam)
{
    if(DYNAMIC_DOWNCAST(CMyView,CWnd::FromHandle(hwnd)) != NULL)
    {
        ::PostMessage(hwnd,WM_MYVIEW_AUTOSAVETIMER,0,0);
    }
    return(TRUE);
}

Note: WM_MYVIEW_AUTOSAVETIMER should be defined as an Application Message, which is based of WM_APP.

Now, your view must process the message and save the file in a restorable way. Add a message map such as this:

ON_MESSAGE(WM_MYVIEW_AUTOSAVETIMER,OnAutosaveTimer)

and prototype the function in your view's class definition as a void accepting the standard WPARAM and LPARAM messages. This function can get a little tricky. The problem is that if the user saves the file in another location, then the restore path for the file will change also. Since the autosave name is based off the actual filename (in case the user needs to manually access the file in some strange incident) and you don't want more than one copy of the autosaved file, your program must always store the name of the last autosave backup and delete the autosaved backup, before it writes its copy of the file. This implementation uses a vector of CStrings to store the backup filenames (this vector will contain either the name of the backup or nothing at all). Additionally, your program must make sure that the directory for your autosave has been created before saving to it! If an error occurs during the process, the choice is up to you how you will recover or handle it.

void CMyView::OnAutosaveTimer(WPARAM w, LPARAM l)
{
    CFileFind CFF;
    if(CFF.FindFile(gm_autosaveDirectory.GetBuffer(1))==FALSE) 
    {
        if(CreateDirectory(gm_autosaveDirectory.GetBuffer(1), 
                                 NULL) ==0) //create directory
        {
            //an error has occured, process the error here
        }

    }

    // document name and your autosave extension
    CString  fname = (gm_autosaveDirectory + 
          "\\"+GetDocument()->GetTitle()+".MBK"); 
    if(m_autosave_names.size() > 0)  //delete old file
    {
        if(CFF.FindFile((*m_autosave_names.begin()))==TRUE)
        {
            if(::DeleteFile(((*m_autosave_names.begin()))) == 0)
            {
                //an error has occured, process the error here if you want
                //however, if the file simply does not exist, 
                //that is fine, ignore the error and continue
            }
        }
        // remove the old filename from the vector
        m_autosave_names.erase(m_autosave_names.begin());
    }
    m_autosave_names.push_back(fname); //add the new filename to the vector

    //call store function
    GetDocument()->StoreAutoRecoveryInformation(fname);
}

The StoreAutoRecoveryInformation function is implementation dependant. An easy implementation would simply serialize the document's true path to an archive, and then call the Serialize function to save the file after the path. For our example, this will suffice:

CMyDoc::StoreAutoRecoveryInformation(CString path)
{
    CFile f;
    if(f.Open(path,CFile::modeWrite | CFile::modeCreate)!=0)
    {
        CArchive ar(f,CArchive::store);
        ar.WriteString(path);
        try
        { 
            Serialize(ar);
        }
        catch(CException *e)
        {
            //write error occured, process here
        }
        ar.Close();
        f.Close();
    }
    else
    {
        //open error occured, process here
    }
}

Autosave Recovery

Rather than writing an entirely new document and view class for recovered files, you can simply modify your Document's Serialize(...) function to check the file extension on serialization. If the extension is that of the autosave file type, perform the appropriate actions.

//helper function to get file extension from a given path
CString MakeExt(CString fname) 
{
    for(int i = fname.GetLength()-1; i >0; i--)
    {
        if(fname.GetAt(i)=='.') return fname.Mid(i);
        if(fname.GetAt(i)=="\\") break;
    }
    return "";
}
.
.
.
CMyDoc::Serialize(CArchive ar)
{
    CString ext=MakeExt(ar.GetFile()->GetFileName());
    ext.MakeLower();
    if(ext == ".mbk" && ar.IsLoading())
    {
        CString s;
        //read path to restore path member variable
        m_restorepath = ar.ReadString(s)
        // read old path to member variable
        m_oldpath = ar.GetFile()->GetFileName();
        CMyBaseDocument::Serialize(); 
          //call the base document's serialize function, 
          // or perform your default serialization here
    }
    // put your default serialization code here
    else CMyBaseDocument::Serialize();
}

Because of the way that the MFC code for document serialization works, the document's view will have to handle changing the document's path to the correct location. (By default, the path will be to the autorestored copy, but you want to restore the original path, so that the user can continue where they left off with as little hassle as possible.) In the OnInitialUpdate function of your view, check to see if the document's restore path has been set. If it has, then you know that the program has just loaded an autorecovered file, and the path and title of the document and view must be changed to the proper location.

CMyDoc *doc=GetDocument();
if(doc->m_restorepath.GetLength() > 0)
{
    doc->SetPathName(doc->m_restorepath,TRUE);
    doc->SetModifiedFlag(TRUE);
    doc->UpdateAllViews(NULL);
    m_autosave_names.push_back(doc->m_oldpath);
}

Your program must autorecover the files each time it loads. This is done by searching the autosave directory for all autosaved files and loading as needed from your InitInstance function. If you allow multiple instances of your program, make sure that the current instance is the only instance before recovering autosaved files.

BOOL bFound = FALSE;
HANDLE hMutexOneInstance = NULL;
#ifdef _WIN32
    hMutexOneInstance = 
      CreateMutex(NULL,TRUE,_T("PreventSecondInstanceMutex"));
    if(GetLastError() == ERROR_ALREADY_EXISTS)
    bFound = TRUE;
#else
    if(m_hPrevInstance != NULL)
    bFound = TRUE;
#endif

#ifdef _WIN32
    if(hMutexOneInstance) ReleaseMutex(hMutexOneInstance);
#endif 
    
.
.
.

if(bFound)
{
    if(CFF.FindFile((gm_autosaveDirectory+"\\"+ 
                   "*.MBK").GetBuffer(1),0)==TRUE)
    {
        if(::MessageBox(NULL,
          "Autosaved files found. Would you like to recover them?\n"
          "(WARNING: IF YOU PRESS NO, YOU WILL NOT BE ABLE TO RECOVER"
          " THE FILES IN THE FUTURE).", "MYDOC", MB_YESNO) != IDNO) 
        {
            while(CFF.FindNextFile() !=0)
            {
                CMyDoc *doc= 
                 (CMyDoc *)OpenDocumentFile(CFF.GetFilePath().GetBuffer(1));
            }
            OpenDocumentFile(CFF.GetFilePath().GetBuffer(1));
        }
        else
        {
            while(CFF.FindNextFile() !=0)
            {
                DeleteFile(CFF.GetFilePath().GetBuffer(1));
            }
            DeleteFile(CFF.GetFilePath().GetBuffer(1));
        }
    }
}

Use class wizard to add the PostNcDestroy member to your view class, and then delete the autosaved file there (it will only be deleted if the window closes normally). The following chunk of code will do that for you.

CFileFind CFF;
if(m_autosave_names.size() > 0)
{
    if(CFF.FindFile(((*m_autosave_names.begin())).c_str())==TRUE)
    {
        ::DeleteFile(((*m_autosave_names.begin())).c_str());
    }
    m_autosave_names.erase(m_autosave_names.begin());
}

Finally, at program close, these files should be deleted and the directory removed. In your ExitInstance function, delete the directory.

CFileFind CFF;
if(CFF.FindFile(gm_autosaveDirectory.GetBuffer(1))==TRUE)
   RemoveDirectory(gm_autosaveDirectory.GetBuffer(1));

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Questionerror LNK2019: unresolved external symbol Pin
driver456711-Mar-13 5:40
driver456711-Mar-13 5:40 
Questionan example please? anyone? Pin
mark gooding25-Oct-05 4:02
mark gooding25-Oct-05 4:02 
GeneralDoesn't work with COleDocument Pin
Vityusha5-Apr-05 9:03
Vityusha5-Apr-05 9:03 
QuestionWhat about autosave in background? Pin
Constructor18-Feb-04 0:16
Constructor18-Feb-04 0:16 
GeneralMultiple Views of a doc Pin
CMYanko12-Mar-03 5:06
CMYanko12-Mar-03 5:06 
GeneralDon't use the Windows directory... Pin
Domenic Denicola17-Nov-02 18:25
Domenic Denicola17-Nov-02 18:25 
GeneralON_MESSAGE(...) Pin
Quentin Pouplard29-Sep-02 12:15
Quentin Pouplard29-Sep-02 12:15 
GeneralRe: ON_MESSAGE(...) Pin
Anonymous16-Jul-03 8:15
Anonymous16-Jul-03 8:15 
GeneralAutoSave using API in VB Pin
Lakshmi Divya2-Aug-02 2:16
Lakshmi Divya2-Aug-02 2:16 
QuestionHow about AutoSave encapsulated? Pin
Benjamin Ng27-Aug-00 22:45
Benjamin Ng27-Aug-00 22:45 
GeneralProblem with same filenames in different directories Pin
Carsten Sperber6-Mar-00 10:22
sussCarsten Sperber6-Mar-00 10:22 
GeneralUpdate / Notes Pin
Jesse Ezell23-Feb-00 20:14
Jesse Ezell23-Feb-00 20:14 
QuestionWhy not WM_APP instead of WM_USER...? Pin
James R. Twine23-Feb-00 6:57
James R. Twine23-Feb-00 6:57 
AnswerRe: Why not WM_APP instead of WM_USER...? Pin
Jesse Ezell23-Feb-00 7:40
Jesse Ezell23-Feb-00 7:40 
GeneralRe: Why not WM_APP instead of WM_USER...? Pin
Chris Maunder24-Feb-00 17:33
cofounderChris Maunder24-Feb-00 17:33 
GeneralMore easily retrieve Temp-path Pin
Alex Marbus (NL)22-Feb-00 4:34
Alex Marbus (NL)22-Feb-00 4:34 
GeneralYour Welcome Pin
Jesse Ezell21-Feb-00 7:45
Jesse Ezell21-Feb-00 7:45 
GeneralWow, thanks Pin
michael21-Feb-00 1:49
michael21-Feb-00 1:49 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.