excelruntime-errorscripting.dictionaryvba

Class does not support Automation or does not support expected interface


I'm importing a kind of CSV security file for reporting in Excel. The file basically has the following format:

!Users
UserA
UserB
UserC
...

!Roles
RoleA
RoleB
RoleC
...

!Permissions
UserA|RoleA
UserA|RoleB
UserC|RoleA
UserB|RoleC
...

The report is a kind of matrix that would look like this:

  | A            | B            | C            | D
--|--------------|--------------|--------------|----------------
1 |              | RoleA        | RoleB        | RoleC
2 | UserA        | Y            | Y            | N
3 | UserB        | N            | N            | Y
4 | UserC        | Y            | N            | N

The easiest way I could think of doing this is to do the following steps:

  1. Open the file and put everything into 3 multi-level dictionaries (One for users, one for roles and one for permissions) using Scripting.Dictionary.
  2. Create a sheet and build the matrix based on the dictionaries.

Obviously, the actual file format and implementation is somewhat more complex, but this is the gist of it.

It works fine for files up to about 10Mb, but when the files start exceeding that number (Thousands of users and roles), I get the following error:

Run-time error '430':

Class does not support Automation or does not support expected interface

This happens on the following line:

Set pubSecClassAccess.Item(vClass).Item(vValue).Item(vUser) = New Scripting.Dictionary

If I look under processes, EXCEL.EXE is using about 1.5Gb of RAM at the point where it throws the error. If I end, rather than debug and then close the workbook, I get the following message:

Excel cannot complete this task with available resources. Choose less data or close other applications.

I'm guessing I run out of RAM even though I still have about 4Gb of my 8Gb available.

My question is: How do I re-factor my code so that it does this without using so much RAM?

The sections in the file could be in a different order (Like the !Permissions could come before the !Users section).


Solution

  • Edit Forgot to say: the macro takes about one minute to process my 55 Mb test file.

    I cannot find anything about the memory requirements of dictionaries. However, since "It throws the error way before I do the sort though", no minor improvement in the way the dictionaries are handled is going to help. My technique is totally different.

    I first generated a test file with the following characteristics:

    The resultant file is nearly 55 Mb and contains over one million permissions. I did not intend to create such a large file but had not thought enough about the implications of an average 25 permissions per user. I should admit that the file contains duplicate permissions. The macro below allows for this error and skips the duplicates.

    My code has several steps:

    I can open Report.txt (which is 21Mb) with Excel and tidy the formatting.

    There are two modules below. The first contains the macro described above. The second contains a routine I use check when processes have completed.

    Option Explicit
    Sub CreateReport()
    
      Dim FileName As Variant
      Dim FlIn As Object
      Dim FlLine As String
      Dim FlLinePart() As String
      Dim FlOut As Object
      Dim FlSysObj As Object
      Dim Found As Boolean
      Dim InxProc As Long
      Dim NumPermissions As Long
      Dim NumRoles As Long
      Dim NumUsers As Long
      Dim PathCrnt As String
      Dim Process() As String
      Dim Roles() As String
      Dim RoleCrnt As Long
      Dim RoleNameLast As String
      Dim TimeNow As Double
      Dim Users() As String
      Dim UserCrnt As Long
      Dim UserNameLast As String
    
      Dim StartTime As Double
    
      StartTime = Timer
    
      ' I find it convenient to have all files in the same folder as the workbook
      ' Change PathCrnt as required
      PathCrnt = ActiveWorkbook.Path & "\"
    
      ' Delete any files left by previous run of macro
      ' Replace Report.txt by your name for output file
      ' =====================================================================================
      For Each FileName In Array("Users.txt", "Roles.txt", "Perms.txt", _
                                 "SortedUsers.txt", "SortedRoles.txt", "SortedPerms.txt", _
                                 "SortUsers.bat", "SortRoles.bat", "SortPerms.bat", _
                                 "Report.txt")
        If Dir$(PathCrnt & FileName) <> "" Then
          Kill PathCrnt & FileName
        End If
      Next
    
      ' Split security log into three separate files: Users.txt, Roles.txt and Perms.txt
      ' =====================================================================================
    
      Set FlSysObj = CreateObject("Scripting.FileSystemObject")
    
      Set FlIn = FlSysObj.OpenTextFile(PathCrnt & "testfile.txt", 1, False, 0)
    
      FlLine = FlIn.ReadLine
      Debug.Assert FlLine = "!Users"
      NumUsers = 0
      Set FlOut = FlSysObj.OpenTextFile(PathCrnt & "Users.txt", 2, True, 0)
    
      Do While Not FlIn.AtEndOfStream
        FlLine = FlIn.ReadLine
        If FlLine <> "" Then
          If FlLine = "!Roles" Then
            Exit Do
          End If
          NumUsers = NumUsers + 1
          FlOut.WriteLine FlLine
        End If
      Loop
      FlOut.Close
    
      Debug.Assert FlLine = "!Roles"
      NumRoles = 0
      Set FlOut = FlSysObj.OpenTextFile(PathCrnt & "Roles.txt", 2, True, 0)
    
      Do While Not FlIn.AtEndOfStream
        FlLine = FlIn.ReadLine
        If FlLine <> "" Then
          If FlLine = "!Permissions" Then
            Exit Do
          End If
          NumRoles = NumRoles + 1
          FlOut.WriteLine FlLine
        End If
      Loop
      FlOut.Close
    
      Debug.Assert FlLine = "!Permissions"
      NumPermissions = 0
      Set FlOut = FlSysObj.OpenTextFile(PathCrnt & "Perms.txt", 2, True, 0)
    
      Do While Not FlIn.AtEndOfStream
        FlLine = FlIn.ReadLine
        If FlLine <> "" Then
          NumPermissions = NumPermissions + 1
          FlOut.WriteLine FlLine
        End If
      Loop
      FlOut.Close
      FlIn.Close
    
      ' Create batch files to sort Users.txt, Roles.txt and Perms.txt
      ' I have successfully used Shell with command line parameters but not tonight
      ' Decided not to waste time investigating my error
      ' ===============================================================================================
    
      Set FlOut = FlSysObj.OpenTextFile(PathCrnt & "SortUsers.bat", 2, True, 0)
      FlOut.Write "Sort <""" & PathCrnt & "Users.txt"" >""" & PathCrnt & "SortedUsers.txt"""
      FlOut.Close
      Set FlOut = FlSysObj.OpenTextFile(PathCrnt & "SortRoles.bat", 2, True, 0)
      FlOut.Write "Sort <""" & PathCrnt & "Roles.txt"" >""" & PathCrnt & "SortedRoles.txt"""
      FlOut.Close
      Set FlOut = FlSysObj.OpenTextFile(PathCrnt & "SortPerms.bat", 2, True, 0)
      FlOut.Write "Sort <""" & PathCrnt & "Perms.txt"" >""" & PathCrnt & "SortedPerms.txt"""
      FlOut.Close
    
      ' Sort Users.txt, Roles.txt and Perms.txt to create sorted versions
      ' ===============================================================================================
    
      Call Shell(PathCrnt & "SortUsers.bat")
      Call Shell(PathCrnt & "SortRoles.bat")
      Call Shell(PathCrnt & "SortPerms.bat")
    
      ' Loop until all the btach files have been completed
      ' ===============================================================================================
    
      Do While True
        Found = False
        Call GetProcessList(Process)
        For InxProc = 1 To UBound(Process)
          If Process(InxProc) = "cmd.exe" Then
            Found = True
            Exit For
          End If
        Next
        If Not Found Then
          Exit Do
        End If
        TimeNow = Now()
        ' Wait 1 second
        Application.Wait TimeSerial(Hour(TimeNow), Minute(TimeNow), Second(TimeNow) + 1)
      Loop
    
      ' Read SortedUsers.txt and SortedRoles.txt into arrays
      ' ===============================================================================================
    
      Set FlIn = FlSysObj.OpenTextFile(PathCrnt & "SortedUsers.txt", 1, False, 0)
      ReDim Users(1 To NumUsers)
      For UserCrnt = 1 To NumUsers
        Users(UserCrnt) = FlIn.ReadLine
      Next
      FlIn.Close
      Set FlIn = FlSysObj.OpenTextFile(PathCrnt & "SortedRoles.txt", 1, False, 0)
      ReDim Roles(1 To NumRoles)
      For RoleCrnt = 1 To NumRoles
        Roles(RoleCrnt) = FlIn.ReadLine
      Next
      FlIn.Close
    
      ' Read SortedPerms.txt and generate Report.txt
      ' ===============================================================================================
    
      Set FlIn = FlSysObj.OpenTextFile(PathCrnt & "SortedPerms.txt", 1, False, 0)
    
      ' Replace Report.txt" with your name for the output file
      Set FlOut = FlSysObj.OpenTextFile(PathCrnt & "Report.txt", 2, True, 0)
    
      ' Create and output header row
      FlLine = """User"""
      For RoleCrnt = 1 To NumRoles
        FlLine = FlLine & ",""" & Roles(RoleCrnt) & """"
      Next
      FlOut.WriteLine FlLine
    
      UserCrnt = 0
      RoleCrnt = 0
      UserNameLast = ""
      RoleNameLast = ""
      FlLine = ""
    
      ' Output header row within do loop
    
      Do While Not FlIn.AtEndOfStream
        FlLinePart = Split(FlIn.ReadLine, "|")
        Debug.Assert UBound(FlLinePart) = 1
        If FlLinePart(0) = UserNameLast And FlLinePart(1) = RoleNameLast Then
          ' My test file contains some duplicate permissions
        Else
          ' Process good permission
          If FlLinePart(0) <> UserNameLast Then
            ' New user or first permission
            If FlLine <> "" Then
              ' Output line for last user
              If RoleCrnt = NumRoles Then
                ' Last role already output
              Else
                ' Add Ns for remaining roles
                FlLine = FlLine & Replace(String(NumRoles - RoleCrnt, "N"), "N", ",N")
              End If
              FlOut.WriteLine FlLine
            End If
            UserCrnt = UserCrnt + 1
            FlLine = Users(UserCrnt)       ' Initialise line for new user
            RoleCrnt = 0
          End If
          Do While FlLinePart(0) > Users(UserCrnt)
            ' This user has no permissions. Output line of Ns for it
            FlLine = FlLine & Replace(String(NumRoles, "N"), "N", ",N")
            FlOut.WriteLine FlLine
            UserCrnt = UserCrnt + 1
            FlLine = Users(UserCrnt)
          Loop
          If FlLinePart(0) < Users(UserCrnt) Then
            Debug.Assert False
            ' User for this permission does not appear in user list
            ' Assume this should not be possible.
            ' Output error message if it does
          Else
            ' Have permission for current user
            ' Find entry in Roles() for permiisoin's role
            Do While True
              RoleCrnt = RoleCrnt + 1
              If FlLinePart(1) > Roles(RoleCrnt) Then
                ' This user does not have this current role
                FlLine = FlLine & ",N"
              ElseIf FlLinePart(1) < Roles(RoleCrnt) Then
                Debug.Assert False
                ' Role for this permission does not appear in role list
                ' Assume this should not be possible.
                ' Output error message if it does
              Else
                ' This user has this permission
                FlLine = FlLine & ",Y"
                Exit Do
              End If
            Loop
          End If
        End If
        UserNameLast = FlLinePart(0)
        RoleNameLast = FlLinePart(1)
      Loop  ' For each permission
                ' Add Ns for remaining roles
      FlLine = FlLine & Replace(String(NumRoles - RoleCrnt, "N"), "N", ",N")
      FlOut.WriteLine FlLine        ' Output final line
    
      FlOut.Close
    
      Debug.Print Format(Timer - StartTime, "#,##0.0")
    
    End Sub
    

    .

    Option Explicit
      ' Source http://vbadud.blogspot.co.uk/2007/06/show-all-processes-using-vba.html
      ' Modified by Tony Dallimore
    
      Const TH32CS_SNAPHEAPLIST = &H1
      Const TH32CS_SNAPPROCESS = &H2
      Const TH32CS_SNAPTHREAD = &H4
      Const TH32CS_SNAPMODULE = &H8
      Const TH32CS_SNAPALL = (TH32CS_SNAPHEAPLIST Or _
                              TH32CS_SNAPPROCESS Or _
                              TH32CS_SNAPTHREAD Or _
                              TH32CS_SNAPMODULE)
      Const TH32CS_INHERIT = &H80000000
      Const MAX_PATH As Integer = 260
    
      Private Type PROCESSENTRY32
        dwSize As Long
        cntUsage As Long
        th32ProcessID As Long
        th32DefaultHeapID As Long
        th32ModuleID As Long
        cntThreads As Long
        th32ParentProcessID As Long
        pcPriClassBase As Long
        dwFlags As Long
        szExeFile As String * MAX_PATH
      End Type
    
      Private Declare Function CreateToolhelp32Snapshot Lib "kernel32" _
                          (ByVal lFlags As Long, ByVal lProcessID As Long) As Long
      Private Declare Sub CloseHandle Lib "kernel32" (ByVal hPass As Long)
    
      ' API Functions to get the processes
      Private Declare Function Process32First Lib "kernel32" _
                          (ByVal hSnapShot As Long, uProcess As PROCESSENTRY32) As Long
      Private Declare Function Process32Next Lib "kernel32" _
                          (ByVal hSnapShot As Long, uProcess As PROCESSENTRY32) As Long
    Public Sub GetProcessList(Process() As String)
    
      Dim hSnapShot As Long          '* Handle
      Dim uProcess As PROCESSENTRY32 '* Process
      Dim lRet                       '* Return Val
    
      Dim InxP As Long
      Dim Pos As Long
    
      ReDim Process(1 To 100)
      InxP = 0      ' Array is empty
    
    '  On Error Resume Next
    
      ' Takes a snapshot of the running processes and the heaps, modules,
      ' and threads used by the processes
    
      hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, 0&)
    
      uProcess.dwSize = Len(uProcess)
    
      ' Retrieve information about the first process encountered in our system snapshot
    
      ' uProcess.szExeFile is a fixed length string of 260 characters.  Each new process
      ' name is terminated with &H0 and overwrites the previous name.  Hence the need to
      ' discard the first &H0 and any characters that follow.
    
      ' In the original code, the first process name was ignored.  During my
      ' experimentation, the first name was always "[System Process]" which appears to be
      ' a header.  I continue to discard the first process name
    
      ' In the original code, the final lRet was output before being tested to be true.
      ' This meant the last name was junk.  I always test lRet before extracting the name.
    
      lRet = Process32First(hSnapShot, uProcess)  ' Ignore "[System]"
      lRet = Process32Next(hSnapShot, uProcess)
      ' lRet is 0 or 1.  1 means uProcess has been loaded with another process.
    
      Do While lRet
    
        InxP = InxP + 1
        If InxP > UBound(Process) Then
          ReDim Preserve Process(1 To UBound(Process) + 100)
        End If
    
        Pos = InStr(1, uProcess.szExeFile, Chr$(0))
        If Pos > 0 Then
          Pos = Pos - 1
        Else
          Pos = 0
        End If
        Process(InxP) = Left$(uProcess.szExeFile, Pos)
    
        lRet = Process32Next(hSnapShot, uProcess)
    
      Loop
    
      CloseHandle hSnapShot
    
      ' This ReDim assumes there is at least one process.
      ReDim Preserve Process(1 To InxP)  ' Discard empty entries
    
    End Sub