0

How can I handle the following exception?

My code:

Public Module RecursiveEnumerableExtensions
    'credits Eric Lippert https://stackoverflow.com/users/88656/eric-lippert
    Iterator Function Traverse(Of T)(ByVal root As T, ByVal children As Func(Of T, IEnumerable(Of T)), ByVal Optional includeSelf As Boolean = True) As IEnumerable(Of T)
        If includeSelf Then Yield root
        Dim stack = New Stack(Of IEnumerator(Of T))()
        Try
            stack.Push(children(root).GetEnumerator())
            While stack.Count <> 0
                Dim enumerator = stack.Peek()
                If Not enumerator.MoveNext() Then
                    stack.Pop()
                    enumerator.Dispose()
                Else
                    Yield enumerator.Current
                    stack.Push(children(enumerator.Current).GetEnumerator())
                End If
            End While
        Finally
            For Each enumerator In stack
                enumerator.Dispose()
            Next
        End Try
    End Function
End Module

Friend Sub Main()
    Dim dinfo As DirectoryInfo = New DirectoryInfo(curDirectory)
    Dim dquery = RecursiveEnumerableExtensions.Traverse(dinfo, Function(d) d.GetDirectories())
End Sub

When I run my code, for some of the directories, which are not accessible, I receive inside the Friend Sub Main() in the part Function(d) d.GetDirectories()) the exception System.UnauthorizedAccessException. I would like to handle this by Try Catch.

I tried to edit the Friend Sub Main() and export the Lambda expression to a function funcGetDirectories, but it fails with following error: System.InvalidCastException

Friend Sub Main()
    Dim dinfo As DirectoryInfo = New DirectoryInfo(curDirectory)
    Dim dquery = RecursiveEnumerableExtensions.Traverse(dinfo, funcGetDirectories(dinfo))
End Sub

Function funcGetDirectories(di As DirectoryInfo)
    Try
        Return di.GetDirectories()
    Catch ex As UnauthorizedAccessException
        Throw
    Catch ex_default As Exception
        Throw
    End Try
End Function

Whats wrong with my Return di.GetDirectories()?

Note I'm working with .NET 4.8.

dbc
  • 104,963
  • 20
  • 228
  • 340
Baku Bakar
  • 442
  • 2
  • 8
  • 20
  • 1
    Declare it `Function funcGetDirectories(di As DirectoryInfo) As IEnumerable(Of DirectoryInfo)`. To call it, use `AddressOf`, e.g. `RecursiveEnumerableExtensions.Traverse(di, AddressOf funcGetDirectories)`. If you want to keep traversing for some specific exception, return `Enumerable.Empty(Of DirectoryInfo)()` for that exception. See: https://dotnetfiddle.net/UrsD0o – dbc Dec 30 '20 at 15:26
  • 1
    If you are defining `funcGetDirectories` in a class rather than a module you might also need to declare it `Shared` (i.e. static) see https://dotnetfiddle.net/Nv77ap – dbc Dec 30 '20 at 15:30
  • works wonderful, thank you a million. One more question, in your first example you did return the `UnauthorizedAccessException` as `Return Enumerable.Empty(Of DirectoryInfo)()`. What if I would like to return instead of empty some text like e.g. `Unauthorized Access`? Is that easy to implement? – Baku Bakar Dec 30 '20 at 15:38
  • @dbc oh no, there is a bigger problem. I have to skip all directories which are in any exception. Otherwise the code will fail later. E.g. in `GetFileSystemAccessRule` in the first line with `System.UnauthorizedAccessException`. Or if I uncomment all lines in the Catch block, i get later in `RecursiveEnumerableExtensions` on the line `stack.Push` the error `System.NullReferenceException`. Easiest way to reproduce this failure is by using the `C:\Windows` as root directory. – Baku Bakar Dec 30 '20 at 16:01

1 Answers1

1

One option is to pass in an error handler to your DirectoryInfo and FileSystemAccessRule enumeration methods. Those methods can catch all errors and pass then to the handler, which can optionally handle the error and allow traversal to continue:

Public Class DirectoryExtensions
    Shared Function GetFileSystemAccessRules(d As DirectoryInfo, ByVal errorHandler As Action(Of Object, DirectoryTraversalErrorEventArgs)) As IEnumerable(Of FileSystemAccessRule)
        Try
            Dim ds As DirectorySecurity = d.GetAccessControl()
            Dim arrRules As AuthorizationRuleCollection = ds.GetAccessRules(True, True, GetType(Security.Principal.NTAccount))
            Return arrRules.Cast(Of FileSystemAccessRule)()
        Catch ex As Exception
            If (Not HandleError(errorHandler, d.FullName, ex))
                Throw
            End If
            Return Enumerable.Empty(Of FileSystemAccessRule)()
        End Try
    End Function
    
    Shared Function EnumerateDirectories(ByVal directory As String, ByVal errorHandler As Action(Of Object, DirectoryTraversalErrorEventArgs)) As IEnumerable(Of DirectoryInfo)
        Dim di As DirectoryInfo
        Try
            di = new DirectoryInfo(directory)
        Catch ex As Exception
            If (Not HandleError(errorHandler, directory, ex))
                Throw
            End If
            Return Enumerable.Empty(Of DirectoryInfo)()
        End Try
        ' In .NET Core 2.1+ it should be able to recursively enumerate directories and ignore errors as follows:
        ' Dim query = { di }.Concat(di.EnumerateDirectories("*", New System.IO.EnumerationOptions With { .RecurseSubdirectories = True, .IgnoreInaccessible = True })))
        ' In the meantime, it's necessary to manually catch and ignore errors.
        Dim query = RecursiveEnumerableExtensions.Traverse(di, 
            Function(d)
                Try
                    Return d.GetDirectories()
                Catch ex As Exception
                    If (Not HandleError(errorHandler, d.FullName, ex))
                        Throw
                    End If
                    Return Enumerable.Empty(Of DirectoryInfo)()
                End Try
            End Function
        )
        Return query
    End Function

    Shared Function EnumerateDirectoryFileSystemAccessRules(ByVal directory As String, ByVal errorHandler As Action(Of Object, DirectoryTraversalErrorEventArgs)) As IEnumerable(Of Tuple(Of DirectoryInfo, IEnumerable(Of FileSystemAccessRule)))
        Return EnumerateDirectories(directory, errorHandler).Select(Function(d) Tuple.Create(d, GetFileSystemAccessRules(d, errorHandler)))
    End Function
        
    Shared Public Function SerializeFileAccessRules(ByVal directory As String, ByVal errorHandler As Action(Of Object, DirectoryTraversalErrorEventArgs), Optional ByVal formatting As Formatting = Formatting.Indented)
        Dim query = EnumerateDirectoryFileSystemAccessRules(directory, errorHandler).Select(
            Function(tuple) New With {
                .directory = tuple.Item1.FullName,
                .permissions = tuple.Item2.Select(
                    Function(a) New With { 
                        .IdentityReference = a.IdentityReference.ToString(),
                        .AccessControlType = a.AccessControlType.ToString(),
                        .FileSystemRights = a.FileSystemRights.ToString(),
                        .IsInherited = a.IsInherited.ToString()
                    }
                )
            }
        )
        Return JsonConvert.SerializeObject(query, formatting)   
    End Function
                                
    Private Shared Function HandleError(ByVal errorHandler As Action(Of Object, DirectoryTraversalErrorEventArgs), ByVal fullName as String, ByVal ex as Exception) As Boolean
        If (errorHandler Is Nothing)
            Return False
        End If
        Dim args As New DirectoryTraversalErrorEventArgs(fullName, ex)
        errorHandler(GetType(DirectoryExtensions), args)
        return args.Handled
    End Function                                
End Class

Public Class DirectoryTraversalErrorEventArgs 
    Inherits EventArgs
    Private _directory As String
    Private _exception As Exception

    Public Sub New(ByVal directory as String, ByVal exception as Exception)
        Me._directory = directory
        Me._exception = exception
    End Sub
    
    Public Property Handled As Boolean = false
    Public Readonly Property Directory As String
        Get
            Return _directory
        End Get
    End Property
    Public Readonly Property Exception As Exception
        Get
            Return _exception
        End Get
    End Property
End Class

Public Module RecursiveEnumerableExtensions
    ' Translated to vb.net from this answer https://stackoverflow.com/a/60997251/3744182
    ' To https://stackoverflow.com/questions/60994574/how-to-extract-all-values-for-all-jsonproperty-objects-with-a-specified-name-fro
    ' which was rewritten from the answer by Eric Lippert https://stackoverflow.com/users/88656/eric-lippert
    ' to "Efficient graph traversal with LINQ - eliminating recursion" https://stackoverflow.com/questions/10253161/efficient-graph-traversal-with-linq-eliminating-recursion
    Iterator Function Traverse(Of T)(ByVal root As T, ByVal children As Func(Of T, IEnumerable(Of T)), ByVal Optional includeSelf As Boolean = True) As IEnumerable(Of T)
        If includeSelf Then Yield root
        Dim stack = New Stack(Of IEnumerator(Of T))()

        Try
            stack.Push(children(root).GetEnumerator())
            While stack.Count <> 0
                Dim enumerator = stack.Peek()
                If Not enumerator.MoveNext() Then
                    stack.Pop()
                    enumerator.Dispose()
                Else
                    Yield enumerator.Current
                    stack.Push(children(enumerator.Current).GetEnumerator())
                End If
            End While
        Finally
            For Each enumerator In stack
                enumerator.Dispose()
            Next
        End Try
    End Function
End Module

Then call the method and accumulate the errors in a list of errors like so:

Dim errors = New List(Of Tuple(Of String, String))
Dim handler As Action(Of Object, DirectoryTraversalErrorEventArgs) = 
    Sub(sender, e)
        errors.Add(Tuple.Create(e.Directory, e.Exception.Message))
        e.Handled = true
    End Sub
Dim json As String = DirectoryExtensions.SerializeFileAccessRules(curDirectory, handler) 
' Output the JSON and the errors somehow
Console.WriteLine(json)
For Each e In errors
    Console.WriteLine("Error in directory {0}: {1}", e.Item1, e.Item2)
Next

Notes:

  • I am using tuples in a couple of places. Newer versions of VB.NET have a cleaner syntax for tuples, see Tuples (Visual Basic) for details.

  • The code manually traverses the directory hierarchy by stacking calls to DirectoryInfo.GetDirectories() and trapping errors from each call.

    In .NET Core 2.1+ it should be possible to recursively enumerate directories and ignore errors by using DirectoryInfo.EnumerateDirectories(String, EnumerationOptions) as follows:

    Dim query = { di }.Concat(di.EnumerateDirectories("*", New System.IO.EnumerationOptions With { .RecurseSubdirectories = True, .IgnoreInaccessible = True })))
    

    This overload does not exist in .Net Framework 4.8 though.

Demo fiddle here

dbc
  • 104,963
  • 20
  • 228
  • 340
  • thank's alot for your answer. Great and helpful as usual! Im testing your suggestion, but still in trouble with exception which it gets not be able to handel it. Actually its the `Return JsonConvert.SerializeObject(query, formatting)` which is reporting `System.UnauthorizedAccessException` on a directory (folder), on which I did remove all his permissions. Maybe you are also able to reproduce the error, when you run it inside IDE and test with a real directory, inside with a folder, which has no security groups / users with permission on it. – Baku Bakar Dec 30 '20 at 18:54
  • @BakuBakar - Please give the full `ToString()` output of the exception including the exception type, message, traceback and inner exception(s) if any. I am not currently running inside an IDE I am on holiday and using https://dotnetfiddle.net/. And what exactly is the problem? Is it that an exception is being thrown that should be caught, or that an exception is bring caught and handled when there should not be any exception at all? Your question was about how to continue after an error, not avoid the error entirely, so if it's the latter please provide a [mcve] or ask another question. – dbc Dec 30 '20 at 19:20
  • `Name: $exception / Value: {"Access to the ""C:\Test\A"" directory is denied."} / Type: System.UnauthorizedAccessException` - my code is the same like your suggestion. problem: when the code runs and there is one directory which isn't accessible because of no permissions, the code fails during debugging. This problem occur on the code mentoined in the questions and on your suggested code. My goal is, to finish the run of the code, even there is a directory with access deny. Ideally the non accessible directory will be listed in a separate file. By the way, happy holiday :-) – Baku Bakar Dec 30 '20 at 19:30
  • @BakuBakar - Again, please give the full `ToString()` output of the exception including the exception type, message, traceback and inner exception(s) if any. Specifically I would need to know where the exception is being thrown, which you don't say. Is it the call to `DirectoryInfo.EnumerateDirectories()`? – dbc Dec 30 '20 at 19:48
  • Also, what version of .Net are you using? – dbc Dec 30 '20 at 19:54
  • Sorry, actually im trying to find out how to make the `ToString()` - asap I will let you know with more information. When I run the code (debug mode) and there is a directory with no permission, it failes inside the `SerializeFileAccessRules` on the line `Return JsonConvert.SerializeObject(query, formatting)`, which gives me a message `Exception Unhandled`. Im able to see into the details inside `QuickWatch`. I'm working with the .NET 4.8. But in case, there are directories which are all accessible, the code runs well. – Baku Bakar Dec 30 '20 at 20:00
  • Just wrap the call to `JsonConvert.SerializeObject(query, formatting) ` with another `Try / Catch` and look at the `ToString()` output of the caught exception, it should show the traceback. – dbc Dec 30 '20 at 20:09
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/226625/discussion-between-baku-bakar-and-dbc). – Baku Bakar Dec 30 '20 at 20:28
  • works wonderful, thank you for all your efforts and your patience. – Baku Bakar Dec 31 '20 at 12:39