Public/Application/Get-CardResponse.ps1
| 
                                function Get-CardResponse { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'Variable used in template')] [system.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Settings variable used in module')] [system.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseUsingScopeModifierInNewRunspaces', '', Justification = 'Variable used in runspace via parameter')] param ( [parameter(Mandatory = $true, ValueFromPipeline = $true)] [string]$Json, [parameter(Mandatory = $false)] [string]$PromptTitle = $_MvRACSettings.'Get-Response'.PromptTitle, [parameter(Mandatory = $false)] [string]$CardTitle = $_MvRACSettings.'Get-Response'.CardTitle, [parameter(Mandatory = $false)] [string]$LogoUrl = $_MvRACSettings.'Get-Response'.LogoUrl, [parameter(Mandatory = $false)] [string]$LogoHeaderText = $_MvRACSettings.'Get-Response'.LogoHeader, [bool]$ShowVersion = $_MvRACSettings.'Get-Response'.ShowVersion, [parameter(Mandatory = $false)] [int]$PortNumber = $_MvRACSettings.'Get-Response'.PortNumber, [parameter(Mandatory = $false)] [string]$HeaderBackgroundStart = $_MvRACSettings.'Get-Response'.HeaderBackgroundStart, [parameter(Mandatory = $false)] [string]$HeaderBackgroundEnd = $_MvRACSettings.'Get-Response'.HeaderBackgroundEnd, [parameter(Mandatory = $false)] [ValidateSet("Browser", "WindowsForms", "EdgeApp")] [string]$ViewMethod = $_MvRACSettings.'Get-Response'.ViewMethod, [parameter(Mandatory = $false)] [int]$WindowWidth = 400, [parameter(Mandatory = $false)] [int]$WindowHeight = 600, [switch]$ServeOnly, [switch]$AutoSize ) #Serve the card as a web page to capture response process { $html = Get-Content -Path "$PSScriptRoot\Templates\PromptCard.html" -Raw # Find an available port if the default is in use $MaxPortRetries = 10 $CurrentPort = $PortNumber $PortFound = $false for ($i = 0; $i -lt $MaxPortRetries; $i++) { $TestPort = $CurrentPort + $i # Test if port is available try { $TestListener = [System.Net.HttpListener]::new() if ($IsWindows) { $TestListener.Prefixes.Add("http://localhost:$TestPort/") } else { $TestListener.Prefixes.Add("http://+:$TestPort/") } $TestListener.Start() $TestListener.Stop() $TestListener.Close() # Port is available $CurrentPort = $TestPort $PortFound = $true if ($i -gt 0) { Write-Verbose "Port $PortNumber was in use, using port $CurrentPort instead" } break } catch { # Port is in use, try next one Write-Verbose "Port $TestPort is in use, trying next port..." continue } } if (-not $PortFound) { Write-Error "Could not find an available port after $MaxPortRetries attempts starting from $PortNumber" return } if ($IsWindows) { $ServiceUrl = "http://localhost:$CurrentPort/" } else { $ServiceUrl = "http://+:$CurrentPort/" } $LogoHeader = $LogoHeaderText if ( $ShowVersion ) { $LogoHeader = "$LogoHeaderText <span class='version'>v$ModuleVersion</span>" } #Read the JSON and only load needed extensions $AvailableExtensions = (Get-ChildItem -Path "$PSScriptRoot\Templates\Extension\Script" -Filter *.js | ForEach-Object { $_.BaseName }) $ExtensionsToLoad = @() foreach ($Extension in $AvailableExtensions) { if ($Json -match [regex]::escape($Extension)) { $ExtensionsToLoad += $Extension } } $ExtensionsJs = '' $ExtensionsCss = '' foreach ($Extension in $ExtensionsToLoad) { #Get the file content $ExtensionPath = "$PSScriptRoot\Templates\Extension\Script\$Extension.js" if (Test-Path -Path $ExtensionPath) { $ExtensionContent = Get-Content -Path $ExtensionPath -Raw $ExtensionsJs += "`n`n// Extension: $Extension`n" + $ExtensionContent } $ExtensionCssPath = "$PSScriptRoot\Templates\Extension\Style\$Extension.css" if (Test-Path -Path $ExtensionCssPath) { $ExtensionCssContent = Get-Content -Path $ExtensionCssPath -Raw $ExtensionsCss += "`n/* Extension: $Extension */`n" + $ExtensionCssContent } } $ExtensionsCss = "<style type='text/css'>$ExtensionsCss</style>" $ResponseGuid = [guid]::NewGuid().ToString() $html = $ExecutionContext.InvokeCommand.ExpandString($html) #Create a task to listen for requests $Runspace = [runspacefactory]::CreateRunspace() $Runspace.Open() $ScriptBlock = { param ($html, $ServiceUrl, $ResponseGuid) $listener = [System.Net.HttpListener]::new() #Test if the host is a windows system to determine the correct prefix $listener.Prefixes.Add($ServiceUrl) $listener.Start() while ($listener.IsListening) { # Wait for request, but handle Ctrl+C safely if ($listener.IsListening) { $context = $listener.GetContext() $request = $context.Request $response = $context.Response if ($request.HttpMethod -eq "GET") { $buffer = [System.Text.Encoding]::UTF8.GetBytes($html) $response.OutputStream.Write($buffer, 0, $buffer.Length) $response.Close() } elseif ($request.HttpMethod -eq "POST") { $reader = New-Object IO.StreamReader($request.InputStream) $data = $reader.ReadToEnd() $reader.Close() $responseString = "Thanks! Data received" $buffer = [System.Text.Encoding]::UTF8.GetBytes($responseString) # Set response headers $response.ContentLength64 = $buffer.Length $response.ContentType = "text/plain; charset=utf-8" $response.StatusCode = 200 # Write response $response.OutputStream.Write($buffer, 0, $buffer.Length) # CRITICAL: Flush and close the output stream before breaking $response.OutputStream.Flush() $response.OutputStream.Close() $response.Close() # Small delay to ensure response is sent Start-Sleep -Milliseconds 100 #Test to see the response GUID matches $jsonData = $data | ConvertFrom-Json if ($jsonData.ResponseGuid -eq $ResponseGuid) { $data break } } } } $listener.Stop() $listener.Close() } $PowerShell = [powershell]::Create() $PowerShell.Runspace = $Runspace [void]($PowerShell.AddScript($ScriptBlock).AddArgument($html).AddArgument($ServiceUrl)) $asyncResult = $PowerShell.BeginInvoke() #Open browser to the page if (!$ServeOnly) { # Initialize EdgeAppProcess variable for window close detection $EdgeAppProcess = $null switch ($ViewMethod) { "EdgeApp" { try { # Use Edge in app mode for clean WebView2 experience Write-Information "Opening in Edge (WebView2 browser mode)..." # Create a wrapper HTML that resizes window and redirects $wrapperHtml = $ExecutionContext.InvokeCommand.ExpandString((Get-Content -Path "$PSScriptRoot\Templates\EdgeAppLoader.html" -Raw)) $tempFile = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "AdaptiveCard_$(Get-Random).html") [System.IO.File]::WriteAllText($tempFile, $wrapperHtml, [System.Text.Encoding]::UTF8) # Open with Edge app mode and capture the process $ParentEdgeProcess = Start-Process "msedge" -ArgumentList "--app=file:///$($tempFile.Replace('\','/'))" -PassThru # Wait for Edge to create the app window Start-Sleep -Milliseconds 100 #loop to find the correct Edge window $MaxPollTries = 50 do { Start-Sleep -Milliseconds 100 $EdgeAppProcess = Get-Process -Name "msedge" -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowTitle -eq $CardTitle -and $_.HasExited -eq $false -and $_.MainWindowHandle -ne 0 } } while (-not $EdgeAppProcess -and $MaxPollTries--) if ($EdgeAppProcess) { Write-Verbose "Found Edge app process: ID=$($EdgeAppProcess.Id), Title='$($EdgeAppProcess.MainWindowTitle)'" } else { Write-Warning "Could not find Edge app window. Window close detection will not work." } # Clean up temp file after a delay Start-Job -ScriptBlock { param($file) Start-Sleep -Seconds 10 if (Test-Path $file) { Remove-Item $file -Force -ErrorAction SilentlyContinue } } -ArgumentList $tempFile | Out-Null } catch { Write-Warning "Failed to launch Edge: $($_.Exception.Message)" Write-Warning "Falling back to default browser..." Start-Process $ServiceUrl } } "Browser" { #Test for parameters that are not compatible with browser view if ( $AutoSize ) { Write-Warning "AutoSize parameter is not supported in Browser view. Ignoring." } if ( $WindowWidth -ne 400 -or $WindowHeight -ne 600 ) { Write-Warning "Custom window size parameters are not supported in Browser view. Ignoring." } Start-Process $ServiceUrl } default {} } $WaitingPrompt = "{blue}[{white}Waiting for user response{gray}{use Ctrl+C to cancel}{blue}]" #Set The Dot count for animation $DotCount = 0 Write-ColoredHost $WaitingPrompt -NoNewLine [console]::CursorVisible = $false #Test to see if $asyncResult halted abnormally if ($asyncResult.IsCompleted -eq $True ) { Write-Warning "Async operation did not complete as expected." #Grab the log stream from the runspace $logStream = $PowerShell.Streams.Error $logStream | ForEach-Object { Write-Verbose "Error: $_" } } try { while ($asyncResult.IsCompleted -eq $false) { #If crtl+c is pressed, stop listening Start-Sleep -Milliseconds 250 $DotCount = ($DotCount + 1) % 7 $Dots = "►" * $DotCount if ($DotCount -eq 0) { $Dots = " " } $PromptToShow = "{blue}[{white}Waiting for user response{gray}(use Ctrl+C to cancel){blue}] $Dots" #Overwrite the previous line $Host.UI.RawUI.CursorPosition = @{X = 0; Y = $Host.UI.RawUI.CursorPosition.Y } #Hide the cursor while waiting Write-ColoredHost ("`r" + $PromptToShow) -NoNewLine #If the the viewMode is EdgeApp and the window is no longer open, cancel waiting if ( $ViewMethod -eq "EdgeApp") { # Check if the Edge process is still running if ($EdgeAppProcess -and $EdgeAppProcess.HasExited -and $asyncResult.IsCompleted -eq $false) { Write-Verbose "EdgeApp window was closed by user" throw "WindowClosed" } } } Write-ColoredHost "{Green}[V]" #Show the cursor again [console]::CursorVisible = $true $data = $PowerShell.EndInvoke($asyncResult) } catch { Write-Error "An error occurred: $_" } finally { if ($null -eq $data) { try { [void](Invoke-WebRequest -Uri $ServiceUrl -Method Post -OperationTimeoutSeconds 1 -ConnectionTimeoutSeconds 1 -Body @{responseGuid = $ResponseGuid }) } catch { [void]$_ } [void]($PowerShell.Stop()) } #Kill the Edge app process if still running # if ($EdgeAppProcess) { # # Stop-Process -Id $EdgeAppProcess.Id -Force -ErrorAction SilentlyContinue # } #Force kill the powershell if still running [void]($PowerShell.Dispose()) #Close the runspace $Runspace.Close() $Runspace.Dispose() } if ( $null -ne $data ) { return $data | ConvertFrom-Json } } } }  |