diff options
Diffstat (limited to '')
-rw-r--r-- | src/pty.c | 497 |
1 files changed, 497 insertions, 0 deletions
diff --git a/src/pty.c b/src/pty.c new file mode 100644 index 0000000..bbe8e22 --- /dev/null +++ b/src/pty.c @@ -0,0 +1,497 @@ +#include <errno.h> +#include <fcntl.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#ifndef _WIN32 +#include <sys/ioctl.h> +#include <sys/wait.h> + +#if defined(__OpenBSD__) || defined(__APPLE__) +#include <util.h> +#elif defined(__FreeBSD__) +#include <libutil.h> +#else +#include <pty.h> +#endif + +#if defined(__APPLE__) +#include <crt_externs.h> +#define environ (*_NSGetEnviron()) +#else +extern char **environ; +#endif +#endif + +#include "pty.h" +#include "utils.h" + +#ifdef _WIN32 +HRESULT (WINAPI *pCreatePseudoConsole)(COORD, HANDLE, HANDLE, DWORD, HPCON *); +HRESULT (WINAPI *pResizePseudoConsole)(HPCON, COORD); +void (WINAPI *pClosePseudoConsole)(HPCON); +#endif + +static void alloc_cb(uv_handle_t *unused, size_t suggested_size, uv_buf_t *buf) { + buf->base = xmalloc(suggested_size); + buf->len = suggested_size; +} + +static void close_cb(uv_handle_t *handle) { free(handle); } + +static void async_free_cb(uv_handle_t *handle) { + free((uv_async_t *) handle -> data); +} + +pty_buf_t *pty_buf_init(char *base, size_t len) { + pty_buf_t *buf = xmalloc(sizeof(pty_buf_t)); + buf->base = xmalloc(len); + memcpy(buf->base, base, len); + buf->len = len; + return buf; +} + +void pty_buf_free(pty_buf_t *buf) { + if (buf == NULL) return; + if (buf->base != NULL) free(buf->base); + free(buf); +} + +static void read_cb(uv_stream_t *stream, ssize_t n, const uv_buf_t *buf) { + uv_read_stop(stream); + pty_process *process = (pty_process *) stream->data; + if (n <= 0) { + if (n == UV_ENOBUFS || n == 0) return; + process->read_cb(process, NULL, true); + goto done; + } + process->read_cb(process, pty_buf_init(buf->base, (size_t) n), false); + +done: + free(buf->base); +} + +static void write_cb(uv_write_t *req, int unused) { + pty_buf_t *buf = (pty_buf_t *) req->data; + pty_buf_free(buf); + free(req); +} + +pty_process *process_init(void *ctx, uv_loop_t *loop, char *argv[], char *envp[]) { + pty_process *process = xmalloc(sizeof(pty_process)); + memset(process, 0, sizeof(pty_process)); + process->ctx = ctx; + process->loop = loop; + process->argv = argv; + process->envp = envp; + process->columns = 80; + process->rows = 24; + process->exit_code = -1; + return process; +} + +bool process_running(pty_process *process) { + return process != NULL && process->pid > 0 && uv_kill(process->pid, 0) == 0; +} + +void process_free(pty_process *process) { + if (process == NULL) return; +#ifdef _WIN32 + if (process->si.lpAttributeList != NULL) { + DeleteProcThreadAttributeList(process->si.lpAttributeList); + free(process->si.lpAttributeList); + } + if (process->pty != NULL) pClosePseudoConsole(process->pty); + if (process->handle != NULL) CloseHandle(process->handle); +#else + close(process->pty); + uv_thread_join(&process->tid); +#endif + if (process->in != NULL) uv_close((uv_handle_t *) process->in, close_cb); + if (process->out != NULL) uv_close((uv_handle_t *) process->out, close_cb); + if (process->argv != NULL) free(process->argv); + if (process->cwd != NULL) free(process->cwd); + char **p = process->envp; + for (; *p; p++) free(*p); + free(process->envp); +} + +void pty_pause(pty_process *process) { + if (process == NULL) return; + if (process->paused) return; + uv_read_stop((uv_stream_t *) process->out); +} + +void pty_resume(pty_process *process) { + if (process == NULL) return; + if (!process->paused) return; + process->out->data = process; + uv_read_start((uv_stream_t *) process->out, alloc_cb, read_cb); +} + +int pty_write(pty_process *process, pty_buf_t *buf) { + if (process == NULL) { + pty_buf_free(buf); + return UV_ESRCH; + } + uv_buf_t b = uv_buf_init(buf->base, buf->len); + uv_write_t *req = xmalloc(sizeof(uv_write_t)); + req->data = buf; + return uv_write(req, (uv_stream_t *) process->in, &b, 1, write_cb); +} + +bool pty_resize(pty_process *process) { + if (process == NULL) return false; + if (process->columns <= 0 || process->rows <= 0) return false; +#ifdef _WIN32 + COORD size = {(int16_t) process->columns, (int16_t) process->rows}; + return pResizePseudoConsole(process->pty, size) == S_OK; +#else + struct winsize size = {process->rows, process->columns, 0, 0}; + return ioctl(process->pty, TIOCSWINSZ, &size) == 0; +#endif +} + +bool pty_kill(pty_process *process, int sig) { + if (process == NULL) return false; +#ifdef _WIN32 + return TerminateProcess(process->handle, 1) != 0; +#else + return uv_kill(-process->pid, sig) == 0; +#endif +} + +#ifdef _WIN32 +bool conpty_init() { + uv_lib_t kernel; + if (uv_dlopen("kernel32.dll", &kernel)) { + uv_dlclose(&kernel); + return false; + } + static struct { + char *name; + FARPROC *ptr; + } conpty_entry[] = {{"CreatePseudoConsole", (FARPROC *) &pCreatePseudoConsole}, + {"ResizePseudoConsole", (FARPROC *) &pResizePseudoConsole}, + {"ClosePseudoConsole", (FARPROC *) &pClosePseudoConsole}, + {NULL, NULL}}; + for (int i = 0; conpty_entry[i].name != NULL && conpty_entry[i].ptr != NULL; i++) { + if (uv_dlsym(&kernel, conpty_entry[i].name, (void **) conpty_entry[i].ptr)) { + uv_dlclose(&kernel); + return false; + } + } + return true; +} + +// convert argv to cmdline for CreateProcessW +static WCHAR *join_args(char **argv) { + char *args = NULL; + char **ptr = argv; + for (; *ptr; ptr++) { + char *quoted = (char *) quote_arg(*ptr); + size_t arg_len = args == NULL ? 1 : strlen(args) + 1; + size_t quoted_len = strlen(quoted); + args = xrealloc(args, arg_len + quoted_len); + if (arg_len == 1) memset(args, 0, 2); + if (arg_len != 1) strcat(args, " "); + strncat(args, quoted, quoted_len); + if (quoted != *ptr) free(quoted); + } + + int len = MultiByteToWideChar(CP_UTF8, 0, args, -1, NULL, 0); + if (len <= 0) goto failed; + WCHAR *ws = (WCHAR *) xmalloc(len * sizeof(WCHAR)); + if (len != MultiByteToWideChar(CP_UTF8, 0, args, -1, ws, len)) { + free(ws); + goto failed; + } + return ws; + +failed: + if (args != NULL) free(args); + return NULL; +} + +static WCHAR *to_utf16(char *str) { + int len = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); + if (len <= 0) return NULL; + WCHAR *wstr = xmalloc((len + 1) * sizeof(WCHAR)); + if (len != MultiByteToWideChar(CP_UTF8, 0, str, -1, wstr, len)) { + free(wstr); + return NULL; + } + wstr[len] = L'\0'; + return wstr; +} + +static bool conpty_setup(HPCON *hnd, COORD size, STARTUPINFOEXW *si_ex, char **in_name, char **out_name) { + static int count = 0; + char buf[256]; + HPCON pty = INVALID_HANDLE_VALUE; + SECURITY_ATTRIBUTES sa = {0}; + HANDLE in_pipe = INVALID_HANDLE_VALUE; + HANDLE out_pipe = INVALID_HANDLE_VALUE; + const DWORD open_mode = PIPE_ACCESS_INBOUND | PIPE_ACCESS_OUTBOUND | FILE_FLAG_FIRST_PIPE_INSTANCE; + const DWORD pipe_mode = PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT; + DWORD pid = GetCurrentProcessId(); + bool ret = false; + + sa.nLength = sizeof(sa); + snprintf(buf, sizeof(buf), "\\\\.\\pipe\\ttyd-term-in-%d-%d", pid, count); + *in_name = strdup(buf); + snprintf(buf, sizeof(buf), "\\\\.\\pipe\\ttyd-term-out-%d-%d", pid, count); + *out_name = strdup(buf); + in_pipe = CreateNamedPipeA(*in_name, open_mode, pipe_mode, 1, 0, 0, 30000, &sa); + out_pipe = CreateNamedPipeA(*out_name, open_mode, pipe_mode, 1, 0, 0, 30000, &sa); + if (in_pipe == INVALID_HANDLE_VALUE || out_pipe == INVALID_HANDLE_VALUE) { + print_error("CreateNamedPipeA"); + goto failed; + } + + HRESULT hr = pCreatePseudoConsole(size, in_pipe, out_pipe, 0, &pty); + if (FAILED(hr)) { + print_error("CreatePseudoConsole"); + goto failed; + } + + si_ex->StartupInfo.cb = sizeof(STARTUPINFOEXW); + si_ex->StartupInfo.dwFlags |= STARTF_USESTDHANDLES; + si_ex->StartupInfo.hStdError = NULL; + si_ex->StartupInfo.hStdInput = NULL; + si_ex->StartupInfo.hStdOutput = NULL; + size_t bytes_required; + InitializeProcThreadAttributeList(NULL, 1, 0, &bytes_required); + si_ex->lpAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST) xmalloc(bytes_required); + if (!InitializeProcThreadAttributeList(si_ex->lpAttributeList, 1, 0, &bytes_required)) { + print_error("InitializeProcThreadAttributeList"); + goto failed; + } + if (!UpdateProcThreadAttribute(si_ex->lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, pty, sizeof(HPCON), + NULL, NULL)) { + print_error("UpdateProcThreadAttribute"); + goto failed; + } + count++; + *hnd = pty; + ret = true; + goto done; + +failed: + ret = false; + free(*in_name); + *in_name = NULL; + free(*out_name); + *out_name = NULL; +done: + if (in_pipe != INVALID_HANDLE_VALUE) CloseHandle(in_pipe); + if (out_pipe != INVALID_HANDLE_VALUE) CloseHandle(out_pipe); + return ret; +} + +static void connect_cb(uv_connect_t *req, int status) { free(req); } + +static void CALLBACK conpty_exit(void *context, BOOLEAN unused) { + pty_process *process = (pty_process *) context; + uv_async_send(&process->async); +} + +static void async_cb(uv_async_t *async) { + pty_process *process = (pty_process *) async->data; + UnregisterWait(process->wait); + + DWORD exit_code; + GetExitCodeProcess(process->handle, &exit_code); + process->exit_code = (int) exit_code; + process->exit_signal = 1; + process->exit_cb(process); + + uv_close((uv_handle_t *) async, async_free_cb); + process_free(process); +} + +int pty_spawn(pty_process *process, pty_read_cb read_cb, pty_exit_cb exit_cb) { + char *in_name = NULL; + char *out_name = NULL; + DWORD flags = EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT; + COORD size = {(int16_t) process->columns, (int16_t) process->rows}; + + if (!conpty_setup(&process->pty, size, &process->si, &in_name, &out_name)) return 1; + + SetConsoleCtrlHandler(NULL, FALSE); + + int status = 1; + process->in = xmalloc(sizeof(uv_pipe_t)); + process->out = xmalloc(sizeof(uv_pipe_t)); + uv_pipe_init(process->loop, process->in, 0); + uv_pipe_init(process->loop, process->out, 0); + + uv_connect_t *in_req = xmalloc(sizeof(uv_connect_t)); + uv_connect_t *out_req = xmalloc(sizeof(uv_connect_t)); + uv_pipe_connect(in_req, process->in, in_name, connect_cb); + uv_pipe_connect(out_req, process->out, out_name, connect_cb); + + PROCESS_INFORMATION pi = {0}; + WCHAR *cmdline, *cwd; + cmdline = join_args(process->argv); + if (cmdline == NULL) goto cleanup; + if (process->envp != NULL) { + char **p = process->envp; + for (; *p; p++) { + WCHAR *env = to_utf16(*p); + if (env == NULL) goto cleanup; + _wputenv(env); + free(env); + } + } + if (process->cwd != NULL) { + cwd = to_utf16(process->cwd); + if (cwd == NULL) goto cleanup; + } + + if (!CreateProcessW(NULL, cmdline, NULL, NULL, FALSE, flags, NULL, cwd, &process->si.StartupInfo, &pi)) { + print_error("CreateProcessW"); + DWORD exitCode = 0; + if (GetExitCodeProcess(pi.hProcess, &exitCode)) printf("== exit code: %d\n", exitCode); + goto cleanup; + } + + process->pid = pi.dwProcessId; + process->handle = pi.hProcess; + process->paused = true; + process->read_cb = read_cb; + process->exit_cb = exit_cb; + process->async.data = process; + uv_async_init(process->loop, &process->async, async_cb); + + if (!RegisterWaitForSingleObject(&process->wait, pi.hProcess, conpty_exit, process, INFINITE, WT_EXECUTEONLYONCE)) { + print_error("RegisterWaitForSingleObject"); + goto cleanup; + } + + status = 0; + +cleanup: + if (in_name != NULL) free(in_name); + if (out_name != NULL) free(out_name); + if (cmdline != NULL) free(cmdline); + if (cwd != NULL) free(cwd); + return status; +} +#else +static bool fd_set_cloexec(const int fd) { + int flags = fcntl(fd, F_GETFD); + if (flags < 0) return false; + return (flags & FD_CLOEXEC) == 0 || fcntl(fd, F_SETFD, flags | FD_CLOEXEC) != -1; +} + +static bool fd_duplicate(int fd, uv_pipe_t *pipe) { + int fd_dup = dup(fd); + if (fd_dup < 0) return false; + + if (!fd_set_cloexec(fd_dup)) return false; + + int status = uv_pipe_open(pipe, fd_dup); + if (status) close(fd_dup); + return status == 0; +} + +static void wait_cb(void *arg) { + pty_process *process = (pty_process *) arg; + + pid_t pid; + int stat; + do + pid = waitpid(process->pid, &stat, 0); + while (pid != process->pid && errno == EINTR); + + if (WIFEXITED(stat)) { + process->exit_code = WEXITSTATUS(stat); + } + if (WIFSIGNALED(stat)) { + int sig = WTERMSIG(stat); + process->exit_code = 128 + sig; + process->exit_signal = sig; + } + + uv_async_send(&process->async); +} + +static void async_cb(uv_async_t *async) { + pty_process *process = (pty_process *) async->data; + process->exit_cb(process); + + uv_close((uv_handle_t *) async, async_free_cb); + process_free(process); +} + +int pty_spawn(pty_process *process, pty_read_cb read_cb, pty_exit_cb exit_cb) { + int status = 0; + + uv_disable_stdio_inheritance(); + + int master, pid; + struct winsize size = {process->rows, process->columns, 0, 0}; + pid = forkpty(&master, NULL, NULL, &size); + if (pid < 0) { + status = -errno; + return status; + } else if (pid == 0) { + setsid(); + if (process->cwd != NULL) chdir(process->cwd); + if (process->envp != NULL) { + char **p = process->envp; + for (; *p; p++) putenv(*p); + } + int ret = execvp(process->argv[0], process->argv); + if (ret < 0) { + perror("execvp failed\n"); + _exit(-errno); + } + } + + int flags = fcntl(master, F_GETFL); + if (flags == -1) { + status = -errno; + goto error; + } + if (fcntl(master, F_SETFL, flags | O_NONBLOCK) == -1) { + status = -errno; + goto error; + } + if (!fd_set_cloexec(master)) { + status = -errno; + goto error; + } + + process->in = xmalloc(sizeof(uv_pipe_t)); + process->out = xmalloc(sizeof(uv_pipe_t)); + uv_pipe_init(process->loop, process->in, 0); + uv_pipe_init(process->loop, process->out, 0); + + if (!fd_duplicate(master, process->in) || !fd_duplicate(master, process->out)) { + status = -errno; + goto error; + } + + process->pty = master; + process->pid = pid; + process->paused = true; + process->read_cb = read_cb; + process->exit_cb = exit_cb; + process->async.data = process; + uv_async_init(process->loop, &process->async, async_cb); + uv_thread_create(&process->tid, wait_cb, process); + + return 0; + +error: + close(master); + uv_kill(pid, SIGKILL); + waitpid(pid, NULL, 0); + return status; +} +#endif |