Coverage Report

Created: 2026-02-14 06:27

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/git/compat/terminal.c
Line
Count
Source
1
#include "git-compat-util.h"
2
#include "compat/terminal.h"
3
#include "gettext.h"
4
#include "sigchain.h"
5
#include "strbuf.h"
6
#include "run-command.h"
7
#include "string-list.h"
8
#include "hashmap.h"
9
10
#if defined(HAVE_DEV_TTY) || defined(GIT_WINDOWS_NATIVE)
11
12
static void restore_term_on_signal(int sig)
13
0
{
14
0
  restore_term();
15
  /* restore_term calls sigchain_pop_common */
16
0
  raise(sig);
17
0
}
18
19
#ifdef HAVE_DEV_TTY
20
21
#define INPUT_PATH "/dev/tty"
22
#define OUTPUT_PATH "/dev/tty"
23
24
static volatile sig_atomic_t term_fd_needs_closing;
25
static int term_fd = -1;
26
static struct termios old_term;
27
28
static const char *background_resume_msg;
29
static const char *restore_error_msg;
30
static volatile sig_atomic_t ttou_received;
31
32
/* async safe error function for use by signal handlers. */
33
static void write_err(const char *msg)
34
0
{
35
0
  write_in_full(2, "error: ", strlen("error: "));
36
0
  write_in_full(2, msg, strlen(msg));
37
0
  write_in_full(2, "\n", 1);
38
0
}
39
40
static void print_background_resume_msg(int signo)
41
0
{
42
0
  int saved_errno = errno;
43
0
  sigset_t mask;
44
0
  struct sigaction old_sa;
45
0
  struct sigaction sa = { .sa_handler = SIG_DFL };
46
47
0
  ttou_received = 1;
48
0
  write_err(background_resume_msg);
49
0
  sigaction(signo, &sa, &old_sa);
50
0
  raise(signo);
51
0
  sigemptyset(&mask);
52
0
  sigaddset(&mask, signo);
53
0
  sigprocmask(SIG_UNBLOCK, &mask, NULL);
54
  /* Stopped here */
55
0
  sigprocmask(SIG_BLOCK, &mask, NULL);
56
0
  sigaction(signo, &old_sa, NULL);
57
0
  errno = saved_errno;
58
0
}
59
60
static void restore_terminal_on_suspend(int signo)
61
0
{
62
0
  int saved_errno = errno;
63
0
  int res;
64
0
  struct termios t;
65
0
  sigset_t mask;
66
0
  struct sigaction old_sa;
67
0
  struct sigaction sa = { .sa_handler = SIG_DFL };
68
0
  int can_restore = 1;
69
70
0
  if (tcgetattr(term_fd, &t) < 0)
71
0
    can_restore = 0;
72
73
0
  if (tcsetattr(term_fd, TCSAFLUSH, &old_term) < 0)
74
0
    write_err(restore_error_msg);
75
76
0
  sigaction(signo, &sa, &old_sa);
77
0
  raise(signo);
78
0
  sigemptyset(&mask);
79
0
  sigaddset(&mask, signo);
80
0
  sigprocmask(SIG_UNBLOCK, &mask, NULL);
81
  /* Stopped here */
82
0
  sigprocmask(SIG_BLOCK, &mask, NULL);
83
0
  sigaction(signo, &old_sa, NULL);
84
0
  if (!can_restore) {
85
0
    write_err(restore_error_msg);
86
0
    goto out;
87
0
  }
88
  /*
89
   * If we resume in the background then we receive SIGTTOU when calling
90
   * tcsetattr() below. Set up a handler to print an error message in that
91
   * case.
92
   */
93
0
  sigemptyset(&mask);
94
0
  sigaddset(&mask, SIGTTOU);
95
0
  sa.sa_mask = old_sa.sa_mask;
96
0
  sa.sa_handler = print_background_resume_msg;
97
0
  sa.sa_flags = SA_RESTART;
98
0
  sigaction(SIGTTOU, &sa, &old_sa);
99
0
 again:
100
0
  ttou_received = 0;
101
0
  sigprocmask(SIG_UNBLOCK, &mask, NULL);
102
0
  res = tcsetattr(term_fd, TCSAFLUSH, &t);
103
0
  sigprocmask(SIG_BLOCK, &mask, NULL);
104
0
  if (ttou_received)
105
0
    goto again;
106
0
  else if (res < 0)
107
0
    write_err(restore_error_msg);
108
0
  sigaction(SIGTTOU, &old_sa, NULL);
109
0
 out:
110
0
  errno = saved_errno;
111
0
}
112
113
static void reset_job_signals(void)
114
0
{
115
0
  if (restore_error_msg) {
116
0
    signal(SIGTTIN, SIG_DFL);
117
0
    signal(SIGTTOU, SIG_DFL);
118
0
    signal(SIGTSTP, SIG_DFL);
119
0
    restore_error_msg = NULL;
120
0
    background_resume_msg = NULL;
121
0
  }
122
0
}
123
124
static void close_term_fd(void)
125
0
{
126
0
  if (term_fd_needs_closing)
127
0
    close(term_fd);
128
0
  term_fd_needs_closing = 0;
129
0
  term_fd = -1;
130
0
}
131
132
void restore_term(void)
133
0
{
134
0
  if (term_fd < 0)
135
0
    return;
136
137
0
  tcsetattr(term_fd, TCSAFLUSH, &old_term);
138
0
  close_term_fd();
139
0
  sigchain_pop_common();
140
0
  reset_job_signals();
141
0
}
142
143
int save_term(enum save_term_flags flags)
144
0
{
145
0
  struct sigaction sa;
146
147
0
  if (term_fd < 0)
148
0
    term_fd = ((flags & SAVE_TERM_STDIN)
149
0
         ? 0
150
0
         : open("/dev/tty", O_RDWR));
151
0
  if (term_fd < 0)
152
0
    return -1;
153
0
  term_fd_needs_closing = !(flags & SAVE_TERM_STDIN);
154
0
  if (tcgetattr(term_fd, &old_term) < 0) {
155
0
    close_term_fd();
156
0
    return -1;
157
0
  }
158
0
  sigchain_push_common(restore_term_on_signal);
159
  /*
160
   * If job control is disabled then the shell will have set the
161
   * disposition of SIGTSTP to SIG_IGN.
162
   */
163
0
  sigaction(SIGTSTP, NULL, &sa);
164
0
  if (sa.sa_handler == SIG_IGN)
165
0
    return 0;
166
167
  /* avoid calling gettext() from signal handler */
168
0
  background_resume_msg = _("cannot resume in the background, please use 'fg' to resume");
169
0
  restore_error_msg = _("cannot restore terminal settings");
170
0
  sa.sa_handler = restore_terminal_on_suspend;
171
0
  sa.sa_flags = SA_RESTART;
172
0
  sigemptyset(&sa.sa_mask);
173
0
  sigaddset(&sa.sa_mask, SIGTSTP);
174
0
  sigaddset(&sa.sa_mask, SIGTTIN);
175
0
  sigaddset(&sa.sa_mask, SIGTTOU);
176
0
  sigaction(SIGTSTP, &sa, NULL);
177
0
  sigaction(SIGTTIN, &sa, NULL);
178
0
  sigaction(SIGTTOU, &sa, NULL);
179
180
0
  return 0;
181
0
}
182
183
static int disable_bits(enum save_term_flags flags, tcflag_t bits)
184
0
{
185
0
  struct termios t;
186
187
0
  if (save_term(flags) < 0)
188
0
    return -1;
189
190
0
  t = old_term;
191
192
0
  t.c_lflag &= ~bits;
193
0
  if (bits & ICANON) {
194
0
    t.c_cc[VMIN] = 1;
195
0
    t.c_cc[VTIME] = 0;
196
0
  }
197
0
  if (!tcsetattr(term_fd, TCSAFLUSH, &t))
198
0
    return 0;
199
200
0
  sigchain_pop_common();
201
0
  reset_job_signals();
202
0
  close_term_fd();
203
0
  return -1;
204
0
}
205
206
static int disable_echo(enum save_term_flags flags)
207
0
{
208
0
  return disable_bits(flags, ECHO);
209
0
}
210
211
static int enable_non_canonical(enum save_term_flags flags)
212
0
{
213
0
  return disable_bits(flags, ICANON | ECHO);
214
0
}
215
216
/*
217
 * On macos it is not possible to use poll() with a terminal so use select
218
 * instead.
219
 */
220
static int getchar_with_timeout(int timeout)
221
0
{
222
0
  struct timeval tv, *tvp = NULL;
223
0
  fd_set readfds;
224
0
  int res;
225
226
0
 again:
227
0
  if (timeout >= 0) {
228
0
    tv.tv_sec = timeout / 1000;
229
0
    tv.tv_usec = (timeout % 1000) * 1000;
230
0
    tvp = &tv;
231
0
  }
232
233
0
  FD_ZERO(&readfds);
234
0
  FD_SET(0, &readfds);
235
0
  res = select(1, &readfds, NULL, NULL, tvp);
236
0
  if (!res)
237
0
    return EOF;
238
0
  if (res < 0) {
239
0
    if (errno == EINTR)
240
0
      goto again;
241
0
    else
242
0
      return EOF;
243
0
  }
244
0
  return getchar();
245
0
}
246
247
#elif defined(GIT_WINDOWS_NATIVE)
248
249
#define INPUT_PATH "CONIN$"
250
#define OUTPUT_PATH "CONOUT$"
251
#define FORCE_TEXT "t"
252
253
static int use_stty = 1;
254
static struct string_list stty_restore = STRING_LIST_INIT_DUP;
255
static HANDLE hconin = INVALID_HANDLE_VALUE;
256
static HANDLE hconout = INVALID_HANDLE_VALUE;
257
static DWORD cmode_in, cmode_out;
258
259
void restore_term(void)
260
{
261
  if (use_stty) {
262
    struct child_process cp = CHILD_PROCESS_INIT;
263
264
    if (stty_restore.nr == 0)
265
      return;
266
267
    strvec_push(&cp.args, "stty");
268
    for (size_t i = 0; i < stty_restore.nr; i++)
269
      strvec_push(&cp.args, stty_restore.items[i].string);
270
    run_command(&cp);
271
    string_list_clear(&stty_restore, 0);
272
    return;
273
  }
274
275
  sigchain_pop_common();
276
277
  if (hconin == INVALID_HANDLE_VALUE)
278
    return;
279
280
  SetConsoleMode(hconin, cmode_in);
281
  CloseHandle(hconin);
282
  if (cmode_out) {
283
    assert(hconout != INVALID_HANDLE_VALUE);
284
    SetConsoleMode(hconout, cmode_out);
285
    CloseHandle(hconout);
286
  }
287
288
  hconin = hconout = INVALID_HANDLE_VALUE;
289
}
290
291
int save_term(enum save_term_flags flags)
292
{
293
  hconin = CreateFileA("CONIN$", GENERIC_READ | GENERIC_WRITE,
294
      FILE_SHARE_READ, NULL, OPEN_EXISTING,
295
      FILE_ATTRIBUTE_NORMAL, NULL);
296
  if (hconin == INVALID_HANDLE_VALUE)
297
    return -1;
298
299
  if (flags & SAVE_TERM_DUPLEX) {
300
    hconout = CreateFileA("CONOUT$", GENERIC_READ | GENERIC_WRITE,
301
      FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
302
      FILE_ATTRIBUTE_NORMAL, NULL);
303
    if (hconout == INVALID_HANDLE_VALUE)
304
      goto error;
305
306
    GetConsoleMode(hconout, &cmode_out);
307
  }
308
309
  GetConsoleMode(hconin, &cmode_in);
310
  use_stty = 0;
311
  sigchain_push_common(restore_term_on_signal);
312
  return 0;
313
error:
314
  CloseHandle(hconin);
315
  hconin = INVALID_HANDLE_VALUE;
316
  return -1;
317
}
318
319
static int disable_bits(enum save_term_flags flags, DWORD bits)
320
{
321
  if (use_stty) {
322
    struct child_process cp = CHILD_PROCESS_INIT;
323
324
    strvec_push(&cp.args, "stty");
325
326
    if (bits & ENABLE_LINE_INPUT) {
327
      string_list_append(&stty_restore, "icanon");
328
      /*
329
       * POSIX allows VMIN and VTIME to overlap with VEOF and
330
       * VEOL - let's hope that is not the case on windows.
331
       */
332
      strvec_pushl(&cp.args, "-icanon", "min", "1", "time", "0", NULL);
333
    }
334
335
    if (bits & ENABLE_ECHO_INPUT) {
336
      string_list_append(&stty_restore, "echo");
337
      strvec_push(&cp.args, "-echo");
338
    }
339
340
    if (bits & ENABLE_PROCESSED_INPUT) {
341
      string_list_append(&stty_restore, "-ignbrk");
342
      string_list_append(&stty_restore, "intr");
343
      string_list_append(&stty_restore, "^c");
344
      strvec_push(&cp.args, "ignbrk");
345
      strvec_push(&cp.args, "intr");
346
      strvec_push(&cp.args, "");
347
    }
348
349
    if (run_command(&cp) == 0)
350
      return 0;
351
352
    /* `stty` could not be executed; access the Console directly */
353
    use_stty = 0;
354
  }
355
356
  if (save_term(flags) < 0)
357
    return -1;
358
359
  if (!SetConsoleMode(hconin, cmode_in & ~bits)) {
360
    CloseHandle(hconin);
361
    hconin = INVALID_HANDLE_VALUE;
362
    sigchain_pop_common();
363
    return -1;
364
  }
365
366
  return 0;
367
}
368
369
static int disable_echo(enum save_term_flags flags)
370
{
371
  return disable_bits(flags, ENABLE_ECHO_INPUT);
372
}
373
374
static int enable_non_canonical(enum save_term_flags flags)
375
{
376
  return disable_bits(flags,
377
          ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT);
378
}
379
380
/*
381
 * Override `getchar()`, as the default implementation does not use
382
 * `ReadFile()`.
383
 *
384
 * This poses a problem when we want to see whether the standard
385
 * input has more characters, as the default of Git for Windows is to start the
386
 * Bash in a MinTTY, which uses a named pipe to emulate a pty, in which case
387
 * our `poll()` emulation calls `PeekNamedPipe()`, which seems to require
388
 * `ReadFile()` to be called first to work properly (it only reports 0
389
 * available bytes, otherwise).
390
 *
391
 * So let's just override `getchar()` with a version backed by `ReadFile()` and
392
 * go our merry ways from here.
393
 */
394
static int mingw_getchar(void)
395
{
396
  DWORD read = 0;
397
  unsigned char ch;
398
399
  if (!ReadFile(GetStdHandle(STD_INPUT_HANDLE), &ch, 1, &read, NULL))
400
    return EOF;
401
402
  if (!read) {
403
    error("Unexpected 0 read");
404
    return EOF;
405
  }
406
407
  return ch;
408
}
409
#define getchar mingw_getchar
410
411
static int getchar_with_timeout(int timeout)
412
{
413
  struct pollfd pfd = { .fd = 0, .events = POLLIN };
414
415
  if (poll(&pfd, 1, timeout) < 1)
416
    return EOF;
417
418
  return getchar();
419
}
420
421
#endif
422
423
#ifndef FORCE_TEXT
424
#define FORCE_TEXT
425
#endif
426
427
char *git_terminal_prompt(const char *prompt, int echo)
428
0
{
429
0
  static struct strbuf buf = STRBUF_INIT;
430
0
  int r;
431
0
  FILE *input_fh, *output_fh;
432
433
0
  input_fh = fopen(INPUT_PATH, "r" FORCE_TEXT);
434
0
  if (!input_fh)
435
0
    return NULL;
436
437
0
  output_fh = fopen(OUTPUT_PATH, "w" FORCE_TEXT);
438
0
  if (!output_fh) {
439
0
    fclose(input_fh);
440
0
    return NULL;
441
0
  }
442
443
0
  if (!echo && disable_echo(0)) {
444
0
    fclose(input_fh);
445
0
    fclose(output_fh);
446
0
    return NULL;
447
0
  }
448
449
0
  fputs(prompt, output_fh);
450
0
  fflush(output_fh);
451
452
0
  r = strbuf_getline_lf(&buf, input_fh);
453
0
  if (!echo) {
454
0
    putc('\n', output_fh);
455
0
    fflush(output_fh);
456
0
  }
457
458
0
  restore_term();
459
0
  fclose(input_fh);
460
0
  fclose(output_fh);
461
462
0
  if (r == EOF)
463
0
    return NULL;
464
0
  return buf.buf;
465
0
}
466
467
/*
468
 * The `is_known_escape_sequence()` function returns 1 if the passed string
469
 * corresponds to an Escape sequence that the terminal capabilities contains.
470
 *
471
 * To avoid depending on ncurses or other platform-specific libraries, we rely
472
 * on the presence of the `infocmp` executable to do the job for us (failing
473
 * silently if the program is not available or refused to run).
474
 */
475
struct escape_sequence_entry {
476
  struct hashmap_entry entry;
477
  char sequence[FLEX_ARRAY];
478
};
479
480
static int sequence_entry_cmp(const void *hashmap_cmp_fn_data UNUSED,
481
            const struct hashmap_entry *he1,
482
            const struct hashmap_entry *he2,
483
            const void *keydata)
484
0
{
485
0
  const struct escape_sequence_entry
486
0
    *e1 = container_of(he1, const struct escape_sequence_entry, entry),
487
0
    *e2 = container_of(he2, const struct escape_sequence_entry, entry);
488
0
  return strcmp(e1->sequence, keydata ? keydata : e2->sequence);
489
0
}
490
491
static int is_known_escape_sequence(const char *sequence)
492
0
{
493
0
  static struct hashmap sequences;
494
0
  static int initialized;
495
496
0
  if (!initialized) {
497
0
    struct child_process cp = CHILD_PROCESS_INIT;
498
0
    struct strbuf buf = STRBUF_INIT;
499
0
    char *p, *eol;
500
501
0
    hashmap_init(&sequences, sequence_entry_cmp, NULL, 0);
502
503
0
    strvec_pushl(&cp.args, "infocmp", "-L", "-1", NULL);
504
0
    if (pipe_command(&cp, NULL, 0, &buf, 0, NULL, 0))
505
0
      strbuf_setlen(&buf, 0);
506
507
0
    for (eol = p = buf.buf; *p; p = eol + 1) {
508
0
      p = strchr(p, '=');
509
0
      if (!p)
510
0
        break;
511
0
      p++;
512
0
      eol = strchrnul(p, '\n');
513
514
0
      if (starts_with(p, "\\E")) {
515
0
        char *comma = memchr(p, ',', eol - p);
516
0
        struct escape_sequence_entry *e;
517
518
0
        p[0] = '^';
519
0
        p[1] = '[';
520
0
        FLEX_ALLOC_MEM(e, sequence, p, comma - p);
521
0
        hashmap_entry_init(&e->entry,
522
0
               strhash(e->sequence));
523
0
        hashmap_add(&sequences, &e->entry);
524
0
      }
525
0
      if (!*eol)
526
0
        break;
527
0
    }
528
0
    initialized = 1;
529
0
  }
530
531
0
  return !!hashmap_get_from_hash(&sequences, strhash(sequence), sequence);
532
0
}
533
534
int read_key_without_echo(struct strbuf *buf)
535
0
{
536
0
  static int warning_displayed;
537
0
  int ch;
538
539
0
  if (warning_displayed || enable_non_canonical(SAVE_TERM_STDIN) < 0) {
540
0
    if (!warning_displayed) {
541
0
      warning("reading single keystrokes not supported on "
542
0
        "this platform; reading line instead");
543
0
      warning_displayed = 1;
544
0
    }
545
546
0
    return strbuf_getline(buf, stdin);
547
0
  }
548
549
0
  strbuf_reset(buf);
550
0
  ch = getchar();
551
0
  if (ch == EOF) {
552
0
    restore_term();
553
0
    return EOF;
554
0
  }
555
0
  strbuf_addch(buf, ch);
556
557
0
  if (ch == '\033' /* ESC */) {
558
    /*
559
     * We are most likely looking at an Escape sequence. Let's try
560
     * to read more bytes, waiting at most half a second, assuming
561
     * that the sequence is complete if we did not receive any byte
562
     * within that time.
563
     *
564
     * Start by replacing the Escape byte with ^[ */
565
0
    strbuf_splice(buf, buf->len - 1, 1, "^[", 2);
566
567
    /*
568
     * Query the terminal capabilities once about all the Escape
569
     * sequences it knows about, so that we can avoid waiting for
570
     * half a second when we know that the sequence is complete.
571
     */
572
0
    while (!is_known_escape_sequence(buf->buf)) {
573
0
      ch = getchar_with_timeout(500);
574
0
      if (ch == EOF)
575
0
        break;
576
0
      strbuf_addch(buf, ch);
577
0
    }
578
0
  }
579
580
0
  restore_term();
581
0
  return 0;
582
0
}
583
584
#else
585
586
int save_term(enum save_term_flags flags)
587
{
588
  /* no duplex support available */
589
  return -!!(flags & SAVE_TERM_DUPLEX);
590
}
591
592
void restore_term(void)
593
{
594
}
595
596
char *git_terminal_prompt(const char *prompt, int echo UNUSED)
597
{
598
  return getpass(prompt);
599
}
600
601
int read_key_without_echo(struct strbuf *buf)
602
{
603
  static int warning_displayed;
604
  const char *res;
605
606
  if (!warning_displayed) {
607
    warning("reading single keystrokes not supported on this "
608
      "platform; reading line instead");
609
    warning_displayed = 1;
610
  }
611
612
  res = getpass("");
613
  strbuf_reset(buf);
614
  if (!res)
615
    return EOF;
616
  strbuf_addstr(buf, res);
617
  return 0;
618
}
619
620
#endif