ExitProcessRequest.psm1

<#
.SYNOPSIS
    Displays a form to request the user to save and close a specified process. If the process is not closed within the specified time limit, it is forcibly closed.
 
.DESCRIPTION
    The Show-ExitProcessRequest function displays a form that requests the user to save and close a specified process. The function allows the user to continue or cancel the process closure. If the process is not closed within the specified time limit, it is forcibly closed.
 
.PARAMETER processName
    The name of the process to be closed.
 
.PARAMETER timeLimit
    The time limit in seconds for the user to save and close the process. Cannot be set below 5 seconds for the sake of usefulness. Default is 120 seconds.
 
.PARAMETER allowCancel
    Specifies whether the user is allowed to cancel the process closure. If this switch is present, the Cancel button will be enabled.
 
.PARAMETER logoPath
    The path to the logo image file to be displayed in the form. If not specified, a default logo will be used.
 
.PARAMETER logoUrl
    The URL of the logo image file to be displayed in the form. If both logoPath and logoUrl are specified, logoPath will be used and logoUrl will be ignored.
 
.PARAMETER displayName
    The display name of the process. This will be shown in the form. Default is the same as the processName.
 
.EXAMPLE
    Show-ExitProcessRequest -processName "Notepad" -timeLimit 60 -allowCancel -logoPath "C:\Images\logo.png"
    This example displays a form requesting the user to save and close the Notepad
#>


function Show-ExitProcessRequest {
    param(
        [Parameter(Mandatory=$true)]
        [string]$processName,

        [Parameter(Mandatory=$false)]
        [int]$timeLimit = 120,

        [Parameter(Mandatory=$false)]
        [switch]$allowCancel,

        [Parameter(Mandatory=$false)]
        [string]$logoPath,

        [Parameter(Mandatory=$false)]
        [string]$logoUrl,

        [Parameter(Mandatory=$false)]
        [string]$displayName = $processName
    )

    Add-Type -AssemblyName System.Windows.Forms

    #Before we do anything, lets see if we even need to bother the user
    if($null -eq (Get-Process "$processName" -ErrorAction SilentlyContinue).HandleCount) {
        Write-Output "No processes found for $displayName. Exiting."
        return
    }

    if ($logoPath -and $logoUrl) {
        Write-Warning "Both logoPath and logoUrl were specified. Using logoPath."
        $logoUrl = $null
    } elseif ($logoPath -eq "" -and $logoUrl -eq "") {
        Write-Warning "Neither logoPath nor logoUrl were specified. Using default logo."
        $logoPath = "$PSScriptRoot\default.png"
    }

    if ($timeLimit -lt 5) {
        Write-Warning "Time limit is less than 5 seconds. Setting to 5 seconds."
        $timeLimit = 5
    }

    #Create a new Form
    $form = New-Object System.Windows.Forms.Form
    $form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog
    $form.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon("$PSHOME\powershell.exe")
    $form.ControlBox = $false
    $form.TopMost = $true
    $form.Text = "Application Exit Request"
    $form.Size = New-Object System.Drawing.Size(400,300)
    $form.StartPosition = "CenterScreen"
    $form.Add_FormClosing({ if ($_.CloseReason -eq "UserClosing" -and $null -eq $form.Tag) { $form.Tag = "Closed" } })

    # Decide if we need to adjust the picture or use the default
    if($logoUrl -ne "") {
        try {
            Invoke-WebRequest $logoUrl -OutFile "$env:TEMP\ExitProcessRequestIcon.png"

            # Load the original image
            $originalImage = [System.Drawing.Image]::FromFile("$env:TEMP\ExitProcessRequestIcon.png")
        }
        catch {
            $originalImage = [System.Drawing.Image]::FromFile("$PSScriptRoot\default.png")
        }

        # Create a new bitmap of the desired size
        $resizedImage = New-Object System.Drawing.Bitmap(100, 100)

        # Get a graphics object from the new bitmap
        $graphics = [System.Drawing.Graphics]::FromImage($resizedImage)

        # Draw the original image onto the new bitmap, effectively resizing it
        $graphics.DrawImage($originalImage, 0, 0, 100, 100)

        # Dispose the original image and graphics object as they are no longer needed
        $originalImage.Dispose()
        $graphics.Dispose()

        # Now use $resizedImage where you used to use $originalImage
        $pictureBox = New-Object System.Windows.Forms.PictureBox
        $pictureBox.Size = New-Object System.Drawing.Size(100,100)
        $pictureBox.Location = New-Object System.Drawing.Point(25,5)
        $pictureBox.Image = $resizedImage
        $form.Controls.Add($pictureBox)
    } else {

        # Load the original image
        $originalImage = [System.Drawing.Image]::FromFile("$logoPath")

        # Create a new bitmap of the desired size
        $resizedImage = New-Object System.Drawing.Bitmap(100, 100)

        # Get a graphics object from the new bitmap
        $graphics = [System.Drawing.Graphics]::FromImage($resizedImage)

        # Draw the original image onto the new bitmap, effectively resizing it
        $graphics.DrawImage($originalImage, 0, 0, 100, 100)

        # Dispose the original image and graphics object as they are no longer needed
        $originalImage.Dispose()
        $graphics.Dispose()

        # Now use $resizedImage where you used to use $originalImage
        $pictureBox = New-Object System.Windows.Forms.PictureBox
        $pictureBox.Size = New-Object System.Drawing.Size(100,100)
        $pictureBox.Location = New-Object System.Drawing.Point(25,5)
        $pictureBox.Image = $resizedImage
        $form.Controls.Add($pictureBox)
    }    
    

    #Add a Label for the instructions
    $label = New-Object System.Windows.Forms.Label
    $label.Size = New-Object System.Drawing.Size(235,100)
    $label.Location = New-Object System.Drawing.Point(130,5)
    $label.Text = "Please save and close $displayName if it is being used. It is about to be forcibly closed."
    $label.Font = New-Object System.Drawing.Font("Verdana", 10)
    $label.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter
    $form.Controls.Add($label)

    #Add a Label for the countdown
    $counterLabel = New-Object System.Windows.Forms.Label
    $counterLabel.Size = New-Object System.Drawing.Size(360,80)
    $counterLabel.Location = New-Object System.Drawing.Point(20,110)
    $counterLabel.Font = New-Object System.Drawing.Font("Verdana", 20)
    $counterLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter
    $form.Controls.Add($counterLabel)

    #Add a Button for "Continue"
    $continueButton = New-Object System.Windows.Forms.Button
    $continueButton.Location = New-Object System.Drawing.Point(5,195)
    $continueButton.Size = New-Object System.Drawing.Size(120,60)
    $continueButton.Text = "Continue"
    $continueButton.Font = New-Object System.Drawing.Font("Verdana", 14)
    $continueButton.Add_Click({ $form.Tag = "Continue"; $form.Close() })
    $form.Controls.Add($continueButton)

    #Add a Button for "Cancel"
    $cancelButton = New-Object System.Windows.Forms.Button
    $cancelButton.Location = New-Object System.Drawing.Point(245,195)
    $cancelButton.Size = New-Object System.Drawing.Size(120,60)
    $cancelButton.Text = "Cancel"
    $cancelButton.Font = New-Object System.Drawing.Font("Verdana", 14)
    $cancelButton.Enabled = $allowCancel.IsPresent
    $cancelButton.Add_Click({ $form.Tag = "Cancelled"; $form.Close() })
    $form.Controls.Add($cancelButton)

    #Start the timer
    $startTime = Get-Date
    $timer = New-Object System.Windows.Forms.Timer
    $timer.Interval = 1000 # 1 second
    $timer.Add_Tick({
        $elapsed = [math]::Round((New-TimeSpan -Start $startTime).TotalSeconds)
        $remaining = $timeLimit - $elapsed
        $counterLabel.Text = "$remaining seconds remaining"
        if ($remaining -le 0) {
            $form.Tag = "Time's up"
            $form.Close()
        }
    })


    #Before we do anything, lets see if we even need to bother the user
    if($null -eq (Get-Process "$processName" -ErrorAction SilentlyContinue).HandleCount) {
        Write-Output "No processes found for $processName. Exiting."
        return
    }


    if ([Environment]::UserInteractive) {
        # This is an interactive session, so we display the form and start the timer

        $timer.Start()

        #Show the Form
        $form.ShowDialog() | Out-Null

        switch ($x) {
            condition {  }
            Default {}
        }

        switch ($form.Tag) {
            "Cancelled" { 
                Write-Error "User cancelled the operation"
            }

            "Continue" {
                    If((Get-Process "$processName" -ErrorAction SilentlyContinue).HandleCount -gt 0) {
                        Get-Process "$processName" | Stop-Process -Force
                        Write-Output "User clicked continue."
                    }
                    else {
                        Write-Output "User clicked continue."
                        Write-Output "No process with the name $processName was found."
                    }
            }
            
            "Closed" {
                Write-Error "User closed the window" 
            }
            
            "Time's up" { 
                Write-Output "Time for $displayName to be closed."

                If((Get-Process "$processName" -ErrorAction SilentlyContinue).HandleCount -gt 0) {
                    Get-Process "$processName" | Stop-Process -Force
                }
                else {
                    Write-Output "No process with the name $processName was found."
                }
            }
        }


        #Clean up
        $timer.Dispose()
        $form.Dispose()
    } else {
        # This is a non-interactive session, so we skip the form and timer and just stop the process

        Stop-Process -Name $processName -Force
        Write-Output "Non-interactive session, $processName was forcibly closed"        
    }
}

Export-ModuleMember -Function Show-ExitProcessRequest