Skip to content

Commit

Permalink
Merge branch 'pw/add-p-single-key'
Browse files Browse the repository at this point in the history
Finishing touches to C rewrite of "git add -i" in single-key
interactive mode.

* pw/add-p-single-key:
  terminal: restore settings on SIGTSTP
  terminal: work around macos poll() bug
  terminal: don't assume stdin is /dev/tty
  terminal: use flags for save_term()
  • Loading branch information
gitster committed Mar 31, 2022
2 parents 83791bc + 0f584de commit d723492
Show file tree
Hide file tree
Showing 2 changed files with 211 additions and 36 deletions.
238 changes: 203 additions & 35 deletions compat/terminal.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#include "git-compat-util.h"
#include "cache.h"
#include "compat/terminal.h"
#include "sigchain.h"
#include "strbuf.h"
Expand All @@ -20,39 +20,171 @@ static void restore_term_on_signal(int sig)
#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);
term_fd = -1;
close_term_fd();
sigchain_pop_common();
reset_job_signals();
}

int save_term(int full_duplex)
int save_term(enum save_term_flags flags)
{
struct sigaction sa;

if (term_fd < 0)
term_fd = open("/dev/tty", O_RDWR);
term_fd = ((flags & SAVE_TERM_STDIN)
? 0
: open("/dev/tty", O_RDWR));
if (term_fd < 0)
return -1;
if (tcgetattr(term_fd, &old_term) < 0)
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(tcflag_t bits)
static int disable_bits(enum save_term_flags flags, tcflag_t bits)
{
struct termios t;

if (save_term(0) < 0)
goto error;
if (save_term(flags) < 0)
return -1;

t = old_term;

Expand All @@ -65,20 +197,50 @@ static int disable_bits(tcflag_t bits)
return 0;

sigchain_pop_common();
error:
close(term_fd);
term_fd = -1;
reset_job_signals();
close_term_fd();
return -1;
}

static int disable_echo(void)
static int disable_echo(enum save_term_flags flags)
{
return disable_bits(ECHO);
return disable_bits(flags, ECHO);
}

static int enable_non_canonical(void)
static int enable_non_canonical(enum save_term_flags flags)
{
return disable_bits(ICANON | ECHO);
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)
Expand Down Expand Up @@ -126,15 +288,15 @@ void restore_term(void)
hconin = hconout = INVALID_HANDLE_VALUE;
}

int save_term(int full_duplex)
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 (full_duplex) {
if (flags & SAVE_TERM_DUPLEX) {
hconout = CreateFileA("CONOUT$", GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
Expand All @@ -154,7 +316,7 @@ int save_term(int full_duplex)
return -1;
}

static int disable_bits(DWORD bits)
static int disable_bits(enum save_term_flags flags, DWORD bits)
{
if (use_stty) {
struct child_process cp = CHILD_PROCESS_INIT;
Expand Down Expand Up @@ -191,7 +353,7 @@ static int disable_bits(DWORD bits)
use_stty = 0;
}

if (save_term(0) < 0)
if (save_term(flags) < 0)
return -1;

if (!SetConsoleMode(hconin, cmode_in & ~bits)) {
Expand All @@ -204,14 +366,15 @@ static int disable_bits(DWORD bits)
return 0;
}

static int disable_echo(void)
static int disable_echo(enum save_term_flags flags)
{
return disable_bits(ENABLE_ECHO_INPUT);
return disable_bits(flags, ENABLE_ECHO_INPUT);
}

static int enable_non_canonical(void)
static int enable_non_canonical(enum save_term_flags flags)
{
return disable_bits(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT);
return disable_bits(flags,
ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT);
}

/*
Expand Down Expand Up @@ -245,6 +408,16 @@ static int mingw_getchar(void)
}
#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
Expand All @@ -267,7 +440,7 @@ char *git_terminal_prompt(const char *prompt, int echo)
return NULL;
}

if (!echo && disable_echo()) {
if (!echo && disable_echo(0)) {
fclose(input_fh);
fclose(output_fh);
return NULL;
Expand Down Expand Up @@ -361,7 +534,7 @@ int read_key_without_echo(struct strbuf *buf)
static int warning_displayed;
int ch;

if (warning_displayed || enable_non_canonical() < 0) {
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");
Expand Down Expand Up @@ -395,12 +568,7 @@ int read_key_without_echo(struct strbuf *buf)
* half a second when we know that the sequence is complete.
*/
while (!is_known_escape_sequence(buf->buf)) {
struct pollfd pfd = { .fd = 0, .events = POLLIN };

if (poll(&pfd, 1, 500) < 1)
break;

ch = getchar();
ch = getchar_with_timeout(500);
if (ch == EOF)
break;
strbuf_addch(buf, ch);
Expand All @@ -413,10 +581,10 @@ int read_key_without_echo(struct strbuf *buf)

#else

int save_term(int full_duplex)
int save_term(enum save_term_flags flags)
{
/* full_duplex == 1, but no support available */
return -full_duplex;
/* no duplex support available */
return -!!(flags & SAVE_TERM_DUPLEX);
}

void restore_term(void)
Expand Down
9 changes: 8 additions & 1 deletion compat/terminal.h
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
#ifndef COMPAT_TERMINAL_H
#define COMPAT_TERMINAL_H

enum save_term_flags {
/* Save input and output settings */
SAVE_TERM_DUPLEX = 1 << 0,
/* Save stdin rather than /dev/tty (fails if stdin is not a terminal) */
SAVE_TERM_STDIN = 1 << 1,
};

/*
* Save the terminal attributes so they can be restored later by a
* call to restore_term(). Note that every successful call to
* save_term() must be matched by a call to restore_term() even if the
* attributes have not been changed. Returns 0 on success, -1 on
* failure.
*/
int save_term(int full_duplex);
int save_term(enum save_term_flags flags);
/* Restore the terminal attributes that were saved with save_term() */
void restore_term(void);

Expand Down

0 comments on commit d723492

Please sign in to comment.