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
    }
}