Search code examples
vb.netfilesystemwatcher

windows service app with FileSystemWatcher starts and then stops


I have a filesystemwatcher watching a folder where cameras save the files. It's supposed to add the name/date to a database so they can be deleted at a later date.

        watcher = New System.IO.FileSystemWatcher()
        Try
            watcher.Path = watchFolder
            watcher.NotifyFilter = IO.NotifyFilters.DirectoryName
            watcher.NotifyFilter = watcher.NotifyFilter Or IO.NotifyFilters.FileName
            watcher.NotifyFilter = watcher.NotifyFilter Or IO.NotifyFilters.Attributes
            AddHandler watcher.Created, AddressOf addFile
            watcher.IncludeSubdirectories = True
            watcher.EnableRaisingEvents = True
        Catch ex As System.IO.IOException
            writeLog("watcher IO error:" & ex.Message)
        Catch ex As NullReferenceException
            writeLog("watcher NullReferenceException: " & ex.Message)
            writeLog("watcher Stack Trace: " & vbCrLf & ex.StackTrace)
        Catch ex As Exception
            writeLog("watcher error:" & ex.Message)
        End Try
    Private Sub addFile(ByVal source As Object, ByVal e As System.IO.FileSystemEventArgs)
        writeLog("examining: " & e.FullPath)
        Try
            If e.ChangeType = IO.WatcherChangeTypes.Created Then
                Try
                    Dim fdir As String = IO.Path.GetDirectoryName(e.FullPath)
                    For Each kvp As KeyValuePair(Of String, Integer) In camList
                        Dim v1 As String = kvp.Key
                        Dim v2 As Integer = kvp.Value
                        If fdir = v1 Then
                            Using connection As New SQLiteConnection(conn)
                                connection.Open()
                                Dim sql As String = "INSERT INTO mycfg (f_name, d_date) VALUES (@value1, @value2)"
                                Dim fdate As Date = Now.AddDays(v2)
                                Dim sdate As String = fdate.ToString("yyyy/MM/dd", CultureInfo.InvariantCulture)
                                Using command As New SQLiteCommand(sql, connection)
                                    command.Parameters.AddWithValue("@value1", e.FullPath)
                                    command.Parameters.AddWithValue("@value2", sdate)
                                    Dim rowsAffected As Integer = command.ExecuteNonQuery()
                                    writeLog(e.FullPath & "-" & $"{rowsAffected} row(s) inserted.")
                                End Using
                            End Using
                        End If
                    Next
                Catch ex As Exception
                    writeLog("error: " & ex.Message)
                End Try
            End If
        Catch ex As System.IO.IOException
            writeLog("watcher IO error2:" & ex.Message)
        Catch ex As NullReferenceException
            writeLog("watcher NullReferenceException2: " & ex.Message)
            writeLog("watcher Stack Trace2: " & vbCrLf & ex.StackTrace)
        Catch ex As Exception
            writeLog("watcher error2:" & ex.Message)
        End Try
    End Sub

If I put an Exit Sub after writeLog at the start of the addFile sub then it will log the new file created but when I remove it I get the following error in my windows event log and the service stops before it can write to my log file.

Application: CamWatcher.exe
Framework Version: v4.0.30319
Description: The process was terminated due to an unhandled exception.
Exception Info: System.IO.FileNotFoundException
   at CamWatcher.Service1.addFile(System.Object, System.IO.FileSystemEventArgs)
   at System.IO.FileSystemWatcher.OnCreated(System.IO.FileSystemEventArgs)
   at System.IO.FileSystemWatcher.NotifyFileSystemEventArgs(Int32, System.String)
   at System.IO.FileSystemWatcher.CompletionStatusChanged(UInt32, UInt32, System.Threading.NativeOverlapped*)
   at System.Threading._IOCompletionCallback.PerformIOCompletionCallback(UInt32, UInt32, System.Threading.NativeOverlapped*)


The files are video files that are created but not closed until the time limit is up on the camera or lack of motion detected. I am thinking that might be the problem but I am not sure how to handle that problem. Nothing is being done to the file itself in my code so I am a bit baffled as to how the watcher can detect it then crash because it isn't there.


Solution

  • The stack trace tells us the error happens in the addFile() method. Significantly, the writeLog() method is not part of the stack trace, which is the only other custom method in use. This lets eliminate access to a log file as the cause of the error.

    With that in mind, there is only one possible line of code in the addFile() method directly that could throw a FileNotFoundException:

    connection.Open()
    

    That line could throw if the sqlite file is missing or similarly not available. Nothing else in addFile() method does any file I/O. Even the Path.GetDirectoryName() method is just string operations. This means the concern that a video file may still be in use shouldn't matter, because we're not actually trying to do anything with the file itself: only the file name, and we already have that in memory.

    This also helps explain why the exception isn't caught by your final exception handler, as the exception is likely faulting the sqlite dll. While it's not clear which provider you are using, the Microsoft.Data.Sqlite and System.Data.Sqlite providers are not documented to throw this exception; they would instead throw an SqliteException. This means you are likely using the Devart.Data.Sqlite provider. Note this provider's Open() method documentation does not share what exceptions it throws.

    So make sure your connection string is correct and the sqlite file is available to the user account running the service, and potentially switch to Microsoft's provider for hopefully better exception behavior.


    fwiw, I also suggest two changes to the method structure. First, use a guard clause for the IO.WatcherChangeTypes.Created test. Second, it would be somewhat strange to have a List(Of KeyValuePair(Of String Integer)) instead of a Dictionary(Of String, Integer). And if you really do have the latter, it seems like the camlist loop should be a dictionary lookup. This will also let you remove one level of try/catch nesting:

    Private Sub addFile(ByVal source As Object, ByVal e As System.IO.FileSystemEventArgs)
        writeLog($"examining: {e.FullPath}")
        If e.ChangeType <> IO.WatcherChangeTypes.Created Then Exit Sub
    
        Dim sql As String = "INSERT INTO mycfg (f_name, d_date) VALUES (@value1, @value2)"
        Try   
            Dim fdir As String = IO.Path.GetDirectoryName(e.FullPath)      
            Dim v2 As Integer            
            If Not camList.TryGetValue(fdir, v2) Then Exit Sub
    
            Using connection As New SQLiteConnection(conn), _
                  command As New SQLiteCommand(sql, conn)
    
                command.Parameters.AddWithValue("@value1", e.FullPath)
                command.Parameters.AddWithValue("@value2", Now.AddDays(v2).ToString("yyyy/MM/dd", CultureInfo.InvariantCulture))
    
                connection.Open()
                Dim rowsAffected As Integer = command.ExecuteNonQuery()
                writeLog($"{e.FullPath}-{rowsAffected} row(s) inserted.")
            End Using
        Catch ex As System.IO.IOException
            writeLog("watcher IO error2:" & ex.Message)
        Catch ex As NullReferenceException
            writeLog("watcher NullReferenceException2: " & ex.Message)
            writeLog("watcher Stack Trace2: " & vbCrLf & ex.StackTrace)
        Catch ex As Exception
            writeLog("watcher error2:" & ex.Message)
        End Try
    End Sub
    

    And we have now notably reduced the method length, indentation, and general complexity.


    But if that really is a List(Of KeyValuePair(Of String, Integer)), I would use a .Where() operation like this:

    Private Sub addFile(ByVal source As Object, ByVal e As System.IO.FileSystemEventArgs)
        writeLog($"examining: {e.FullPath}")
        If e.ChangeType <> IO.WatcherChangeTypes.Created Then Exit Sub
    
        Dim sql As String = "INSERT INTO mycfg (f_name, d_date) VALUES (@value1, @value2)"
        Try   
            Using connection As New SQLiteConnection(conn), _
                  command As New SQLiteCommand(sql, conn)
    
                command.Parameters.AddWithValue("@value1", e.FullPath)
                Dim v2Param As SqliteParameter = command.Parameters.Add("@value2", SqliteType.DateTime)
                conn.Open()
    
                Dim pairs = camList.Where(Function(kvp) kvp.Key = IO.Path.GetDirectoryName(e.FullPath)) 
                For Each kvp As KeyValuePair(Of String, Integer) In pairs
                    v2param.Value = Today.AddDays(v2).ToString("yyyy/MM/dd", , CultureInfo.InvariantCulture)
    
                    Dim rowsAffected As Integer = command.ExecuteNonQuery()
                    writeLog($"{e.FullPath}-{rowsAffected} row(s) inserted.")
                Next
            End Using
        Catch ex As System.IO.IOException
            writeLog("watcher IO error2:" & ex.Message)
        Catch ex As NullReferenceException
            writeLog("watcher NullReferenceException2: " & ex.Message)
            writeLog("watcher Stack Trace2: " & vbCrLf & ex.StackTrace)
        Catch ex As Exception
            writeLog("watcher error2:" & ex.Message)
        End Try
    End Sub
    

    And this will be more efficient, because you only need to open/close the connection once.