Tag: <span>VB.NET</span>

After reading this post at StackOverflow, I realized that I was probably wrong to think it was OK to directly access the registry keys that Windows uses to keep track of installed services. Though the names and locations of the keys probably won’t change any time soon, it’s always better to use a higher level of abstraction when dealing with Windows internals. In this case, WMI fits the bill very nicely.

I had not written any WMI-related code up til now, simply because I’ve always been able to find other methods to get the information I need. But after reading that post and knowing of a particular program of mine that does access the registry directly to determine the start mode for a service, I decided to re-write that code to use WMI via the System.Management namespace in the .NET framework.

I came across a couple of different C# samples on the web and was able to modify them to fit my needs. Below is a new class I created for service management that will hopefully grow over time.

Imports System.Management

'''
''' Used to manage Windows services.
''' 
Public Class ServiceManagement
    'Enumeration of service startup modes
    Public Enum ServiceStartMode
        Boot = 0
        System = 1
        Automatic = 2
        Manual = 3
        Disabled = 4
    End Enum

    'Return values for service-related WMI routines
    Public Enum ReturnValue
        Success = 0
        NotSupported = 1
        AccessDenied = 2
        DependentServicesRunning = 3
        InvalidServiceControl = 4
        ServiceCannotAcceptControl = 5
        ServiceNotActive = 6
        ServiceRequestTimeout = 7
        UnknownFailure = 8
        PathNotFound = 9
        ServiceAlreadyRunning = 10
        ServiceDatabaseLocked = 11
        ServiceDependencyDeleted = 12
        ServiceDependencyFailure = 13
        ServiceDisabled = 14
        ServiceLogonFailure = 15
        ServiceMarkedForDeletion = 16
        ServiceNoThread = 17
        StatusCircularDependency = 18
        StatusDuplicateName = 19
        StatusInvalidName = 20
        StatusInvalidParameter = 21
        StatusInvalidServiceAccount = 22
        StatusServiceExists = 23
        ServiceAlreadyPaused = 24
        ServiceNotFound = 25
    End Enum

    '''
    ''' Gets the current start mode for the given service.
    ''' 
    '''Name of an installed service.
    ''' Numerical code indicating the service's start mode.
    Public Shared Function GetStartMode(ByVal serviceName As String) As ServiceStartMode
        Dim service As ManagementObject = Nothing
        Dim servicePath As String
        Dim startMode As String

        GetStartMode = ServiceStartMode.Disabled

        Try
            servicePath = String.Format("Win32_Service.Name=""{0}""", serviceName)
            service = New ManagementObject(servicePath)
            startMode = service.GetPropertyValue("StartMode").ToString

            'We can't return the exact property value because StartMode will
            'show as 'Auto' rather than 'Automatic'. Since the ChangeStartMode 
            'WMI method will need to have 'Automatic' passed when setting the 
            'service to auto-start, our ServiceStartMode enum uses 'Automatic'
            Select Case startMode
                Case "Boot"
                    Return ServiceStartMode.Boot
                Case "System"
                    Return ServiceStartMode.System
                Case "Auto"
                    Return ServiceStartMode.Automatic
                Case "Manual"
                    Return ServiceStartMode.Manual
                Case "Disabled"
                    Return ServiceStartMode.Disabled
            End Select
        Catch
            Throw
        Finally
            If service IsNot Nothing Then
                service.Dispose()
            End If
        End Try
    End Function

    '''
    ''' Sets the start mode for the given service.
    ''' 
    '''Name of an installed service.
    '''Numerical code representing the new start mode.
    ''' Error code indicating the result of the change operation.
    Public Shared Function SetStartMode(ByVal serviceName As String, ByVal startMode As ServiceStartMode) 
                                        As ReturnValue
        Dim service As ManagementObject = Nothing
        Dim inParams As ManagementBaseObject = Nothing
        Dim outParams As ManagementBaseObject = Nothing
        Dim servicePath As String

        SetStartMode = ReturnValue.UnknownFailure

        Try
            servicePath = String.Format("Win32_Service.Name=""{0}""", serviceName)
            service = New ManagementObject(servicePath)
            inParams = service.GetMethodParameters("ChangeStartMode")
            inParams("StartMode") = startMode.ToString()
            outParams = service.InvokeMethod("ChangeStartMode", inParams, Nothing)

            Return DirectCast([Enum].Parse(GetType(ReturnValue), 
                                           outParams("ReturnValue").ToString()), ReturnValue)
        Catch
            Throw
        Finally
            If inParams IsNot Nothing Then
                inParams.Dispose()
            End If

            If outParams IsNot Nothing Then
                outParams.Dispose()
            End If

            If service IsNot Nothing Then
                service.Dispose()
            End If
        End Try
    End Function
End Class

My thanks go out to Mitchel Sellers for informing me that DotNetNuke has a bulk install feature. I had a need to modify an existing Windows service so it could silently update some DNN modules. I was able to make it work in this same service a couple years ago by modifying the DNN 4.3 source code to allow the main DotNetNuke.dll library to function outside of a web application (no easy feat, that). From there I could use the PaInstaller class to load a module. But then it wouldn’t work against 4.9.0 portals.

When I tried to do the same thing with the 4.9.0 source, it failed miserably. It just wouldn’t create an instance of SqlDataProvider, no matter how much I coaxed it. The better way was to have my service download the new module files (zips) to the install/module folder under the portal’s root, create an HttpWebRequest to http://mysite/install/install.aspx?mode=installresources, then parse the response to check for success.

The response parsing was easier than I thought it would be. I did the bulk update first using a web browser so I could see what I was dealing with. The HTML itself was nice in that it put non-blank space elements between each module name and result, so I could use Split() to break everything out. I ignored the first two elements of the resultant array since it contained all the unimportant stuff before the list of modules, then simply looped over the array and verified the word Success was in each even-numbered element, like so:

Dim aResults() As String

aResults = Split(response, "&nbsp;")

If aResults.Length > 0 Then
    For i As Integer = 2 To aResults.Length - 1
        If i Mod 2 = 0 Then
            If Not aResults(i).Contains("Success") Then
                Return False
            End If
        End If
    Next i

    Return True
End If

I’m not sure why I had never heard of this feature over the course of developing several custom DNN modules, even though I’ve spent untold hours sifting through the forums at dotnetnuke.com and the Internet at large for detailed information on the portal’s architecture. In any case, that was the one new thing I learned that day.

A recent project of mine required querying Active Directory for all the groups in which a given user is a member, or if there isn’t a domain then searching the local machine. It also had to return all groups defined in a given domain. In the course of putting the code together, I created a library for future projects that needed to perform such queries. I recently made a major improvement to the code that retrieves a user’s AD groups and thought it would be good to share what I have so far.

It’s based on some bits of code I found on the web. Initially GetADGroupMembership would only return the first-level Active Directory groups the user was in, but that’s no good if the user is in a group that is in another group that is in yet another group that’s the one you really care about. So I added some recursion to bring up the entire group chain. It seems to perform well enough with our domain, which admittedly doesn’t have a great deal of nesting.

Imports System.DirectoryServices

Public Class LDAP
    Public Shared Function GetADGroupMembership(ByVal ldapPath As String, ByVal userName As String) 
                                                As List(Of String)
        Dim lstResults As List(Of String)
        Dim lstGroups As List(Of String)
        Dim lstSubGroups As List(Of String)
        Dim filter As String

        GetADGroupMembership = Nothing

        filter = "(&amp;(objectClass=user)(samAccountName=" &amp; userName &amp; "))"
        lstResults = RunSearch(ldapPath, filter, "MemberOf")
        lstGroups = New List(Of String)

        If lstResults IsNot Nothing Then
            For Each result As String In lstResults
                lstGroups.Add(result)
                lstSubGroups = GetADGroupMembershipByGroup(ldapPath, result)

                For Each subGroup As String In lstSubGroups
                    If Not lstGroups.Contains(subGroup) Then
                        lstGroups.Add(subGroup)
                    End If
                Next subGroup
            Next result
        End If

        Return lstGroups
    End Function

    Public Shared Function GetLocalGroupMembership(ByVal ldapPath As String, ByVal userName As String,
                                                   ByVal password As String) As List(Of String)
        Dim de As DirectoryEntry = Nothing
        Dim colGroups As Object
        Dim lstGroups As List(Of String)

        GetLocalGroupMembership = Nothing

        Try
            de = New DirectoryEntry(ldapPath, userName, password, AuthenticationTypes.Secure)
            colGroups = de.Invoke("Groups")
            lstGroups = New List(Of String)

            For Each o As Object In colGroups
                lstGroups.Add(o.Name)
            Next o

            Return lstGroups
        Catch
            Throw
        Finally
            If de IsNot Nothing Then
                de.Dispose()
            End If
        End Try
    End Function

    Public Shared Function GetADGroups(ByVal ldapPath As String) As List(Of String)
        Dim lstGroups As List(Of String)
        Dim filter As String

        GetADGroups = Nothing

        filter = "(&amp;(objectClass=group))"
        lstGroups = RunSearch(ldapPath, filter)
        lstGroups.Sort()

        Return lstGroups
    End Function

    Private Shared Function GetADGroupMembershipByGroup(ByVal ldapPath As String, ByVal groupName As String) 
                                                        As List(Of String)
        Dim lstResults As List(Of String)
        Dim lstGroups As List(Of String)
        Dim lstSubGroups As List(Of String)
        Dim filter As String

        GetADGroupMembershipByGroup = Nothing

        filter = "(&amp;(objectCategory=group)(cn=" &amp; groupName &amp; "))"
        lstResults = RunSearch(ldapPath, filter, "MemberOf"
        lstGroups = New List(Of String)

        If lstResults IsNot Nothing Then
            For Each result As String In lstResults
                lstGroups.Add(result)

                'Retrieve all groups that the current group is a member of
                lstSubGroups = GetADGroupMembershipByGroup(ldapPath, result)
                For Each subGroup As String In lstSubGroups
                    If Not lstGroups.Contains(subGroup) Then
                        lstGroups.Add(subGroup)
                    End If
                Next subGroup
            Next result
        End If

        Return lstGroups
    End Function

    Private Shared Function RunSearch(ByVal ldapPath As String, ByVal filter As String,
                                      Optional ByVal propertyName As String = "") As List(Of String)
        Dim lstResults As List(Of String)
        Dim de As System.DirectoryServices.DirectoryEntry = Nothing
        Dim deSearcher As System.DirectoryServices.DirectorySearcher = Nothing
        Dim results As System.DirectoryServices.SearchResultCollection
        Dim res As System.DirectoryServices.SearchResult

        RunSearch = Nothing

        Try
            de = New System.DirectoryServices.DirectoryEntry(ldapPath)
            deSearcher = New System.DirectoryServices.DirectorySearcher(de)
            deSearcher.Filter = filter
            deSearcher.SearchScope = SearchScope.Subtree
            results = deSearcher.FindAll

            lstResults = New List(Of String)

            For Each res In results
                If propertyName = "" Then
                    'If no specific property is being sought, simply return the common name
                    lstResults.Add(TrimToName(res.GetDirectoryEntry.Name))
                Else
                    For Each o As Object In res.Properties(propertyName)
                        lstResults.Add(TrimToName(o))
                    Next o
                End If
            Next res

            Return lstResults
        Catch
            Throw
        Finally
            If deSearcher IsNot Nothing Then
                deSearcher.Dispose()
            End If

            If de IsNot Nothing Then
                de.Dispose()
            End If
        End Try
    End Function

    Private Shared Function TrimToName(ByVal path As String) As String
        Dim parts() As String

        parts = path.Split(",")
        Return parts(0).Replace("CN=", String.Empty)
    End Function
End Class

Update:
This library has grown and improved quite a bit over time, and I recently re-wrote it in C# and posted it on GitHub.