pglet.psm1
<# References: https://learn-powershell.net/2013/04/19/sharing-variables-and-live-objects-between-powershell-runspaces/ Default session variables: PGLET_CONNECTION_ID - the last page connection ID. PGLET_CONTROL_ID - the last added control ID. PGLET_EVENT_TARGET - the last received event target (control ID). PGLET_EVENT_NAME - the last received event name. PGLET_EVENT_DATA - the last received event data. #> $global:PGLET_EXE = "" $global:PGLET_CONNECTIONS = [hashtable]::Synchronized(@{}) $global:PGLET_CONNECTION_ID = "" function Connect-PgletApp { [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 0, HelpMessage = "The name of Pglet app.")] [string]$Name, [Parameter(Mandatory = $true, HelpMessage = "A handler script block for a new user session.")] [scriptblock]$ScriptBlock, [Parameter(Mandatory = $false, HelpMessage = "Makes the app available as public at pglet.io service or a self-hosted Pglet server")] [switch]$Web, [Parameter(Mandatory = $false, HelpMessage = "Makes the app available as private at pglet.io hosted service.")] [switch]$Private, [Parameter(Mandatory = $false, HelpMessage = "Connects to the app on a self-hosted Pglet server.")] [string]$Server, [Parameter(Mandatory = $false, HelpMessage = "Authentication token for pglet.io service or a self-hosted Pglet server.")] [string]$Token, [Parameter(Mandatory = $false, HelpMessage = "Do not open browser window")] [switch]$NoWindow ) $ErrorActionPreference = "Stop" $pargs = @() $pargs += "app" if ($Name) { $pargs += $Name } if ($Web.IsPresent) { $pargs += "--web" } if ($Private.IsPresent) { $pargs += "--private" } if ($NoWindow.IsPresent) { $pargs += "--no-window" } if ($Server) { $pargs += "--server" $pargs += $Server } if ($Token) { $pargs += "--token" $pargs += $Token } $Sessions = [hashtable]::Synchronized(@{}) $sessionsMonitor = { function Write-Trace($value) { [System.Console]::WriteLine($value) } try { while ($true) { $sids = @() $sids += $Sessions.Keys foreach($sid in $sids) { $session = $Sessions[$sid] if ($session.AsyncHandler.IsCompleted) { try { Write-Trace "Session exited: $sid" $session.PowerShell.EndInvoke($session.AsyncHandler) } catch { Write-Trace "Error terminating session: $_" } $session.Runspace.Close() $session.PowerShell.Dispose() $Sessions.Remove($sid) } } Start-Sleep -s 1 } } catch { Write-Trace "An error occurred in session monitor: $_" } finally { Write-Trace "Sessions monitor stopped" } } # sessions monitor $rsMonitor = [runspacefactory]::CreateRunspace() $psMonitor = [powershell]::Create() $psMonitor.Runspace = $rsMonitor $rsMonitor.Open() | Out-Null $rsMonitor.SessionStateProxy.SetVariable('Sessions', $Sessions) $psMonitor.AddScript($sessionsMonitor) | Out-Null $psMonitor.BeginInvoke() | Out-Null try { & $PGLET_EXE $pargs | ForEach-Object { if (-not $PageURL) { $PageURL = $_ Write-Host "Page URL: $PageURL" return } $sessionID = $_ $Runspace = [runspacefactory]::CreateRunspace() $PowerShell = [powershell]::Create() $PowerShell.Runspace = $Runspace $Runspace.Open() | Out-Null $PowerShell.AddScript("Import-Module ([IO.Path]::Combine('$PSScriptRoot', 'pglet.psm1'))") | Out-Null $PowerShell.AddScript('$global:PGLET_CONNECTION_ID="' + $sessionID + '"') | Out-Null $PowerShell.AddScript('$global:PGLET_CONNECTIONS=[hashtable]::Synchronized(@{})') | Out-Null $PowerShell.AddScript($ScriptBlock) | Out-Null # add session to monitor $Sessions[$sessionID] = @{ SessionID = $sessionID PowerShell = $PowerShell Runspace = $Runspace AsyncHandler = $PowerShell.BeginInvoke() } Write-Trace "Session started: $sessionID" } } finally { Write-Host "Terminating app..." # terminate all running runspaces $sids = @() $sids += $Sessions.Keys foreach($sid in $sids) { $session = $Sessions[$sid] if (-not $session.AsyncHandler.IsCompleted) { Write-Host "Terminate session:" $sid $session.PowerShell.Stop() $session.Runspace.Close() $session.PowerShell.Dispose() $Sessions.Remove($sid) } } $rsMonitor.Close() $psMonitor.Dispose() } } function Connect-PgletPage { [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 0, HelpMessage = "The name of Pglet page.")] [string]$Name, [Parameter(Mandatory = $false, HelpMessage = "Makes the page available as public at pglet.io service or a self-hosted Pglet server")] [switch]$Web, [Parameter(Mandatory = $false, HelpMessage = "Makes the page available as private at pglet.io service or a self-hosted Pglet server.")] [switch]$Private, [Parameter(Mandatory = $false, HelpMessage = "Connects to the page on a self-hosted Pglet server.")] [string]$Server, [Parameter(Mandatory = $false, HelpMessage = "Authentication token for pglet.io service or a self-hosted Pglet server.")] [string]$Token, [Parameter(Mandatory = $false, HelpMessage = "Do not open browser window")] [switch]$NoWindow ) $ErrorActionPreference = "Stop" $pargs = @() $pargs += "page" if ($Name) { $pargs += $Name } if ($Web.IsPresent) { $pargs += "--web" } if ($Private.IsPresent) { $pargs += "--private" } if ($NoWindow.IsPresent) { $pargs += "--no-window" } if ($Server) { $pargs += "--server" $pargs += $Server } if ($Token) { $pargs += "--token" $pargs += $Token } # run pglet client and get results $presults = (& $PGLET_EXE $pargs) if ($presults -match "(?<pipe_id>[^\s]+)\s(?<page_url>[^\s]+)") { $pipeId = $Matches["pipe_id"] $PageURL = $Matches["page_url"] } else { throw "Invalid pglet results: $presults" } $global:PGLET_CONNECTION_ID = $pipeId Write-Host "Page URL: $PageURL" return $pipeId } function openConnection($pipeId) { $conn = $PGLET_CONNECTIONS[$pipeId] if ($conn) { return $conn } if (-not $IsLinux -and -not $IsMacOS) { # use named pipes on Windows $conn = @{ pipe = new-object System.IO.Pipes.NamedPipeClientStream($pipeId) eventPipe = new-object System.IO.Pipes.NamedPipeClientStream("$pipeId.events") } # connect pipes $conn.pipe.Connect(5000) $conn.eventPipe.Connect(5000) # create readers and writers $conn.pipeReader = new-object System.IO.StreamReader($conn.pipe) $conn.pipeWriter = new-object System.IO.StreamWriter($conn.pipe) $conn.pipeWriter.AutoFlush = $true $conn.eventPipeReader = new-object System.IO.StreamReader($conn.eventPipe) } else { $conn = @{ pipeName = $pipeId } } $global:PGLET_CONNECTIONS.Add($pipeId, $conn) return $conn } function Disconnect-Pglet { [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 0, HelpMessage = "Page connection ID.")] [string]$Page ) $ErrorActionPreference = "Stop" $pipeId = $Page if (-not $pipeId) { $pipeId = $PGLET_CONNECTION_ID } if (-not $pipeId) { throw "No active connections." } $conn = $PGLET_CONNECTIONS[$pipeId] if ($conn -and (-not $IsLinux -and -not $IsMacOS)) { $conn.pipe.Close() $conn.eventPipe.Close() } } function Invoke-Pglet { [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 0, HelpMessage = "Pglet command to send.")] [string]$Command, [Parameter(Mandatory = $false, Position = 1, HelpMessage = "Page connection ID.")] [string]$Page ) $ErrorActionPreference = "Stop" $pipeId = $Page if (-not $pipeId) { $pipeId = $PGLET_CONNECTION_ID } if (-not $pipeId) { throw "No active connections." } $conn = openConnection $pipeId $waitResult = $true if ($Command -match "(?<commandName>[^\s]+)\s(.*)") { $commandName = $Matches["commandName"] #Write-Host "COMMAND: $commandName" if ($commandName.toLower().endsWith("f")) { $waitResult = $false } } if ($IsLinux -or $IsMacOS) { $conn.pipeWriter = New-Object System.IO.StreamWriter($conn.pipeName) } # send command $conn.pipeWriter.WriteLine($command) if ($IsLinux -or $IsMacOS) { $conn.pipeWriter.Close() } if ($waitResult) { # parse results $ERROR_RESULT = "error" try { if ($IsLinux -or $IsMacOS) { $conn.pipeReader = New-Object System.IO.StreamReader($conn.pipeName) } $result = $conn.pipeReader.ReadLine() if ($result.StartsWith("$ERROR_RESULT ")) { throw $result.Substring($ERROR_RESULT.Length + 1) } elseif ($result -match "(?<lines_count>[\d]+)\s(?<result>.*)") { $lines_count = [int]$Matches["lines_count"] $result = $Matches["result"] # read the rest of multi-line result for($i = 0; $i -lt $lines_count; $i++) { $line = $conn.pipeReader.ReadLine() $result = "$result`n$line" } } else { throw "Invalid result: $result" } return $result } finally { if ($IsLinux -or $IsMacOS) { $conn.pipeReader.Close() } } } } function Wait-PgletEvent() { [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 0, HelpMessage = "Page connection ID.")] [string]$Page ) $ErrorActionPreference = "Stop" $pipeId = $Page if (-not $pipeId) { $pipeId = $PGLET_CONNECTION_ID } if (-not $pipeId) { throw "No active connections." } $conn = openConnection $pipeId if ($IsLinux -or $IsMacOS) { $conn.eventPipeReader = New-Object System.IO.StreamReader("$($conn.pipeName).events") } $line = $conn.eventPipeReader.ReadLine() if ($IsLinux -or $IsMacOS) { $conn.eventPipeReader.Close() } #Write-Host "Event: $line" if ($line -match "(?<target>[^\s]+)\s(?<name>[^\s]+)(\s(?<data>.+))*") { return @{ Target = $Matches["target"] Name = $Matches["name"] Data = $Matches["data"] } } else { throw "Invalid event data: $line" } } function Write-Trace { param( [Parameter(Mandatory=$true, Position=0, ValueFromRemainingArguments=$true)] [string]$value ) [System.Console]::WriteLine($value) } function installPglet { [CmdletBinding()] Param() $ErrorActionPreference = "Stop" $global:PGLET_EXE = "pglet.exe" if ($IsLinux -or $IsMacOS) { $global:PGLET_EXE = "pglet" } # check if pglet.exe is in PATH (dev mode) $pgletInPath = Get-Command $global:PGLET_EXE -ErrorAction SilentlyContinue if ($pgletInPath) { Write-Verbose "Pglet in PATH found: $($pgletInPath.Path)" $global:PGLET_EXE = $pgletInPath.Path return } $pgletHome = [IO.Path]::Combine($HOME, ".pglet") $pgletBin = [IO.Path]::Combine($pgletHome, "bin") $global:PGLET_EXE = [IO.Path]::Combine($pgletBin, $global:PGLET_EXE) # create bin dir if (-not (Test-Path $pgletBin)) { Write-Verbose "Creating $pgletBin directory" New-Item -ItemType Directory -Path $pgletBin -Force | Out-Null } # target $fileName = "pglet-windows-amd64.zip" if ($IsLinux) { $fileName = "pglet-linux-amd64.tar.gz" } elseif ($IsMacOS) { $fileName = "pglet-darwin-amd64.tar.gz" } # GitHub requires TLS 1.2 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # Min version required by PS module $ver = $MyInvocation.MyCommand.Module.PrivateData.Pglet.MinimumVersion # Installed version if (Test-Path $PGLET_EXE) { try { $installedVer = (& $PGLET_EXE --version) } catch {} } if ($installedVer -and ($installedVer -eq $ver)) { Write-Verbose "Newer version is already installed" return } Write-Host "Installing Pglet v$ver..." -NoNewline $pgletUri = "https://github.com/pglet/pglet/releases/download/v$ver/$fileName" $packagePath = [IO.Path]::Combine($pgletHome, $fileName) (New-Object Net.WebClient).DownloadFile($pgletUri, $packagePath) Write-Verbose "Unzipping..." if ($IsLinux -or $IsMacOS) { # untar tar zxf $packagePath -C $pgletBin } else { # unzip Expand-Archive -Path $packagePath -DestinationPath $pgletBin -Force } Remove-Item $packagePath -Force $installedVer = (& $PGLET_EXE --version) Write-Host "OK" } installPglet -verbose New-Alias -Name ipg -Value Invoke-Pglet # Exported functions Export-ModuleMember -Function Connect-PgletApp, Connect-PgletPage, Disconnect-Pglet, Invoke-Pglet, Wait-PgletEvent, Write-Trace -Alias ipg |