Repair-PowerShellGetModuleRoleCapabilities.ps1
<#PSScriptInfo .VERSION 1.0 .GUID aa740796-8b48-4b9f-bb1c-efc4875f5406 .AUTHOR Brian Scholer .COMPANYNAME .COPYRIGHT Brian Scholer .TAGS JEA RoleCapability JustEnoughAdministration .LICENSEURI https://github.com/briantist/jea-role-cap-hack/blob/master/LICENSE .PROJECTURI https://github.com/briantist/jea-role-cap-hack .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES - 1.0: Initial Release .PRIVATEDATA #> <# .DESCRIPTION Hacks around JEAs unawareness of versioned modules See also: https://github.com/PowerShell/PowerShell/issues/4105 Runs as an infinite loop monitoring the filesystem unless used with the -NoMonitor parameter. #> [CmdletBinding()] Param( [Parameter()] [String] [ValidateNotNullOrEmpty()] $ModulePathEnvironmentVariable = 'PSModulePath' , [Parameter()] [UInt32] $EventIntervalSeconds = 10 , [Parameter()] [Switch] $NoMonitor ) function New-Watcher { [CmdletBinding()] param( [Parameter( Mandatory, ValueFromPipeline )] [ValidateNotNullOrEmpty()] [System.IO.DirectoryInfo] $Path , [Parameter( Mandatory )] [ScriptBlock] $Action ) Process { Write-Verbose -Message "Adding watcher for path '$($Path.FullName)'." $FileSystemWatcher = [System.IO.FileSystemWatcher]::new($Path.FullName) $FileSystemWatcher.IncludeSubdirectories = $true $EventHandlers = foreach($EventName in 'Created','Deleted','Renamed') { # Changed is too noisy, not needed for this Register-ObjectEvent -InputObject $FileSystemWatcher -EventName $EventName -Action $Action -MessageData $Path } $FileSystemWatcher.EnableRaisingEvents = $true New-Object -TypeName PSObject -Property @{ Path = $FileSystemWatcher.Path PathInfo = $Path Watcher = $FileSystemWatcher Handlers = $EventHandlers } } } function Remove-Watcher { [CmdletBinding()] param( [Parameter( Mandatory, ValueFromPipeline )] [ValidateNotNullOrEmpty()] [PSObject] $WatcherInfo ) Process { Write-Verbose -Message "Removing watcher for path '$($WatcherInfo.Path)'." foreach ($handler in $WatcherInfo.Handlers) { Unregister-Event -SubscriptionId $handler.Id -Force Remove-Job -Id $handler.Id -Force } $WatcherInfo.Watcher.EnableRaisingEvents = $false $WatcherInfo.Watcher.Dispose() } } $ActionHandler = { param( $ManualPath # Manually passed path, didn't come from event ) # $Info = $Event.SourceEventArgs # $Name = $Info.Name # $FullPath = $Info.FullPath | Get-Item # $OldFullPath = $Info.OldFullPath # $OldName = $Info.OldName # $ChangeType = $Info.ChangeType # $Timestamp = $Event.TimeGenerated $ModPath = if ($Event) { $Event.MessageData } else { $ManualPath } $roleCap = 'RoleCapabilities' # let's not to try actually figure out what happened in this event # instead we'll just process the entire module directory 😬 Write-Verbose -Message "Enumerating path '$($ModPath.FullName)':" foreach ($module in $ModPath.EnumerateDirectories()) { if(@( '{0}.psm1' -f $module.BaseName ,'{0}.psd1' -f $module.BaseName ,'DSCResources' ,'PSGetModuleInfo.xml' ).Where({ $module.FullName | Join-Path -ChildPath $_ | Test-Path })) { # this is a regular module directory (probably) Write-Verbose -Message "-- Skipping '$module' because it seems like like a regular module." continue } $latest = $module.EnumerateDirectories() | Where-Object -FilterScript { $_.BaseName -as [System.Version] } | Sort-Object -Property { $_.BaseName -as [System.Version] } -Descending | Select-Object -First 1 Write-Verbose -Message "-- Latest version of '$module' seems to be '$latest'" $latestRoles = ($latest.FullName | Join-Path -ChildPath $roleCap) -as [System.IO.DirectoryInfo] Write-Verbose -Message "-- Looking for latest roles in '$($latestRoles.FullName)'." if ($latestRoles.Exists) { Write-Verbose -Message "---- found!" } $upperLevelRoles = ($module.FullName | Join-Path -ChildPath $roleCap) -as [System.IO.DirectoryInfo] if ($upperLevelRoles.Exists) { $upperLevelRoles = $upperLevelRoles | Get-Item if ( $upperLevelRoles.LinkType -and $upperLevelRoles.LinkType -eq 'SymbolicLink' -and $upperLevelRoles.Target.Count -eq 1 -and ( -not $latestRoles.Exists -or ( $upperLevelRoles.Target[0].TrimEnd([System.IO.Path]::DirectorySeparatorChar) -ne $latestRoles.FullName.TrimEnd([System.IO.Path]::DirectorySeparatorChar) ) )) { # delete a symlinked role cap folder if the latest module doesn't have one # or if it does and the symlink doesn't point there $upperLevelRoles.Delete() } else { # tf exists but we didn't delete it, so pass Write-Verbose -Message "-- '$($upperLevelRoles.FullName)' exists but won't be deleted or overwritten; skipping." continue } } if ($latestRoles.Exists) { # create the symlink pointing into the latest modules role caps Write-Verbose -Message ("-- Creating symlink from '{0}' to '{1}'." -f $upperLevelRoles.FullName, $latestRoles.FullName) $null = New-Item -Path $upperLevelRoles.FullName -Value $latestRoles.FullName -ItemType SymbolicLink -Force } } } try { $watchers = @{} # forever loop for() { $paths = [System.Environment]::GetEnvironmentVariable( $ModulePathEnvironmentVariable, [System.EnvironmentVariableTarget]::Machine ).Split([System.IO.Path]::PathSeparator) -as [System.IO.DirectoryInfo[]] $keys = $watchers.Keys | Out-String -Stream foreach ($key in $keys) { if ($key -notin $paths.FullName) { Remove-Watcher -WatcherInfo $watchers[$key] $watchers.Remove($key) } } foreach ($path in $paths) { if ($path.FullName -notin $keys) { if ($Path.Exists) { # call the action on the path once so that we don't wait for a change to process what's already in there Invoke-Command -ScriptBlock $ActionHandler -ArgumentList $path if ($NoMonitor) { # don't add a watcher if we're just doing this as a one-time thing continue } $watchers[$path.FullName] = New-Watcher -Path $path -Action $ActionHandler } else { Write-Verbose -Message "Skipping path '$($Path.FullName)' because it doesn't exist or is not a directory." } } } if ($NoMonitor) { # exit the loop if we're not using watchers break } Wait-Event -Timeout $EventIntervalSeconds } } finally { # kill the event handlers Get-EventSubscriber -Force | Unregister-Event -Force } |