You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
278 lines
12 KiB
278 lines
12 KiB
#!SEMICOLONS must be placed after each command as scriptdata needs to be sent to powershell directly with the -c parameter! |
|
; |
|
# consolemode_server_async.ps1 - punkshell console-mode fallback server (canonical copy) (G-106) |
|
# Persistent named-pipe server used by punk::console when twapi is absent on windows. |
|
# The Tcl side (punk::console::system) resolves this file - or falls back to its embedded copy - and launches: |
|
# pwsh|powershell -nop -nol -noni -c <this text with the <punkshell_*> placeholders substituted> |
|
# The -c (command string) mechanism is deliberate: it needs no script file at run time (kits) and is not |
|
# subject to ExecutionPolicy restrictions that can block -File runs. |
|
# The server process shares the launching process's console; it must NOT have its stdin redirected |
|
# (the console input handle is how it reaches the console), and it never reads stdin. |
|
# Placeholders (each overridable by a positional arg for standalone debug runs): |
|
# <punkshell_consoleid> $args[0] unique id suffix for the pipe name |
|
# <punkshell_parentpid> $args[1] pid of the owning tcl process; the server exits when it does |
|
# <punkshell_psdebug> $args[2] "1" enables diagnostic write-host output (default silent) |
|
# Standalone debug run example (from this directory): |
|
# pwsh -nop -nol -f consolemode_server_async.ps1 test1 0 1 |
|
# Protocol (one line per named-pipe connection): enableraw | disableraw | ping | exit |
|
# MAINTENANCE: src/modules/punk/console-999999.0a1.0.tm carries this file's text as |
|
# punk::console::system::ps_consolemode_script_embedded (the last-resort resolution for kits and |
|
# unusual cwds). Keep the two in sync - the console testsuite (psfallback.test) fails when they diverge. |
|
; |
|
# The NativeConsoleMethods C# snippet below derives from posh-git (github.com/dahlbyk/posh-git): |
|
# Copyright (c) 2010-2018 Keith Dahlby, Keith Hill, and contributors - MIT license. |
|
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: |
|
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. |
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|
; |
|
if ($PSVersionTable.PSVersion.Major -le 5) { |
|
# For Windows PowerShell, we want to remove any PowerShell 7 paths from PSModulePath |
|
#snipped from https://github.com/PowerShell/DSC/pull/777/commits/af9b99a4d38e0cf1e54c4bbd89cbb6a8a8598c4e |
|
; |
|
$env:PSModulePath = ($env:PSModulePath -split ';' | Where-Object { $_ -notlike '*\powershell\*' }) -join ';'; |
|
}; |
|
|
|
$consoleModeSource = @" |
|
using System; |
|
using System.Runtime.InteropServices; |
|
|
|
public class NativeConsoleMethods |
|
{ |
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] |
|
public static extern IntPtr GetStdHandle(int handleId); |
|
|
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] |
|
public static extern bool GetConsoleMode(IntPtr hConsoleOutput, out uint dwMode); |
|
|
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] |
|
public static extern bool SetConsoleMode(IntPtr hConsoleOutput, uint dwMode); |
|
|
|
public static uint GetConsoleMode(bool input = false) |
|
{ |
|
var handle = GetStdHandle(input ? -10 : -11); |
|
uint mode; |
|
if (GetConsoleMode(handle, out mode)) |
|
{ |
|
return mode; |
|
} |
|
return 0xffffffff; |
|
} |
|
|
|
public static uint SetConsoleMode(bool input, uint mode) |
|
{ |
|
var handle = GetStdHandle(input ? -10 : -11); |
|
if (SetConsoleMode(handle, mode)) |
|
{ |
|
return GetConsoleMode(input); |
|
} |
|
return 0xffffffff; |
|
} |
|
} |
|
"@ |
|
; |
|
[Flags()] |
|
enum ConsoleModeInputFlags |
|
{ |
|
ENABLE_PROCESSED_INPUT = 0x0001 |
|
ENABLE_LINE_INPUT = 0x0002 |
|
ENABLE_ECHO_INPUT = 0x0004 |
|
ENABLE_WINDOW_INPUT = 0x0008 |
|
ENABLE_MOUSE_INPUT = 0x0010 |
|
ENABLE_INSERT_MODE = 0x0020 |
|
ENABLE_QUICK_EDIT_MODE = 0x0040 |
|
ENABLE_EXTENDED_FLAGS = 0x0080 |
|
ENABLE_AUTO_POSITION = 0x0100 |
|
ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200 |
|
}; |
|
|
|
if (!('NativeConsoleMethods' -as [System.Type])) { |
|
Add-Type $consoleModeSource |
|
}; |
|
|
|
function rawmode { |
|
param ( |
|
[validateSet('enable', 'disable')] |
|
[string]$Action |
|
); |
|
if (!('NativeConsoleMethods' -as [System.Type])) { |
|
Add-Type $consoleModeSource |
|
}; |
|
$inputFlags = [NativeConsoleMethods]::GetConsoleMode($true); |
|
$resultflags = $inputflags; |
|
if (($inputflags -band [ConsoleModeInputFlags]::ENABLE_LINE_INPUT) -eq [ConsoleModeInputFlags]::ENABLE_LINE_INPUT) { |
|
#cooked mode |
|
$initialstate = "cooked"; |
|
if ($action -eq "enable") { |
|
#disable cooked flags |
|
$disable = [uint32](-bnot [uint32][ConsoleModeInputFlags]::ENABLE_LINE_INPUT) -band ( -bnot [uint32][ConsoleModeInputFlags]::ENABLE_ECHO_INPUT); |
|
$adjustedflags = $inputflags -band ($disable); |
|
$resultflags = [NativeConsoleMethods]::SetConsoleMode($true,$adjustedflags); |
|
}; |
|
} else { |
|
#raw mode |
|
$initialstate = "raw"; |
|
if ($action -eq "disable") { |
|
#set cooked flags |
|
$adjustedflags = $inputflags -bor [ConsoleModeInputFlags]::ENABLE_LINE_INPUT -bor [ConsoleModeInputFlags]::ENABLE_ECHO_INPUT; |
|
$resultflags = [NativeConsoleMethods]::SetConsoleMode($true,$adjustedflags); |
|
}; |
|
}; |
|
}; |
|
|
|
$consoleid = $args[0]; |
|
if ([string]::IsNullOrEmpty($consoleid)) { |
|
$consoleid = "<punkshell_consoleid>" |
|
}; |
|
$parentpidraw = $args[1]; |
|
if ([string]::IsNullOrEmpty($parentpidraw)) { |
|
$parentpidraw = "<punkshell_parentpid>" |
|
}; |
|
$psdebugraw = $args[2]; |
|
if ([string]::IsNullOrEmpty($psdebugraw)) { |
|
$psdebugraw = "<punkshell_psdebug>" |
|
}; |
|
$psdebug = ($psdebugraw -eq "1"); |
|
$pipeName = "punkshell_ps_consolemode_$consoleid"; |
|
function dbg { |
|
param([string]$m); |
|
if ($psdebug) { |
|
write-host "consolemode_server($pipeName): $m" |
|
}; |
|
}; |
|
dbg "starting - parentpidraw: $parentpidraw"; |
|
|
|
# Parent-process watch: hold a Process object obtained once by pid (handle-based, so immune to |
|
# pid reuse) and poll HasExited in the main loop. This is the orphan-prevention guarantee - the |
|
# owning tcl process does not need to send exit for this server to go away (G-106). |
|
# An unparseable/zero parent pid (e.g a standalone run with placeholders unsubstituted) disables the watch. |
|
$parentproc = $null; |
|
[int]$parentpidnum = 0; |
|
if ([int]::TryParse($parentpidraw, [ref]$parentpidnum) -and ($parentpidnum -gt 0)) { |
|
try { |
|
$parentproc = [System.Diagnostics.Process]::GetProcessById($parentpidnum); |
|
} catch { |
|
dbg "parent process $parentpidnum not found - exiting"; |
|
exit 1; |
|
}; |
|
}; |
|
|
|
$sharedData = [hashtable]::Synchronized(@{}); |
|
$scriptblock = { |
|
param($tsv); |
|
Add-Type -AssemblyName System.IO.Pipes; |
|
$keepgoing = $true; |
|
while ($keepgoing) { |
|
$pipeServer = $null; |
|
try { |
|
$pipeServer = New-Object System.IO.Pipes.NamedPipeServerStream($pipeName); |
|
try { |
|
$pipeServer.WaitForConnection(); |
|
$reader = New-Object System.IO.StreamReader($pipeServer); |
|
$message = $reader.ReadLine(); |
|
if ($message -eq "exit") { |
|
$tsv.State = "done"; |
|
[void]$msync.Set(); |
|
$keepgoing = $false; |
|
} elseif (($message -eq "enableraw") -or ($message -eq "disableraw")) { |
|
$tsv.Message = $message; |
|
[void]$msync.Set(); |
|
} elseif ($message -eq "ping") { |
|
# liveness probe - wake the main loop, no mode action |
|
; |
|
[void]$msync.Set(); |
|
}; |
|
# A null message (a connection that closed without sending a line - e.g a probe) |
|
# and unknown messages are ignored; only an explicit exit message or |
|
# parent-process death shuts the server down. |
|
# Do NOT Close/Dispose the StreamReader here: that disposes the underlying pipe |
|
# stream and a subsequent Disconnect/Dispose on it throws, killing this listener. |
|
# Disposing the pipe stream (finally below) is the whole per-connection cleanup. |
|
} finally { |
|
if ($null -ne $pipeServer) { |
|
$pipeServer.Dispose(); |
|
}; |
|
}; |
|
} catch { |
|
# unexpected listener failure (e.g pipe name already in use) - shut down rather than spin |
|
; |
|
$tsv.State = "done"; |
|
[void]$msync.Set(); |
|
$keepgoing = $false; |
|
}; |
|
}; |
|
}; |
|
|
|
$exitcode = 0; |
|
try { |
|
# AutoResetEvent: WaitOne consumes the signal atomically, so a Set that lands while the main |
|
# loop is servicing a message is never lost (the next WaitOne returns immediately). |
|
$syncEvent = New-Object System.Threading.AutoResetEvent($false); |
|
|
|
$runspace = [runspacefactory]::CreateRunspace(); |
|
[void]$runspace.Open(); |
|
$runspace.SessionStateProxy.SetVariable("pipeName", $pipeName); |
|
$runspace.SessionStateProxy.SetVariable("msync", $syncEvent); |
|
|
|
$powershell = [System.Management.Automation.PowerShell]::Create(); |
|
$powershell.Runspace = $runspace; |
|
[void]$powershell.Addscript($scriptblock).AddArgument($sharedData); |
|
|
|
$sharedData.State = "running"; |
|
$sharedData.Message = ""; |
|
$asyncResult = $powershell.BeginInvoke(); |
|
|
|
dbg "named pipe server started in runspace"; |
|
while ($true) { |
|
[void]$syncEvent.WaitOne(5000); |
|
$msg = $sharedData.Message; |
|
$sharedData.Message = ""; |
|
if ($msg -eq "enableraw") { |
|
dbg "enableraw"; |
|
$null = rawmode 'enable'; |
|
} elseif ($msg -eq "disableraw") { |
|
dbg "disableraw"; |
|
$null = rawmode 'disable'; |
|
}; |
|
if ($sharedData.State -eq "done") { |
|
dbg "exit message received"; |
|
break; |
|
}; |
|
if (($null -ne $parentproc) -and $parentproc.HasExited) { |
|
dbg "parent process $parentpidnum has exited"; |
|
break; |
|
}; |
|
}; |
|
} finally { |
|
# The listener runspace may be parked in WaitForConnection - connect once and send exit to |
|
# unblock it, otherwise the process could linger on a parked runspace thread. Skipped when |
|
# the listener already shut itself down (State done - it exited on an exit message or error). |
|
if ($sharedData.State -ne "done") { |
|
try { |
|
$cli = New-Object System.IO.Pipes.NamedPipeClientStream($pipeName); |
|
$cli.connect(1000); |
|
$writer = new-object System.IO.StreamWriter($cli); |
|
$writer.writeline("exit"); |
|
$writer.flush(); |
|
$cli.Dispose(); |
|
} catch { |
|
dbg "listener unblock skipped: $($PSItem.Exception.Message)"; |
|
}; |
|
}; |
|
try { |
|
if ($null -ne $runspace) { |
|
$runspace.Close(); |
|
$runspace.Dispose(); |
|
}; |
|
} catch { |
|
dbg "runspace tidyup error: $($PSItem.Exception.Message)"; |
|
}; |
|
try { |
|
if ($null -ne $powershell) { |
|
$powershell.dispose(); |
|
}; |
|
} catch { |
|
dbg "powershell tidyup error: $($PSItem.Exception.Message)"; |
|
}; |
|
}; |
|
dbg "shutdown complete"; |
|
exit $exitcode;
|
|
|