pty.c (10663B)
1 /* Stub code for controlling terminals. */ 2 3 #ifdef _WIN32 4 5 #define WINVER 0x0600 6 #define _WIN32_WINNT 0x0600 7 8 #ifndef UNICODE 9 #define UNICODE 10 #endif 11 12 #endif /* _WIN32 */ 13 14 #include <caml/mlvalues.h> 15 #include <caml/alloc.h> // alloc_tuple 16 #include <caml/memory.h> // Store_field 17 #include <caml/fail.h> // failwith 18 #include <caml/unixsupport.h> // uerror, unix_error 19 #include <errno.h> // ENOSYS 20 #include <caml/version.h> 21 #if OCAML_VERSION < 41300 22 #define CAML_INTERNALS /* was needed from OCaml 4.06 to 4.12 */ 23 #endif 24 #include <caml/osdeps.h> 25 26 #if OCAML_VERSION_MAJOR < 5 27 #define caml_unix_error unix_error 28 #define caml_uerror uerror 29 #define caml_win32_maperr win32_maperr 30 #define caml_win32_alloc_handle win_alloc_handle 31 #endif 32 33 // openpty 34 #if defined(__linux) || defined(__CYGWIN__) 35 #include <pty.h> 36 #define HAS_OPENPTY 1 37 #endif 38 39 #if defined(__APPLE__) || defined(__NetBSD__) 40 #include <util.h> 41 #define HAS_OPENPTY 1 42 #endif 43 44 #ifdef __FreeBSD__ 45 #include <sys/types.h> 46 #include <libutil.h> 47 #define HAS_OPENPTY 1 48 #endif 49 50 #ifdef HAS_OPENPTY 51 52 #include <sys/ioctl.h> 53 #include <sys/types.h> 54 55 CAMLprim value setControllingTerminal(value fdVal) { 56 CAMLparam1(fdVal); 57 #if defined(__CYGWIN__) 58 /* [Unix.setsid] is not implemented on Cygwin; call it here instead */ 59 if (setsid() < 0) caml_uerror("setsid", Nothing); 60 #endif 61 int fd = Int_val(fdVal); 62 if (ioctl(fd, TIOCSCTTY, (char *) 0) < 0) 63 caml_uerror("ioctl", (value) 0); 64 CAMLreturn(Val_unit); 65 } 66 67 /* c_openpty: unit -> (int * Unix.file_descr) */ 68 CAMLprim value c_openpty(value unit) { 69 CAMLparam0(); 70 CAMLlocal1(pair); 71 int master, slave; 72 if (openpty(&master,&slave,NULL,NULL,NULL) < 0) 73 caml_uerror("openpty", (value) 0); 74 pair = caml_alloc_tuple(2); 75 Store_field(pair,0,Val_int(master)); 76 Store_field(pair,1,Val_int(slave)); 77 CAMLreturn(pair); 78 } 79 80 #else // not HAS_OPENPTY 81 82 CAMLprim value setControllingTerminal(value fdVal) { 83 caml_unix_error(ENOSYS, "setControllingTerminal", Nothing); 84 } 85 86 CAMLprim value c_openpty(value unit) { 87 caml_unix_error(ENOSYS, "openpty", Nothing); 88 } 89 90 #endif 91 92 #ifdef _WIN32 93 94 #include <windows.h> 95 96 #define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE 0x00020016 97 98 typedef VOID* HPCON; 99 100 typedef HRESULT (WINAPI *sCreatePseudoConsole) 101 (COORD size, HANDLE hInput, HANDLE hOutput, DWORD dwFlags, HPCON* phPC); 102 103 typedef void (WINAPI *sClosePseudoConsole) (HPCON hPC); 104 105 sCreatePseudoConsole pCreatePseudoConsole; 106 sClosePseudoConsole pClosePseudoConsole; 107 108 CAMLprim value win_openpty(value unit) 109 { 110 CAMLparam0(); 111 CAMLlocal4(tup, tmp1, tmp2, tmp3); 112 HPCON pty; 113 HANDLE i1, i2, o1, o2; 114 COORD size = { .X = 80, .Y = 25 }; 115 116 HMODULE kernel32_module = GetModuleHandleW(L"kernel32.dll"); 117 if (kernel32_module == NULL) { 118 caml_unix_error(ENOSYS, "openpty", Nothing); 119 } 120 121 /* This is the only way to use the new API while remaining compatible 122 * with older Windows versions. */ 123 pCreatePseudoConsole = (sCreatePseudoConsole) 124 GetProcAddress(kernel32_module, "CreatePseudoConsole"); 125 if (pCreatePseudoConsole == NULL) { 126 caml_unix_error(ENOSYS, "openpty", Nothing); 127 } 128 129 /* Read-write pipes don't seem to work well with PTY and cause deadlocks. 130 * socketpair() is not supported by Windows and emulation code is just too 131 * much to carry around (also, emulation by PF_INET sockets is a bit messy; 132 * emulation by PF_UNIX sockets is only supported starting Windows 10 1803). 133 * Simpler to use two separate pipes then. */ 134 if (!CreatePipe(&i1, &o1, NULL, 0)) { 135 caml_win32_maperr(GetLastError()); 136 caml_uerror("openpty", Nothing); 137 } 138 if (!CreatePipe(&i2, &o2, NULL, 0)) { 139 caml_win32_maperr(GetLastError()); 140 if (o1 != INVALID_HANDLE_VALUE) CloseHandle(o1); 141 if (i1 != INVALID_HANDLE_VALUE) CloseHandle(i1); 142 caml_uerror("openpty", Nothing); 143 } 144 145 if (pCreatePseudoConsole(size, i1, o2, 0, &pty) != S_OK) { 146 caml_win32_maperr(GetLastError()); 147 if (o1 != INVALID_HANDLE_VALUE) CloseHandle(o1); 148 if (i1 != INVALID_HANDLE_VALUE) CloseHandle(i1); 149 if (o2 != INVALID_HANDLE_VALUE) CloseHandle(o2); 150 if (i2 != INVALID_HANDLE_VALUE) CloseHandle(i2); 151 caml_uerror("openpty", Nothing); 152 } 153 154 tmp1 = caml_alloc_tuple(2); 155 Store_field(tmp1, 0, caml_win32_alloc_handle(i2)); 156 Store_field(tmp1, 1, caml_win32_alloc_handle(o1)); 157 158 tmp2 = caml_alloc(1, Abstract_tag); 159 *((HPCON *) Data_abstract_val(tmp2)) = pty; 160 161 tmp3 = caml_alloc_tuple(2); 162 Store_field(tmp3, 0, caml_win32_alloc_handle(i1)); 163 Store_field(tmp3, 1, caml_win32_alloc_handle(o2)); 164 165 tup = caml_alloc_tuple(3); 166 Store_field(tup, 0, tmp1); 167 Store_field(tup, 1, tmp2); 168 Store_field(tup, 2, tmp3); 169 170 CAMLreturn(tup); 171 } 172 173 CAMLprim value win_closepty(value pty) 174 { 175 CAMLparam1(pty); 176 177 HMODULE kernel32_module = GetModuleHandleW(L"kernel32.dll"); 178 if (kernel32_module == NULL) { 179 caml_unix_error(ENOSYS, "closepty", Nothing); 180 } 181 182 /* This is the only way to use the new API while remaining compatible 183 * with older Windows versions. */ 184 pClosePseudoConsole = (sClosePseudoConsole) 185 GetProcAddress(kernel32_module, "ClosePseudoConsole"); 186 if (pClosePseudoConsole == NULL) { 187 caml_unix_error(ENOSYS, "closepty", Nothing); 188 } 189 190 pClosePseudoConsole(*((HPCON *) Data_abstract_val(pty))); 191 192 CAMLreturn(Val_unit); 193 } 194 195 static void prepareSiWithPty(value prog, STARTUPINFOEX *si, HPCON pty) 196 { 197 #ifndef PROC_THREAD_ATTRIBUTE_HANDLE_LIST 198 caml_unix_error(ENOSYS, "create_process_pty", prog); 199 #else 200 SIZE_T size; 201 202 ZeroMemory(si, sizeof(*si)); 203 si->StartupInfo.cb = sizeof(STARTUPINFOEX); 204 205 /* 2 attributes, as PROC_THREAD_ATTRIBUTE_HANDLE_LIST will be added later. */ 206 InitializeProcThreadAttributeList(NULL, 2, 0, &size); 207 si->lpAttributeList = malloc(size); 208 if (!si->lpAttributeList) { 209 caml_unix_error(ENOMEM, "create_process_pty", prog); 210 } 211 212 if (!InitializeProcThreadAttributeList(si->lpAttributeList, 2, 0, &size)) { 213 caml_win32_maperr(GetLastError()); 214 free(si->lpAttributeList); 215 caml_uerror("create_process_pty", prog); 216 } 217 218 if (!UpdateProcThreadAttribute(si->lpAttributeList, 0, 219 PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, pty, sizeof(pty), NULL, NULL)) { 220 caml_win32_maperr(GetLastError()); 221 DeleteProcThreadAttributeList(si->lpAttributeList); 222 free(si->lpAttributeList); 223 caml_uerror("create_process_pty", prog); 224 } 225 #endif 226 } 227 228 CAMLprim value w_create_process_pty 229 (value prog, value args, value pty, value fd1, value fd2) 230 { 231 CAMLparam5(prog, args, pty, fd1, fd2); 232 int res, flags; 233 BOOL err = FALSE; 234 PROCESS_INFORMATION pi; 235 STARTUPINFOEX si; 236 wchar_t fullname[MAX_PATH]; 237 wchar_t *wprog, *wargs; 238 HANDLE hp; 239 HANDLE inherit[2]; 240 HPCON hpc = *((HPCON *) Data_abstract_val(pty)); 241 242 #ifndef PROC_THREAD_ATTRIBUTE_HANDLE_LIST 243 caml_unix_error(ENOSYS, "create_process_pty", prog); 244 #else 245 wprog = caml_stat_strdup_to_utf16(String_val(prog)); 246 247 res = SearchPathW(NULL, wprog, L".exe", MAX_PATH, fullname, NULL); 248 caml_stat_free(wprog); 249 if (res == 0) { 250 caml_win32_maperr(GetLastError()); 251 caml_uerror("create_process_pty", prog); 252 } 253 254 prepareSiWithPty(prog, &si, hpc); 255 256 hp = GetCurrentProcess(); 257 if (!DuplicateHandle(hp, Handle_val(fd1), hp, &(si.StartupInfo.hStdInput), 258 0, TRUE, DUPLICATE_SAME_ACCESS)) { 259 caml_win32_maperr(GetLastError()); 260 err = TRUE; 261 goto clean1; 262 } 263 if (!DuplicateHandle(hp, Handle_val(fd2), hp, &(si.StartupInfo.hStdOutput), 264 0, TRUE, DUPLICATE_SAME_ACCESS)) { 265 caml_win32_maperr(GetLastError()); 266 err = TRUE; 267 goto clean2; 268 } 269 /* hStdError is set to NULL because of Cygwin-linked child processes 270 otherwise not detecting the controlling terminal. This would not be 271 relevant for a native Windows binary but it may happen that Windows users 272 have Cygwin or MSYS2 (or Git for Windows) in path before Windows system 273 dirs, so the ssh client ends up being a Cygwin/MSYS2 binary. If hStdError 274 is not set to NULL then a Cygwin/MSYS2 ssh executed with a Windows pty 275 will not have functions that require a controlling terminal (such as 276 interactive auth). */ 277 si.StartupInfo.hStdError = NULL; 278 si.StartupInfo.dwFlags = STARTF_USESTDHANDLES; 279 280 inherit[0] = si.StartupInfo.hStdInput; 281 inherit[1] = si.StartupInfo.hStdOutput; 282 if (!UpdateProcThreadAttribute(si.lpAttributeList, 0, 283 PROC_THREAD_ATTRIBUTE_HANDLE_LIST, inherit, sizeof(HANDLE) * 2, 284 NULL, NULL)) { 285 caml_win32_maperr(GetLastError()); 286 err = TRUE; 287 goto clean4; 288 } 289 290 flags = GetPriorityClass(GetCurrentProcess()); 291 flags |= EXTENDED_STARTUPINFO_PRESENT; 292 293 wargs = caml_stat_strdup_to_utf16(String_val(args)); 294 res = CreateProcessW(fullname, wargs, NULL, NULL, TRUE, flags, 295 NULL, NULL, &si.StartupInfo, &pi); 296 caml_stat_free(wargs); 297 if (res == 0) { 298 caml_win32_maperr(GetLastError()); 299 err = TRUE; 300 } 301 302 clean4: 303 clean3: 304 CloseHandle(si.StartupInfo.hStdOutput); 305 clean2: 306 CloseHandle(si.StartupInfo.hStdInput); 307 clean1: 308 DeleteProcThreadAttributeList(si.lpAttributeList); 309 free(si.lpAttributeList); 310 311 if (err) { 312 caml_uerror("create_process_pty", prog); 313 } 314 315 CloseHandle(pi.hThread); 316 CAMLreturn(Val_long(pi.hProcess)); 317 #endif 318 } 319 320 CAMLprim value win_alloc_console(value unit) 321 { 322 CAMLparam0(); 323 CAMLlocal1(some); 324 HANDLE stderr_orig; 325 FILE *ign; 326 327 stderr_orig = (HANDLE) GetStdHandle(STD_ERROR_HANDLE); 328 329 if (!AllocConsole()) { 330 caml_win32_maperr(GetLastError()); 331 caml_uerror("alloc_console", Nothing); 332 } 333 334 /* If a new console was allocated then we need to make sure that both the 335 * Windows C runtime stderr and the STD_ERROR_HANDLE for the child process 336 * are associated with the new console, unless already redirected or 337 * associated elsewhere. 338 * We are not interested in stdin and stdout for this specific scenario 339 * (as a fallback for the real pty). */ 340 if (_fileno(stderr) < 0) freopen_s(&ign, "CONOUT$", "w", stderr); 341 342 if (!GetFileType(stderr_orig)) { 343 some = caml_alloc(1, 0); 344 Store_field(some, 0, 345 caml_win32_alloc_handle((HANDLE) GetStdHandle(STD_ERROR_HANDLE))); 346 CAMLreturn(some); 347 } 348 349 CAMLreturn(Val_int(0)); 350 } 351 352 #else // not _WIN32 353 354 CAMLprim value w_create_process_pty_native 355 (value prog, value args, value pty, value fd1, value fd2, value fd3) 356 { 357 caml_unix_error(ENOSYS, "create_process_pty", Nothing); 358 } 359 360 CAMLprim value w_create_process_pty(value *argv, int argn) 361 { 362 caml_unix_error(ENOSYS, "create_process_pty", Nothing); 363 } 364 365 CAMLprim value win_openpty() 366 { 367 caml_unix_error(ENOSYS, "openpty", Nothing); 368 } 369 370 CAMLprim value win_closepty(value pty) 371 { 372 caml_unix_error(ENOSYS, "closepty", Nothing); 373 } 374 375 CAMLprim value win_alloc_console() 376 { 377 caml_unix_error(ENOSYS, "alloc_console", Nothing); 378 } 379 380 #endif