JBUtils.psm1
<# .SYNOPSIS Converts a SecureString to plain text in one step. .DESCRIPTION Converts a SecureString to plain text in one step. .PARAMETER SecureString SecureString to convert. .EXAMPLE $secureStringVar | ConvertFrom-EncryptedSecureString .LINK https://stackoverflow.com/a/28353003/14628263 .NOTES N/A #> function ConvertFrom-EncryptedSecureString { [CmdletBinding()] [OutputType([String])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [SecureString]$SecureString ) process { $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString) [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR) } } <# .SYNOPSIS Changes the security protocol of the current session to 1.2. .DESCRIPTION Changes the security protocol of the current session to 1.2. .EXAMPLE Enable-Tls12 Enables TLS 1.2 for the current session. .EXAMPLE Enable-Tls12 -Persist Enables TLS 1.2 machine-wide. .LINK https://docs.microsoft.com/en-us/troubleshoot/azure/active-directory/enable-support-tls-environment .NOTES N/A #> function Enable-Tls12 { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingCmdletAliases', '')] [CmdletBinding()] param ( [Switch]$Persist ) Write-Verbose -Message '[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 if ($Persist) { $null = Test-PSEnvironment -CheckAdmin -Exit $psRegPaths = ( 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client', 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server' ) foreach ($regPath in $psRegPaths) { $progress = @{ Activity = 'Modifying Registry' Status = "Key: $regPath" } Write-Progress @progress $null = New-Item -Path $regPath -Force $null = New-ItemProperty -Path $regPath -Name DisabledByDefault -Value 0 -PropertyType DWord -Force $null = New-ItemProperty -Path $regPath -Name Enabled -Value 1 -PropertyType DWord -Force } # Use reg instead of New-ItemProperty because it's not clear how # to modify both the 32 and 64 bit registries via PowerShell $startProcess = @{ FilePath = 'reg' NoNewWindow = $true Wait = $true } $regPaths = ( 'HKLM\SOFTWARE\Microsoft\.NETFramework\v2.0.50727', 'HKLM\SOFTWARE\Microsoft\.NETFramework\v4.0.30319', 'HKLM\SOFTWARE\Wow6432Node\Microsoft\.NETFramework\v2.0.50727', 'HKLM\SOFTWARE\Wow6432Node\Microsoft\.NetFramework\v4.0.30319' ) $regValues = ( 'SystemDefaultTlsVersions', 'SchUseStrongCrypto' ) $enable = ( '/t', 'REG_DWORD', '/d', '1' ) $registries = ( '/reg:32', '/reg:64' ) $totalCommands = $regPaths.Count * $regValues.Count * $registries.Count $count = 0 foreach ($regPath in $regPaths) { foreach ($regValue in $regValues) { foreach ($registry in $registries) { $count++ $startProcess['ArgumentList'] = ( 'add', $regPath, '/f' ) # Start-Process @startProcess $startProcess['ArgumentList'] += ( '/v', $regValue, $registry ) $startProcess['ArgumentList'] += $enable $message = 'reg ' + ($startProcess['ArgumentList']) -join ' ' Write-Verbose -Message $message $progress = @{ Activity = 'Modifying Registry' Status = "Key: $regPath, SubKey: $regValue, Value: 1" CurrentOperation = "$count/$totalCommands" PercentComplete = (($count / $totalCommands) * 100) } Write-Progress @progress Start-Process @startProcess -RedirectStandardOutput "$env:TEMP/stdout.log" } } } Write-Progress @progress -Completed } } <# .SYNOPSIS Exports a screenshot as a bitmap. .DESCRIPTION Exports a screenshot as a bitmap. .PARAMETER OutFile Path to save the screenshot to. .EXAMPLE Export-Screenshot -OutFile $env:USERPROFILE/Downloads/Screenshot.bmp .NOTES N/A .LINK https://www.pdq.com/blog/capturing-screenshots-with-powershell-and-net/ #> function Export-Screenshot { [CmdletBinding()] param ( [String]$OutFile = "$env:TEMP/$( New-Guid ).bmp" ) begin { Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing $screen = [System.Windows.Forms.SystemInformation]::VirtualScreen $Script:width = $screen.Width $Script:height = $screen.Height $Script:left = $screen.Left $Script:top = $screen.Top } process { $bitmap = New-Object -TypeName System.Drawing.Bitmap -ArgumentList ($Script:width, $Script:height) $graphic = [System.Drawing.Graphics]::FromImage($bitmap) $graphic.CopyFromScreen($Script:left, $Script:top, 0, 0, $bitmap.Size) $bitmap.Save($OutFile) Get-Item -Path $OutFile } } <# .SYNOPSIS Gets the value of of an environment variable. .DESCRIPTION Gets the value of of an environment variable. .PARAMETER Name Name of the environment variable. .PARAMETER Scope Environment scope: Machine, Process, or User. .EXAMPLE Get-EnvironmentVariable -Name PATH -Scope User .NOTES N/A #> function Get-EnvironmentVariable { [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [String[]]$Name, [ValidateSet('Machine', 'Process', 'User')] [String]$Scope = 'Process' ) process { foreach ($item in $Name) { [PSCustomObject]@{ Name = $item Value = [System.Environment]::GetEnvironmentVariable($item, $Scope) Scope = $Scope } } } } <# .SYNOPSIS A simple function wrapper for getting the value of $PSVersionTable.PSVersion. .DESCRIPTION A simple function wrapper for getting the value of $PSVersionTable.PSVersion, used for unit testing. .EXAMPLE Get-PSVersion .NOTES N/A #> function Get-PSVersion { [CmdletBinding()] param() $PSVersionTable.PSVersion } <# .SYNOPSIS Sets the git user name and email in a given scope. .DESCRIPTION Sets the git user name and email in a given scope. .PARAMETER Path Path of the repo. .PARAMETER UserName User name to add to the git config. .PARAMETER UserEmail Email to add to the git config. .PARAMETER Scope Scope of the git config. .EXAMPLE Initialize-GitConfig -UserName "Git User" -UserEmail email@example.com .NOTES N/A #> function Initialize-GitConfig { [CmdletBinding()] param ( [Parameter(ValueFromPipelineByPropertyName = $true)] [Alias('FullName', 'LiteralPath')] [String[]]$Path = $PWD, [String]$UserName = $env:BUILD_REQUESTEDFOR, [String]$UserEmail = $env:BUILD_REQUESTEDFOREMAIL, [ValidateSet('System', 'Global', 'WorkTree', 'Local')] [String]$Scope = 'Local' ) begin { $script:CurrentLocation = $PWD $script:Git = @{ FilePath = Get-Command -Name git.exe | Select-Object -ExpandProperty Source NoNewWindow = $true Wait = $true } $script:Config = @( 'config', "--$($Scope.ToLower())" ) } process { Start-Process @script:Git -ArgumentList ($script:Config + ('http.version', 'HTTP/1.1')) $userEntries = @( @{ ArgumentList = $script:Config + ( '--replace-all', 'user.email', "`"$UserEmail`"" ) }, @{ ArgumentList = $script:Config + ( '--replace-all', 'user.name', "`"$UserName`"" ) } ) foreach ($location in $Path) { foreach ($user in $userEntries) { Start-Process @script:Git @user -WorkingDirectory $location } } } end { Set-Location -Path $script:CurrentLocation } } <# .SYNOPSIS Resets the current console to the default colors. .DESCRIPTION Resets the current console to the default colors. .EXAMPLE Reset-ConsoleColor .NOTES N/A #> function Reset-ConsoleColor { [CmdletBinding()] param () [Console]::ResetColor() } <# .SYNOPSIS Sets an environment variable. .DESCRIPTION Sets an environment variable. .PARAMETER Name Name of the environment variable. .PARAMETER Value Parameter description .PARAMETER Scope Environment scope: Machine, Process, or User. .PARAMETER Append Appends the given value to the variable instead of replacing it. .PARAMETER Delete Deletes the named environment variable. .PARAMETER PassThru Outputs an environment variable object to the pipeline. .PARAMETER Force Sets the value even if it already exists in the environment variable, and doesn't prompt to modify the Path. .EXAMPLE Set-EnvironmentVariable -Name PATH -Value 'C:/Program Files/NuGet' -Scope Machine -Append Adds 'C:/Program Files/NuGet' to the System PATH variable. .NOTES N/A #> function Set-EnvironmentVariable { [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'Set')] param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [String]$Name, [Parameter(ParameterSetName = 'Set', ValueFromPipelineByPropertyName = $true)] [String]$Value, [ValidateSet('Machine', 'Process', 'User')] [String]$Scope = 'Process', [Parameter(ParameterSetName = 'Set')] [Switch]$Append, [Parameter(ParameterSetName = 'Delete')] [Switch]$Delete, [Parameter(ParameterSetName = 'Set')] [Switch]$PassThru, [Parameter(ParameterSetName = 'Set')] [Switch]$Force ) begin { if ($Scope -eq 'Machine') { $null = Test-PSEnvironment -CheckAdmin -Exit } } process { if ($Delete) { if ($PSCmdlet.ShouldProcess("$($Scope):$Name", 'Delete') -or $Force) { [System.Environment]::SetEnvironmentVariable($Name, $null, $Scope) Set-Item -Path "env:$Name" -Value $null -Force } return } $envVariableValue = Get-EnvironmentVariable -Name $Name -Scope $Scope | Select-Object -ExpandProperty Value if ($null -eq $envVariableValue) { Write-Verbose -Message "Environment variable $($Scope):$Name does not exist." $envVariableValue = '' $isNewVar = $true } $isNotArray = $envVariableValue -notmatch [System.IO.Path]::PathSeparator $isNotEqualToEnvValue = $envVariableValue.Trim() -ne $Value.Trim() $isNotArrayIsNotEqual = $isNotArray -and $isNotEqualToEnvValue $isArray = $envVariableValue -match [System.IO.Path]::PathSeparator $isNotValueInEnvArray = $envVariableValue -notmatch [Regex]::Escape($Value) $isArrayIsNotMatching = $isArray -and $isNotValueInEnvArray if ($Force -or $isNotArrayIsNotEqual -or $isArrayIsNotMatching) { if ($Name -eq 'PATH' -and (!$Append) -and (!$Force)) { Write-Warning -Message ( "This will overwrite all entries in the $($Scope):PATH variable with: $Value" ) $shouldAppend = Read-Host -Prompt "Should $Value be appended instead? (y/n)" if ($shouldAppend -eq 'y') { $Append = $true } } # Ensure that the existing PATH variable doesn't get corrupted when appending $valueWithPathSeparator = if ( $Append -and $Name -eq 'PATH' -and $Value[0] -ne [System.IO.Path]::PathSeparator ) { [System.IO.Path]::PathSeparator + $Value } else { $Value } $finalValue = if ($Append) { $envVariableValue + $valueWithPathSeparator } else { $valueWithPathSeparator } if ($PSCmdlet.ShouldProcess("$($Scope):$Name", "Setting value to $finalValue") -or $Force) { [System.Environment]::SetEnvironmentVariable($Name, $finalValue, $Scope) if ($Name -match 'path' -and $isArray) { $scopedValue = ( Get-Item -Path "env:$($Name.ToUpper())" ).Value.Split([System.IO.Path]::PathSeparator) $newValue = $finalValue.Split([System.IO.Path]::PathSeparator) if ( Compare-Object ` -ReferenceObject ( $scopedValue | Sort-Object ) ` -DifferenceObject ( $newValue | Sort-Object ) ) { $combinedValue = $scopedValue + $newValue $finalValue = $combinedValue -join [System.IO.Path]::PathSeparator } } if ( $Scope -eq 'Process' -or $isNewVar -or ($Name -match 'PATH' -and $isArray) ) { Set-Item -Path "env:$Name" -Value $finalValue -Force } } } else { Write-Warning -Message ( "The environment variable $($Scope):$Name already contains a value of $Value. " + 'Nothing was modified; use -Force to overwrite it.' ) } if ($PassThru) { Get-EnvironmentVariable -Name $Name -Scope $Scope } } } <# .SYNOPSIS Shows available colors that can be used in the console. .DESCRIPTION Shows available colors that can be used in the console and examples of what they will look like. .EXAMPLE Show-ConsoleColor Lists available colors and shows what the color will look like in the current console. .NOTES N/A #> function Show-ConsoleColor { [CmdletBinding()] param() Write-Host -Object ('{0,-120}' -f ' ') -ForegroundColor Black -BackgroundColor White foreach ($heading in 'Color', 'Foreground', 'Background') { Write-Host -Object ('{0,-40}' -f $heading) -ForegroundColor Black -BackgroundColor White -NoNewline } Write-Host $colors = [enum]::GetValues([System.ConsoleColor]) foreach ($color in $colors) { $object = @{ Object = ('{0,-40}' -f $color) } Write-Host @object -NoNewline Write-Host @object -ForegroundColor $color -NoNewline Write-Host @object -ForegroundColor $colors[$colors.Count - $color - 1] -BackgroundColor $color } } <# .SYNOPSIS Starts a process using the .net Process class instead of Start-Process. .DESCRIPTION Starts a process using the .net Process class instead of Start-Process. This allows console output to be captured as well as non-zero exit codes at the same time. .PARAMETER FilePath Path to the process to start. .PARAMETER ArgumentList Arguments for the process. .PARAMETER WorkingDirectory Directory to execute the process in. .PARAMETER PassThru Pass the output to the pipeline. .PARAMETER Title Title of the progress bar that displays while the CLI is running. .PARAMETER NoProgress Output the command details to the information stream instead of as a progress bar. .PARAMETER RedirectStandardError Redirects the standard error stream to the standard output stream. Exe's that use StdErr for info: git .EXAMPLE Start-CliProcess -FilePath 'cmd' -ArgumentList '/c "echo hello"' -PassThru .NOTES Replaces Start-Process and Invoke-Process. Notes on avoiding process deadlocks when updating the output logic: https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process.standarderror#remarks #> function Start-CliProcess { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingCmdletAliases', '')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] [CmdletBinding()] param ( [Parameter(ValueFromPipelineByPropertyName = $true, Position = 0)] [Alias('LiteralPath', 'FullName')] [String[]]$FilePath, [String[]]$ArgumentList, [Parameter(ValueFromPipelineByPropertyName = $true)] [Alias('Directory')] [String]$WorkingDirectory = $PWD, [Switch]$PassThru, [Alias('Activity')] [String]$Title = 'Start CLI Process', [Switch]$NoProgress, [Switch]$RedirectStandardError ) begin { $script:processes = @() $script:noProgress = if ($NoProgress -or $env:AGENT_JOBNAME) { $true } else { $false } } process { $procInfoArgs = if ($ArgumentList.Count -gt 1) { $ArgumentList | ForEach-Object -Process { $thisTrimmed = $_.Trim() if ($thisTrimmed -match ' ' -and $thisTrimmed -notmatch '"') { "`"$thisTrimmed`"" } else { $thisTrimmed } } } elseif ($ArgumentList.Count -eq 1) { $ArgumentList.Trim() } foreach ($file in $FilePath) { $fileInfo = ( Get-Command -Name $file ).Source | Get-Item $processInfo = New-Object -TypeName 'System.Diagnostics.ProcessStartInfo' $processInfo.FileName = $fileInfo.FullName $processInfo.Arguments = $procInfoArgs $processInfo.WorkingDirectory = $WorkingDirectory $processInfo.CreateNoWindow = $true $processInfo.UseShellExecute = $false $processInfo.RedirectStandardOutput = $true $processInfo.RedirectStandardError = $true $process = New-Object -TypeName 'System.Diagnostics.Process' $process.StartInfo = $processInfo $progress = @{ Activity = $Title Status = "Starting $(( Get-Command -Name $fileInfo.FullName ).Name) in $WorkingDirectory" CurrentOperation = "`"$($fileInfo.FullName)`" $($ArgumentList -join ' ')" } if (!$script:noProgress) { Write-Progress @progress } else { Write-ProgressToHost @progress } $null = $process.Start() $procState = 'Running' do { if ($procState -eq 'LastRun') { $readMethod = 'ReadToEnd' $procState = 'Finished' } else { $readMethod = 'ReadLine' } while (!$process.StandardOutput.EndOfStream) { $process.StandardOutput.$readMethod() | ForEach-Object -Process { if ($PassThru) { $_ } else { Write-Host -Object $_ } if (!$script:noProgress) { Write-Progress @progress } } } $stdErr = @() while (!$process.StandardError.EndOfStream) { $process.StandardError.$readMethod() | ForEach-Object -Process { if ($_) { $stdErr += $_ } if ($PassThru) { $_ } elseif ($RedirectStandardError) { Write-Host -Object $_ } if (!$script:noProgress) { Write-Progress @progress } } } if ($stdErr) { $stdErrString = $stdErr -join "`n" if (!$RedirectStandardError) { Write-Error ` -Message $stdErrString ` -Category FromStdErr ` -TargetObject $fileInfo.Name if (!$script:noProgress) { Write-Progress @progress } } } if ($process.HasExited -and $procState -eq 'Running') { $procState = 'LastRun' } } while ($procState -ne 'Finished') if (!$script:noProgress) { Write-Progress @progress -Completed } if ( ($process.ExitCode -ne 0) -and ($ErrorActionPreference -ne 'SilentlyContinue') -and ($ErrorActionPreference -ne 'Ignore') ) { throw $process.ExitCode } $script:processes += $process.Id } } end { if ($script:processes) { Stop-Process -Id $script:processes -Force -PassThru -ErrorAction SilentlyContinue | Wait-Process } } } <# .SYNOPSIS Starts a System.Diagnostics.Stopwatch instance. .DESCRIPTION Starts a System.Diagnostics.Stopwatch instance. .PARAMETER InputObject An existing Stopwatch object to start. .EXAMPLE $sw = Start-Stopwatch .NOTES N/A #> function Start-Stopwatch { [OutputType([System.Diagnostics.Stopwatch])] [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] [System.Object]$InputObject ) process { if ($InputObject) { $InputObject.Start() $InputObject } else { [System.Diagnostics.Stopwatch]::StartNew() } } } <# .SYNOPSIS Waits for a specified time with the option to press a key to continue. .DESCRIPTION Waits for a specified time with the option to press a key to continue. .PARAMETER Seconds The amount of seconds to wait before continuing. .PARAMETER NoBreak Removes the option to press a key to continue. .EXAMPLE Start-Timeout -Seconds 5 .NOTES Is just a more PS friendly wrapper for timeout.exe #> function Start-Timeout { [CmdletBinding()] param ( [Int]$Seconds = 0, [Switch]$NoBreak ) if (Test-IsNonInteractiveShell) { for ($i = $Seconds; $i -ge 0; $i--) { $activity = "Waiting for $i seconds," Write-Progress -Activity $activity -Status 'press CTRL+C to quit ...' Start-Sleep -Seconds 1 } Write-Progress $activity -Completed } else { . "$PSScriptRoot/private/Invoke-Timeout.ps1" $scriptString = "Invoke-Timeout /t $Seconds" if ($NoBreak) { $scriptString += ' /nobreak' } $timeout = [ScriptBlock]::Create($scriptString) Invoke-Command -ScriptBlock $timeout } } <# .SYNOPSIS Stops any processes that may interfere with a product build. .DESCRIPTION Stops any processes that may interfere with a product build, includes Visual Studio and Selenium ChromeDriver by default. .PARAMETER Optional A list of process names that will prompt to stop before stopping them. Always includes Visual Studio. .PARAMETER Required A list of process names that will be stopped without prompting. Always includes Selenium ChromeDriver. .EXAMPLE Stop-DevProcess .EXAMPLE Stop-DevProcess -Required @('*chromedriver', 'foviawebsdk') #> function Stop-DevProcess { [CmdletBinding( SupportsShouldProcess = $true, ConfirmImpact = 'Low' )] param ( [Parameter(Position = 0)] [string[]]$Optional, [string[]]$Required ) # $Optional += @( # 'devenv' # ) $Required += @( '*chromedriver' ) $null = Test-PSEnvironment -CheckAdmin -Exit foreach ($processName in $Optional) { $processes = @( Get-Process -Name $processName -ErrorAction SilentlyContinue ) foreach ($process in $processes) { $programName = if ($process.Product) { $process.Product } else { $process.ProcessName } $nameString = "$programName [$($process.Id)]" $stop = Read-Host -Prompt ( "$nameString is running and could cause issues. Would you like to stop it? (y/n):" ) if ($stop.ToLower() -eq 'y') { if ($PSCmdlet.ShouldProcess($process, 'Stop-Process')) { $process | Stop-ProcessTree } } } } foreach ($processName in $Required) { $processes = @( Get-Process -Name $processName -ErrorAction SilentlyContinue ) foreach ($process in $processes) { $programName = if ($process.Product) { $process.Product } else { $process.ProcessName } $nameString = "$programName [$($process.Id)]" $stopMessage = "$nameString is running and will be stopped." Write-Host -Object $stopMessage if ($PSCmdlet.ShouldProcess($process, 'Stop-Process')) { $process | Stop-ProcessTree } } } } <# .SYNOPSIS Stops a process and all child processes that it spawned. .DESCRIPTION Stops a process and all child processes that it spawned. .PARAMETER ProcessId ID of the process to stop. .EXAMPLE Get-Process cmd | Stop-ProcessTree .LINK https://stackoverflow.com/a/55942155/14628263 .NOTES N/A #> function Stop-ProcessTree { [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(ValueFromPipelineByPropertyName = $true)] [Alias('Id')] [Int[]]$ProcessId ) begin { $script:CurrentProcess = Get-CimInstance -ClassName Win32_Process | Where-Object -FilterScript { $_.ProcessId -eq $PID } } process { foreach ($id in $ProcessId) { Get-CimInstance -ClassName Win32_Process | Where-Object -FilterScript { $_.ParentProcessId -eq $id -and ( $script:CurrentProcess.ProcessId, $script:CurrentProcess.ParentProcessId ) -notcontains $_.ProcessId } | ForEach-Object -Process { Stop-ProcessTree -ProcessId $_.ProcessId } $processToStop = Get-Process -Id $ProcessId -ErrorAction SilentlyContinue if ($processToStop) { <# Use for debugging unexpected kill behavior $processToStop | Format-Table -HideTableHeaders | Out-File -FilePath "$PSScriptRoot/Stop-ProcessTree.log" -Append #> $processToStop | Stop-Process -Force -PassThru | Wait-Process } } } } <# .SYNOPSIS Stops a stopwatch started from Start-Stopwatch. .DESCRIPTION Stops a stopwatch started from Start-Stopwatch. .PARAMETER InputObject A stopwatch object from Start-StopWatch. .EXAMPLE $sw = Start-Stopwatch; $sw | Stop-Stopwatch .NOTES N/A #> function Stop-Stopwatch { [OutputType([System.Diagnostics.Stopwatch])] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [System.Object]$InputObject ) process { $InputObject.Stop() $InputObject } } <# .SYNOPSIS Tests whether the current console is running with admin priviledges. .DESCRIPTION Tests whether the current console is running with admin priviledges. .EXAMPLE Test-IsAdmin .NOTES N/A #> function Test-IsAdmin { [OutputType([Bool])] [CmdletBinding()] param () $currentUser = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() $currentUser.IsInRole([Security.Principal.WindowsBuiltInRole]'Administrator') } <# .SYNOPSIS Short description .DESCRIPTION Long description .PARAMETER Path Parameter description .EXAMPLE An example .LINK https://mcpmag.com/articles/2018/07/10/check-for-locked-file-using-powershell.aspx .NOTES General notes #> function Test-IsFileLocked { [OutputType([Bool])] [CmdletBinding()] param ( [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] [Alias('FullName', 'PSPath', 'LiteralPath', 'Path')] [string[]]$FilePath ) process { foreach ($file in $FilePath) { $file = Convert-Path -Path $file if ([System.IO.File]::Exists($file)) { try { $filestream = [System.IO.File]::Open($file, 'Open', 'Write') $filestream.Close() $filestream.Dispose() $false } catch [System.UnauthorizedAccessException] { $true } catch { $true } } } } } <# .SYNOPSIS Returns boolean determining if prompt was run non-interactively. .DESCRIPTION First, we check `[Environment]::UserInteractive` to determine if the shell is running interactively. An example of not running interactively would be if the shell is running as a service. If we are running interactively, we check the Command Line Arguments to see if the `-NonInteractive` switch was used; or an abbreviation of the switch. .LINK https://github.com/Vertigion/Test-IsNonInteractiveShell #> function Test-IsNonInteractiveShell { [CmdletBinding()] [OutputType([Boolean])] param () if ([Environment]::UserInteractive) { $commandLineArgs = [Environment]::GetCommandLineArgs() $isNonInteractive = $commandLineArgs -contains '-NonInteractive' $isVsCode = foreach ($arg in $commandLineArgs) { if ($arg -match "-HostProfileId\ 'Microsoft\.VSCode'") { $true } } } if ( $env:AGENT_JOBNAME -or ( $isNonInteractive -and !$isVsCode ) ) { $true } else { $false } } <# .SYNOPSIS Checks whether the current PowerShell environment is sufficient to run the script. .DESCRIPTION Checks whether the current PowerShell environment is sufficient to run the script by checking the installed version and whether it is being ran as an admin. .PARAMETER MinimumVersion Minimum version of PowerShell to check for. .PARAMETER MaximumVersion Minimum version of PowerShell to check for. .PARAMETER CheckAdmin Check to see whether the prompt is running as an administrator. .PARAMETER Exit Throws an error instead of returning $false. .EXAMPLE Test-PSEnvironment .EXAMPLE Test-PSEnvironment -CheckAdmin -Exit .NOTES N/A #> function Test-PSEnvironment { [OutputType([Bool])] [CmdletBinding()] param ( [AllowEmptyString()] [AllowNull()] [System.Object] $MinimumVersion = [System.Version]::new('5.1.0'), [AllowEmptyString()] [AllowNull()] [System.Object] $MaximumVersion, [Switch] $CheckAdmin, [Switch] $Exit ) $errMsg = @() if ($CheckAdmin -eq $true) { if ( Test-IsAdmin ) { Write-Verbose -Message 'Host is running with admin priviledges.' } else { $errMsg += 'Host is not running with admin priviledges.' } } $hostVersion = Get-PSVersion if ($MinimumVersion) { if ($MinimumVersion.GetType().Name -eq 'String') { $MinimumVersion = [System.Version]::new($MinimumVersion) } Write-Verbose -Message ( "Checking host version: $hostVersion against minimum " + "version $MinimumVersion." ) if ($hostVersion -lt $MinimumVersion) { $errMsg += ( "The minimum version of Windows PowerShell that is required by the script ($MinimumVersion) " + "does not match the currently running version ($hostVersion) of Windows PowerShell." ) } } if ($MaximumVersion) { if ($MaximumVersion.GetType().Name -eq 'String') { $MaximumVersion = [System.Version]::new($MaximumVersion) } Write-Verbose -Message ( "Checking host version: $hostVersion against maximum " + "version $MaximumVersion." ) if ($hostVersion -gt $MaximumVersion) { $errMsg += ( "The maximum version of Windows PowerShell that is required by the script ($MaximumVersion) " + "does not match the currently running version ($hostVersion) of Windows PowerShell." ) } } if ($errMsg) { $false if ($Exit) { throw $errMsg } else { Write-Host -Object $errMsg } } else { $true } } <# .SYNOPSIS Uninstalls a program by name. .DESCRIPTION Uninstalls a program by name, instead of having to call its installer. .PARAMETER Name Name to search for. Accepts wildcard characters. Accepts pipeline input. .EXAMPLE Uninstall-ProgramByName -Name 'visual studio code' .NOTES General notes #> function Uninstall-ProgramByName { #[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingCmdletAliases', '')] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [String]$Name, [Switch]$Wmi ) begin { $null = Test-PSEnvironment -MinimumVersion 5.1 -CheckAdmin -Exit } process { $Activity = "Uninstall $Name" if ($Wmi) { $status = 'Finding the installation...' Write-Progress -Activity $Activity -Status $status $attempts = 0 $app = @() do { $attempts++ $uninstallError = $false if ($attempts -gt 1) { Write-Progress -Activity $Activity -Status $status -CurrentOperation "Attempt: $attempts" } try { Write-Verbose -Message 'Gathering all installed apps...' $apps = Get-CimInstance -ClassName 'Win32_Product' -ErrorAction Stop Write-Verbose -Message "Finding $Name to uninstall..." $app += $apps | Where-Object { $_.Name -match [Regex]::Escape($Name) } } catch { Write-Verbose -Message "Attempt $attempts failed." Write-Verbose -Message $_ $uninstallError = $true } } while (($uninstallError -eq $true) -and ($attempts -le 3)) if (!($app)) { Write-Progress -Activity $Activity -Completed Write-Warning "$Name is either not installed, or can't be found by this function." } else { Write-Verbose -Message "Uninstalled $Name on attempt $attempts." } foreach ($instance in $app) { Write-Verbose -Message "Uninstalling $($instance.Name) $($instance.Version)..." Write-Progress -Activity $Activity -Status "Uninstalling $($instance.Name) $($instance.Version)..." Invoke-CimMethod -InputObject $instance -MethodName 'Uninstall' Write-Progress -Activity $Activity -Completed } } else { Write-Progress -Activity $Activity -Status 'Finding the installation...' $programLocations = @( 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall', 'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall' ) $apps = Get-ChildItem -Path $programLocations | Get-ItemProperty | Sort-Object -Property DisplayName $app = @() $app += $apps | Where-Object { $_.DisplayName -match [Regex]::Escape($Name) } if (!($app)) { Write-Progress -Activity $Activity -Completed Write-Warning "$Name is either not installed, or can't be found by this function." } foreach ($instance in $app) { Write-Verbose -Message "Uninstalling $($instance.Name) $($instance.DisplayVersion)..." Write-Progress ` -Activity $Activity ` -Status "Uninstalling $($instance.Name) $($instance.DisplayVersion)..." $uninstallString = $instance.UninstallString $isExeOnly = Test-Path -LiteralPath $uninstallString if (!$isExeOnly) { $uninstallString += ' /passive /norestart' # Need to explicitly set uninstall for installers that just call themselves again to uninstall $uninstallString = $uninstallString.Replace('/I', '/uninstall ') } $process = Start-Process -FilePath cmd -ArgumentList ('/c', $uninstallString) -PassThru $process | Wait-Process $process | Select-Object -Property ProcessName, ExitCode Write-Progress -Activity $Activity -Completed } } } } <# .SYNOPSIS Use in place of Write-Progress to output the content to the console instead of a progress bar. #> function Write-ProgressToHost { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingCmdletAliases', '')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] [CmdletBinding()] param( [Parameter(Position = 0, Mandatory = $true)] [String]$Activity, [String]$Status, [Int32]$Id, [Int32]$PercentComplete, [Int32]$SecondsRemaining, [String]$CurrentOperation, [Int32]$ParentId, [Switch]$Completed, [Int32]$SourceId ) $paramOutputOrder = @( 'Activity', 'Status', 'PercentComplete', 'CurrentOperation', 'SecondsRemaining', 'Completed' ) $params = $paramOutputOrder | Where-Object -FilterScript { $PSBoundParameters.Keys -contains $_ } $paramValues = $params | ForEach-Object -Process { switch ($_) { 'PercentComplete' { $percent = '[' $progressByTen = [Math]::Floor($PercentComplete / 10) for ($i = 0; $i -lt $progressByTen; $i++) { $percent += '#' } for ($i = 0; $i -lt 10 - $progressByTen; $i++) { $percent += ' ' } $percent += ']' $percent } 'SecondsRemaining' { "-$($SecondsRemaining)s" } 'Completed' { 'Completed' } Default { $PSBoundParameters[$_] } } } $message = '' if ($env:AGENT_JOBNAME) { $message += '##[info] ' } $message += ($paramValues -join ' | ') if ($ProgressPreference -eq 'Continue') { Write-Host -Object $message -ForegroundColor DarkGray } else { Write-Verbose -Message $message } } |