Win32: Thread-safe windows console output

Winansi.c has many static variables that are accessed and modified from the [v][f]printf / fputs functions overridden in the file. This may cause multi threaded git commands that print to the console to produce corrupted output or even crash. Additionally, winansi.c doesn't override all functions that can be used to print to the console (e.g. fwrite, write, fputc are missing), so that ANSI escapes don't work properly for some git commands (e.g. git-grep). Instead of doing ANSI emulation in just a few wrapped functions on top of the IO API, let's plug into the IO system and take advantage of the thread safety inherent to the IO system. Redirect stdout and stderr to a pipe if they point to the console. A background thread reads from the pipe, handles ANSI escape sequences and UTF-8 to UTF-16 conversion, then writes to the console. The pipe-based stdout and stderr replacements must be set to unbuffered, as MSVCRT doesn't support line buffering and fully buffered streams are inappropriate for console output. Due to the byte-oriented pipe, ANSI escape sequences and multi-byte UTF-8 sequences can no longer be expected to arrive in one piece. Replace the string-based ansi_emulate() with a simple stateful parser (this also fixes colored diff hunk headers, which were broken as of commit 2efcc977). Override isatty to return true for the pipes redirecting to the console. Exec/spawn obtain the original console handle to pass to the next process via winansi_get_osfhandle(). All other overrides are gone, the default stdio implementations work as expected with the piped stdout/stderr descriptors. Global variables are either initialized on startup (single threaded) or exclusively modified by the background thread. Threads communicate through the pipe, no further synchronization is necessary. The background thread is terminated by disonnecting the pipe after flushing the stdio and pipe buffers. This doesn't work for anonymous pipes (created via CreatePipe), as DisconnectNamedPipe only works on the read end, which discards remaining data. Thus we have to setup the pipe manually, with the write end beeing the server (opened with CreateNamedPipe) and the read end the client (opened with CreateFile). Limitations: doesn't track reopened or duped file descriptors, i.e.: - fdopen(1/2) returns fully buffered streams - dup(1/2), dup2(1/2) returns normal pipe descriptors (i.e. isatty() = false, winansi_get_osfhandle won't return the original console handle) Currently, only the git-format-patch command uses xfdopen(xdup(1)) (see "realstdout" in builtin/log.c), but works well with these limitations. Many thanks to Atsushi Nakagawa <atnak@chejz.com> for suggesting and reviewing the thread-exit-mechanism. Signed-off-by: Karsten Blees <blees@dcon.de> Signed-off-by: Stepan Kasal <kasal@ucw.cz> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Karsten Blees committed Jan 14, 2012 at 22:24 UTC eac14f8909d986a38d9eab4a7f926af41dd64a4f
3 files changed +273 -149
compat/mingw.c
+6 -3
@@ -865,9 +865,9 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **env,
865 memset(&si, 0, sizeof(si));
866 si.cb = sizeof(si);
867 si.dwFlags = STARTF_USESTDHANDLES;
868 - si.hStdInput = (HANDLE) _get_osfhandle(fhin);
869 - si.hStdOutput = (HANDLE) _get_osfhandle(fhout);
870 - si.hStdError = (HANDLE) _get_osfhandle(fherr);
868 + si.hStdInput = winansi_get_osfhandle(fhin);
869 + si.hStdOutput = winansi_get_osfhandle(fhout);
870 + si.hStdError = winansi_get_osfhandle(fherr);
871
872 /* concatenate argv, quoting args as we go */
873 strbuf_init(&args, 0);
@@ -1946,4 +1946,7 @@ void mingw_startup()
1946 _setmode(_fileno(stdin), _O_BINARY);
1947 _setmode(_fileno(stdout), _O_BINARY);
1948 _setmode(_fileno(stderr), _O_BINARY);
1949 +
1950 + /* initialize Unicode console */
1951 + winansi_init();
1952 }
compat/mingw.h
+4 -8
@@ -317,14 +317,10 @@ int mingw_raise(int sig);
317 * ANSI emulation wrappers
318 */
319
320 -int winansi_fputs(const char *str, FILE *stream);
321 -int winansi_printf(const char *format, ...) __attribute__((format (printf, 1, 2)));
322 -int winansi_fprintf(FILE *stream, const char *format, ...) __attribute__((format (printf, 2, 3)));
323 -int winansi_vfprintf(FILE *stream, const char *format, va_list list);
324 -#define fputs winansi_fputs
325 -#define printf(...) winansi_printf(__VA_ARGS__)
326 -#define fprintf(...) winansi_fprintf(__VA_ARGS__)
327 -#define vfprintf winansi_vfprintf
320 +void winansi_init(void);
321 +int winansi_isatty(int fd);
322 +HANDLE winansi_get_osfhandle(int fd);
323 +#define isatty winansi_isatty
324
325 /*
326 * git specific compatibility
compat/winansi.c
+263 -138
@@ -4,18 +4,13 @@
4
5 #undef NOGDI
6 #include "../git-compat-util.h"
7 -#include <malloc.h>
7 #include <wingdi.h>
8 #include <winreg.h>
9
10 /*
11 Functions to be wrapped:
12 */
14 -#undef printf
15 -#undef fprintf
16 -#undef fputs
17 -#undef vfprintf
18 -/* TODO: write */
13 +#undef isatty
14
15 /*
16 ANSI codes used by git: m, K
@@ -28,7 +23,10 @@ static HANDLE console;
23 static WORD plain_attr;
24 static WORD attr;
25 static int negative;
31 -static FILE *last_stream = NULL;
26 +static int non_ascii_used = 0;
27 +static HANDLE hthread, hread, hwrite;
28 +static HANDLE hwrite1 = INVALID_HANDLE_VALUE, hwrite2 = INVALID_HANDLE_VALUE;
29 +static HANDLE hconsole1, hconsole2;
30
31 #ifdef __MINGW32__
32 typedef struct _CONSOLE_FONT_INFOEX {
@@ -44,27 +42,19 @@ typedef struct _CONSOLE_FONT_INFOEX {
42 typedef BOOL (WINAPI *PGETCURRENTCONSOLEFONTEX)(HANDLE, BOOL,
43 PCONSOLE_FONT_INFOEX);
44
47 -static void print_font_warning(void)
45 +static void warn_if_raster_font(void)
46 {
49 - warning("Your console font probably doesn\'t support Unicode. If "
50 - "you experience strange characters in the output, consider "
51 - "switching to a TrueType font such as Lucida Console!");
52 -}
53 -
54 -static void check_truetype_font(void)
55 -{
56 - static int truetype_font_checked;
47 DWORD fontFamily = 0;
48 PGETCURRENTCONSOLEFONTEX pGetCurrentConsoleFontEx;
49
60 - /* don't do this twice */
61 - if (truetype_font_checked)
50 + /* don't bother if output was ascii only */
51 + if (!non_ascii_used)
52 return;
63 - truetype_font_checked = 1;
53
54 /* GetCurrentConsoleFontEx is available since Vista */
55 pGetCurrentConsoleFontEx = (PGETCURRENTCONSOLEFONTEX) GetProcAddress(
67 - GetModuleHandle("kernel32.dll"), "GetCurrentConsoleFontEx");
56 + GetModuleHandle("kernel32.dll"),
57 + "GetCurrentConsoleFontEx");
58 if (pGetCurrentConsoleFontEx) {
59 CONSOLE_FONT_INFOEX cfi;
60 cfi.cbSize = sizeof(cfi);
@@ -73,8 +63,8 @@ static void check_truetype_font(void)
63 } else {
64 /* pre-Vista: check default console font in registry */
65 HKEY hkey;
76 - if (ERROR_SUCCESS == RegOpenKeyExA(HKEY_CURRENT_USER, "Console", 0,
77 - KEY_READ, &hkey)) {
66 + if (ERROR_SUCCESS == RegOpenKeyExA(HKEY_CURRENT_USER, "Console",
67 + 0, KEY_READ, &hkey)) {
68 DWORD size = sizeof(fontFamily);
69 RegQueryValueExA(hkey, "FontFamily", NULL, NULL,
70 (LPVOID) &fontFamily, &size);
@@ -82,61 +72,64 @@ static void check_truetype_font(void)
72 }
73 }
74
85 - if (!(fontFamily & TMPF_TRUETYPE))
86 - atexit(print_font_warning);
75 + if (!(fontFamily & TMPF_TRUETYPE)) {
76 + const wchar_t *msg = L"\nWarning: Your console font probably "
77 + L"doesn\'t support Unicode. If you experience strange "
78 + L"characters in the output, consider switching to a "
79 + L"TrueType font such as Consolas!\n";
80 + DWORD dummy;
81 + WriteConsoleW(console, msg, wcslen(msg), &dummy, NULL);
82 + }
83 }
84
89 -static int is_console(FILE *stream)
85 +static int is_console(int fd)
86 {
87 CONSOLE_SCREEN_BUFFER_INFO sbi;
88 HANDLE hcon;
89
90 static int initialized = 0;
91
96 - /* use cached value if stream hasn't changed */
97 - if (stream == last_stream)
98 - return console != NULL;
99 -
100 - last_stream = stream;
101 - console = NULL;
102 -
103 - /* get OS handle of the stream */
104 - hcon = (HANDLE) _get_osfhandle(_fileno(stream));
92 + /* get OS handle of the file descriptor */
93 + hcon = (HANDLE) _get_osfhandle(fd);
94 if (hcon == INVALID_HANDLE_VALUE)
95 return 0;
96
97 + /* check if its a device (i.e. console, printer, serial port) */
98 + if (GetFileType(hcon) != FILE_TYPE_CHAR)
99 + return 0;
100 +
101 /* check if its a handle to a console output screen buffer */
102 if (!GetConsoleScreenBufferInfo(hcon, &sbi))
103 return 0;
104
105 + /* initialize attributes */
106 if (!initialized) {
107 attr = plain_attr = sbi.wAttributes;
108 negative = 0;
109 initialized = 1;
110 }
111
118 - console = hcon;
112 return 1;
113 }
114
122 -static int write_console(const char *str, size_t len)
115 +#define BUFFER_SIZE 4096
116 +#define MAX_PARAMS 16
117 +
118 +static void write_console(unsigned char *str, size_t len)
119 {
124 - /* convert utf-8 to utf-16, write directly to console */
125 - int wlen = MultiByteToWideChar(CP_UTF8, 0, str, len, NULL, 0);
126 - wchar_t *wbuf = (wchar_t *) alloca(wlen * sizeof(wchar_t));
127 - MultiByteToWideChar(CP_UTF8, 0, str, len, wbuf, wlen);
120 + /* only called from console_thread, so a static buffer will do */
121 + static wchar_t wbuf[2 * BUFFER_SIZE + 1];
122 + DWORD dummy;
123
129 - WriteConsoleW(console, wbuf, wlen, NULL, NULL);
124 + /* convert utf-8 to utf-16 */
125 + int wlen = xutftowcsn(wbuf, (char*) str, ARRAY_SIZE(wbuf), len);
126
131 - /*
132 - * if non-ascii characters are printed, check that the current console
133 - * font supports this
134 - */
135 - if (wlen != len)
136 - check_truetype_font();
127 + /* write directly to console */
128 + WriteConsoleW(console, wbuf, wlen, &dummy, NULL);
129
138 - /* return original (utf-8 encoded) length */
139 - return len;
130 + /* remember if non-ascii characters are printed */
131 + if (wlen != len)
132 + non_ascii_used = 1;
133 }
134
135 #define FOREGROUND_ALL (FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE)
@@ -182,18 +175,13 @@ static void erase_in_line(void)
175 &dummy);
176 }
177
185 -
186 -static const char *set_attr(const char *str)
178 +static void set_attr(char func, const int *params, int paramlen)
179 {
188 - const char *func;
189 - size_t len = strspn(str, "0123456789;");
190 - func = str + len;
191 -
192 - switch (*func) {
180 + int i;
181 + switch (func) {
182 case 'm':
194 - do {
195 - long val = strtol(str, (char **)&str, 10);
196 - switch (val) {
183 + for (i = 0; i < paramlen; i++) {
184 + switch (params[i]) {
185 case 0: /* reset */
186 attr = plain_attr;
187 negative = 0;
@@ -316,9 +304,7 @@ static const char *set_attr(const char *str)
304 /* Unsupported code */
305 break;
306 }
319 - str++;
320 - } while (*(str-1) == ';');
321 -
307 + }
308 set_console_attr();
309 break;
310 case 'K':
@@ -328,112 +314,251 @@ static const char *set_attr(const char *str)
314 /* Unsupported code */
315 break;
316 }
331 -
332 - return func + 1;
317 }
318
335 -static int ansi_emulate(const char *str, FILE *stream)
336 -{
337 - int rv = 0;
338 - const char *pos = str;
319 +enum {
320 + TEXT = 0, ESCAPE = 033, BRACKET = '['
321 +};
322
340 - fflush(stream);
323 +static DWORD WINAPI console_thread(LPVOID unused)
324 +{
325 + unsigned char buffer[BUFFER_SIZE];
326 + DWORD bytes;
327 + int start, end = 0, c, parampos = 0, state = TEXT;
328 + int params[MAX_PARAMS];
329 +
330 + while (1) {
331 + /* read next chunk of bytes from the pipe */
332 + if (!ReadFile(hread, buffer + end, BUFFER_SIZE - end, &bytes,
333 + NULL)) {
334 + /* exit if pipe has been closed or disconnected */
335 + if (GetLastError() == ERROR_PIPE_NOT_CONNECTED ||
336 + GetLastError() == ERROR_BROKEN_PIPE)
337 + break;
338 + /* ignore other errors */
339 + continue;
340 + }
341
342 - while (*pos) {
343 - pos = strstr(str, "\033[");
344 - if (pos) {
345 - size_t len = pos - str;
342 + /* scan the bytes and handle ANSI control codes */
343 + bytes += end;
344 + start = end = 0;
345 + while (end < bytes) {
346 + c = buffer[end++];
347 + switch (state) {
348 + case TEXT:
349 + if (c == ESCAPE) {
350 + /* print text seen so far */
351 + if (end - 1 > start)
352 + write_console(buffer + start,
353 + end - 1 - start);
354 +
355 + /* then start parsing escape sequence */
356 + start = end - 1;
357 + memset(params, 0, sizeof(params));
358 + parampos = 0;
359 + state = ESCAPE;
360 + }
361 + break;
362 +
363 + case ESCAPE:
364 + /* continue if "\033[", otherwise bail out */
365 + state = (c == BRACKET) ? BRACKET : TEXT;
366 + break;
367 +
368 + case BRACKET:
369 + /* parse [0-9;]* into array of parameters */
370 + if (c >= '0' && c <= '9') {
371 + params[parampos] *= 10;
372 + params[parampos] += c - '0';
373 + } else if (c == ';') {
374 + /*
375 + * next parameter, bail out if out of
376 + * bounds
377 + */
378 + parampos++;
379 + if (parampos >= MAX_PARAMS)
380 + state = TEXT;
381 + } else {
382 + /*
383 + * end of escape sequence, change
384 + * console attributes
385 + */
386 + set_attr(c, params, parampos + 1);
387 + start = end;
388 + state = TEXT;
389 + }
390 + break;
391 + }
392 + }
393
347 - if (len) {
348 - size_t out_len = write_console(str, len);
349 - rv += out_len;
350 - if (out_len < len)
351 - return rv;
394 + /* print remaining text unless parsing an escape sequence */
395 + if (state == TEXT && end > start) {
396 + /* check for incomplete UTF-8 sequences and fix end */
397 + if (buffer[end - 1] >= 0x80) {
398 + if (buffer[end -1] >= 0xc0)
399 + end--;
400 + else if (end - 1 > start &&
401 + buffer[end - 2] >= 0xe0)
402 + end -= 2;
403 + else if (end - 2 > start &&
404 + buffer[end - 3] >= 0xf0)
405 + end -= 3;
406 }
407
354 - str = pos + 2;
355 - rv += 2;
408 + /* print remaining complete UTF-8 sequences */
409 + if (end > start)
410 + write_console(buffer + start, end - start);
411
357 - pos = set_attr(str);
358 - rv += pos - str;
359 - str = pos;
412 + /* move remaining bytes to the front */
413 + if (end < bytes)
414 + memmove(buffer, buffer + end, bytes - end);
415 + end = bytes - end;
416 } else {
361 - size_t len = strlen(str);
362 - rv += write_console(str, len);
363 - return rv;
417 + /* all data has been consumed, mark buffer empty */
418 + end = 0;
419 }
420 }
366 - return rv;
367 -}
368 -
369 -int winansi_fputs(const char *str, FILE *stream)
370 -{
371 - int rv;
372 -
373 - if (!is_console(stream))
374 - return fputs(str, stream);
421
376 - rv = ansi_emulate(str, stream);
422 + /* check if the console font supports unicode */
423 + warn_if_raster_font();
424
378 - if (rv >= 0)
379 - return 0;
380 - else
381 - return EOF;
425 + CloseHandle(hread);
426 + return 0;
427 }
428
384 -int winansi_vfprintf(FILE *stream, const char *format, va_list list)
429 +static void winansi_exit(void)
430 {
386 - int len, rv;
387 - char small_buf[256];
388 - char *buf = small_buf;
389 - va_list cp;
390 -
391 - if (!is_console(stream))
392 - goto abort;
431 + /* flush all streams */
432 + _flushall();
433 +
434 + /* signal console thread to exit */
435 + FlushFileBuffers(hwrite);
436 + DisconnectNamedPipe(hwrite);
437 +
438 + /* wait for console thread to copy remaining data */
439 + WaitForSingleObject(hthread, INFINITE);
440 +
441 + /* cleanup handles... */
442 + if (hwrite1 != INVALID_HANDLE_VALUE)
443 + CloseHandle(hwrite1);
444 + if (hwrite2 != INVALID_HANDLE_VALUE)
445 + CloseHandle(hwrite2);
446 + CloseHandle(hwrite);
447 + CloseHandle(hthread);
448 +}
449
394 - va_copy(cp, list);
395 - len = vsnprintf(small_buf, sizeof(small_buf), format, cp);
396 - va_end(cp);
450 +static void die_lasterr(const char *fmt, ...)
451 +{
452 + va_list params;
453 + va_start(params, fmt);
454 + errno = err_win_to_posix(GetLastError());
455 + die_errno(fmt, params);
456 + va_end(params);
457 +}
458
398 - if (len > sizeof(small_buf) - 1) {
399 - buf = malloc(len + 1);
400 - if (!buf)
401 - goto abort;
459 +static HANDLE duplicate_handle(HANDLE hnd)
460 +{
461 + HANDLE hresult, hproc = GetCurrentProcess();
462 + if (!DuplicateHandle(hproc, hnd, hproc, &hresult, 0, TRUE,
463 + DUPLICATE_SAME_ACCESS))
464 + die_lasterr("DuplicateHandle(%li) failed", (long) hnd);
465 + return hresult;
466 +}
467
403 - len = vsnprintf(buf, len + 1, format, list);
404 - }
468 +static HANDLE redirect_console(FILE *stream, HANDLE *phcon, int new_fd)
469 +{
470 + /* get original console handle */
471 + int fd = _fileno(stream);
472 + HANDLE hcon = (HANDLE) _get_osfhandle(fd);
473 + if (hcon == INVALID_HANDLE_VALUE)
474 + die_errno("_get_osfhandle(%i) failed", fd);
475
406 - rv = ansi_emulate(buf, stream);
476 + /* save a copy to phcon and console (used by the background thread) */
477 + console = *phcon = duplicate_handle(hcon);
478
408 - if (buf != small_buf)
409 - free(buf);
410 - return rv;
479 + /* duplicate new_fd over fd (closes fd and associated handle (hcon)) */
480 + if (_dup2(new_fd, fd))
481 + die_errno("_dup2(%i, %i) failed", new_fd, fd);
482
412 -abort:
413 - rv = vfprintf(stream, format, list);
414 - return rv;
483 + /* no buffering, or stdout / stderr will be out of sync */
484 + setbuf(stream, NULL);
485 + return (HANDLE) _get_osfhandle(fd);
486 }
487
417 -int winansi_fprintf(FILE *stream, const char *format, ...)
488 +void winansi_init(void)
489 {
419 - va_list list;
420 - int rv;
490 + int con1, con2, hwrite_fd;
491 + char name[32];
492
422 - va_start(list, format);
423 - rv = winansi_vfprintf(stream, format, list);
424 - va_end(list);
493 + /* check if either stdout or stderr is a console output screen buffer */
494 + con1 = is_console(1);
495 + con2 = is_console(2);
496 + if (!con1 && !con2)
497 + return;
498
426 - return rv;
499 + /* create a named pipe to communicate with the console thread */
500 + sprintf(name, "\\\\.\\pipe\\winansi%lu", GetCurrentProcessId());
501 + hwrite = CreateNamedPipe(name, PIPE_ACCESS_OUTBOUND,
502 + PIPE_TYPE_BYTE | PIPE_WAIT, 1, BUFFER_SIZE, 0, 0, NULL);
503 + if (hwrite == INVALID_HANDLE_VALUE)
504 + die_lasterr("CreateNamedPipe failed");
505 +
506 + hread = CreateFile(name, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
507 + if (hread == INVALID_HANDLE_VALUE)
508 + die_lasterr("CreateFile for named pipe failed");
509 +
510 + /* start console spool thread on the pipe's read end */
511 + hthread = CreateThread(NULL, 0, console_thread, NULL, 0, NULL);
512 + if (hthread == INVALID_HANDLE_VALUE)
513 + die_lasterr("CreateThread(console_thread) failed");
514 +
515 + /* schedule cleanup routine */
516 + if (atexit(winansi_exit))
517 + die_errno("atexit(winansi_exit) failed");
518 +
519 + /* create a file descriptor for the write end of the pipe */
520 + hwrite_fd = _open_osfhandle((long) duplicate_handle(hwrite), _O_BINARY);
521 + if (hwrite_fd == -1)
522 + die_errno("_open_osfhandle(%li) failed", (long) hwrite);
523 +
524 + /* redirect stdout / stderr to the pipe */
525 + if (con1)
526 + hwrite1 = redirect_console(stdout, &hconsole1, hwrite_fd);
527 + if (con2)
528 + hwrite2 = redirect_console(stderr, &hconsole2, hwrite_fd);
529 +
530 + /* close pipe file descriptor (also closes the duped hwrite) */
531 + close(hwrite_fd);
532 }
533
429 -int winansi_printf(const char *format, ...)
534 +static int is_same_handle(HANDLE hnd, int fd)
535 {
431 - va_list list;
432 - int rv;
536 + return hnd != INVALID_HANDLE_VALUE && hnd == (HANDLE) _get_osfhandle(fd);
537 +}
538
434 - va_start(list, format);
435 - rv = winansi_vfprintf(stdout, format, list);
436 - va_end(list);
539 +/*
540 + * Return true if stdout / stderr is a pipe redirecting to the console.
541 + */
542 +int winansi_isatty(int fd)
543 +{
544 + if (fd == 1 && is_same_handle(hwrite1, 1))
545 + return 1;
546 + else if (fd == 2 && is_same_handle(hwrite2, 2))
547 + return 1;
548 + else
549 + return isatty(fd);
550 +}
551
438 - return rv;
552 +/*
553 + * Returns the real console handle if stdout / stderr is a pipe redirecting
554 + * to the console. Allows spawn / exec to pass the console to the next process.
555 + */
556 +HANDLE winansi_get_osfhandle(int fd)
557 +{
558 + if (fd == 1 && is_same_handle(hwrite1, 1))
559 + return hconsole1;
560 + else if (fd == 2 && is_same_handle(hwrite2, 2))
561 + return hconsole2;
562 + else
563 + return (HANDLE) _get_osfhandle(fd);
564 }