PSRedstone.psm1
class Redstone { hidden [string] $_Publisher = $null hidden [string] $_Product = $null hidden [string] $_Version = 'None' hidden [string] $_Action = $null hidden [hashtable] $_CimInstance = $null hidden [hashtable] $_Env = $null hidden [hashtable] $_OS = $null hidden [hashtable] $_ProfileList = $null [int] $ExitCode = 0 [System.Collections.ArrayList] $Exiting = @() [hashtable] $Settings = @{} [bool] $IsElevated = $null # Use the default settings, don't read any of the settings in from the registry. In production this is never set. [bool] $OnlyUseDefaultSettings = $false [hashtable] $Debug = @{} static Redstone() { # Creating some custom setters that update other properties, like Log Paths, when related properties are changed. Update-TypeData -TypeName 'Redstone' -MemberName 'Publisher' -MemberType 'ScriptProperty' -Value { # Getter return $this._Publisher } -SecondValue { param($value) # Setter $this._Publisher = $value $this.SetUpLog() } -Force Update-TypeData -TypeName 'Redstone' -MemberName 'Product' -MemberType 'ScriptProperty' -Value { # Getter return $this._Product } -SecondValue { param($value) # Setter $this._Product = $value $this.SetUpLog() } -Force Update-TypeData -TypeName 'Redstone' -MemberName 'Version' -MemberType 'ScriptProperty' -Value { # Getter return $this._Version } -SecondValue { param($value) # Setter $this._Version = $value $this.SetUpLog() } -Force Update-TypeData -TypeName 'Redstone' -MemberName 'Action' -MemberType 'ScriptProperty' -Value { # Getter return $this._Action } -SecondValue { param($value) # Setter $this._Action = $value $this.SetUpLog() } -Force Update-TypeData -TypeName 'Redstone' -MemberName 'CimInstance' -MemberType 'ScriptProperty' -Value { # Getter $className = $MyInvocation.Line.Split('.')[2] return $this.GetCimInstance($className, $true) } -Force Update-TypeData -TypeName 'Redstone' -MemberName 'Env' -MemberType 'ScriptProperty' -Value { # Getter if (-not $this._Env) { # This is the Lazy Loading logic. $this.SetUpEnv() } return $this._Env } -Force Update-TypeData -TypeName 'Redstone' -MemberName 'OS' -MemberType 'ScriptProperty' -Value { # Getter if (-not $this._OS) { # This is the Lazy Loading logic. $this.SetUpOS() } return $this._OS } -Force Update-TypeData -TypeName 'Redstone' -MemberName 'ProfileList' -MemberType 'ScriptProperty' -Value { # Getter if (-not $this._ProfileList) { # This is the Lazy Loading logic. $this.SetUpProfileList() } return $this._ProfileList } -Force } Redstone() { $this.SetUpSettings() $this.Settings.JSON = @{} $settingsFiles = @( [IO.FileInfo] ([IO.Path]::Combine($PWD.ProviderPath, 'settings.json')) [IO.FileInfo] ([IO.Path]::Combine(([IO.FileInfo] $this.Debug.PSCallStack[2].ScriptName).Directory.FullName, 'settings.json')) [IO.FileInfo] ([IO.Path]::Combine(([IO.DirectoryInfo] $PWD.ProviderPath).Parent, 'settings.json')) [IO.FileInfo] ([IO.Path]::Combine(([IO.FileInfo] $this.Debug.PSCallStack[2].ScriptName).Directory.Parent.FullName, 'settings.json')) ) foreach ($location in $settingsFiles) { if ($location.Exists) { $this.Settings.JSON.File = $location $this.Settings.JSON.Data = Get-Content $this.Settings.JSON.File.FullName | ConvertFrom-Json break } } if (-not $this.Settings.JSON.File.Exists) { Throw [System.IO.FileNotFoundException] ('Could NOT find settings file in any of these locations: {0}' -f ($settingsFiles.FullName -join ', ')) } $this.SetDefaultSettingsFromRegistry($this.Settings.Registry.Key) $this.SetPSDefaultParameterValues($this.Settings.Functions) $this.set__Publisher($this.Settings.JSON.Data.Publisher) $this.set__Product($this.Settings.JSON.Data.Product) $this.set__Version($this.Settings.JSON.Data.Version) $this.set__Action($( if ($this.Settings.JSON.Data.Action) { $this.Settings.JSON.Data.Action } else { ([IO.FileInfo] $this.Debug.PSCallStack[2].ScriptName).BaseName } )) $this.SetUpLog() } Redstone([IO.FileInfo] $Settings) { $this.SetUpSettings() $this.Settings.JSON = @{} $this.Settings.JSON.File = [IO.FileInfo] $Settings if ($this.Settings.JSON.File.Exists) { $this.Settings.JSON.Data = Get-Content $this.Settings.JSON.File.FullName | ConvertFrom-Json } else { Throw [System.IO.FileNotFoundException] $this.Settings.JSON.File.FullName } $this.SetDefaultSettingsFromRegistry($this.Settings.Registry.Key) $this.SetPSDefaultParameterValues($this.Settings.Functions) $this.set__Publisher($this.Settings.JSON.Data.Publisher) $this.set__Product($this.Settings.JSON.Data.Product) $this.set__Version($this.Settings.JSON.Data.Version) $this.set__Action($( if ($this.Settings.JSON.Data.Action) { $this.Settings.JSON.Data.Action } else { ([IO.FileInfo] $this.Debug.PSCallStack[2].ScriptName).BaseName } )) $this.SetUpLog() } Redstone([string] $Publisher, [string] $Product, [string] $Version, [string] $Action) { $this.SetUpSettings() $this.SetDefaultSettingsFromRegistry($this.Settings.Registry.Key) $this.SetPSDefaultParameterValues($this.Settings.Functions) $this.set__Publisher($Publisher) $this.set__Product($Product) $this.set__Version($Version) $this.set__Action($Action) $this.SetUpLog() } hidden [object] GetCimInstance($ClassName) { return $this.GetCimInstance($ClassName, $false, $false) } hidden [object] GetCimInstance($ClassName, $ReturnCimInstanceNotClass) { return $this.GetCimInstance($ClassName, $ReturnCimInstanceNotClass, $false) } hidden [object] GetCimInstance($ClassName, $ReturnCimInstanceNotClass, $Refresh) { # This is the Lazy Loading logic. if (-not $this._CimInstance) { $this._CimInstance = @{} } if ($Refresh -or ($ClassName -and -not $this._CimInstance.$ClassName)) { $this._CimInstance.Set_Item($ClassName, (Get-CimInstance -ClassName $ClassName -ErrorAction 'Ignore')) } if ($ReturnCimInstanceNotClass) { return $this._CimInstance } else { return $this._CimInstance.$ClassName } } [object] CimInstanceRefreshed($ClassName) { return $this.GetCimInstance($ClassName, $false, $true) } hidden [bool] Is64BitOperatingSystem() { if ('Is64BitOperatingSystem' -in $this.Debug.Keys) { return $this.Debug.Is64BitOperatingSystem } else { return ([System.Environment]::Is64BitOperatingSystem) } } hidden [System.Collections.DictionaryEntry] Is64BitOperatingSystem([bool] $Override) { # Used for Pester Testing $this.Debug.Is64BitOperatingSystem = $Override return ($this.Debug.GetEnumerator() | Where-Object{ $_.Name -eq 'Is64BitOperatingSystem' }) } hidden [bool] Is64BitProcess() { if ('Is64BitProcess' -in $this.Debug.Keys) { return $this.Debug.Is64BitProcess } else { return ([System.Environment]::Is64BitProcess) } } hidden [System.Collections.DictionaryEntry] Is64BitProcess([bool] $Override) { # Used for Pester Testing $this.Debug.Is64BitProcess = $Override return ($this.Debug.GetEnumerator() | Where-Object{ $_.Name -eq 'Is64BitProcess' }) } hidden [void] SetUpEnv() { # This section $this._Env = @{} if ($this.Is64BitOperatingSystem()) { # x64 OS if ($this.Is64BitProcess()) { # x64 Process $this._Env.CommonProgramFiles = $env:CommonProgramFiles $this._Env.'CommonProgramFiles(x86)' = ${env:CommonProgramFiles(x86)} $this._Env.PROCESSOR_ARCHITECTURE = $env:PROCESSOR_ARCHITECTURE $this._Env.ProgramFiles = $env:ProgramFiles $this._Env.'ProgramFiles(x86)' = ${env:ProgramFiles(x86)} $this._Env.System32 = "${env:SystemRoot}\System32" $this._Env.SysWOW64 = "${env:SystemRoot}\SysWOW64" } else { # Running as x86 on x64 OS $this._Env.CommonProgramFiles = $env:CommonProgramW6432 $this._Env.'CommonProgramFiles(x86)' = ${env:CommonProgramFiles(x86)} $this._Env.PROCESSOR_ARCHITECTURE = $env:PROCESSOR_ARCHITEW6432 $this._Env.ProgramFiles = $env:ProgramW6432 $this._Env.'ProgramFiles(x86)' = ${env:ProgramFiles(x86)} $this._Env.System32 = "${env:SystemRoot}\SysNative" $this._Env.SysWOW64 = "${env:SystemRoot}\SysWOW64" } } else { # x86 OS $this._Env.CommonProgramFiles = $env:CommonProgramFiles $this._Env.'CommonProgramFiles(x86)' = $env:CommonProgramFiles $this._Env.PROCESSOR_ARCHITECTURE = $env:PROCESSOR_ARCHITECTURE $this._Env.ProgramFiles = $env:ProgramFiles $this._Env.'ProgramFiles(x86)' = $env:ProgramFiles $this._Env.System32 = "${env:SystemRoot}\System32" $this._Env.SysWOW64 = "${env:SystemRoot}\System32" } } hidden [void] SetUpLog() { $this.Settings.Log = @{} if ($this.IsElevated) { $private:Directory = [IO.DirectoryInfo] "${env:SystemRoot}\Logs\Redstone" } else { $private:Directory = [IO.DirectoryInfo] "${env:Temp}\Logs\Redstone" } if (-not $private:Directory.Exists) { New-Item -ItemType 'Directory' -Path $private:Directory.FullName -Force | Out-Null $private:Directory.Refresh() } $this.Settings.Log.File = [IO.FileInfo] (Join-Path $private:Directory.FullName ('{0} {1} {2} {3}.log' -f $this.Publisher, $this.Product, $this.Version, $this.Action)) $this.Settings.Log.FileF = (Join-Path $private:Directory.FullName ('{0} {1} {2} {3}{{0}}.log' -f $this.Publisher, $this.Product, $this.Version, $this.Action)) -as [string] $this.PSDefaultParameterValuesSetUp() } hidden [void] SetUpSettings() { $this.Debug = @{ MyInvocation = $MyInvocation PSCallStack = (Get-PSCallStack) } $this.IsElevated = (New-Object System.Security.Principal.WindowsPrincipal([System.Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) $this.Settings = @{} $this.Settings.Registry = @{ Key = Get-RedstoneRegistryValueOrDefault ([string]::Empty) 'RegistryKeyRoot' 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\PSRedstone' -RegistryKeyRoot 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\PSRedstone' } } hidden [void] SetUpOS() { $this._OS = @{} [bool] $this._OS.Is64BitOperatingSystem = [System.Environment]::Is64BitOperatingSystem [bool] $this._OS.Is64BitProcess = [System.Environment]::Is64BitProcess [bool] $this._OS.Is64BitProcessor = ($this.GetCimInstance('Win32_Processor')| Where-Object { $_.DeviceID -eq 'CPU0' }).AddressWidth -eq '64' [bool] $this._OS.IsMachinePartOfDomain = $this.GetCimInstance('Win32_ComputerSystem').PartOfDomain [string] $this._OS.MachineWorkgroup = $null [string] $this._OS.MachineADDomain = $null [string] $this._OS.LogonServer = $null [string] $this._OS.MachineDomainController = $null if ($this._OS.IsMachinePartOfDomain) { [string] $this._OS.MachineADDomain = $this.GetCimInstance('Win32_ComputerSystem').Domain | Where-Object { $_ } | ForEach-Object { $_.ToLower() } try { [string] $this._OS.LogonServer = $env:LOGONSERVER | Where-Object { (($_) -and (-not $_.Contains('\\MicrosoftAccount'))) } | ForEach-Object { $_.TrimStart('\') } | ForEach-Object { ([System.Net.Dns]::GetHostEntry($_)).HostName } [string] $this._OS.MachineDomainController = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().FindDomainController().Name } catch { Write-Verbose 'Not in AD' } } else { [string] $this._OS.MachineWorkgroup = $this.GetCimInstance('Win32_ComputerSystem').Domain | Where-Object { $_ } | ForEach-Object { $_.ToUpper() } } [string] $this._OS.MachineDNSDomain = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties().DomainName | Where-Object { $_ } | ForEach-Object { $_.ToLower() } [string] $this._OS.UserDNSDomain = $env:USERDNSDOMAIN | Where-Object { $_ } | ForEach-Object { $_.ToLower() } [string] $this._OS.UserDomain = $env:USERDOMAIN | Where-Object { $_ } | ForEach-Object { $_.ToUpper() } [string] $this._OS.Name = $this.GetCimInstance('Win32_OperatingSystem').Name.Trim() [string] $this._OS.ShortName = (($this._OS.Name).Split('|')[0] -replace '\w+\s+(Windows [\d\.]+\s+\w+)', '$1').Trim() [string] $this._OS.ShorterName = (($this._OS.Name).Split('|')[0] -replace '\w+\s+(Windows [\d\.]+)\s+\w+', '$1').Trim() [string] $this._OS.ServicePack = $this.GetCimInstance('Win32_OperatingSystem').CSDVersion [version] $this._OS.Version = [System.Environment]::OSVersion.Version # Get the operating system type [int32] $this._OS.ProductType = $this.GetCimInstance('Win32_OperatingSystem').ProductType [bool] $this._OS.IsServerOS = [bool]($this._OS.ProductType -eq 3) [bool] $this._OS.IsDomainControllerOS = [bool]($this._OS.ProductType -eq 2) [bool] $this._OS.IsWorkStationOS = [bool]($this._OS.ProductType -eq 1) Switch ($this._OS.ProductType) { 1 { [string] $this._OS.ProductTypeName = 'Workstation' } 2 { [string] $this._OS.ProductTypeName = 'Domain Controller' } 3 { [string] $this._OS.ProductTypeName = 'Server' } Default { [string] $this._OS.ProductTypeName = 'Unknown' } } } hidden [void] SetUpProfileList() { Write-Debug 'GETTER: ProfileList' if (-not $this._ProfileList) { Write-Debug 'GETTER: Setting up ProfileList' $this._ProfileList = @{} $regProfileListPath = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' $regProfileList = Get-Item $regProfileListPath foreach ($property in $regProfileList.Property) { $value = if ($dirInfo = (Get-ItemProperty -Path $regProfileListPath).$property -as [IO.DirectoryInfo]) { $dirInfo } else { (Get-ItemProperty -Path $regProfileListPath).$property } $this._ProfileList.Add($property, $value) } [System.Collections.ArrayList] $this._ProfileList.Profiles = @() foreach ($userProfile in (Get-ChildItem $regProfileListPath)) { [hashtable] $user = @{} $user.Add('SID', $userProfile.PSChildName) $user.Add('Path', ((Get-ItemProperty "${regProfileListPath}\$($userProfile.PSChildName)").ProfileImagePath -as [IO.DirectoryInfo])) $objSID = New-Object System.Security.Principal.SecurityIdentifier($user.SID) try { $objUser = $objSID.Translate([System.Security.Principal.NTAccount]) $domainUsername = $objUser.Value } catch [System.Management.Automation.MethodInvocationException] { Write-Warning "Unable to translate the SID ($($user.SID)) to a Username." $domainUsername = $null } $domain, $username = $domainUsername.Split('\') try { $user.Add('Domain', $domain.Trim()) } catch { $user.Add('Domain', $null) } try { $user.Add('Username', $username.Trim()) } catch { $user.Add('Username', $domainUsername) } ($this._ProfileList.Profiles).Add($user) | Out-Null } } } hidden [void] PSDefaultParameterValuesSetUp() { $global:PSDefaultParameterValues.Set_Item('*-Redstone*:LogFile', $this.Settings.Log.File.FullName) $global:PSDefaultParameterValues.Set_Item('*-Redstone*:LogFileF', $this.Settings.Log.FileF) $global:PSDefaultParameterValues.Set_Item('*-Redstone*:LogFileF', $this.Settings.Log.FileF) $global:PSDefaultParameterValues.Set_Item('Get-RedstoneRegistryValueOrDefault:OnlyUseDefaultSettings', (Get-RedstoneRegistryValueOrDefault 'Settings\Functions\Get-RedstoneRegistryValueOrDefault' 'OnlyUseDefaultSettings' $false -RegistryKeyRoot $this.Settings.Registry.Key)) $global:PSDefaultParameterValues.Set_Item('Get-RedstoneRegistryValueOrDefault:RegistryKeyRoot', $this.Settings.Registry.Key) $global:PSDefaultParameterValues.Set_Item('Write-Log:FilePath', $this.Settings.Log.File.FullName) } hidden [psobject] GetRegOrDefault($RegistryKey, $RegistryValue, $DefaultValue) { Write-Verbose "[Redstone GetRegOrDefault] > $($MyInvocation.BoundParameters | ConvertTo-Json -Compress)" Write-Debug "[Redstone GetRegOrDefault] Function Invocation: $($MyInvocation | Out-String)" if ($this.OnlyUseDefaultSettings) { Write-Verbose "[Redstone GetRegOrDefault] OnlyUseDefaultSettings Set; Returning: ${DefaultValue}" return $DefaultValue } try { $ret = Get-ItemPropertyValue -Path ('{0}\{1}' -f $this.RegistryKeyRoot, $RegistryKey) -Name $RegistryValue -ErrorAction 'Stop' Write-Verbose "[Redstone GetRegOrDefault] Registry Set; Returning: ${ret}" return $ret } catch [System.Management.Automation.PSArgumentException] { Write-Verbose "[Redstone GetRegOrDefault] Registry Not Set; Returning Default: ${DefaultValue}" $Error.RemoveAt(0) # This isn't a real error, so I don't want it in the error record. return $DefaultValue } catch [System.Management.Automation.ItemNotFoundException] { Write-Verbose "[Redstone GetRegOrDefault] Registry Not Set; Returning Default: ${DefaultValue}" $Error.RemoveAt(0) # This isn't a real error, so I don't want it in the error record. return $DefaultValue } } [string] GetRegValueDoNotExpandEnvironmentNames($Key, $Value) { $item = Get-Item $Key if ($item) { return $item.GetValue($Value, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) } else { return $null } } [psobject] GetSpecialFolders() { $specialFolders = [ordered] @{} foreach ($folder in ([Environment+SpecialFolder]::GetNames([Environment+SpecialFolder]) | Sort-Object)) { $specialFolders.Add($folder, $this.GetSpecialFolder($folder)) } return ([psobject] $specialFolders) } [IO.DirectoryInfo] GetSpecialFolder([string] $Name) { return ([Environment]::GetFolderPath($Name) -as [IO.DirectoryInfo]) } [void] Quit() { Write-Debug ('[Redstone.Quit 0] > {0}' -f ($MyInvocation | Out-String)) [void] $this.Quit(0, $true , 0) } [void] Quit($ExitCode = 0) { Write-Verbose ('[Redstone.Quit 1] > {0}' -f ($MyInvocation | Out-String)) $this.ExitCode = if ($ExitCode -eq 'line_number') { (Get-PSCallStack)[1].Location.Split(':')[1].Replace('line', '') -as [int] } else { $ExitCode } [void] $this.Quit($this.ExitCode, $false , 55550000) } [void] Quit($ExitCode = 0, [boolean] $ExitCodeAdd = $false) { Write-Verbose ('[Redstone.Quit 1] > {0}' -f ($MyInvocation | Out-String)) $this.ExitCode = if ($ExitCode -eq 'line_number') { (Get-PSCallStack)[1].Location.Split(':')[1].Replace('line', '') -as [int] } else { $ExitCode } [void] $this.Quit($this.ExitCode, $ExitCodeAdd , 55550000) } [void] Quit($ExitCode = 0, [boolean] $ExitCodeAdd = $false, [int] $ExitCodeErrorBase = 55550000) { Write-Debug ('[Redstone.Quit 3] > {0}' -f ($MyInvocation | Out-String)) Write-Verbose ('[Redstone.Quit] ExitCode: {0}' -f $ExitCode) $this.ExitCode = if ($ExitCode -eq 'line_number') { (Get-PSCallStack)[1].Location.Split(':')[1].Replace('line', '') -as [int] } else { $ExitCode -as [int] } if ($ExitCodeAdd) { Write-Information ('[Redstone.Quit] ExitCodeErrorBase: {0}' -f $ExitCodeErrorBase) if (($this.ExitCode -lt 0) -and ($ExitCodeErrorBase -gt 0)) { # Always Exit positive Write-Verbose ('[Redstone.Quit] ExitCodeErrorBase: {0}' -f $ExitCodeErrorBase) $ExitCodeErrorBase = $ExitCodeErrorBase * -1 Write-Verbose ('[Redstone.Quit] ExitCodeErrorBase: {0}' -f $ExitCodeErrorBase) } if (([string] $this.ExitCode).Length -gt 4) { Write-Warning "[Redstone.Quit] ExitCode should not be added to Base when more than 4 digits. Doing it anyway ..." } if ($this.ExitCode -eq 0) { Write-Warning "[Redstone.Quit] ExitCode 0 being added may cause failure; not sure if this is expected. Doing it anyway ..." } $this.ExitCode = $this.ExitCode + $ExitCodeErrorBase } Write-Information ('[Redstone.Quit] ExitCode: {0}' -f $this.ExitCode) # Debug.Quit.DoNotExit is used in Pester testing. if (-not $this.Debug.Quit.DoNotExit) { $global:Host.SetShouldExit($ExitCode) Exit $ExitCode } } <# Dig through the Registry Key and import all the Keys and Values into the $global:Redstone objet. There's a fundamental flaw that I haven't addressed yet. - if there's a value and sub-key with the same name at the same key level, the sub-key won't be processed. #> hidden [void] SetDefaultSettingsFromRegistry([string] $Key) { if (Test-Path $Key) { $this.SetDefaultSettingsFromRegistrySubKey($this.Settings, $Key) foreach ($item in (Get-ChildItem $Key -Recurse -ErrorAction 'Ignore')) { $private:psPath = $item.PSPath.Split(':')[-1].Replace($Key.Split(':')[-1], $null) $private:node = $this.Settings foreach ($child in ($private:psPath.Trim('\').Split('\'))) { if (-not $node.$child) { [hashtable] $node.$child = @{} } $node = $node.$child } $this.SetDefaultSettingsFromRegistrySubKey($node, $item.PSPath) } } } hidden [void] SetDefaultSettingsFromRegistrySubKey([hashtable] $Hash, [string] $Key) { foreach ($regValue in (Get-Item $Key -ErrorAction 'Ignore').Property) { $Hash.Set_Item($regValue, (Get-ItemProperty -Path $Key -Name $regValue).$regValue) } } hidden [void] SetPSDefaultParameterValues([hashtable] $FunctionParameters) { if ($FunctionParameters) { foreach ($function in $FunctionParameters.GetEnumerator()) { Write-Debug ('[Redstone::SetPSDefaultParameterValues] Function Type: [{0}]' -f $function.GetType().FullName) Write-Debug ('[Redstone::SetPSDefaultParameterValues] Function: {0}: {1}' -f $function.Name, ($function.Value | ConvertTo-Json)) foreach ($parameter in $function.Value.GetEnumerator()) { Write-Debug ('[Redstone::SetPSDefaultParameterValues] Parameter: {0}: {1}' -f $parameter.Name, ($parameter.Value | ConvertTo-Json)) Write-Debug ('[Redstone::SetPSDefaultParameterValues] PSDefaultParameterValues: {0}:{1} :: {2}' -f $function.Name, $parameter.Name, $parameter.Value) $global:PSDefaultParameterValues.Set_Item(('{0}:{1}' -f $function.Name, $parameter.Name), $parameter.Value) } } } } } <# .SYNOPSIS Is the current process elevated (running as administrator)? .OUTPUTS [bool] .EXAMPLE PS > Assert-RedstoneIsElevated True #> function Assert-RedstoneIsElevated { [OutputType([bool])] [CmdletBinding()] Param() Write-Verbose ('[Assert-RedstoneIsElevated] >') Write-Debug ('[Assert-RedstoneIsElevated] > {0}' -f ($MyInvocation | Out-String)) $isElevated = (New-Object System.Security.Principal.WindowsPrincipal([System.Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) Write-Verbose ('[Assert-RedstoneIsElevated] IsElevated: {0}' -f $isElevated) return $isElevated } <# .SYNOPSIS Wait, up to a timeout value, to check if current thread is able to acquire an exclusive lock on a system mutex. .DESCRIPTION A mutex can be used to serialize applications and prevent multiple instances from being opened at the same time. Wait, up to a timeout (default is 1 millisecond), for the mutex to become available for an exclusive lock. .PARAMETER MutexName The name of the system mutex. .PARAMETER MutexWaitTimeInMilliseconds The number of milliseconds the current thread should wait to acquire an exclusive lock of a named mutex. Default is: $Redstone.Settings.'Test-RedstoneIsMutexAvailable'.MutexWaitTimeInMilliseconds A wait time of -1 milliseconds means to wait indefinitely. A wait time of zero does not acquire an exclusive lock but instead tests the state of the wait handle and returns immediately. .EXAMPLE Assert-RedstoneIsMutexAvailable -MutexName 'Global\_MSIExecute' -MutexWaitTimeInMilliseconds 500 .EXAMPLE Assert-RedstoneIsMutexAvailable -MutexName 'Global\_MSIExecute' -MutexWaitTimeInMilliseconds (New-TimeSpan -Minutes 5).TotalMilliseconds .EXAMPLE Assert-RedstoneIsMutexAvailable -MutexName 'Global\_MSIExecute' -MutexWaitTimeInMilliseconds (New-TimeSpan -Seconds 60).TotalMilliseconds .NOTES This is an internal script function and should typically not be called directly. .LINK http://msdn.microsoft.com/en-us/library/aa372909(VS.85).asp http://psappdeploytoolkit.com #> function Assert-RedstoneIsMutexAvailable { [OutputType([bool])] [CmdletBinding()] Param ( [Parameter(Mandatory=$true)] [ValidateLength(1,260)] [string] $MutexName, [Parameter(Mandatory=$false)] [ValidateRange(-1,[int32]::MaxValue)] [int32] $MutexWaitTimeInMilliseconds = 300000 #5min ) Write-Information "> $($MyInvocation.BoundParameters | ConvertTo-Json -Compress)" Write-Debug "Function Invocation: $($MyInvocation | Out-String)" ## Initialize Variables [timespan] $MutexWaitTime = [timespan]::FromMilliseconds($MutexWaitTimeInMilliseconds) if ($MutexWaitTime.TotalMinutes -ge 1) { [string] $WaitLogMsg = "$($MutexWaitTime.TotalMinutes) minute(s)" } elseif ($MutexWaitTime.TotalSeconds -ge 1) { [string] $WaitLogMsg = "$($MutexWaitTime.TotalSeconds) second(s)" } else { [string] $WaitLogMsg = "$($MutexWaitTime.Milliseconds) millisecond(s)" } [boolean] $IsUnhandledException = $false [boolean] $IsMutexFree = $false [Threading.Mutex] $OpenExistingMutex = $null Write-Information "Check to see if mutex [$MutexName] is available. Wait up to [$WaitLogMsg] for the mutex to become available." try { ## Using this variable allows capture of exceptions from .NET methods. Private scope only changes value for current function. $private:previousErrorActionPreference = $ErrorActionPreference $ErrorActionPreference = 'Stop' ## Open the specified named mutex, if it already exists, without acquiring an exclusive lock on it. If the system mutex does not exist, this method throws an exception instead of creating the system object. [Threading.Mutex] $OpenExistingMutex = [Threading.Mutex]::OpenExisting($MutexName) ## Attempt to acquire an exclusive lock on the mutex. Use a Timespan to specify a timeout value after which no further attempt is made to acquire a lock on the mutex. $IsMutexFree = $OpenExistingMutex.WaitOne($MutexWaitTime, $false) } catch [Threading.WaitHandleCannotBeOpenedException] { ## The named mutex does not exist $IsMutexFree = $true } catch [ObjectDisposedException] { ## Mutex was disposed between opening it and attempting to wait on it $IsMutexFree = $true } catch [UnauthorizedAccessException] { ## The named mutex exists, but the user does not have the security access required to use it $IsMutexFree = $false } catch [Threading.AbandonedMutexException] { ## The wait completed because a thread exited without releasing a mutex. This exception is thrown when one thread acquires a mutex object that another thread has abandoned by exiting without releasing it. $IsMutexFree = $true } catch { $IsUnhandledException = $true ## Return $true, to signify that mutex is available, because function was unable to successfully complete a check due to an unhandled exception. Default is to err on the side of the mutex being available on a hard failure. Write-Error "Unable to check if mutex [$MutexName] is available due to an unhandled exception. Will default to return value of [$true]. `n$(Resolve-Error)" $IsMutexFree = $true } finally { if ($IsMutexFree) { if (-not $IsUnhandledException) { Write-Information "Mutex [$MutexName] is available for an exclusive lock." } } else { if ($MutexName -eq 'Global\_MSIExecute') { ## Get the command line for the MSI installation in progress try { [string] $msiInProgressCmdLine = Get-CimInstance -Class 'Win32_Process' -Filter "name = 'msiexec.exe'" -ErrorAction 'Stop' | Where-Object { $_.CommandLine } | Select-Object -ExpandProperty 'CommandLine' | Where-Object { $_ -match '\.msi' } | ForEach-Object { $_.Trim() } } catch { Write-Warning ('Unexpected/Unhandled Error caught: {0}' -f $_) } Write-Warning "Mutex [$MutexName] is not available for an exclusive lock because the following MSI installation is in progress [$msiInProgressCmdLine]." } else { Write-Information "Mutex [$MutexName] is not available because another thread already has an exclusive lock on it." } } if (($null -ne $OpenExistingMutex) -and ($IsMutexFree)) { ## Release exclusive lock on the mutex $null = $OpenExistingMutex.ReleaseMutex() $OpenExistingMutex.Close() } if ($private:previousErrorActionPreference) { $ErrorActionPreference = $private:previousErrorActionPreference } } return $IsMutexFree } function Assert-RedstoneIsNonInteractiveShell { # Test each Arg for match of abbreviated '-NonInteractive' command. $NonInteractive = [Environment]::GetCommandLineArgs() | Where-Object{ $_ -like '-NonI*' } if ([Environment]::UserInteractive -and -not $NonInteractive) { # We are in an interactive shell. return $false } return $true } #Requires -RunAsAdministrator function Dismount-RedstoneWim { [CmdletBinding()] param ( # Specifies a path to one or more locations. [Parameter( Mandatory=$false, Position=0, ParameterSetName="ParameterSetName", ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, HelpMessage="Path to one or more locations." )] [ValidateNotNullOrEmpty()] [IO.DirectoryInfo] $MountPath = ([IO.Path]::Combine($PWD, 'RedstoneMount')), [Parameter(Mandatory = $false)] [IO.FileInfo] $LogFileF ) begin { Write-Verbose "[Dismount-RedstoneWim] > $($MyInvocation.BoundParameters | ConvertTo-Json -Compress)" Write-Debug "[Dismount-RedstoneWim] Function Invocation: $($MyInvocation | Out-String)" $windowsImage = @{ Path = $MountPath.FullName Discard = $true ErrorAction = 'Stop' } if ($LogFileF) { $windowsImage.Add('LogPath', ($LogFileF -f 'DISM')) } <# Script used inside of the Scheduled Task that's created, if needed. #> $mounted = { $mountedInvalid = Get-WindowsImage -Mounted | Where-Object { $_.MountStatus -eq 'Invalid' } $errorOccured = $false foreach ($mountedWim in $mountedInvalid) { $windowsImage = @{ Path = $mountedWim.Path Discard = $true ErrorAction = 'Stop' } try { Dismount-WindowsImage @windowsImage } catch { $errorOccured = $true } } if (-not $errorOccured) { Clear-WindowsCorruptMountPoint Unregister-ScheduledTask -TaskName 'Redstone Cleanup WIM' -Confirm:$false } } $encodedCommand = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($mounted.ToString())) $cleanupTaskAction = @{ Execute = 'powershell.exe' Argument = '-Exe Bypass -Win Hidden -NoProfile -NonInteractive -EncodedCommand {0}' -f $encodedCommand.tostring() } } process { ## dismount the WIM whether we succeeded or failed try { Write-Verbose "[Dismount-RedstoneWim] Dismount-WindowImage: $($windowsImage | ConvertTo-Json)" Dismount-WindowsImage @windowsImage } catch [System.Runtime.InteropServices.COMException] { Write-Warning ('[Dismount-RedstoneWim] [{0}] {1}' -f $_.Exception.GetType().FullName, $_.Exception.Message) if ($_.Exception.Message -eq 'The system cannot find the file specified.') { Throw $_ } else { # $_.Exception.Message -eq 'The system cannot find the file specified.' ## failed to cleanly dismount, so set a task to cleanup after reboot Write-Verbose ('[Dismount-RedstoneWim] Scheduled Task Action: {0}' -f ($cleanupTaskAction | ConvertTo-Json)) $scheduledTaskAction = New-ScheduledTaskAction @cleanupTaskAction $scheduledTaskTrigger = New-ScheduledTaskTrigger -AtStartup $scheduledTask = @{ Action = $scheduledTaskAction Trigger = $scheduledTaskTrigger TaskName = 'Redstone Cleanup WIM' Description = 'Clean up WIM Mount points that failed to dismount properly.' User = 'NT AUTHORITY\SYSTEM' RunLevel = 'Highest' Force = $true } Write-Verbose ('[Dismount-RedstoneWim] Scheduled Task: {0}' -f ($scheduledTask | ConvertTo-Json)) Register-ScheduledTask @scheduledTask } } $clearWindowsCorruptMountPoint = @{} if ($LogFileF) { $windowsImage.Add('LogPath', ($LogFileF -f ('DISM'))) } Clear-WindowsCorruptMountPoint @clearWindowsCorruptMountPoint } end {} } <# .SYNOPSIS Retrieves information about installed applications. .DESCRIPTION Retrieves information about installed applications by querying the registry. You can specify an application name, a product code, or both. Returns information about application publisher, name & version, product code, uninstall string, quiet uninstall string, install source, location, date, and application architecture. .PARAMETER Name The name of the application to retrieve information for. Performs a regex match on the application display name by default. .PARAMETER Exact Specifies that the named application must be matched using the exact name. .PARAMETER WildCard Specifies that the named application must be matched using a wildcard search. .PARAMETER ProductCode The product code of the application to retrieve information for. .PARAMETER IncludeUpdatesAndHotfixes Include matches against updates and hotfixes in results. .PARAMETER UninstallRegKeys Default: `$global:Redstone.Settings.Functions.Get-InstalledApplication.UninstallRegKeys` Private Parameter; used for debug overrides. .EXAMPLE Get-RedstoneInstalledApplication -Name 'Adobe Flash' .EXAMPLE Get-RedstoneInstalledApplication -ProductCode '{1AD147D0-BE0E-3D6C-AC11-64F6DC4163F1}' .NOTES .LINK http://psappdeploytoolkit.com #> Function Get-RedstoneInstalledApplication { [CmdletBinding()] Param ( [Parameter(Mandatory=$false)] [ValidateNotNullorEmpty()] [string[]] $Name, [Parameter(Mandatory=$false)] [switch] $Exact = $false, [Parameter(Mandatory=$false)] [switch] $WildCard, [Parameter(Mandatory=$false)] [ValidateNotNullorEmpty()] [string] $ProductCode, [Parameter(Mandatory=$false)] [switch] $IncludeUpdatesAndHotfixes, [Parameter(Mandatory=$false, HelpMessage="Private Parameter; used for debug overrides.")] [ValidateNotNullorEmpty()] [string[]] $UninstallRegKeys = @( 'HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall', 'HKLM:SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall' ) ) Write-Information "[Get-RedstoneInstalledApplication] > $($MyInvocation.BoundParameters | ConvertTo-Json -Compress)" Write-Debug "[Get-RedstoneInstalledApplication] Function Invocation: $($MyInvocation | Out-String)" if ($Name) { Write-Information "[Get-RedstoneInstalledApplication] Get information for installed Application Name(s) [$($name -join ', ')]..." } if ($ProductCode) { Write-Information "[Get-RedstoneInstalledApplication] Get information for installed Product Code [$ProductCode]..." } ## Enumerate the installed applications from the registry for applications that have the "DisplayName" property [psobject[]]$regKeyApplication = @() foreach ($regKey in $UninstallRegKeys) { Write-Verbose "[Get-RedstoneInstalledApplication] Checking Key: ${regKey}" if (Test-Path -LiteralPath $regKey -ErrorAction 'SilentlyContinue' -ErrorVariable '+ErrorUninstallKeyPath') { [psobject[]]$UninstallKeyApps = Get-ChildItem -LiteralPath $regKey -ErrorAction 'SilentlyContinue' -ErrorVariable '+ErrorUninstallKeyPath' foreach ($UninstallKeyApp in $UninstallKeyApps) { Write-Verbose "[Get-RedstoneInstalledApplication] Checking Key: $($UninstallKeyApp.PSChildName)" try { [psobject]$regKeyApplicationProps = Get-ItemProperty -LiteralPath $UninstallKeyApp.PSPath -ErrorAction 'Stop' if ($regKeyApplicationProps.DisplayName) { [psobject[]]$regKeyApplication += $regKeyApplicationProps } } catch { Write-Warning "[Get-RedstoneInstalledApplication] Unable to enumerate properties from registry key path [$($UninstallKeyApp.PSPath)]. `n$(Resolve-Error)" continue } } } } if ($ErrorUninstallKeyPath) { Write-Warning "[Get-RedstoneInstalledApplication] The following error(s) took place while enumerating installed applications from the registry. `n$(Resolve-Error -ErrorRecord $ErrorUninstallKeyPath)" } ## Create a custom object with the desired properties for the installed applications and sanitize property details [psobject[]]$installedApplication = @() foreach ($regKeyApp in $regKeyApplication) { try { [string]$appDisplayName = '' [string]$appDisplayVersion = '' [string]$appPublisher = '' ## Bypass any updates or hotfixes if (-not $IncludeUpdatesAndHotfixes) { if ($regKeyApp.DisplayName -match '(?i)kb\d+') { continue } if ($regKeyApp.DisplayName -match 'Cumulative Update') { continue } if ($regKeyApp.DisplayName -match 'Security Update') { continue } if ($regKeyApp.DisplayName -match 'Hotfix') { continue } } ## Remove any control characters which may interfere with logging and creating file path names from these variables $appDisplayName = $regKeyApp.DisplayName -replace '[^\u001F-\u007F]','' $appDisplayVersion = $regKeyApp.DisplayVersion -replace '[^\u001F-\u007F]','' $appPublisher = $regKeyApp.Publisher -replace '[^\u001F-\u007F]','' ## Determine if application is a 64-bit application [boolean]$Is64BitApp = if (([System.Environment]::Is64BitOperatingSystem) -and ($regKeyApp.PSPath -notmatch '^Microsoft\.PowerShell\.Core\\Registry::HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node')) { $true } else { $false } if ($ProductCode) { ## Verify if there is a match with the product code passed to the script if ($regKeyApp.PSChildName -match [regex]::Escape($productCode)) { Write-Information "[Get-RedstoneInstalledApplication] Found installed application [$appDisplayName] version [$appDisplayVersion] matching product code [$productCode]." $installedApplication += New-Object -TypeName 'PSObject' -Property @{ UninstallSubkey = $regKeyApp.PSChildName ProductCode = $regKeyApp.PSChildName -as [guid] DisplayName = $appDisplayName DisplayVersion = $appDisplayVersion UninstallString = $regKeyApp.UninstallString QuietUninstallString = $regKeyApp.QuietUninstallString InstallSource = $regKeyApp.InstallSource InstallLocation = $regKeyApp.InstallLocation InstallDate = $regKeyApp.InstallDate Publisher = $appPublisher Is64BitApplication = $Is64BitApp PSPath = $regKeyApp.PSPath } } } if ($name) { ## Verify if there is a match with the application name(s) passed to the script foreach ($application in $Name) { $applicationMatched = $false if ($exact) { # Check for an exact application name match if ($regKeyApp.DisplayName -eq $application) { $applicationMatched = $true Write-Information "[Get-RedstoneInstalledApplication] Found installed application [$appDisplayName] version [$appDisplayVersion] using exact name matching for search term [$application]." } } elseif ($WildCard.IsPresent) { # Check for wildcard application name match if ($regKeyApp.DisplayName -like $application) { $applicationMatched = $true Write-Information "[Get-RedstoneInstalledApplication] Found installed application [$appDisplayName] version [$appDisplayVersion] using wildcard matching for search term [$application]." } } # Check for a regex application name match elseif ($regKeyApp.DisplayName -match [regex]::Escape($application)) { $applicationMatched = $true Write-Information "[Get-RedstoneInstalledApplication] Found installed application [$appDisplayName] version [$appDisplayVersion] using regex matching for search term [$application]." } if ($applicationMatched) { # $installedApplication += $regKeyApp $installedApplication += New-Object -TypeName 'PSObject' -Property @{ UninstallSubkey = $regKeyApp.PSChildName ProductCode = $regKeyApp.PSChildName -as [guid] DisplayName = $appDisplayName DisplayVersion = $appDisplayVersion UninstallString = $regKeyApp.UninstallString QuietUninstallString = $regKeyApp.QuietUninstallString InstallSource = $regKeyApp.InstallSource InstallLocation = $regKeyApp.InstallLocation InstallDate = $regKeyApp.InstallDate Publisher = $appPublisher Is64BitApplication = $Is64BitApp PSPath = $regKeyApp.PSPath } } } } } catch { Write-Error "[Get-RedstoneInstalledApplication] Failed to resolve application details from registry for [$appDisplayName]. `n$(Resolve-Error)" continue } } return $installedApplication } <# .SYNOPSIS Get message for MSI error code .DESCRIPTION Get message for MSI error code by reading it from msimsg.dll .PARAMETER MsiErrorCode MSI error code .PARAMETER MsiLog MSI Log File. Parsed if ErrorCode is 1603. .EXAMPLE Get-RedstoneMsiExitCodeMessage -MsiExitCode 1618 .NOTES This is an internal script function and should typically not be called directly. .LINK http://msdn.microsoft.com/en-us/library/aa368542(v=vs.85).aspx http://psappdeploytoolkit.com #> function Get-RedstoneMsiExitCodeMessage { [CmdletBinding()] Param ( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [int32] $MsiExitCode , [Parameter(Mandatory=$false)] [ValidateNotNullorEmpty()] [string] $MsiLog ) Write-Information "> $($MyInvocation.BoundParameters | ConvertTo-Json -Compress)" Write-Debug "Function Invocation: $($MyInvocation | Out-String)" switch ($MsiExitCode) { # MsiExec.exe and InstMsi.exe Error Messages # https://msdn.microsoft.com/en-us/library/aa368542(v=vs.85).aspx 1603 { $return = 'ERROR_INSTALL_FAILURE: A fatal error occurred during installation.' $return += "`nLook for `"return value 3`" in the MSI log file. The real cause of this error will be just before this line." if ($MsiLog) { $return += "`nImporting `"return value 3`" info from the MSI log, but you might still want to look at the MSI log:" $log_contents = Get-Content $MsiLog [System.Collections.ArrayList] $return_value_3_lines = @() foreach ($line in $log_contents) { if ($line -ilike '*return value 3*') { $return_value_3_lines.Add($line) | Out-Null } } foreach ($return_value_3 in $return_value_3_lines) { $i = $log_contents.IndexOf($return_value_3) $return += "`n`t$(Split-Path $MsiLog -Leaf):$($i-1) : $($log_contents[$i-1])" $return += "`n`t$(Split-Path $MsiLog -Leaf):$($i) : $($log_contents[$i])" } } } 3010 { Write-Information "Standard Message: Restart required. The installation or update for the product required a restart for all changes to take effect. The restart was deferred to a later time." $return = (Get-Content $MsiLog)[-10..-1] | Where-Object { $_.Trim() -ne '' } | Out-String } default { $code = @' enum LoadLibraryFlags : int { DONT_RESOLVE_DLL_REFERENCES = 0x00000001, LOAD_IGNORE_CODE_AUTHZ_LEVEL = 0x00000010, LOAD_LIBRARY_AS_DATAFILE = 0x00000002, LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE = 0x00000040, LOAD_LIBRARY_AS_IMAGE_RESOURCE = 0x00000020, LOAD_WITH_ALTERED_SEARCH_PATH = 0x00000008 } [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = false)] static extern IntPtr LoadLibraryEx(string lpFileName, IntPtr hFile, LoadLibraryFlags dwFlags); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] static extern int LoadString(IntPtr hInstance, int uID, StringBuilder lpBuffer, int nBufferMax); // Get MSI exit code message from msimsg.dll resource dll public static string GetMessageFromMsiExitCode(int errCode) { IntPtr hModuleInstance = LoadLibraryEx("msimsg.dll", IntPtr.Zero, LoadLibraryFlags.LOAD_LIBRARY_AS_DATAFILE); StringBuilder sb = new StringBuilder(255); LoadString(hModuleInstance, errCode, sb, sb.Capacity + 1); return sb.ToString(); } '@ [string[]] $ReferencedAssemblies = 'System', 'System.IO', 'System.Reflection' try { Add-Type -Name 'MsiMsg' -MemberDefinition $code -ReferencedAssemblies $ReferencedAssemblies -UsingNamespace 'System.Text' -IgnoreWarnings -ErrorAction 'Stop' } catch [System.Exception] { # Add-Type : Cannot add type. The type name 'Microsoft.PowerShell.Commands.AddType.AutoGeneratedTypes.MsiMsg' already exists. Write-Warning $_ } $return = [Microsoft.PowerShell.Commands.AddType.AutoGeneratedTypes.MsiMsg]::GetMessageFromMsiExitCode($MsiExitCode) } } Write-Information "Return: ${return}" return $return } <# .SYNOPSIS Get all of the properties from a Windows Installer database table or the Summary Information stream and return as a custom object. .DESCRIPTION Use the Windows Installer object to read all of the properties from a Windows Installer database table or the Summary Information stream. .PARAMETER Path The fully qualified path to an database file. Supports .msi and .msp files. .PARAMETER TransformPath The fully qualified path to a list of MST file(s) which should be applied to the MSI file. .PARAMETER Table The name of the the MSI table from which all of the properties must be retrieved. Default is: 'Property'. .PARAMETER TablePropertyNameColumnNum Specify the table column number which contains the name of the properties. Default is: 1 for MSIs and 2 for MSPs. .PARAMETER TablePropertyValueColumnNum Specify the table column number which contains the value of the properties. Default is: 2 for MSIs and 3 for MSPs. .PARAMETER GetSummaryInformation Retrieves the Summary Information for the Windows Installer database. Summary Information property descriptions: https://msdn.microsoft.com/en-us/library/aa372049(v=vs.85).aspx .PARAMETER ContinueOnError Continue if an error is encountered. Default is: $true. .EXAMPLE # Retrieve all of the properties from the default 'Property' table. Get-RedstoneMsiTableProperty -Path 'C:\Package\AppDeploy.msi' -TransformPath 'C:\Package\AppDeploy.mst' Get-RedstoneMsiTableProperty -Path 'C:\Package\AppDeploy.msi' -TransformPath 'C:\Package\AppDeploy.mst' .EXAMPLE # Retrieve all of the properties from the 'Property' table and then pipe to Select-Object to select the ProductCode property. Get-RedstoneMsiTableProperty -Path 'C:\Package\AppDeploy.msi' -TransformPath 'C:\Package\AppDeploy.mst' -Table 'Property' | Select-Object -ExpandProperty ProductCode Get-RedstoneMsiTableProperty -Path 'C:\Package\AppDeploy.msi' -TransformPath 'C:\Package\AppDeploy.mst' -Table 'Property' | Select-Object -ExpandProperty ProductCode .EXAMPLE # Retrieves the Summary Information for the Windows Installer database. Get-RedstoneMsiTableProperty -Path 'C:\Package\AppDeploy.msi' -GetSummaryInformation Get-RedstoneMsiTableProperty -Path 'C:\Package\AppDeploy.msi' -GetSummaryInformation .NOTES This is an internal script function and should typically not be called directly. .LINK http://psappdeploytoolkit.com #> function Get-RedstoneMsiTableProperty { [CmdletBinding(DefaultParameterSetName='TableInfo')] Param ( [Parameter(Mandatory=$true, Position=0)] [ValidateScript({ Test-Path -LiteralPath $_ -PathType 'Leaf' })] [string] $Path , [Parameter(Mandatory=$false)] [ValidateScript({ Test-Path -LiteralPath $_ -PathType 'Leaf' })] [string[]] $TransformPath , [Parameter(Mandatory=$false,ParameterSetName='TableInfo')] [ValidateNotNullOrEmpty()] [string] $Table = $(If ([IO.Path]::GetExtension($Path) -eq '.msi') { 'Property' } Else { 'MsiPatchMetadata' }) , [Parameter(Mandatory=$false,ParameterSetName='TableInfo')] [ValidateNotNullorEmpty()] [int32] $TablePropertyNameColumnNum = $(If ([IO.Path]::GetExtension($Path) -eq '.msi') { 1 } Else { 2 }) , [Parameter(Mandatory=$false,ParameterSetName='TableInfo')] [ValidateNotNullorEmpty()] [int32] $TablePropertyValueColumnNum = $(If ([IO.Path]::GetExtension($Path) -eq '.msi') { 2 } Else { 3 }) , [Parameter(Mandatory=$false,ParameterSetName='SummaryInfo')] [ValidateNotNullorEmpty()] [switch] $GetSummaryInformation = $false , [Parameter(Mandatory=$false)] [ValidateNotNullorEmpty()] [boolean] $ContinueOnError = $true ) Begin { <# .SYNOPSIS Get a property from any object. .DESCRIPTION Get a property from any object. .PARAMETER InputObject Specifies an object which has properties that can be retrieved. .PARAMETER PropertyName Specifies the name of a property to retrieve. .PARAMETER ArgumentList Argument to pass to the property being retrieved. .EXAMPLE Get-ObjectProperty -InputObject $Record -PropertyName 'StringData' -ArgumentList @(1) .NOTES This is an internal script function and should typically not be called directly. .LINK http://psappdeploytoolkit.com #> function Private:Get-ObjectProperty { [CmdletBinding()] Param ( [Parameter(Mandatory=$true,Position=0)] [ValidateNotNull()] [object]$InputObject, [Parameter(Mandatory=$true,Position=1)] [ValidateNotNullorEmpty()] [string]$PropertyName, [Parameter(Mandatory=$false,Position=2)] [object[]]$ArgumentList ) Begin { } Process { ## Retrieve property Write-Output -InputObject $InputObject.GetType().InvokeMember($PropertyName, [Reflection.BindingFlags]::GetProperty, $null, $InputObject, $ArgumentList, $null, $null, $null) } End { } } } Process { Try { If ($PSCmdlet.ParameterSetName -eq 'TableInfo') { Write-Information "Read data from Windows Installer database file [${Path}] in table [${Table}]." } Else { Write-Information "Read the Summary Information from the Windows Installer database file [${Path}]." } ## Create a Windows Installer object [__comobject]$Installer = New-Object -ComObject 'WindowsInstaller.Installer' -ErrorAction 'Stop' ## Determine if the database file is a patch (.msp) or not If ([IO.Path]::GetExtension($Path) -eq '.msp') { [boolean]$IsMspFile = $true } ## Define properties for how the MSI database is opened [int32]$msiOpenDatabaseModeReadOnly = 0 [int32]$msiSuppressApplyTransformErrors = 63 [int32]$msiOpenDatabaseMode = $msiOpenDatabaseModeReadOnly [int32]$msiOpenDatabaseModePatchFile = 32 If ($IsMspFile) { [int32]$msiOpenDatabaseMode = $msiOpenDatabaseModePatchFile } ## Open database in read only mode [__comobject]$Database = Invoke-ObjectMethod -InputObject $Installer -MethodName 'OpenDatabase' -ArgumentList @($Path, $msiOpenDatabaseMode) ## Apply a list of transform(s) to the database If (($TransformPath) -and (-not $IsMspFile)) { ForEach ($Transform in $TransformPath) { $null = Invoke-ObjectMethod -InputObject $Database -MethodName 'ApplyTransform' -ArgumentList @($Transform, $msiSuppressApplyTransformErrors) } } ## Get either the requested windows database table information or summary information if ($PSCmdlet.ParameterSetName -eq 'TableInfo') { ## Open the requested table view from the database [__comobject]$View = Invoke-ObjectMethod -InputObject $Database -MethodName 'OpenView' -ArgumentList @("SELECT * FROM ${Table}") $null = Invoke-ObjectMethod -InputObject $View -MethodName 'Execute' ## Create an empty object to store properties in [psobject]$TableProperties = New-Object -TypeName 'PSObject' ## Retrieve the first row from the requested table. If the first row was successfully retrieved, then save data and loop through the entire table. # https://msdn.microsoft.com/en-us/library/windows/desktop/aa371136(v=vs.85).aspx [__comobject]$Record = Invoke-ObjectMethod -InputObject $View -MethodName 'Fetch' While ($Record) { # Read string data from record and add property/value pair to custom object $TableProperties | Add-Member -MemberType 'NoteProperty' -Name (Get-ObjectProperty -InputObject $Record -PropertyName 'StringData' -ArgumentList @($TablePropertyNameColumnNum)) -Value (Get-ObjectProperty -InputObject $Record -PropertyName 'StringData' -ArgumentList @($TablePropertyValueColumnNum)) -Force # Retrieve the next row in the table [__comobject]$Record = Invoke-ObjectMethod -InputObject $View -MethodName 'Fetch' } Write-Output -InputObject $TableProperties } else { ## Get the SummaryInformation from the windows installer database [__comobject]$SummaryInformation = Get-ObjectProperty -InputObject $Database -PropertyName 'SummaryInformation' [hashtable]$SummaryInfoProperty = @{} ## Summary property descriptions: https://msdn.microsoft.com/en-us/library/aa372049(v=vs.85).aspx $SummaryInfoProperty.Add('CodePage', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(1))) $SummaryInfoProperty.Add('Title', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(2))) $SummaryInfoProperty.Add('Subject', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(3))) $SummaryInfoProperty.Add('Author', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(4))) $SummaryInfoProperty.Add('Keywords', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(5))) $SummaryInfoProperty.Add('Comments', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(6))) $SummaryInfoProperty.Add('Template', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(7))) $SummaryInfoProperty.Add('LastSavedBy', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(8))) $SummaryInfoProperty.Add('RevisionNumber', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(9))) $SummaryInfoProperty.Add('LastPrinted', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(11))) $SummaryInfoProperty.Add('CreateTimeDate', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(12))) $SummaryInfoProperty.Add('LastSaveTimeDate', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(13))) $SummaryInfoProperty.Add('PageCount', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(14))) $SummaryInfoProperty.Add('WordCount', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(15))) $SummaryInfoProperty.Add('CharacterCount', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(16))) $SummaryInfoProperty.Add('CreatingApplication', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(18))) $SummaryInfoProperty.Add('Security', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(19))) [psobject]$SummaryInfoProperties = New-Object -TypeName 'PSObject' -Property $SummaryInfoProperty Write-Output -InputObject $SummaryInfoProperties } } Catch { $resolvedError = if (Get-Command 'Resolve-Error' -ErrorAction 'Ignore') { Resolve-Error } else { $null } Write-Error ('Failed to get the MSI table [{0}]. {1}' -f $Table, $resolvedError) If (-not $ContinueOnError) { Throw ('Failed to get the MSI table [{0}]. {1}' -f $Table, $_.Exception.Message) } } Finally { Try { If ($View) { $null = Invoke-ObjectMethod -InputObject $View -MethodName 'Close' -ArgumentList @() Try { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($View) } Catch { } } ElseIf($SummaryInformation) { Try { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($SummaryInformation) } Catch { } } } Catch { } Try { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($DataBase) } Catch { } Try { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($Installer) } Catch { } } } End {} } <# .DESCRIPTION Recursively probe registry key's sub-key's and values and output a sorted array. #> function Get-RedstoneRecursiveRegistryKey { param( [Parameter(Mandatory = $true)] [String] $RegPath ) # Declare an arraylist to which the recursive function below can append values. [System.Collections.ArrayList] $RegKeysArray = 'KeyName', 'ValueName', 'Value' $Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $ComputerName) $RegKey= $Reg.OpenSubKey($RegPath); function DigThroughKeys() { param ( [Parameter(Mandatory = $true)] [AllowNull()] [AllowEmptyString()] [Microsoft.Win32.RegistryKey] $Key ) #If it has no subkeys, retrieve the values and append to them to the global array. if($Key.SubKeyCount-eq 0) { Foreach($value in $Key.GetValueNames()) { if($null -ne $Key.GetValue($value)) { $item = New-Object psobject; $item | Add-Member -NotePropertyName "KeyName" -NotePropertyValue $Key.Name; $item | Add-Member -NotePropertyName "ValueName" -NotePropertyValue $value.ToString(); $item | Add-Member -NotePropertyName "Value" -NotePropertyValue $Key.GetValue($value); [void] $RegKeysArray.Add($item); } } } else { if($Key.ValueCount -gt 0) { Foreach($value in $Key.GetValueNames()) { if($null -ne $Key.GetValue($value)) { $item = New-Object PSObject; $item | Add-Member -NotePropertyName "KeyName" -NotePropertyValue $Key.Name; $item | Add-Member -NotePropertyName "ValueName" -NotePropertyValue $value.ToString(); $item | Add-Member -NotePropertyName "Value" -NotePropertyValue $Key.GetValue($value); [void] $RegKeysArray.Add($item); } } } #Recursive lookup happens here. If the key has subkeys, send the key(s) back to this same function. if($Key.SubKeyCount -gt 0) { ForEach($subKey in $Key.GetSubKeyNames()) { DigThroughKeys -Key $Key.OpenSubKey($subKey); } } } } #Replace the value following ComputerName to fit your needs. This works, and is most useful, when scanning remote computers. DigThroughKeys -Key $RegKey #Write the output to the console. $RegKeysArray | Select-Object KeyName, ValueName, Value | Sort-Object ValueName | Format-Table $Reg.Close(); return $RegKeysArray; } <# .SYNOPSIS Get a registry value without expanding environment variables. .OUTPUTS [bool] .EXAMPLE PS > Get-RedstoneRegistryValueDoNotExpandEnvironmentName HKCU:\Thing Foo True #> function Get-RedstoneRegistryValueDoNotExpandEnvironmentName { [OutputType([bool])] [CmdletBinding()] Param( [Parameter()] [string] $Key, [Parameter()] [string] $Value ) Write-Verbose ('[Get-RedstoneRegistryValueDoNotExpandEnvironmentName] >') Write-Debug ('[Get-RedstoneRegistryValueDoNotExpandEnvironmentName] > {0}' -f ($MyInvocation | Out-String)) $item = Get-Item $Key if ($item) { return $item.GetValue($Value, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) } else { return $null } } function Get-RedstoneRegistryValueOrDefault { [CmdletBinding()] param( [Parameter(Mandatory = $false, Position = 0)] [string] $RegistryKey, [Parameter(Mandatory = $true, Position = 1)] [string] $RegistryValue, [Parameter(Mandatory = $true, Position = 2)] $DefaultData, [Parameter(Mandatory = $false)] [string] $RegistryKeyRoot, [Parameter(HelpMessage = 'Do Not Expand Environment Variables.')] [switch] $DoNotExpand, [Parameter(HelpMessage = 'For development.')] [bool] $OnlyUseDefaultSettings ) Write-Verbose "[Get-RedstoneRegistryValueOrDefault] > $($MyInvocation.BoundParameters | ConvertTo-Json -Compress)" Write-Debug "[Get-RedstoneRegistryValueOrDefault] Function Invocation: $($MyInvocation | Out-String)" if ($OnlyUseDefaultSettings) { Write-Verbose "[Get-RedstoneRegistryValueOrDefault] OnlyUseDefaultSettings Set; Returning: ${DefaultValue}" return $DefaultData } if ($RegistryKeyRoot -as [bool]) { $RegistryDrives = (Get-PSDrive -PSProvider Registry).Name + 'Registry:' | ForEach-Object { '{0}:' -f $_ } if ($RegistryKey -notmatch ($RegistryDrives -join '|')) { $RegistryKey = Join-Path $RegistryKeyRoot $RegistryKey Write-Debug "[Get-RedstoneRegistryValueOrDefault] RegistryKey adjusted to: ${RegistryKey}" } } try { if ($DoNotExpand.IsPresent) { $result = Get-RegistryValueDoNotExpandEnvironmentName -Key $RegistryKey -Value $RegistryValue Write-Verbose "[Get-RedstoneRegistryValueOrDefault] Registry Set; Returning: ${result}" } else { $result = Get-ItemPropertyValue -Path $RegistryKey -Name $RegistryValue -ErrorAction 'Stop' Write-Verbose "[Get-RedstoneRegistryValueOrDefault] Registry Set; Returning: ${result}" } return $result } catch [System.Management.Automation.PSArgumentException] { Write-Verbose "[Get-RedstoneRegistryValueOrDefault] Registry Not Set; Returning Default: ${DefaultValue}" if ($Error) { $Error.RemoveAt(0) } # This isn't a real error, so I don't want it in the error record. return $DefaultData } catch [System.Management.Automation.ItemNotFoundException] { Write-Verbose "[Get-RedstoneRegistryValueOrDefault] Registry Not Set; Returning Default: ${DefaultValue}" if ($Error) { $Error.RemoveAt(0) } # This isn't a real error, so I don't want it in the error record. return $DefaultData } } <# .SYNOPSIS Runs the given command in ComSpec (aka: Command Prompt). .DESCRIPTION This just runs a command in ComSpec by passing it to `Invoke-RedstoneRun`. If you don't *need* ComSpec to run the command, it's normally best to just use `Invoke-RedstoneRun`. .PARAMETER Cmd Under normal usage, the string passed in here just gets appended to `cmd.exe /c `. .PARAMETER KeepOpen Applies /K instead of /C, but *why would you want to do this?* /C Carries out the command specified by string and then terminates /K Carries out the command specified by string but remains .PARAMETER StringMod Applies /S: Modifies the treatment of string after /C or /K (run cmd.exe below) .PARAMETER Quiet Applies /Q: Turns echo off .PARAMETER DisableAutoRun Applies /D: Disable execution of AutoRun commands from registry (see below) .PARAMETER ANSI Applies /A: Causes the output of internal commands to a pipe or file to be ANSI .PARAMETER Unicode Applies /U: Causes the output of internal commands to a pipe or file to be Unicode .OUTPUTS [Hashtable] As returned from `Invoke-RedstoneRun`. @{ 'Process' = $proc; # The result from Start-Process; as returned from `Invoke-RedstoneRun`. 'StdOut' = $stdout; 'StdErr' = $stderr; } .EXAMPLE Invoke-RedstoneCmd "MKLINK /D Temp C:\Temp" .LINK https://git.cas.unt.edu/winstall/winstall/wikis/Invoke-RedstoneCmd #> function Invoke-RedstoneCmd { [CmdletBinding()] param ( [Parameter(Mandatory=$true, Position=1)] [string] $Cmd, [Parameter(Mandatory=$false)] [switch] $KeepOpen, [Parameter(Mandatory=$false)] [switch] $StringMod, [Parameter(Mandatory=$false)] [switch] $Quiet, [Parameter(Mandatory=$false)] [switch] $DisableAutoRun, [Parameter(Mandatory=$false)] [switch] $ANSI, [Parameter(Mandatory=$false)] [switch] $Unicode ) Write-Information "[Invoke-RedstoneCmd] > $($MyInvocation.BoundParameters | ConvertTo-Json -Compress)" Write-Debug "[Invoke-RedstoneCmd] Function Invocation: $($MyInvocation | Out-String)" [System.Collections.ArrayList] $ArgumentList = @() if ($KeepOpen) { $ArgumentList.Add('/K') } else { $ArgumentList.Add('/C') } if ($StringMod) { $ArgumentList.Add('/S') } if ($Quiet) { $ArgumentList.Add('/Q') } if ($DisableAutoRun) { $ArgumentList.Add('/D') } if ($ANSI) { $ArgumentList.Add('/A') } if ($Unicode) { $ArgumentList.Add('/U') } $ArgumentList.Add($Cmd) Write-Verbose "[Invoke-RedstoneCmd] Executing: cmd $($ArgumentList -join ' ')" Write-Verbose "[Invoke-RedstoneCmd] Invoke-RedstoneRun ..." $proc = Invoke-RedstoneRun -FilePath $env:ComSpec -ArgumentList $ArgumentList Write-Verbose "[Invoke-RedstoneCmd] ExitCode: $($proc.Process.ExitCode)" Write-Information "[Invoke-RedstoneCmd] Return: $($proc | Out-String)" return $proc } function Invoke-RedstoneDownload { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [uri] $Uri, [Parameter()] [IO.FileInfo] $OutFile, [Parameter()] [IO.DirectoryInfo] $OutFolder, [Parameter()] [hashtable] $Checksum ) Write-Debug ('[Invoke-RedstoneDownload] MyInvocation: {0}' -f ($MyInvocation | Out-String)) if (-not $OutFile.Directory.Exists -and -not $OutFolder.Exists) { $directory = if ($OutFile) { $OutFile.DirectoryName } else { $OutFolder.FullName } New-Item -Path $directory -ItemType Directory } if ($OutFolder) { [IO.FileInfo] $OutFile = Join-Path $OutFolder.FullName $Uri.Segments[-1] } $startBitsTransfer = @{ Source = $Uri.AbsoluteUri Destination = $OutFile.FullName ErrorAction = 'Stop' } Write-Verbose ('[Invoke-RedstoneDownload] startBitsTransfer: {0}' -f ($startBitsTransfer | ConvertTo-Json)) try { Start-BitsTransfer @startBitsTransfer } catch { Write-Warning ('[Invoke-RedstoneDownload] BitsTransfer Failed: {0}' -f $_) try { (New-Object Net.WebClient).DownloadFile($startBitsTransfer.Source, $startBitsTransfer.Destination) } catch { Write-Warning ('[Invoke-RedstoneDownload] WebClient Failed: {0}' -f $_) Invoke-WebRequest -Uri $startBitsTransfer.Source -OutFile $startBitsTransfer.Destination } } if ($Checksum) { $hash = Get-FileHash -LiteralPath $startBitsTransfer.Destination -Algorithm $Checksum.Algorithm Write-Verbose ('[Invoke-RedstoneDownload] Downloaded File Hash: {0}' -f ($hash | ConvertTo-Json)) if ($Checksum.Hash -ne $hash.Hash) { Remove-Item -LiteralPath $startBitsTransfer.Destination -Force Throw ('Unexpected Hash; Downloaded file deleted!') } } $OutFile.Refresh() return $OutFile } <# .EXAMPLE $MountPath.FullName | Invoke-RedstoneForceEmptyDirectory #> function Invoke-RedstoneForceEmptyDirectory { [CmdletBinding()] param ( [Parameter( Mandatory=$true, Position=0, ParameterSetName="ParameterSetName", ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, HelpMessage="Path to one or more locations." )] [Alias("PSPath")] [ValidateNotNullOrEmpty()] [IO.DirectoryInfo] $Path ) begin {} process { foreach ($p in $Path) { if (-not $p.Exists) { New-Item -ItemType 'Directory' -Path $p.FullName -Force | Out-Null $p.Refresh() } else { # Path Exists if ((Get-ChildItem $p.FullName | Measure-Object).Count) { # Path (Directory) is NOT empty. try { $p.FullName | Remove-Item -Recurse -Force } catch [System.ComponentModel.Win32Exception] { if ($_.Exception.Message -eq 'Access to the cloud file is denied') { Write-Warning ('[{0}] {1}' -f $_.Exception.GetType().FullName, $_.Exception.Message) # It seems the problem comes from a directory, not the files themselves, # so using a small workaround using Get-ChildItem to list and then delete # all files helps to get rid of all files. foreach ($item in (Get-ChildItem -LiteralPath $p.FullName -File -Recurse)) { Remove-Item -LiteralPath $item.Fullname -Recurse -Force } } else { Throw $_ } } New-Item -ItemType 'Directory' -Path $p.FullName -Force | Out-Null $p.Refresh() } } } } end {} } <# .SYNOPSIS Executes msiexec.exe to perform the following actions for MSI & MSP files and MSI product codes: install, uninstall, patch, repair, active setup. .DESCRIPTION Executes msiexec.exe to perform the following actions for MSI & MSP files and MSI product codes: install, uninstall, patch, repair, active setup. If the -Action parameter is set to "Install" and the MSI is already installed, the function will exit. Sets default switches to be passed to msiexec based on the preferences in the XML configuration file. Automatically generates a log file name and creates a verbose log file for all msiexec operations. Expects the MSI or MSP file to be located in the "Files" sub directory of the App Deploy Toolkit. Expects transform files to be in the same directory as the MSI file. .PARAMETER Action The action to perform. Options: Install, Uninstall, Patch, Repair, ActiveSetup. .PARAMETER Path The path to the MSI/MSP file or the product code of the installed MSI. .PARAMETER Transforms The name of the transform file(s) to be applied to the MSI. Relational paths from the working dir, then the MSI are looked for ... in that order. Multiple transforms can be specified; separated by a comma. .PARAMETER Patches The name of the patch (msp) file(s) to be applied to the MSI for use with the "Install" action. The patch file is expected to be in the same directory as the MSI file. .PARAMETER MsiDisplay Overrides the default MSI Display Settings. Default: $global:Winstall.Settings.Functions.InvokeMSI.Display .PARAMETER Parameters Overrides the default parameters specified in the XML configuration file. Install default is: "REBOOT=ReallySuppress /QB!". Uninstall default is: "REBOOT=ReallySuppress /QN". .PARAMETER SecureParameters Hides all parameters passed to the MSI or MSP file from the toolkit Log file. .PARAMETER LoggingOptions Overrides the default logging options specified in the XML configuration file. Default options are: "/log" (aka: "/L*v") .PARAMETER WorkingDirectory Overrides the working directory. The working directory is set to the location of the MSI file. .PARAMETER SkipMSIAlreadyInstalledCheck Skips the check to determine if the MSI is already installed on the system. Default is: $false. .PARAMETER PassThru Returns ExitCode, StdOut, and StdErr output from the process. .PARAMETER LogFileF When using [Redstone], this will be overridden via $PSDefaultParameters. Default: $global:Winstall.Settings.Logs.PathF .EXAMPLE # Installs an MSI Invoke-RedstoneMSI -Action 'Install' -Path 'Adobe_FlashPlayer_11.2.202.233_x64_EN.msi' .EXAMPLE # Installs an MSI, applying a transform and overriding the default MSI toolkit parameters Invoke-RedstoneMSI -Action 'Install' -Path 'Adobe_FlashPlayer_11.2.202.233_x64_EN.msi' -Transform 'Adobe_FlashPlayer_11.2.202.233_x64_EN_01.mst' -Parameters '/QN' .EXAMPLE # Installs an MSI and stores the result of the execution into a variable by using the -PassThru option [psobject] $ExecuteMSIResult = Invoke-RedstoneMSI -Action 'Install' -Path 'Adobe_FlashPlayer_11.2.202.233_x64_EN.msi' -PassThru .EXAMPLE # Uninstalls an MSI using a product code Invoke-RedstoneMSI -Action 'Uninstall' -Path '{26923b43-4d38-484f-9b9e-de460746276c}' .EXAMPLE # Installs an MSP Invoke-RedstoneMSI -Action 'Patch' -Path 'Adobe_Reader_11.0.3_EN.msp' .EXAMPLE $msi = @{ Action = 'Install' Parameters = @( 'USERNAME="{0}"' -f $settings.Installer.UserName 'COMPANYNAME="{0}"' -f $settings.Installer.CompanyName 'SERIALNUMBER="{0}"' -f $settings.Installer.SerialNumber ) } if ([Environment]::Is64BitOperatingSystem) { Invoke-RedstoneMSI @msi -Path 'Origin2016Sr2Setup32and64Bit.msi' } else { Invoke-RedstoneMSI @msi -Path 'Origin2016Sr2Setup32Bit.msi' } .NOTES .LINK http://psappdeploytoolkit.com #> function Invoke-RedstoneMSI { [CmdletBinding()] Param ( [Parameter(Mandatory=$false)] [ValidateSet('Install','Uninstall','Patch','Repair','ActiveSetup')] [string] $Action = 'Install', [Parameter(Position=0, Mandatory=$true, HelpMessage='Please enter either the path to the MSI/MSP file or the ProductCode')] [ValidateNotNullorEmpty()] [Alias('FilePath')] [string] $Path, [Parameter(Mandatory=$false)] [ValidateNotNullorEmpty()] [string[]] $Transforms, [Parameter(Mandatory=$false)] [Alias('Arguments')] [ValidateNotNullorEmpty()] [string[]] $Parameters = @('REBOOT=ReallySuppress'), [Parameter(Mandatory=$false)] [ValidateNotNullorEmpty()] [switch] $SecureParameters = $false, [Parameter(Mandatory=$false)] [ValidateNotNullorEmpty()] [string[]] $Patches, [Parameter(Mandatory=$false)] [ValidateNotNullorEmpty()] [string] $LoggingOptions = '/log', [Parameter(Mandatory=$false)] [ValidateNotNullorEmpty()] [string] $WorkingDirectory, [Parameter(Mandatory=$false)] [ValidateNotNullorEmpty()] [switch] $SkipMSIAlreadyInstalledCheck = $false, [Parameter(Mandatory=$false)] [ValidateNotNullorEmpty()] [string] $MsiDisplay = '/qn', [Parameter(Mandatory=$false)] [ValidateNotNullorEmpty()] [string] $WindowStyle = 'Hidden', [Parameter(Mandatory=$false)] [ValidateNotNullorEmpty()] [switch] $PassThru, [Parameter(Mandatory=$false, HelpMessage='When using [Redstone], this will be overridden via $PSDefaultParameters.')] [ValidateNotNullorEmpty()] [string] $LogFileF = "${env:Temp}\{Invoke-RedstoneMsi_{1}_{0}.log" ) Write-Verbose "[Invoke-RedstoneMsi] > $($MyInvocation.BoundParameters | ConvertTo-Json -Compress)" Write-Debug "[Invoke-RedstoneMsi] Function Invocation: $($MyInvocation | Out-String)" ## Initialize variable indicating whether $Path variable is a Product Code or not $PathIsProductCode = ($Path -as [guid]) -as [bool] ## Build the MSI Parameters switch ($Action) { 'Install' { $option = '/i' $msiDefaultParams = $MsiDisplay } 'Uninstall' { $option = '/x' $msiDefaultParams = $MsiDisplay } 'Patch' { $option = '/update' $msiDefaultParams = $MsiDisplay } 'Repair' { $option = '/f' $msiDefaultParams = $MsiDisplay } 'ActiveSetup' { $option = '/fups' } } ## If the MSI is in the Files directory, set the full path to the MSI if ($PathIsProductCode) { [string] $msiFile = $Path [string] $msiLogFile = $LogFileF -f ".msi.${Action}", ($Path -as [guid]).Guid } else { [string] $msiFile = (Resolve-Path $Path -ErrorAction 'Stop').Path [string] $msiLogFile = $LogFileF -f ".msi.${Action}", ($Path -as [IO.FileInfo]).BaseName } ## Set the working directory of the MSI if ((-not $PathIsProductCode) -and (-not $workingDirectory)) { [string] $workingDirectory = Split-Path -Path $msiFile -Parent } ## Enumerate all transforms specified, qualify the full path if possible and enclose in quotes [System.Collections.ArrayList] $mst = @() foreach ($transform in $Transforms) { try { $mst = Resolve-Path $transform -ErrorAction 'Stop' } catch [System.Management.Automation.ItemNotFoundException] { if ($workingDirectory) { $mst.Add((Join-Path "${workingDirectory}\${transform}" -Resolve -ErrorAction 'Stop')) | Out-Null } else { $mst.Add($transform) | Out-Null } } } [string] $mstFile = "`"$($mst -join ';')`"" ## Enumerate all patches specified, qualify the full path if possible and enclose in quotes [System.Collections.ArrayList] $msp = @() foreach ($patch in $Patches) { try { $msp = Resolve-Path $patch -ErrorAction 'Stop' } catch [System.Management.Automation.ItemNotFoundException] { if ($workingDirectory) { $msp.Add((Join-Path "${workingDirectory}\${patch}" -Resolve -ErrorAction 'Stop')) | Out-Null } else { $msp.Add($patch) | Out-Null } } } [string] $mspFile = "`"$($msp -join ';')`"" ## Get the ProductCode of the MSI if ($PathIsProductCode) { [string] $MSIProductCode = $Path } elseif ([IO.Path]::GetExtension($msiFile) -eq '.msi') { try { [hashtable] $Get_MsiTablePropertySplat = @{ 'Path' = $msiFile; 'Table' = 'Property'; 'ContinueOnError' = $false; } if ($mst) { $Get_MsiTablePropertySplat.Add('TransformPath', $mst) } [string] $MSIProductCode = Get-RedstoneMsiTableProperty @Get_MsiTablePropertySplat | Select-Object -ExpandProperty 'ProductCode' -ErrorAction 'Stop' Write-Information "[Invoke-RedstoneMsi] Got the ProductCode from the MSI file: ${MSIProductCode}" } catch { Write-Information "[Invoke-RedstoneMsi] Failed to get the ProductCode from the MSI file. Continuing with requested action [${Action}].$([Environment]::NewLine)$([Environment]::NewLine)$_" } } ## Start building the MsiExec command line starting with the base action and file [System.Collections.ArrayList] $argsMSI = @() if ($msiDefaultParams) { $argsMSI.Add($msiDefaultParams) | Out-Null } $argsMSI.Add($option) | Out-Null ## Enclose the MSI file in quotes to avoid issues with spaces when running msiexec $argsMSI.Add("`"${msiFile}`"") | Out-Null if ($Transforms) { $argsMSI.Add("TRANSFORMS=${mstFile}") | Out-Null $argsMSI.Add("TRANSFORMSSECURE=1") | Out-Null } if ($Patches) { $argsMSI.Add("PATCH=${mspFile}") | Out-Null } if ($Parameters) { foreach ($param in $Parameters) { $argsMSI.Add($param) | Out-Null } } $argsMSI.Add($LoggingOptions) | Out-Null $argsMSI.Add("`"$msiLogFile`"") | Out-Null ## Check if the MSI is already installed. If no valid ProductCode to check, then continue with requested MSI action. [boolean] $IsMsiInstalled = $false if ($MSIProductCode -and (-not $SkipMSIAlreadyInstalledCheck)) { [psobject] $MsiInstalled = Get-RedstoneInstalledApplication -ProductCode $MSIProductCode if ($MsiInstalled) { [boolean] $IsMsiInstalled = $true } } else { if ($Action -ine 'Install') { [boolean] $IsMsiInstalled = $true } } if ($IsMsiInstalled -and ($Action -ieq 'Install')) { Write-Information "[Invoke-RedstoneMsi] The MSI is already installed on this system. Skipping action [${Action}]..." } elseif ($IsMsiInstalled -or ((-not $IsMsiInstalled) -and ($Action -eq 'Install'))) { Write-Information "[Invoke-RedstoneMsi] Executing MSI action [${Action}]..." # Build the hashtable with the options that will be passed to Invoke-Run using splatting [hashtable] $invokeRun = @{ 'FilePath' = (Get-Command 'msiexec' -ErrorAction 'Stop').Source 'ArgumentList' = $argsMSI 'PassThru' = $PassThru.IsPresent -as [bool] } if ($WorkingDirectory) { $invokeRun.Add( 'WorkingDirectory', $WorkingDirectory) } ## If MSI install, check to see if the MSI installer service is available or if another MSI install is already underway. ## Please note that a race condition is possible after this check where another process waiting for the MSI installer ## to become available grabs the MSI Installer mutex before we do. Not too concerned about this possible race condition. [boolean] $msiExecAvailable = Assert-RedstoneIsMutexAvailable -MutexName 'Global\_MSIExecute' Start-Sleep -Seconds 1 if (-not $msiExecAvailable) { # Default MSI exit code for install already in progress Write-Warning '[Invoke-RedstoneMsi] Please complete in progress MSI installation before proceeding with this install.' $msg = Get-RedstoneMsiExitCodeMessage 1618 Write-Error "[Invoke-RedstoneMsi] 1618: ${msg}" & $Redstone.Quit 1618 $false } # Call the Invoke-Run function if ($PassThru) { $result = Invoke-RedstoneRun @invokeRun if ($result.Process.ExitCode -ne 0) { $Redstone.ExitCode = $result.Process.ExitCode $msg = Get-RedstoneMsiExitCodeMessage $Redstone.ExitCode -MsiLog $msiLogFile Write-Warning "[Invoke-RedstoneMsi] $($result.Process.ExitCode): ${msg}" } Write-Information "[Invoke-RedstoneMsi] Return: $($result | Out-String)" return $result } else { Invoke-RedstoneRun @invokeRun } } else { Write-Warning "[Invoke-RedstoneMsi] The MSI is not installed on this system. Skipping action [${Action}]..." } } <# .SYNOPSIS Runs the given command. .DESCRIPTION This command sends a single command to `Start-Process` in a way that is standardized for Winstall. For convenience, you can use the `Cmd` parameter, passing a single string that contains your executable and parameters; see examples. The command will return a `[hashtable]` including the Process results, standard output, and standard error. This function has been vetted for several years, but if you run into issues, try using `Start-Process`. .PARAMETER Cmd This is the command you wish to run, including arguments, as a single string. .PARAMETER FilePath Specifies the optional path and file name of the program that runs in the process. Enter the name of an executable file or of a document, such as a .txt or .doc file, that is associated with a program on the computer. Passes Directly to `Start-Process`; see `Get-Help Start-Process`. .PARAMETER ArgumentList Specifies parameters or parameter values to use when this cmdlet starts the process. Passes Directly to `Start-Process`; see `Get-Help Start-Process`. .PARAMETER WorkingDirectory Specifies the location of the executable file or document that runs in the process. The default is the current folder. Passes Directly to `Start-Process`; see `Get-Help Start-Process`. .PARAMETER PassThru Default: $$true Returns a process object for each process that the cmdlet started. By default, this cmdlet does generate output. Passes Directly to `Start-Process`; see `Get-Help Start-Process`. .PARAMETER Wait Default: $true Indicates that this cmdlet waits for the specified process to complete before accepting more input. This parameter suppresses the command prompt or retains the window until the process finishes. Passes Directly to `Start-Process`; see `Get-Help Start-Process`. .PARAMETER WindowStyle Default: Hidden Specifies the state of the window that is used for the new process. The acceptable values for this parameter are: Normal, Hidden, Minimized, and Maximized. Passes Directly to `Start-Process`; see `Get-Help Start-Process`. .OUTPUTS [hashtable] @{ 'Process' = $proc; # The result from Start-Process. 'StdOut' = $stdout; # This is an array, as returned from `Get-Content`. 'StdErr' = $stderr; # This is an array, as returned from `Get-Content`. } .EXAMPLE # Use `Cmd` parameter $result = Invoke-RedstoneRun """${firefox_setup_exe}"" /INI=""${ini}""" .EXAMPLE # Use `FilePath` and `ArgumentList` parameters $result = Invoke-RedstoneRun -FilePath $firefox_setup_exe -ArgumentList @("/INI=""${ini}""") .EXAMPLE # Get the ExitCode $result = Invoke-RedstoneRun """${firefox_setup_exe}"" /INI=""${ini}""" $result.Process.ExitCode .LINK https://git.cas.unt.edu/winstall/winstall/wikis/Invoke-RedstoneRun #> function Invoke-RedstoneRun { [OutputType([hashtable])] [CmdletBinding()] param ( [Parameter(Mandatory=$true, Position=0, ParameterSetName='Cmd')] [string] $Cmd, [Parameter(Mandatory=$true, ParameterSetName='FilePath')] [string] $FilePath, [Parameter(Mandatory=$false, ParameterSetName='FilePath')] [string[]] $ArgumentList, [Parameter(Mandatory=$false)] [string] $WorkingDirectory, [Parameter(Mandatory=$false)] [boolean] $PassThru = $true, [Parameter(Mandatory=$false)] [boolean] $Wait = $true, [Parameter(Mandatory=$false)] [string] $WindowStyle = 'Hidden', [Parameter(Mandatory=$false)] [IO.FileInfo] $LogFile ) Write-Information ('[Invoke-RedstoneRun] > {0}' -f ($MyInvocation.BoundParameters | ConvertTo-Json -Compress)) -Tags 'Redstone','Invoke-RedstoneRun' Write-Debug ('[Invoke-RedstoneRun] Function Invocation: {0}' -f ($MyInvocation | Out-String)) if ($PsCmdlet.ParameterSetName -ieq 'Cmd') { Write-Verbose ('[Invoke-RedstoneRun] Executing: {0}' -f $cmd) if ($Cmd -match '^(?:"([^"]+)")$|^(?:"([^"]+)") (.+)$|^(?:([^\s]+))$|^(?:([^\s]+)) (.+)$') { # https://regex101.com/r/uU4vH1/1 Write-Verbose "Cmd Match: $($Matches | Out-String)" if ($Matches[1]) { $FilePath = $Matches[1] } elseif ($Matches[2]) { $FilePath = $Matches[2] $ArgumentList = $Matches[3] } elseif ($Matches[4]) { $FilePath = $Matches[4] } elseif ($Matches[5]) { $FilePath = $Matches[5] $ArgumentList = $Matches[6] } } else { Throw [System.Management.Automation.ParameterBindingException] ('Cmd Match Error: {0}' -f $cmd) } } [string] $processGuid = New-Guid [IO.FileInfo] $stdout = New-TemporaryFile [IO.FileInfo] $stderr = New-TemporaryFile [string] $stdoutFullName = $stdout.FullName [string] $stderrFullName = $stderr.FullName [string] $logFileFullName = $logFile.FullName [hashtable] $startProcess = @{ 'FilePath' = $FilePath 'PassThru' = $PassThru 'Wait' = $Wait 'WindowStyle' = $WindowStyle 'RedirectStandardError' = $stderr.FullName 'RedirectStandardOutput' = $stdout.FullName } if ($ArgumentList) { [void] $startProcess.Add('ArgumentList', $ArgumentList) } if ($WorkingDirectory) { [void] $startProcess.Add('WorkingDirectory', $WorkingDirectory) } if ($LogFile) { # Monitor STDOUT and send to Log $stdout_job = Start-Job -Name "StdOut ${processGuid}" -ScriptBlock { while (-not (Test-Path $using:stdoutFullName)) { Start-Sleep -Milliseconds 100 } Write-Verbose "Monitoring STDOUT!" Get-Content $using:stdoutFullName -Wait | ForEach-Object { "STDOUT: $_" | Out-File -Encoding 'utf8' -LiteralPath $using:logFileFullName -Append -Force } } # Monitor STDERR and send to Log $stderr_job = Start-Job -Name "StdErr ${processGuid}" -ScriptBlock { while (-not (Test-Path $using:stderrFullName)) { Start-Sleep -Milliseconds 100 } Write-Verbose "Monitoring STDERR!" Get-Content $using:stderrFullName -Wait | ForEach-Object { "STDERR: $_" | Out-File -Encoding 'utf8' -LiteralPath $using:logFileFullName -Append -Force } } } Write-Information ('[Invoke-RedstoneRun] Start-Process: {0}' -f (ConvertTo-Json $startProcess)) -Tags 'Redstone','Invoke-RedstoneRun' $proc = Start-Process @startProcess Write-Verbose ('[Invoke-RedstoneRun] ExitCode:' -f $proc.ExitCode) $stdout_job | Stop-Job -ErrorAction 'SilentlyContinue' $stderr_job | Stop-Job -ErrorAction 'SilentlyContinue' $return = @{ 'Process' = $proc 'StdOut' = (Get-Content $stdout.FullName | Out-String).Trim().Split([System.Environment]::NewLine) 'StdErr' = (Get-Content $stderr.FullName | Out-String).Trim().Split([System.Environment]::NewLine) } $stdout.FullName | Remove-Item -ErrorAction 'SilentlyContinue' -Force $stderr.FullName | Remove-Item -ErrorAction 'SilentlyContinue' -Force try { Write-Information ('[Invoke-RedstoneRun] Return: {0}' -f (ConvertTo-Json $return -Depth 1 -ErrorAction 'Stop')) -Tags 'Redstone','Invoke-RedstoneRun' } catch { Write-Information ('[Invoke-RedstoneRun] Return: {0}' -f ($return | Out-String)) -Tags 'Redstone','Invoke-RedstoneRun' } return $return } #Requires -RunAsAdministrator <# .EXAMPLE try { Mount-RedstoneWim ... do some things ... } catch { } finally { Dismount-RedstoneWim } #> function Mount-RedstoneWim { [CmdletBinding()] param ( # Specifies a path to one or more locations. [Parameter( Mandatory=$true, Position=0, ParameterSetName="ParameterSetName", ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, HelpMessage="Path to one or more locations." )] [Alias("PSPath")] [ValidateNotNullOrEmpty()] [IO.FileInfo] $ImagePath, # Specifies a path to one or more locations. [Parameter( Mandatory=$false, Position=0, ParameterSetName="ParameterSetName", ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, HelpMessage="Path to one or more locations." )] [ValidateNotNullOrEmpty()] [IO.DirectoryInfo] $MountPath = ([IO.Path]::Combine($env:Temp, 'RedstoneMount')), [Parameter(Mandatory = $false)] [int] $ImageIndex = 1, [Parameter(Mandatory = $false)] [IO.FileInfo] $LogFileF ) begin { Write-Verbose "[Mount-RedstoneWim] > $($MyInvocation.BoundParameters | ConvertTo-Json -Compress)" Write-Debug "[Mount-RedstoneWim] Function Invocation: $($MyInvocation | Out-String)" } process { # $MyInvocation # $MountPath.FullName $MountPath.FullName | Invoke-RedstoneForceEmptyDirectory $MountPath.Refresh() $windowsImage = @{ ImagePath = $ImagePath.FullName Index = $ImageIndex Path = $MountPath.FullName } if ($LogFileF) { $windowsImage.Add('LogPath', ($LogFileF -f 'DISM')) } Write-Verbose "[Mount-RedstoneWim] Mount-WindowImage: $($windowsImage | ConvertTo-Json)" Mount-WindowsImage @windowsImage } end {} } <# .SYNOPSIS Create a RedStone Class. .DESCRIPTION Create a Redstone Class with an easy to use function. .PARAMETER SettingsJson Type: [string] Path to the settings.json file. .PARAMETER Publisher Type: [string] Name of the publisher, like "Mozilla". .PARAMETER Product Type: [string] Name of the product, like "Firefox ESR". .PARAMETER Version Type: [string] Version of the product, like "108.0.1". This was deliberatly not cast as a [version] to allow handling of non-semantic versioning. .PARAMETER Action Type: [string] Action that is being taken. This is purely cosmetic and directly affects the log name. For Example: - Using the examples from the Publisher, Product, and Version parameters. - Set action to 'install' The log file name will be: Mozilla Firefox ESR 108.0.1 Install.log If you don't specify an action, the action will be taken from the name of the script your calling this function from. .OUTPUTS System.Array with two Values: 1. Redstone. The Redstone class 2. PSObject. The results of parsing the provided settings.json file. Null if parameters supplied. .NOTES - Allows access to the Redstone class without having to use `Using Module Redstone`. - Ref: https://stephanevg.github.io/powershell/class/module/DATA-How-To-Write-powershell-Modules-with-classes/ #> function New-Redstone { [OutputType([array])] [CmdletBinding(DefaultParameterSetName='NoParams')] param ( [Parameter( Mandatory = $true, Position = 1, ParameterSetName = 'SettingsJson', HelpMessage = 'Path to the settings.json file.' )] [IO.FileInfo] $SettingsJson, [Parameter( Mandatory = $true, Position = 1, ParameterSetName = 'ManuallyDefined', HelpMessage = 'Name of the publisher, like "Mozilla".' )] [string] $Publisher, [Parameter( Mandatory = $true, Position = 2, ParameterSetName = 'ManuallyDefined', HelpMessage = 'Name of the product, like "Firefox ESR".' )] [string] $Product, [Parameter( Mandatory = $true, Position = 3, ParameterSetName = 'ManuallyDefined', HelpMessage = 'Version of the product, like "108.0.1".' )] [string] $Version, [Parameter( Mandatory = $true, Position = 4, ParameterSetName = 'ManuallyDefined', HelpMessage = 'Action that is being taken.' )] [string] $Action ) switch ($PSCmdlet.ParameterSetName) { 'SettingsJson' { $redstone = [Redstone]::new($SettingsJson) return @( $redstone $redstone.Settings.JSON.Data ) } 'ManuallyDefined' { $redstone = [Redstone]::new($Publisher, $Product, $Version, $Action) return @( $redstone $redstone.Settings.JSON.Data ) } default { # NoParams $redstone = [Redstone]::new() return @( $redstone $redstone.Settings.JSON.Data ) } } } #Requires -RunAsAdministrator <# .EXAMPLE New-RedstoneWim -ImagePath "PSRedstone.wim" -CapturePath "PSRedstone" -Name "PSRedstone" #> function New-RedstoneWim { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory = $true)] [IO.FileInfo] $ImagePath, [Parameter(Mandatory = $true)] [IO.DirectoryInfo] $CapturePath, [Parameter(Mandatory = $true)] [String] $Name, [Parameter(Mandatory = $false)] [IO.FileInfo] $LogFileF ) begin { Write-Verbose "[New-RedstoneWim] > $($MyInvocation.BoundParameters | ConvertTo-Json -Compress)" Write-Debug "[New-RedstoneWim] Function Invocation: $($MyInvocation | Out-String)" } process { if (-not $ImagePath.Directory.Exists) { New-Item -ItemType 'Directory' -Path $ImagePath.Directory.FullName -Force | Out-Null $ImagePath.Refresh() } $windowsImage = @{ ImagePath = $ImagePath.FullName CapturePath = $CapturePath.FullName Name = $Name } if ($LogFileF) { $windowsImage.Add('LogPath', ($LogFileF -f 'DISM')) } if ($WhatIf.IsPresent) { Write-Information ('What if: Performing the operation "New-WindowsImage" with parameters: {0}' -f ($windowsImage | ConvertTo-Json)) -InformationAction Continue } else { New-WindowsImage @windowsImage } } end {} } $psd1 = Import-PowerShellDataFile ([IO.Path]::Combine($PSScriptRoot, 'PSRedstone.psd1')) # Check if the current context is elevated (Are we running as an administrator?) if ((New-Object System.Security.Principal.WindowsPrincipal([System.Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)) { # Anytime this Module is used, the version and timestamp will be stored in the registry. # This will allow more intelligent purging of unused versions. $versionUsed = @{ LiteralPath = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\VertigoRay\PSRedstone\VersionsUsed' Name = $psd1.ModuleVersion Value = (Get-Date -Format 'O') Force = $true } Write-Debug ('Version Used: {0}' -f ($versionUsed | ConvertTo-Json)) if (-not (Test-Path $versionUsed.LiteralPath)) { New-Item -Path $versionUsed.LiteralPath -Force } Set-ItemProperty @versionUsed } # Load Module Members $moduleMember = @{ Cmdlet = $psd1.CmdletsToExport Function = $psd1.FunctionsToExport Alias = $psd1.AliasesToExport } if ($psd1.VariablesToExport) { $moduleMember.Set_Item('Variable', $psd1.VariablesToExport) } Export-ModuleMember @moduleMember |