vtemupty.pas 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. {
  2. Double Commander
  3. -------------------------------------------------------------------------
  4. Windows pseudoterminal device implementation
  5. Copyright (C) 2021 Alexander Koblov ([email protected])
  6. Permission is hereby granted, free of charge, to any person obtaining
  7. a copy of this software and associated documentation files (the
  8. "Software"), to deal in the Software without restriction, including
  9. without limitation the rights to use, copy, modify, merge, publish,
  10. distribute, sublicense, and/or sell copies of the Software, and to
  11. permit persons to whom the Software is furnished to do so, subject to
  12. the following conditions:
  13. The above copyright notice and this permission notice shall be included
  14. in all copies or substantial portions of the Software.
  15. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  16. EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  17. MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
  18. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
  19. CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
  20. TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
  21. SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  22. }
  23. unit VTEmuPty;
  24. {$mode delphi}
  25. interface
  26. uses
  27. Classes, SysUtils, LCLProc, LCLType, Windows, VTEmuCtl;
  28. type
  29. { TPtyDevice }
  30. TPtyDevice = class(TCustomPtyDevice)
  31. private
  32. FPty: PVOID;
  33. FSize: TCoord;
  34. FLength: Integer;
  35. FThread: TThread;
  36. FPipeIn, FPipeOut: THandle;
  37. FBuffer: array[0..8191] of AnsiChar;
  38. protected
  39. procedure ReadySync;
  40. procedure ReadThread;
  41. procedure DestroyPseudoConsole;
  42. procedure SetConnected(AValue: Boolean); override;
  43. function CreatePseudoConsole(const ACommand: String): Boolean;
  44. public
  45. constructor Create(AOwner: TComponent); override;
  46. destructor Destroy; override;
  47. function WriteStr(const Str: string): Integer; override;
  48. function SetCurrentDir(const Path: String): Boolean; override;
  49. function SetScreenSize(aCols, aRows: Integer): Boolean; override;
  50. end;
  51. implementation
  52. uses
  53. CTypes, DCOSUtils, DCConvertEncoding;
  54. type
  55. TConsoleType = (ctNone, ctNative, ctEmulate);
  56. var
  57. ConsoleType: TConsoleType = ctNone;
  58. procedure ClosePipe(var AHandle: THandle);
  59. begin
  60. if (AHandle <> INVALID_HANDLE_VALUE) then
  61. begin
  62. CloseHandle(AHandle);
  63. AHandle:= INVALID_HANDLE_VALUE;
  64. end;
  65. end;
  66. {
  67. *******************************************************************************
  68. Windows Pseudo Console (ConPTY), Windows 10 1809 and higher
  69. *******************************************************************************
  70. }
  71. const
  72. EXTENDED_STARTUPINFO_PRESENT = $00080000;
  73. PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = $00020016;
  74. type
  75. PHPCON = ^HPCON;
  76. HPCON = type PVOID;
  77. SIZE_T = type ULONG_PTR;
  78. PSIZE_T = type PULONG_PTR;
  79. LPPROC_THREAD_ATTRIBUTE_LIST = type PVOID;
  80. STARTUPINFOEXW = record
  81. StartupInfo: STARTUPINFOW;
  82. lpAttributeList: LPPROC_THREAD_ATTRIBUTE_LIST;
  83. end;
  84. var
  85. CreatePseudoConsole: function(size: COORD; hInput: HANDLE; hOutput: HANDLE;
  86. dwFlags: DWORD; phPC: PHPCON): HRESULT; stdcall;
  87. ClosePseudoConsole: procedure(hPC: HPCON); stdcall;
  88. ResizePseudoConsole: function(hPC: HPCON; size: COORD): HRESULT; stdcall;
  89. InitializeProcThreadAttributeList: function(lpAttributeList: LPPROC_THREAD_ATTRIBUTE_LIST;
  90. dwAttributeCount: DWORD; dwFlags: DWORD;
  91. lpSize: PSIZE_T): BOOL; stdcall;
  92. UpdateProcThreadAttribute: function(lpAttributeList: LPPROC_THREAD_ATTRIBUTE_LIST;
  93. dwFlags: DWORD; Attribute: DWORD_PTR;
  94. lpValue: PVOID; cbSize: SIZE_T;
  95. lpPreviousValue: PVOID; lpReturnSize: PSIZE_T): BOOL; stdcall;
  96. DeleteProcThreadAttributeList: procedure(lpAttributeList: LPPROC_THREAD_ATTRIBUTE_LIST); stdcall;
  97. function CreatePseudoConsoleNew(const ACommand: String; phPC: PPointer; phPipeIn, phPipeOut: PHandle; ASize: COORD): Boolean;
  98. var
  99. attrListSize: SIZE_T = 0;
  100. startupInfo: STARTUPINFOEXW;
  101. piClient: PROCESS_INFORMATION;
  102. hPipePTYIn: HANDLE = INVALID_HANDLE_VALUE;
  103. hPipePTYOut: HANDLE = INVALID_HANDLE_VALUE;
  104. begin
  105. startupInfo:= Default(STARTUPINFOEXW);
  106. Result:= CreatePipe(hPipePTYIn, phPipeOut^, nil, 0) and
  107. CreatePipe(phPipeIn^, hPipePTYOut, nil, 0);
  108. if Result then
  109. begin
  110. Result:= CreatePseudoConsole(ASize, hPipePTYIn, hPipePTYOut, 0, phPC) = S_OK;
  111. // We can close the handles here because they are duplicated in the ConHost
  112. if Result then
  113. begin
  114. CloseHandle(hPipePTYIn);
  115. CloseHandle(hPipePTYOut);
  116. hPipePTYIn:= INVALID_HANDLE_VALUE;
  117. hPipePTYOut:=INVALID_HANDLE_VALUE;
  118. end;
  119. end;
  120. if Result then
  121. begin
  122. startupInfo.StartupInfo.cb:= SizeOf(STARTUPINFOEXW);
  123. InitializeProcThreadAttributeList(nil, 1, 0, @attrListSize);
  124. startupInfo.lpAttributeList:= GetMem(attrListSize);
  125. Result:= Assigned(startupInfo.lpAttributeList);
  126. if Result then
  127. begin
  128. // Initialize thread attribute list and set Pseudo Console attribute
  129. Result:= InitializeProcThreadAttributeList(startupInfo.lpAttributeList, 1, 0, @attrListSize) and
  130. UpdateProcThreadAttribute(startupInfo.lpAttributeList,0,
  131. PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
  132. phPC^, SizeOf(HPCON), nil, nil);
  133. end;
  134. end;
  135. if Result then
  136. begin
  137. Result:= CreateProcessW(nil, PWideChar(CeUtf8ToUtf16(ACommand)),
  138. nil, nil, False, EXTENDED_STARTUPINFO_PRESENT,
  139. nil, nil, @startupInfo.StartupInfo, @piClient);
  140. end;
  141. if not Result then
  142. begin
  143. ClosePipe(phPipeIn^);
  144. ClosePipe(phPipeOut^);
  145. ClosePipe(hPipePTYIn);
  146. ClosePipe(hPipePTYOut);
  147. end;
  148. // Cleanup attribute list
  149. if Assigned(startupInfo.lpAttributeList) then
  150. begin
  151. DeleteProcThreadAttributeList(startupInfo.lpAttributeList);
  152. FreeMem(startupInfo.lpAttributeList);
  153. end;
  154. end;
  155. function InitializeNew: Boolean;
  156. var
  157. hModule: HINST;
  158. begin
  159. Result:= (Win32MajorVersion >= 10);
  160. if Result then
  161. begin
  162. hModule:= GetModuleHandle(Kernel32);
  163. CreatePseudoConsole:= GetProcAddress(hModule, 'CreatePseudoConsole');
  164. Result:= Assigned(CreatePseudoConsole);
  165. if Result then
  166. begin
  167. ClosePseudoConsole:= GetProcAddress(hModule, 'ClosePseudoConsole');
  168. ResizePseudoConsole:= GetProcAddress(hModule, 'ResizePseudoConsole');
  169. UpdateProcThreadAttribute:= GetProcAddress(hModule, 'UpdateProcThreadAttribute');
  170. DeleteProcThreadAttributeList:= GetProcAddress(hModule, 'DeleteProcThreadAttributeList');
  171. InitializeProcThreadAttributeList:= GetProcAddress(hModule, 'InitializeProcThreadAttributeList');
  172. end;
  173. end;
  174. end;
  175. {
  176. *******************************************************************************
  177. WinPTY
  178. *******************************************************************************
  179. }
  180. const
  181. WINPTY_MOUSE_MODE_AUTO = 1;
  182. WINPTY_MOUSE_MODE_FORCE = 2;
  183. // Agent RPC call: process creation
  184. WINPTY_SPAWN_FLAG_AUTO_SHUTDOWN = 1;
  185. WINPTY_SPAWN_FLAG_EXIT_AFTER_SHUTDOWN = 2;
  186. // Configuration of a new agent
  187. WINPTY_FLAG_CONERR = $01;
  188. WINPTY_FLAG_PLAIN_OUTPUT = $02;
  189. WINPTY_FLAG_COLOR_ESCAPES = $04;
  190. WINPTY_FLAG_ALLOW_CURPROC_DESKTOP_CREATION = $08;
  191. // Error codes
  192. WINPTY_ERROR_SUCCESS = 0;
  193. WINPTY_ERROR_OUT_OF_MEMORY = 1;
  194. WINPTY_ERROR_SPAWN_CREATE_PROCESS_FAILED = 2;
  195. WINPTY_ERROR_LOST_CONNECTION = 3;
  196. WINPTY_ERROR_AGENT_EXE_MISSING = 4;
  197. WINPTY_ERROR_UNSPECIFIED = 5;
  198. WINPTY_ERROR_AGENT_DIED = 6;
  199. WINPTY_ERROR_AGENT_TIMEOUT = 7;
  200. WINPTY_ERROR_AGENT_CREATION_FAILED = 8;
  201. type
  202. winpty_t = record end;
  203. Pwinpty_t = ^winpty_t;
  204. winpty_result_t = type DWORD;
  205. winpty_config_t = record end;
  206. Pwinpty_config_t = ^winpty_config_t;
  207. winpty_error_t = record end;
  208. winpty_error_ptr_t = ^winpty_error_t;
  209. Pwinpty_error_ptr_t = ^winpty_error_ptr_t;
  210. winpty_spawn_config_t = record end;
  211. Pwinpty_spawn_config_t = ^winpty_spawn_config_t;
  212. var
  213. winpty_config_new: function(agentFlags: UInt64; err: Pwinpty_error_ptr_t): Pwinpty_config_t; cdecl;
  214. winpty_config_free: procedure(cfg: Pwinpty_config_t); cdecl;
  215. winpty_config_set_initial_size: procedure(cfg: Pwinpty_config_t; cols, rows: cint); cdecl;
  216. winpty_config_set_mouse_mode: procedure(cfg: Pwinpty_config_t; mouseMode: cint); cdecl;
  217. winpty_open: function(const cfg: Pwinpty_config_t; err: Pwinpty_error_ptr_t): Pwinpty_t; cdecl;
  218. winpty_free: procedure(wp: Pwinpty_t); cdecl;
  219. winpty_error_code: function(err: winpty_error_ptr_t): winpty_result_t; cdecl;
  220. winpty_error_msg: function(err: winpty_error_ptr_t): LPCWSTR; cdecl;
  221. winpty_error_free: procedure(err: winpty_error_ptr_t); cdecl;
  222. winpty_spawn_config_new: function(spawnFlags: UInt64; appname, cmdline, cwd,
  223. env: LPCWSTR; err: Pwinpty_error_ptr_t): Pwinpty_spawn_config_t; cdecl;
  224. winpty_spawn_config_free: procedure(cfg: Pwinpty_spawn_config_t); cdecl;
  225. winpty_spawn: function(wp: Pwinpty_t; const cfg: Pwinpty_spawn_config_t;
  226. process_handle, thread_handle: PHandle;
  227. create_process_error: PDWORD; err: Pwinpty_error_ptr_t): BOOL; cdecl;
  228. winpty_set_size: function(wp: Pwinpty_t; cols, rows: cint; err: Pwinpty_error_ptr_t): BOOL; cdecl;
  229. winpty_conin_name: function(wp: Pwinpty_t): LPCWSTR; cdecl;
  230. winpty_conout_name: function(wp: Pwinpty_t): LPCWSTR; cdecl;
  231. winpty_conerr_name: function(wp: Pwinpty_t): LPCWSTR; cdecl;
  232. function CreatePseudoConsoleOld(const ACommand: String; phPC: PPointer; phPipeIn, phPipeOut: PHandle; ASize: COORD): Boolean;
  233. var
  234. childHandle: HANDLE;
  235. lastError: DWORD = 0;
  236. agentCfg: Pwinpty_config_t;
  237. spawnCfg: Pwinpty_spawn_config_t;
  238. agentFlags: DWORD = WINPTY_FLAG_ALLOW_CURPROC_DESKTOP_CREATION;
  239. begin
  240. // SetEnvironmentVariableW('WINPTY_SHOW_CONSOLE', '1');
  241. agentCfg:= winpty_config_new(agentFlags, nil);
  242. Result:= Assigned(agentCfg);
  243. if Result then
  244. begin
  245. winpty_config_set_initial_size(agentCfg, ASize.X, ASize.Y);
  246. phPC^:= winpty_open(agentCfg, nil);
  247. Result:= Assigned(phPC^);
  248. winpty_config_free(agentCfg);
  249. if Result then
  250. begin
  251. phPipeIn^:= CreateFileW(winpty_conout_name(phPC^), GENERIC_READ, 0, nil, OPEN_EXISTING, 0, 0);
  252. phPipeOut^:= CreateFileW(winpty_conin_name(phPC^), GENERIC_WRITE, 0, nil, OPEN_EXISTING, 0, 0);
  253. spawnCfg:= winpty_spawn_config_new(WINPTY_SPAWN_FLAG_AUTO_SHUTDOWN,
  254. nil, PWideChar(CeUtf8ToUtf16(ACommand)), nil, nil, nil);
  255. Result:= Assigned(spawnCfg);
  256. if Result then
  257. begin
  258. Result:= winpty_spawn(phPC^, spawnCfg, @childHandle, nil, @lastError, nil);
  259. winpty_spawn_config_free(spawnCfg);
  260. end;
  261. if not Result then
  262. begin
  263. ClosePipe(phPipeIn^);
  264. ClosePipe(phPipeOut^);
  265. winpty_free(phPC^);
  266. end;
  267. end;
  268. end;
  269. end;
  270. var
  271. libwinpty: HINST;
  272. function InitializeOld: Boolean;
  273. begin
  274. libwinpty:= LoadLibrary('winpty.dll');
  275. Result:= (libwinpty <> 0);
  276. if Result then
  277. begin
  278. winpty_config_new:= GetProcAddress(libwinpty, 'winpty_config_new');
  279. winpty_config_free:= GetProcAddress(libwinpty, 'winpty_config_free');
  280. winpty_config_set_initial_size:= GetProcAddress(libwinpty, 'winpty_config_set_initial_size');
  281. winpty_config_set_mouse_mode:= GetProcAddress(libwinpty, 'winpty_config_set_mouse_mode');
  282. winpty_open:= GetProcAddress(libwinpty, 'winpty_open');
  283. winpty_free:= GetProcAddress(libwinpty, 'winpty_free');
  284. winpty_error_code:= GetProcAddress(libwinpty, 'winpty_error_code');
  285. winpty_error_msg:= GetProcAddress(libwinpty, 'winpty_error_msg');
  286. winpty_error_free:= GetProcAddress(libwinpty, 'winpty_error_free');
  287. winpty_spawn_config_new:= GetProcAddress(libwinpty, 'winpty_spawn_config_new');
  288. winpty_spawn_config_free:= GetProcAddress(libwinpty, 'winpty_spawn_config_free');
  289. winpty_spawn:= GetProcAddress(libwinpty, 'winpty_spawn');
  290. winpty_set_size:= GetProcAddress(libwinpty, 'winpty_set_size');
  291. winpty_conin_name:= GetProcAddress(libwinpty, 'winpty_conin_name');
  292. winpty_conout_name:= GetProcAddress(libwinpty, 'winpty_conout_name');
  293. winpty_conerr_name:= GetProcAddress(libwinpty, 'winpty_conerr_name');
  294. end;
  295. end;
  296. { TPtyDevice }
  297. procedure TPtyDevice.SetConnected(AValue: Boolean);
  298. var
  299. AShell: String;
  300. begin
  301. if FConnected = AValue then Exit;
  302. FConnected:= AValue;
  303. if FConnected then
  304. begin
  305. AShell:= mbGetEnvironmentVariable('ComSpec');
  306. if Length(AShell) = 0 then AShell:= 'cmd.exe';
  307. FConnected:= CreatePseudoConsole(AShell);
  308. if FConnected then
  309. begin
  310. FThread:= TThread.ExecuteInThread(ReadThread);
  311. end;
  312. end
  313. else begin
  314. DestroyPseudoConsole;
  315. end;
  316. end;
  317. procedure TPtyDevice.ReadySync;
  318. begin
  319. if Assigned(FOnRxBuf) then
  320. FOnRxBuf(Self, FBuffer, FLength);
  321. end;
  322. procedure TPtyDevice.ReadThread;
  323. begin
  324. while FConnected do
  325. begin
  326. FLength:= FileRead(FPipeIn, FBuffer, SizeOf(FBuffer));
  327. if (FLength > 0) then
  328. begin
  329. TThread.Synchronize(nil, ReadySync);
  330. end;
  331. end;
  332. end;
  333. procedure TPtyDevice.DestroyPseudoConsole;
  334. begin
  335. case ConsoleType of
  336. ctNative: ClosePseudoConsole(FPty);
  337. ctEmulate: winpty_free(FPty);
  338. end;
  339. FPty:= nil;
  340. ClosePipe(FPipeIn);
  341. ClosePipe(FPipeOut);
  342. end;
  343. function TPtyDevice.CreatePseudoConsole(const ACommand: String): Boolean;
  344. begin
  345. case ConsoleType of
  346. ctNative: Result:= CreatePseudoConsoleNew(ACommand, @FPty, @FPipeIn, @FPipeOut, FSize);
  347. ctEmulate: Result:= CreatePseudoConsoleOld(ACommand, @FPty, @FPipeIn, @FPipeOut, FSize);
  348. ctNone: Result:= False;
  349. end;
  350. end;
  351. constructor TPtyDevice.Create(AOwner: TComponent);
  352. begin
  353. inherited Create(AOwner);
  354. FSize.X:= 80;
  355. FSize.Y:= 25;
  356. FPipeIn:= INVALID_HANDLE_VALUE;
  357. FPipeOut:= INVALID_HANDLE_VALUE;
  358. end;
  359. destructor TPtyDevice.Destroy;
  360. begin
  361. inherited Destroy;
  362. SetConnected(False);
  363. end;
  364. function TPtyDevice.SetCurrentDir(const Path: String): Boolean;
  365. begin
  366. Result:= WriteStr('cd /D "' + Path + '"' + #13#10) > 0;
  367. end;
  368. function TPtyDevice.WriteStr(const Str: string): Integer;
  369. begin
  370. Result:= FileWrite(FPipeOut, Pointer(Str)^, Length(Str));
  371. end;
  372. function TPtyDevice.SetScreenSize(aCols, aRows: Integer): Boolean;
  373. var
  374. ASize: TCoord;
  375. begin
  376. if (FPty = nil) then Exit(False);
  377. if (ConsoleType = ctEmulate) then
  378. begin
  379. Result:= winpty_set_size(FPty, aCols, aRows, nil);
  380. end
  381. else if (ConsoleType = ctNative) then
  382. begin
  383. ASize.Y:= aRows;
  384. ASize.X:= aCols;
  385. Result:= Succeeded(ResizePseudoConsole(FPty, ASize));
  386. end;
  387. if Result then
  388. begin
  389. FSize.Y:= aRows;
  390. FSize.X:= aCols;
  391. end;
  392. end;
  393. procedure Initialize;
  394. begin
  395. if InitializeNew then
  396. ConsoleType:= ctNative
  397. else if InitializeOld then
  398. ConsoleType:= ctEmulate;
  399. end;
  400. initialization
  401. Initialize;
  402. end.