| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474 |
- {
- Double Commander
- -------------------------------------------------------------------------
- Windows pseudoterminal device implementation
- Copyright (C) 2021 Alexander Koblov ([email protected])
- 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
- 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.
|