mirror of
https://github.com/doublecmd/doublecmd.git
synced 2026-06-21 09:58:13 +00:00
481 lines
15 KiB
ObjectPascal
481 lines
15 KiB
ObjectPascal
{
|
|
Double Commander
|
|
-------------------------------------------------------------------------
|
|
Windows pseudoterminal device implementation
|
|
|
|
Copyright (C) 2021 Alexander Koblov (alexx2000@mail.ru)
|
|
|
|
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.
|
|
}
|
|
|
|
unit VTEmuPty;
|
|
|
|
{$mode delphi}
|
|
|
|
interface
|
|
|
|
uses
|
|
Classes, SysUtils, LCLProc, LCLType, Windows, VTEmuCtl;
|
|
|
|
type
|
|
|
|
{ TPtyDevice }
|
|
|
|
TPtyDevice = class(TCustomPtyDevice)
|
|
private
|
|
FPty: PVOID;
|
|
FSize: TCoord;
|
|
FLength: Integer;
|
|
FThread: TThread;
|
|
FPipeIn, FPipeOut: THandle;
|
|
FBuffer: array[0..8191] of AnsiChar;
|
|
protected
|
|
procedure ReadySync;
|
|
procedure ReadThread;
|
|
procedure DestroyPseudoConsole;
|
|
procedure SetConnected(AValue: Boolean); override;
|
|
function CreatePseudoConsole(const ACommand: String): Boolean;
|
|
public
|
|
constructor Create(AOwner: TComponent); override;
|
|
destructor Destroy; override;
|
|
function WriteStr(const Str: string): Integer; override;
|
|
function SetCurrentDir(const Path: String): Boolean; override;
|
|
function SetScreenSize(aCols, aRows: Integer): Boolean; override;
|
|
end;
|
|
|
|
implementation
|
|
|
|
uses
|
|
CTypes, DCOSUtils, DCConvertEncoding;
|
|
|
|
type
|
|
TConsoleType = (ctNone, ctNative, ctEmulate);
|
|
|
|
var
|
|
ConsoleType: TConsoleType = ctNone;
|
|
|
|
procedure ClosePipe(var AHandle: THandle);
|
|
begin
|
|
if (AHandle <> INVALID_HANDLE_VALUE) then
|
|
begin
|
|
CloseHandle(AHandle);
|
|
AHandle:= INVALID_HANDLE_VALUE;
|
|
end;
|
|
end;
|
|
|
|
{
|
|
*******************************************************************************
|
|
Windows Pseudo Console (ConPTY), Windows 10 1809 and higher
|
|
*******************************************************************************
|
|
}
|
|
|
|
const
|
|
EXTENDED_STARTUPINFO_PRESENT = $00080000;
|
|
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = $00020016;
|
|
|
|
type
|
|
PHPCON = ^HPCON;
|
|
HPCON = type PVOID;
|
|
SIZE_T = type ULONG_PTR;
|
|
PSIZE_T = type PULONG_PTR;
|
|
LPPROC_THREAD_ATTRIBUTE_LIST = type PVOID;
|
|
|
|
STARTUPINFOEXW = record
|
|
StartupInfo: STARTUPINFOW;
|
|
lpAttributeList: LPPROC_THREAD_ATTRIBUTE_LIST;
|
|
end;
|
|
|
|
var
|
|
CreatePseudoConsole: function(size: COORD; hInput: HANDLE; hOutput: HANDLE;
|
|
dwFlags: DWORD; phPC: PHPCON): HRESULT; stdcall;
|
|
ClosePseudoConsole: procedure(hPC: HPCON); stdcall;
|
|
ResizePseudoConsole: function(hPC: HPCON; size: COORD): HRESULT; stdcall;
|
|
InitializeProcThreadAttributeList: function(lpAttributeList: LPPROC_THREAD_ATTRIBUTE_LIST;
|
|
dwAttributeCount: DWORD; dwFlags: DWORD;
|
|
lpSize: PSIZE_T): BOOL; stdcall;
|
|
UpdateProcThreadAttribute: function(lpAttributeList: LPPROC_THREAD_ATTRIBUTE_LIST;
|
|
dwFlags: DWORD; Attribute: DWORD_PTR;
|
|
lpValue: PVOID; cbSize: SIZE_T;
|
|
lpPreviousValue: PVOID; lpReturnSize: PSIZE_T): BOOL; stdcall;
|
|
DeleteProcThreadAttributeList: procedure(lpAttributeList: LPPROC_THREAD_ATTRIBUTE_LIST); stdcall;
|
|
|
|
function CreatePseudoConsoleNew(const ACommand: String; phPC: PPointer; phPipeIn, phPipeOut: PHandle; ASize: COORD): Boolean;
|
|
var
|
|
attrListSize: SIZE_T = 0;
|
|
startupInfo: STARTUPINFOEXW;
|
|
piClient: PROCESS_INFORMATION;
|
|
hPipePTYIn: HANDLE = INVALID_HANDLE_VALUE;
|
|
hPipePTYOut: HANDLE = INVALID_HANDLE_VALUE;
|
|
begin
|
|
startupInfo:= Default(STARTUPINFOEXW);
|
|
Result:= CreatePipe(hPipePTYIn, phPipeOut^, nil, 0) and
|
|
CreatePipe(phPipeIn^, hPipePTYOut, nil, 0);
|
|
if Result then
|
|
begin
|
|
Result:= CreatePseudoConsole(ASize, hPipePTYIn, hPipePTYOut, 0, phPC) = S_OK;
|
|
// We can close the handles here because they are duplicated in the ConHost
|
|
if Result then
|
|
begin
|
|
CloseHandle(hPipePTYIn);
|
|
CloseHandle(hPipePTYOut);
|
|
hPipePTYIn:= INVALID_HANDLE_VALUE;
|
|
hPipePTYOut:=INVALID_HANDLE_VALUE;
|
|
end;
|
|
end;
|
|
if Result then
|
|
begin
|
|
startupInfo.StartupInfo.cb:= SizeOf(STARTUPINFOEXW);
|
|
InitializeProcThreadAttributeList(nil, 1, 0, @attrListSize);
|
|
startupInfo.lpAttributeList:= GetMem(attrListSize);
|
|
Result:= Assigned(startupInfo.lpAttributeList);
|
|
if Result then
|
|
begin
|
|
// Initialize thread attribute list and set Pseudo Console attribute
|
|
Result:= InitializeProcThreadAttributeList(startupInfo.lpAttributeList, 1, 0, @attrListSize) and
|
|
UpdateProcThreadAttribute(startupInfo.lpAttributeList,0,
|
|
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
|
|
phPC^, SizeOf(HPCON), nil, nil);
|
|
end;
|
|
end;
|
|
if Result then
|
|
begin
|
|
Result:= CreateProcessW(nil, PWideChar(CeUtf8ToUtf16(ACommand)),
|
|
nil, nil, False, EXTENDED_STARTUPINFO_PRESENT,
|
|
nil, nil, @startupInfo.StartupInfo, @piClient);
|
|
end;
|
|
if not Result then
|
|
begin
|
|
ClosePipe(phPipeIn^);
|
|
ClosePipe(phPipeOut^);
|
|
ClosePipe(hPipePTYIn);
|
|
ClosePipe(hPipePTYOut);
|
|
end;
|
|
// Cleanup attribute list
|
|
if Assigned(startupInfo.lpAttributeList) then
|
|
begin
|
|
DeleteProcThreadAttributeList(startupInfo.lpAttributeList);
|
|
FreeMem(startupInfo.lpAttributeList);
|
|
end;
|
|
end;
|
|
|
|
function InitializeNew: Boolean;
|
|
var
|
|
hModule: HINST;
|
|
begin
|
|
Result:= (Win32MajorVersion >= 10);
|
|
if Result then
|
|
begin
|
|
hModule:= GetModuleHandle(Kernel32);
|
|
CreatePseudoConsole:= GetProcAddress(hModule, 'CreatePseudoConsole');
|
|
Result:= Assigned(CreatePseudoConsole);
|
|
if Result then
|
|
begin
|
|
ClosePseudoConsole:= GetProcAddress(hModule, 'ClosePseudoConsole');
|
|
ResizePseudoConsole:= GetProcAddress(hModule, 'ResizePseudoConsole');
|
|
UpdateProcThreadAttribute:= GetProcAddress(hModule, 'UpdateProcThreadAttribute');
|
|
DeleteProcThreadAttributeList:= GetProcAddress(hModule, 'DeleteProcThreadAttributeList');
|
|
InitializeProcThreadAttributeList:= GetProcAddress(hModule, 'InitializeProcThreadAttributeList');
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
{
|
|
*******************************************************************************
|
|
WinPTY
|
|
*******************************************************************************
|
|
}
|
|
|
|
const
|
|
WINPTY_MOUSE_MODE_AUTO = 1;
|
|
WINPTY_MOUSE_MODE_FORCE = 2;
|
|
// Agent RPC call: process creation
|
|
WINPTY_SPAWN_FLAG_AUTO_SHUTDOWN = 1;
|
|
WINPTY_SPAWN_FLAG_EXIT_AFTER_SHUTDOWN = 2;
|
|
// Configuration of a new agent
|
|
WINPTY_FLAG_CONERR = $01;
|
|
WINPTY_FLAG_PLAIN_OUTPUT = $02;
|
|
WINPTY_FLAG_COLOR_ESCAPES = $04;
|
|
WINPTY_FLAG_ALLOW_CURPROC_DESKTOP_CREATION = $08;
|
|
// Error codes
|
|
WINPTY_ERROR_SUCCESS = 0;
|
|
WINPTY_ERROR_OUT_OF_MEMORY = 1;
|
|
WINPTY_ERROR_SPAWN_CREATE_PROCESS_FAILED = 2;
|
|
WINPTY_ERROR_LOST_CONNECTION = 3;
|
|
WINPTY_ERROR_AGENT_EXE_MISSING = 4;
|
|
WINPTY_ERROR_UNSPECIFIED = 5;
|
|
WINPTY_ERROR_AGENT_DIED = 6;
|
|
WINPTY_ERROR_AGENT_TIMEOUT = 7;
|
|
WINPTY_ERROR_AGENT_CREATION_FAILED = 8;
|
|
|
|
type
|
|
winpty_t = record end;
|
|
Pwinpty_t = ^winpty_t;
|
|
|
|
winpty_result_t = type DWORD;
|
|
|
|
winpty_config_t = record end;
|
|
Pwinpty_config_t = ^winpty_config_t;
|
|
|
|
winpty_error_t = record end;
|
|
winpty_error_ptr_t = ^winpty_error_t;
|
|
Pwinpty_error_ptr_t = ^winpty_error_ptr_t;
|
|
|
|
winpty_spawn_config_t = record end;
|
|
Pwinpty_spawn_config_t = ^winpty_spawn_config_t;
|
|
|
|
var
|
|
winpty_config_new: function(agentFlags: UInt64; err: Pwinpty_error_ptr_t): Pwinpty_config_t; cdecl;
|
|
winpty_config_free: procedure(cfg: Pwinpty_config_t); cdecl;
|
|
|
|
winpty_config_set_initial_size: procedure(cfg: Pwinpty_config_t; cols, rows: cint); cdecl;
|
|
winpty_config_set_mouse_mode: procedure(cfg: Pwinpty_config_t; mouseMode: cint); cdecl;
|
|
|
|
winpty_open: function(const cfg: Pwinpty_config_t; err: Pwinpty_error_ptr_t): Pwinpty_t; cdecl;
|
|
winpty_free: procedure(wp: Pwinpty_t); cdecl;
|
|
|
|
winpty_error_code: function(err: winpty_error_ptr_t): winpty_result_t; cdecl;
|
|
winpty_error_msg: function(err: winpty_error_ptr_t): LPCWSTR; cdecl;
|
|
winpty_error_free: procedure(err: winpty_error_ptr_t); cdecl;
|
|
|
|
winpty_spawn_config_new: function(spawnFlags: UInt64; appname, cmdline, cwd,
|
|
env: LPCWSTR; err: Pwinpty_error_ptr_t): Pwinpty_spawn_config_t; cdecl;
|
|
winpty_spawn_config_free: procedure(cfg: Pwinpty_spawn_config_t); cdecl;
|
|
|
|
winpty_spawn: function(wp: Pwinpty_t; const cfg: Pwinpty_spawn_config_t;
|
|
process_handle, thread_handle: PHandle;
|
|
create_process_error: PDWORD; err: Pwinpty_error_ptr_t): BOOL; cdecl;
|
|
|
|
winpty_set_size: function(wp: Pwinpty_t; cols, rows: cint; err: Pwinpty_error_ptr_t): BOOL; cdecl;
|
|
|
|
winpty_conin_name: function(wp: Pwinpty_t): LPCWSTR; cdecl;
|
|
winpty_conout_name: function(wp: Pwinpty_t): LPCWSTR; cdecl;
|
|
winpty_conerr_name: function(wp: Pwinpty_t): LPCWSTR; cdecl;
|
|
|
|
function CreatePseudoConsoleOld(const ACommand: String; phPC: PPointer; phPipeIn, phPipeOut: PHandle; ASize: COORD): Boolean;
|
|
var
|
|
childHandle: HANDLE;
|
|
lastError: DWORD = 0;
|
|
agentCfg: Pwinpty_config_t;
|
|
spawnCfg: Pwinpty_spawn_config_t;
|
|
agentFlags: DWORD = WINPTY_FLAG_ALLOW_CURPROC_DESKTOP_CREATION;
|
|
begin
|
|
// SetEnvironmentVariableW('WINPTY_SHOW_CONSOLE', '1');
|
|
agentCfg:= winpty_config_new(agentFlags, nil);
|
|
Result:= Assigned(agentCfg);
|
|
if Result then
|
|
begin
|
|
winpty_config_set_initial_size(agentCfg, ASize.X, ASize.Y);
|
|
|
|
phPC^:= winpty_open(agentCfg, nil);
|
|
|
|
Result:= Assigned(phPC^);
|
|
|
|
winpty_config_free(agentCfg);
|
|
|
|
if Result then
|
|
begin
|
|
phPipeIn^:= CreateFileW(winpty_conout_name(phPC^), GENERIC_READ, 0, nil, OPEN_EXISTING, 0, 0);
|
|
phPipeOut^:= CreateFileW(winpty_conin_name(phPC^), GENERIC_WRITE, 0, nil, OPEN_EXISTING, 0, 0);
|
|
|
|
spawnCfg:= winpty_spawn_config_new(WINPTY_SPAWN_FLAG_AUTO_SHUTDOWN,
|
|
nil, PWideChar(CeUtf8ToUtf16(ACommand)), nil, nil, nil);
|
|
Result:= Assigned(spawnCfg);
|
|
if Result then
|
|
begin
|
|
Result:= winpty_spawn(phPC^, spawnCfg, @childHandle, nil, @lastError, nil);
|
|
winpty_spawn_config_free(spawnCfg);
|
|
end;
|
|
|
|
if not Result then
|
|
begin
|
|
ClosePipe(phPipeIn^);
|
|
ClosePipe(phPipeOut^);
|
|
winpty_free(phPC^);
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
var
|
|
libwinpty: HINST;
|
|
|
|
function InitializeOld: Boolean;
|
|
begin
|
|
libwinpty:= LoadLibrary('winpty.dll');
|
|
Result:= (libwinpty <> 0);
|
|
if Result then
|
|
begin
|
|
winpty_config_new:= GetProcAddress(libwinpty, 'winpty_config_new');
|
|
winpty_config_free:= GetProcAddress(libwinpty, 'winpty_config_free');
|
|
|
|
winpty_config_set_initial_size:= GetProcAddress(libwinpty, 'winpty_config_set_initial_size');
|
|
winpty_config_set_mouse_mode:= GetProcAddress(libwinpty, 'winpty_config_set_mouse_mode');
|
|
|
|
winpty_open:= GetProcAddress(libwinpty, 'winpty_open');
|
|
winpty_free:= GetProcAddress(libwinpty, 'winpty_free');
|
|
|
|
winpty_error_code:= GetProcAddress(libwinpty, 'winpty_error_code');
|
|
winpty_error_msg:= GetProcAddress(libwinpty, 'winpty_error_msg');
|
|
winpty_error_free:= GetProcAddress(libwinpty, 'winpty_error_free');
|
|
|
|
winpty_spawn_config_new:= GetProcAddress(libwinpty, 'winpty_spawn_config_new');
|
|
winpty_spawn_config_free:= GetProcAddress(libwinpty, 'winpty_spawn_config_free');
|
|
|
|
winpty_spawn:= GetProcAddress(libwinpty, 'winpty_spawn');
|
|
winpty_set_size:= GetProcAddress(libwinpty, 'winpty_set_size');
|
|
|
|
winpty_conin_name:= GetProcAddress(libwinpty, 'winpty_conin_name');
|
|
winpty_conout_name:= GetProcAddress(libwinpty, 'winpty_conout_name');
|
|
winpty_conerr_name:= GetProcAddress(libwinpty, 'winpty_conerr_name');
|
|
end;
|
|
end;
|
|
|
|
{ TPtyDevice }
|
|
|
|
procedure TPtyDevice.SetConnected(AValue: Boolean);
|
|
var
|
|
AShell: String;
|
|
begin
|
|
if FConnected = AValue then Exit;
|
|
FConnected:= AValue;
|
|
|
|
if FConnected then
|
|
begin
|
|
AShell:= mbGetEnvironmentVariable('ComSpec');
|
|
if Length(AShell) = 0 then AShell:= 'cmd.exe';
|
|
FConnected:= CreatePseudoConsole(AShell);
|
|
if FConnected then
|
|
begin
|
|
// Windows 11 requires auto wrap mode
|
|
if (ConsoleType = ctNative) and (Win32BuildNumber >= 22000) then
|
|
begin
|
|
StrCopy(FBuffer, #27'[?7h');
|
|
FLength:= 5;
|
|
ReadySync;
|
|
end;
|
|
FThread:= TThread.ExecuteInThread(ReadThread);
|
|
end;
|
|
end
|
|
else begin
|
|
DestroyPseudoConsole;
|
|
end;
|
|
end;
|
|
|
|
procedure TPtyDevice.ReadySync;
|
|
begin
|
|
if Assigned(FOnRxBuf) then
|
|
FOnRxBuf(Self, FBuffer, FLength);
|
|
end;
|
|
|
|
procedure TPtyDevice.ReadThread;
|
|
begin
|
|
while FConnected do
|
|
begin
|
|
FLength:= FileRead(FPipeIn, FBuffer, SizeOf(FBuffer));
|
|
|
|
if (FLength > 0) then
|
|
begin
|
|
TThread.Synchronize(nil, ReadySync);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TPtyDevice.DestroyPseudoConsole;
|
|
begin
|
|
case ConsoleType of
|
|
ctNative: ClosePseudoConsole(FPty);
|
|
ctEmulate: winpty_free(FPty);
|
|
end;
|
|
FPty:= nil;
|
|
ClosePipe(FPipeIn);
|
|
ClosePipe(FPipeOut);
|
|
end;
|
|
|
|
function TPtyDevice.CreatePseudoConsole(const ACommand: String): Boolean;
|
|
begin
|
|
case ConsoleType of
|
|
ctNative: Result:= CreatePseudoConsoleNew(ACommand, @FPty, @FPipeIn, @FPipeOut, FSize);
|
|
ctEmulate: Result:= CreatePseudoConsoleOld(ACommand, @FPty, @FPipeIn, @FPipeOut, FSize);
|
|
ctNone: Result:= False;
|
|
end;
|
|
end;
|
|
|
|
constructor TPtyDevice.Create(AOwner: TComponent);
|
|
begin
|
|
inherited Create(AOwner);
|
|
|
|
FSize.X:= 80;
|
|
FSize.Y:= 25;
|
|
FPipeIn:= INVALID_HANDLE_VALUE;
|
|
FPipeOut:= INVALID_HANDLE_VALUE;
|
|
end;
|
|
|
|
destructor TPtyDevice.Destroy;
|
|
begin
|
|
inherited Destroy;
|
|
SetConnected(False);
|
|
end;
|
|
|
|
function TPtyDevice.SetCurrentDir(const Path: String): Boolean;
|
|
begin
|
|
Result:= WriteStr('cd /D "' + Path + '"' + #13#10) > 0;
|
|
end;
|
|
|
|
function TPtyDevice.WriteStr(const Str: string): Integer;
|
|
begin
|
|
Result:= FileWrite(FPipeOut, Pointer(Str)^, Length(Str));
|
|
end;
|
|
|
|
function TPtyDevice.SetScreenSize(aCols, aRows: Integer): Boolean;
|
|
var
|
|
ASize: TCoord;
|
|
begin
|
|
if (FPty = nil) then Exit(False);
|
|
|
|
if (ConsoleType = ctEmulate) then
|
|
begin
|
|
Result:= winpty_set_size(FPty, aCols, aRows, nil);
|
|
end
|
|
else if (ConsoleType = ctNative) then
|
|
begin
|
|
ASize.Y:= aRows;
|
|
ASize.X:= aCols;
|
|
Result:= Succeeded(ResizePseudoConsole(FPty, ASize));
|
|
end;
|
|
if Result then
|
|
begin
|
|
FSize.Y:= aRows;
|
|
FSize.X:= aCols;
|
|
end;
|
|
end;
|
|
|
|
procedure Initialize;
|
|
begin
|
|
if InitializeNew then
|
|
ConsoleType:= ctNative
|
|
else if InitializeOld then
|
|
ConsoleType:= ctEmulate;
|
|
end;
|
|
|
|
initialization
|
|
Initialize;
|
|
|
|
end.
|
|
|