KnowDotNet Visual Organizer

Using the FileSystemWatcher in a Windows Service

Recovery of Files If Service Fails

by Les Smith
Print this Article Discuss in Forums

In a previous article (Click to See Article), I described the process of creating a Windows Service that uses the FileSystemWatcher to track the movement of files on an FTP Server.  In that article, I said that rare circumstances could cause the service application to go down and thereby cause us to lose the record of files being created.  In that article, I stressed the requirement to contain as little business logic as possible in the service to reduce the amount of code that could possibly fail.

In this article, I will discuss the reading of the file created by the Windows Service application, and also the recovery methods that are in place to recover the track of files moved while the service is down on rare occassions.  This article will demonstrate the use of several new methods found in the System.IO Namespace and at the same time demonstrate the methodology of recursively traversing multiple directories and their respective sub directories.

As you will recall, if you read the previous article, the Windows Service application runs on the FTP Server computer, creating a text file record of file movement.  It simply records one line for each file created.  That line contains the time of the creation and the name of the newly created file.  The service application does not care what kind of file is being created, as that is the business of the application described by this article.

In this application, a Timer fires every five minutes to call the
LoadFiles method, shown under Figure 1.  This method attempts to rename the text file created by the Windows Service application.  Once it is renamed successfully, it is read a line at a time, and each file and time is passed to a method called LoadOneFile.  That method will not be shown here, but it is the method that determines whether a file is of interest and if it is, it is recorded in a SQL Server database.  By renaming the file, that the service is writing to, we literally take that file away from the service, thereby causing it to create a new instance of the same filename.

Since both this application and the service application are real-time programs, they do not display message boxes.  First, there is no one to answer the message, and second; we cannot allow the applications to stop.  Therefore, we display error messages in a multi-line text box and try again the next time the timer fires.  In some applications, when we encounter an error, we send emails to IT personnel, but for this article, I have omitted that code.

Figure 1 - LoadFiles Method.

   Private Overloads Sub LoadFiles()
      
' rename the current file that the file watcher service is writing to
      ' call LoadOneFile passing a single file to be loaded or discarded
      ' If successful in processing all of the files, mark the processed file
      ' and exit
      Dim i As Integer
      Dim start As Integer
      Dim RenameSuuccessful As Boolean = False
      Dim hIN As Integer
      Dim MyFile As String = ""
      
Dim SQL As String
      Dim dt As New DataTable()

      
Try
         DoEvents()

        
If Dir(FWServiceFile).Length = 0 Then
            Exit Sub ' nothing to do, no files have been created
         End If

         ' clear the metrics
         With Me
            .txtLog.Text = ""
            .miActivityCounter = 0
            .miSqlErrorCount = 0
        
End With

         ' try renaming the file
         MyFile = "I:\FileWatcherFiles\FW" & _
            Format(Today, "MMddyyyy") & Format(Now, "HHmmss") & ".TXT"
        
Do While Not RenameSuuccessful
            
Try
               Rename(FWServiceFile, MyFile)
               RenameSuuccessful =
True
            Catch
               start = Microsoft.VisualBasic.Timer
              
Do While Microsoft.VisualBasic.Timer - start < 1<BR>                   DoEvents()
              
Loop
            End Try
         Loop

         ' now the file is renamed, start loading to database
         ' get a handle and open the file
         Me.lblCurrentFile.Text = MyFile
         DoEvents()
         hIN = FreeFile()
        
Try
            FileOpen(hIN, MyFile, OpenMode.Input, OpenAccess.Read)
        
Catch ex As System.Exception
            Me.txtLog.AppendText("Err: " & ex.ToString & vbCrLf)
            
Exit Sub
         End Try

         ' loop thru the file and load the files to database
         Dim dataLine As String
         Dim fileTime As String
         Dim fileName As String
         Dim trash As String
         Dim s() As String

         Do While Not EOF(hIN)
            dataLine = LineInput(hIN)
            
If dataLine.Substring(0, 2) = "TM" Then
               s = dataLine.Split(vbTab)
              
' s(1) will be time string
               ' s(3) will be the file and path

               ' call the database recorder, it will record the
               ' creation of the file in database, only if the
               ' file is of an interesting type
               If Not LoadOneFile(s(3), s(1), Connection) Then
                  Me.miSqlErrorCount += 1
                  
Me.lblSqlErrCount.Text = miSqlErrorCount
              
End If
               DoEvents()
            
End If
         Loop

         Try
            FileClose(hIN)
        
Catch ex As System.Exception
            
Me.txtLog.AppendText("Err: " & ex.ToString & vbCrLf)
        
End Try

         ' move the processed file to the archive.  If the process
         ' does not get to here and the file is not moved, then it
         ' was not processed successfully...
         Try
            Dim archiveFile As String = Replace(MyFile, _
              "I:\FileWatcherFiles", "I:\FileWatcherFiles\FWFArchive")
            System.IO.File.Move(MyFile, archiveFile)
        
Catch ex As System.Exception
            
Me.txtLog.AppendText("Err: " & ex.ToString & vbCrLf)
        
End Try
      Catch ex As System.Exception
            
Me.txtLog.AppendText("Err: " & ex.ToString & vbCrLf)
      
End Try
   End Sub

In the rare case the Windows Service Application should go down, or for any other reason it should fail, the client application has the ability to search the FTP Servers (I: Drive).  The UI for this application has a button to start the recovery process.  When clicked, it calls the
TraverseDir  method.  The object of this method is to Trace all directories and their sub directories on the FTP server.  The directories, that are of interest have a structure like I:\Top\IN\FC\WT\12-23-2003.  Since the date portion of the path can sometimes be in a format that is not necessarily a valid date (e.g., 12232003), the RegEx is used to convert those instances to a valid date.  We need to do this so that we can limit the search of the server by date ranges.  The input parameter to the TraverseDir method is a top level directory on the server.  The list of these top level directories is created by code that has nothing to do with the demonstration code for this article and therefore is omitted from the article.

The recovery code will create a collection of all files that could have been created during a time range and that, due to a failure of the Windows Service application, may not have been recorded in our database.  Obviously, this method will pick up files that did get recorded.  In that case, they will not be inserted into the database again.  Their records will simply be updated with the latest information retrieved by the recovery code.  This is done because a file could be moved around during the failure period and our database has to keep track of where all of the interesting files are, even when they are moved after being created.

Figure 2 - Recovery Code.

   ' trace recovery code
   Public Structure MissedFile
      
Public Filename As String
      Public CreatedDate As DateTime
  
End Structure

   Public colFiles As New Collection()
  
Public dStart As DateTime

  
Public reDateFolder As New _
      Regex("^.*?\\(?<month>[1-9]|0[1-9]|1[0-2])-?(?<day>[1-9]" & _
            "|0[1-9]|[1-3][0-9])-?(?<year>20[0-9]{2}|[0-1][0-9])(\\.*)?$")
  
Public sReplacePattern As String = "${month}/${day}/${year}"



   Private Sub TraverseDir(ByVal sDir As String)
      
' This is a recursive function that traverses
      ' a directory structure and finds files that are
      ' either not in date folders or are in date folders
      ' that are on or after the target date

      Dim sSubDirs As String()
      
Dim s As String
      Dim d As DateTime
      
Dim sR As String

      Try
         Try
            
' New .NET function for retrieving all sub directories
            ' for the top level directory (sDir)

            sSubDirs = Directory.GetDirectories(sDir)
        
Catch es As System.Exception
            ' here we display a message box, because the recovery
            ' process is attended

         End Try

         For Each s In sSubDirs
            
'Force the date folder to be in a readable format
            sR = reDateFolder.Replace(s, MesReplacePattern).ToString

            
'If this path does contain a date folder, check to see
            '  if it is one of the dates we are looking for
            If sR.Length > 0 AndAlso reDateFolder.IsMatch(s) Then
               Try
                  d = DateTime.Parse(sR)
                  
'Go back one day from the start just to make sure
                  If d >= dStart.AddDays(-1) Then
                     'Traverse this directory and check for missed files
                     TraverseDir(s)
                     GetMissedFiles(s)
                  
End If
               Catch ex As FormatException
                  
'This is not a valid date - Traverse
                  TraverseDir(s)
                  GetMissedFiles(s)
              
End Try
            Else
               'There is no date folder - Traverse
               TraverseDir(s)
               GetMissedFiles(s)
            
End If
         Next
      Catch ez As StopTracingDirectoryException

        
Throw ez
      
Catch ex As System.Exception
         MsgBox(ex.ToString)
      
End Try
   End Sub

   Public Sub GetMissedFiles(ByVal sDir As String)
      
'This adds all files created after the given date
      'in the given directory to a collection
      Dim mf As MissedFile
      
Dim s As String
      Dim d As DateTime

      
Dim sFiles As String()
      
' new .NET method for retrieving all of the files
      ' in the passed directory

      sFiles = Directory.GetFiles(sDir)

      
For Each s In sFiles
         d = File.GetCreationTime(s)
        
'See if this file was created on or after our start date
         If d >= dStart Then
            'If it is a new file, add it and its creation time to the collection
            mf = New MissedFile()
            mf.Filename = s
            mf.CreatedDate = d
            colFiles.Add(mf)
        
End If
      Next

   End Sub

Top of Page

Writing Add-Ins for Visual Studio .NET
Writing Add-ins for Visual Studio .NET
by Les Smith
Apress Publishing