git/compat/terminal.c
Calvin Wan da9502ff4d treewide: remove unnecessary includes for wrapper.h
Signed-off-by: Calvin Wan <calvinwan@google.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
2023-07-05 11:41:59 -07:00

620 lines
14 KiB
C

#include "git-compat-util.h"
#include "compat/terminal.h"
#include "gettext.h"
#include "sigchain.h"
#include "strbuf.h"
#include "run-command.h"
#include "string-list.h"
#include "hashmap.h"
#if defined(HAVE_DEV_TTY) || defined(GIT_WINDOWS_NATIVE)
static void restore_term_on_signal(int sig)
{
restore_term();
/* restore_term calls sigchain_pop_common */
raise(sig);
}
#ifdef HAVE_DEV_TTY
#define INPUT_PATH "/dev/tty"
#define OUTPUT_PATH "/dev/tty"
static volatile sig_atomic_t term_fd_needs_closing;
static int term_fd = -1;
static struct termios old_term;
static const char *background_resume_msg;
static const char *restore_error_msg;
static volatile sig_atomic_t ttou_received;
/* async safe error function for use by signal handlers. */
static void write_err(const char *msg)
{
write_in_full(2, "error: ", strlen("error: "));
write_in_full(2, msg, strlen(msg));
write_in_full(2, "\n", 1);
}
static void print_background_resume_msg(int signo)
{
int saved_errno = errno;
sigset_t mask;
struct sigaction old_sa;
struct sigaction sa = { .sa_handler = SIG_DFL };
ttou_received = 1;
write_err(background_resume_msg);
sigaction(signo, &sa, &old_sa);
raise(signo);
sigemptyset(&mask);
sigaddset(&mask, signo);
sigprocmask(SIG_UNBLOCK, &mask, NULL);
/* Stopped here */
sigprocmask(SIG_BLOCK, &mask, NULL);
sigaction(signo, &old_sa, NULL);
errno = saved_errno;
}
static void restore_terminal_on_suspend(int signo)
{
int saved_errno = errno;
int res;
struct termios t;
sigset_t mask;
struct sigaction old_sa;
struct sigaction sa = { .sa_handler = SIG_DFL };
int can_restore = 1;
if (tcgetattr(term_fd, &t) < 0)
can_restore = 0;
if (tcsetattr(term_fd, TCSAFLUSH, &old_term) < 0)
write_err(restore_error_msg);
sigaction(signo, &sa, &old_sa);
raise(signo);
sigemptyset(&mask);
sigaddset(&mask, signo);
sigprocmask(SIG_UNBLOCK, &mask, NULL);
/* Stopped here */
sigprocmask(SIG_BLOCK, &mask, NULL);
sigaction(signo, &old_sa, NULL);
if (!can_restore) {
write_err(restore_error_msg);
goto out;
}
/*
* If we resume in the background then we receive SIGTTOU when calling
* tcsetattr() below. Set up a handler to print an error message in that
* case.
*/
sigemptyset(&mask);
sigaddset(&mask, SIGTTOU);
sa.sa_mask = old_sa.sa_mask;
sa.sa_handler = print_background_resume_msg;
sa.sa_flags = SA_RESTART;
sigaction(SIGTTOU, &sa, &old_sa);
again:
ttou_received = 0;
sigprocmask(SIG_UNBLOCK, &mask, NULL);
res = tcsetattr(term_fd, TCSAFLUSH, &t);
sigprocmask(SIG_BLOCK, &mask, NULL);
if (ttou_received)
goto again;
else if (res < 0)
write_err(restore_error_msg);
sigaction(SIGTTOU, &old_sa, NULL);
out:
errno = saved_errno;
}
static void reset_job_signals(void)
{
if (restore_error_msg) {
signal(SIGTTIN, SIG_DFL);
signal(SIGTTOU, SIG_DFL);
signal(SIGTSTP, SIG_DFL);
restore_error_msg = NULL;
background_resume_msg = NULL;
}
}
static void close_term_fd(void)
{
if (term_fd_needs_closing)
close(term_fd);
term_fd_needs_closing = 0;
term_fd = -1;
}
void restore_term(void)
{
if (term_fd < 0)
return;
tcsetattr(term_fd, TCSAFLUSH, &old_term);
close_term_fd();
sigchain_pop_common();
reset_job_signals();
}
int save_term(enum save_term_flags flags)
{
struct sigaction sa;
if (term_fd < 0)
term_fd = ((flags & SAVE_TERM_STDIN)
? 0
: open("/dev/tty", O_RDWR));
if (term_fd < 0)
return -1;
term_fd_needs_closing = !(flags & SAVE_TERM_STDIN);
if (tcgetattr(term_fd, &old_term) < 0) {
close_term_fd();
return -1;
}
sigchain_push_common(restore_term_on_signal);
/*
* If job control is disabled then the shell will have set the
* disposition of SIGTSTP to SIG_IGN.
*/
sigaction(SIGTSTP, NULL, &sa);
if (sa.sa_handler == SIG_IGN)
return 0;
/* avoid calling gettext() from signal handler */
background_resume_msg = _("cannot resume in the background, please use 'fg' to resume");
restore_error_msg = _("cannot restore terminal settings");
sa.sa_handler = restore_terminal_on_suspend;
sa.sa_flags = SA_RESTART;
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGTSTP);
sigaddset(&sa.sa_mask, SIGTTIN);
sigaddset(&sa.sa_mask, SIGTTOU);
sigaction(SIGTSTP, &sa, NULL);
sigaction(SIGTTIN, &sa, NULL);
sigaction(SIGTTOU, &sa, NULL);
return 0;
}
static int disable_bits(enum save_term_flags flags, tcflag_t bits)
{
struct termios t;
if (save_term(flags) < 0)
return -1;
t = old_term;
t.c_lflag &= ~bits;
if (bits & ICANON) {
t.c_cc[VMIN] = 1;
t.c_cc[VTIME] = 0;
}
if (!tcsetattr(term_fd, TCSAFLUSH, &t))
return 0;
sigchain_pop_common();
reset_job_signals();
close_term_fd();
return -1;
}
static int disable_echo(enum save_term_flags flags)
{
return disable_bits(flags, ECHO);
}
static int enable_non_canonical(enum save_term_flags flags)
{
return disable_bits(flags, ICANON | ECHO);
}
/*
* On macos it is not possible to use poll() with a terminal so use select
* instead.
*/
static int getchar_with_timeout(int timeout)
{
struct timeval tv, *tvp = NULL;
fd_set readfds;
int res;
again:
if (timeout >= 0) {
tv.tv_sec = timeout / 1000;
tv.tv_usec = (timeout % 1000) * 1000;
tvp = &tv;
}
FD_ZERO(&readfds);
FD_SET(0, &readfds);
res = select(1, &readfds, NULL, NULL, tvp);
if (!res)
return EOF;
if (res < 0) {
if (errno == EINTR)
goto again;
else
return EOF;
}
return getchar();
}
#elif defined(GIT_WINDOWS_NATIVE)
#define INPUT_PATH "CONIN$"
#define OUTPUT_PATH "CONOUT$"
#define FORCE_TEXT "t"
static int use_stty = 1;
static struct string_list stty_restore = STRING_LIST_INIT_DUP;
static HANDLE hconin = INVALID_HANDLE_VALUE;
static HANDLE hconout = INVALID_HANDLE_VALUE;
static DWORD cmode_in, cmode_out;
void restore_term(void)
{
if (use_stty) {
int i;
struct child_process cp = CHILD_PROCESS_INIT;
if (stty_restore.nr == 0)
return;
strvec_push(&cp.args, "stty");
for (i = 0; i < stty_restore.nr; i++)
strvec_push(&cp.args, stty_restore.items[i].string);
run_command(&cp);
string_list_clear(&stty_restore, 0);
return;
}
sigchain_pop_common();
if (hconin == INVALID_HANDLE_VALUE)
return;
SetConsoleMode(hconin, cmode_in);
CloseHandle(hconin);
if (cmode_out) {
assert(hconout != INVALID_HANDLE_VALUE);
SetConsoleMode(hconout, cmode_out);
CloseHandle(hconout);
}
hconin = hconout = INVALID_HANDLE_VALUE;
}
int save_term(enum save_term_flags flags)
{
hconin = CreateFileA("CONIN$", GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
if (hconin == INVALID_HANDLE_VALUE)
return -1;
if (flags & SAVE_TERM_DUPLEX) {
hconout = CreateFileA("CONOUT$", GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
if (hconout == INVALID_HANDLE_VALUE)
goto error;
GetConsoleMode(hconout, &cmode_out);
}
GetConsoleMode(hconin, &cmode_in);
use_stty = 0;
sigchain_push_common(restore_term_on_signal);
return 0;
error:
CloseHandle(hconin);
hconin = INVALID_HANDLE_VALUE;
return -1;
}
static int disable_bits(enum save_term_flags flags, DWORD bits)
{
if (use_stty) {
struct child_process cp = CHILD_PROCESS_INIT;
strvec_push(&cp.args, "stty");
if (bits & ENABLE_LINE_INPUT) {
string_list_append(&stty_restore, "icanon");
/*
* POSIX allows VMIN and VTIME to overlap with VEOF and
* VEOL - let's hope that is not the case on windows.
*/
strvec_pushl(&cp.args, "-icanon", "min", "1", "time", "0", NULL);
}
if (bits & ENABLE_ECHO_INPUT) {
string_list_append(&stty_restore, "echo");
strvec_push(&cp.args, "-echo");
}
if (bits & ENABLE_PROCESSED_INPUT) {
string_list_append(&stty_restore, "-ignbrk");
string_list_append(&stty_restore, "intr");
string_list_append(&stty_restore, "^c");
strvec_push(&cp.args, "ignbrk");
strvec_push(&cp.args, "intr");
strvec_push(&cp.args, "");
}
if (run_command(&cp) == 0)
return 0;
/* `stty` could not be executed; access the Console directly */
use_stty = 0;
}
if (save_term(flags) < 0)
return -1;
if (!SetConsoleMode(hconin, cmode_in & ~bits)) {
CloseHandle(hconin);
hconin = INVALID_HANDLE_VALUE;
sigchain_pop_common();
return -1;
}
return 0;
}
static int disable_echo(enum save_term_flags flags)
{
return disable_bits(flags, ENABLE_ECHO_INPUT);
}
static int enable_non_canonical(enum save_term_flags flags)
{
return disable_bits(flags,
ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT);
}
/*
* Override `getchar()`, as the default implementation does not use
* `ReadFile()`.
*
* This poses a problem when we want to see whether the standard
* input has more characters, as the default of Git for Windows is to start the
* Bash in a MinTTY, which uses a named pipe to emulate a pty, in which case
* our `poll()` emulation calls `PeekNamedPipe()`, which seems to require
* `ReadFile()` to be called first to work properly (it only reports 0
* available bytes, otherwise).
*
* So let's just override `getchar()` with a version backed by `ReadFile()` and
* go our merry ways from here.
*/
static int mingw_getchar(void)
{
DWORD read = 0;
unsigned char ch;
if (!ReadFile(GetStdHandle(STD_INPUT_HANDLE), &ch, 1, &read, NULL))
return EOF;
if (!read) {
error("Unexpected 0 read");
return EOF;
}
return ch;
}
#define getchar mingw_getchar
static int getchar_with_timeout(int timeout)
{
struct pollfd pfd = { .fd = 0, .events = POLLIN };
if (poll(&pfd, 1, timeout) < 1)
return EOF;
return getchar();
}
#endif
#ifndef FORCE_TEXT
#define FORCE_TEXT
#endif
char *git_terminal_prompt(const char *prompt, int echo)
{
static struct strbuf buf = STRBUF_INIT;
int r;
FILE *input_fh, *output_fh;
input_fh = fopen(INPUT_PATH, "r" FORCE_TEXT);
if (!input_fh)
return NULL;
output_fh = fopen(OUTPUT_PATH, "w" FORCE_TEXT);
if (!output_fh) {
fclose(input_fh);
return NULL;
}
if (!echo && disable_echo(0)) {
fclose(input_fh);
fclose(output_fh);
return NULL;
}
fputs(prompt, output_fh);
fflush(output_fh);
r = strbuf_getline_lf(&buf, input_fh);
if (!echo) {
putc('\n', output_fh);
fflush(output_fh);
}
restore_term();
fclose(input_fh);
fclose(output_fh);
if (r == EOF)
return NULL;
return buf.buf;
}
/*
* The `is_known_escape_sequence()` function returns 1 if the passed string
* corresponds to an Escape sequence that the terminal capabilities contains.
*
* To avoid depending on ncurses or other platform-specific libraries, we rely
* on the presence of the `infocmp` executable to do the job for us (failing
* silently if the program is not available or refused to run).
*/
struct escape_sequence_entry {
struct hashmap_entry entry;
char sequence[FLEX_ARRAY];
};
static int sequence_entry_cmp(const void *hashmap_cmp_fn_data UNUSED,
const struct escape_sequence_entry *e1,
const struct escape_sequence_entry *e2,
const void *keydata)
{
return strcmp(e1->sequence, keydata ? keydata : e2->sequence);
}
static int is_known_escape_sequence(const char *sequence)
{
static struct hashmap sequences;
static int initialized;
if (!initialized) {
struct child_process cp = CHILD_PROCESS_INIT;
struct strbuf buf = STRBUF_INIT;
char *p, *eol;
hashmap_init(&sequences, (hashmap_cmp_fn)sequence_entry_cmp,
NULL, 0);
strvec_pushl(&cp.args, "infocmp", "-L", "-1", NULL);
if (pipe_command(&cp, NULL, 0, &buf, 0, NULL, 0))
strbuf_setlen(&buf, 0);
for (eol = p = buf.buf; *p; p = eol + 1) {
p = strchr(p, '=');
if (!p)
break;
p++;
eol = strchrnul(p, '\n');
if (starts_with(p, "\\E")) {
char *comma = memchr(p, ',', eol - p);
struct escape_sequence_entry *e;
p[0] = '^';
p[1] = '[';
FLEX_ALLOC_MEM(e, sequence, p, comma - p);
hashmap_entry_init(&e->entry,
strhash(e->sequence));
hashmap_add(&sequences, &e->entry);
}
if (!*eol)
break;
}
initialized = 1;
}
return !!hashmap_get_from_hash(&sequences, strhash(sequence), sequence);
}
int read_key_without_echo(struct strbuf *buf)
{
static int warning_displayed;
int ch;
if (warning_displayed || enable_non_canonical(SAVE_TERM_STDIN) < 0) {
if (!warning_displayed) {
warning("reading single keystrokes not supported on "
"this platform; reading line instead");
warning_displayed = 1;
}
return strbuf_getline(buf, stdin);
}
strbuf_reset(buf);
ch = getchar();
if (ch == EOF) {
restore_term();
return EOF;
}
strbuf_addch(buf, ch);
if (ch == '\033' /* ESC */) {
/*
* We are most likely looking at an Escape sequence. Let's try
* to read more bytes, waiting at most half a second, assuming
* that the sequence is complete if we did not receive any byte
* within that time.
*
* Start by replacing the Escape byte with ^[ */
strbuf_splice(buf, buf->len - 1, 1, "^[", 2);
/*
* Query the terminal capabilities once about all the Escape
* sequences it knows about, so that we can avoid waiting for
* half a second when we know that the sequence is complete.
*/
while (!is_known_escape_sequence(buf->buf)) {
ch = getchar_with_timeout(500);
if (ch == EOF)
break;
strbuf_addch(buf, ch);
}
}
restore_term();
return 0;
}
#else
int save_term(enum save_term_flags flags)
{
/* no duplex support available */
return -!!(flags & SAVE_TERM_DUPLEX);
}
void restore_term(void)
{
}
char *git_terminal_prompt(const char *prompt, int echo)
{
return getpass(prompt);
}
int read_key_without_echo(struct strbuf *buf)
{
static int warning_displayed;
const char *res;
if (!warning_displayed) {
warning("reading single keystrokes not supported on this "
"platform; reading line instead");
warning_displayed = 1;
}
res = getpass("");
strbuf_reset(buf);
if (!res)
return EOF;
strbuf_addstr(buf, res);
return 0;
}
#endif