Sync with 'maint'
[gitweb.git] / compat / winansi.c
index dedce2104eaf5cefbb1abef1b7921eb99c67a75e..efc5bb3a4b63166eccb33f6ec6a8ad57e4c9ac36 100644 (file)
@@ -2,15 +2,10 @@
  * Copyright 2008 Peter Harris <git@peter.is-a-geek.org>
  */
 
+#undef NOGDI
 #include "../git-compat-util.h"
-
-/*
- Functions to be wrapped:
-*/
-#undef printf
-#undef fprintf
-#undef fputs
-/* TODO: write */
+#include <wingdi.h>
+#include <winreg.h>
 
 /*
  ANSI codes used by git: m, K
@@ -23,29 +18,114 @@ static HANDLE console;
 static WORD plain_attr;
 static WORD attr;
 static int negative;
+static int non_ascii_used = 0;
+static HANDLE hthread, hread, hwrite;
+static HANDLE hconsole1, hconsole2;
+
+#ifdef __MINGW32__
+typedef struct _CONSOLE_FONT_INFOEX {
+       ULONG cbSize;
+       DWORD nFont;
+       COORD dwFontSize;
+       UINT FontFamily;
+       UINT FontWeight;
+       WCHAR FaceName[LF_FACESIZE];
+} CONSOLE_FONT_INFOEX, *PCONSOLE_FONT_INFOEX;
+#endif
+
+typedef BOOL (WINAPI *PGETCURRENTCONSOLEFONTEX)(HANDLE, BOOL,
+               PCONSOLE_FONT_INFOEX);
+
+static void warn_if_raster_font(void)
+{
+       DWORD fontFamily = 0;
+       PGETCURRENTCONSOLEFONTEX pGetCurrentConsoleFontEx;
+
+       /* don't bother if output was ascii only */
+       if (!non_ascii_used)
+               return;
+
+       /* GetCurrentConsoleFontEx is available since Vista */
+       pGetCurrentConsoleFontEx = (PGETCURRENTCONSOLEFONTEX) GetProcAddress(
+                       GetModuleHandle("kernel32.dll"),
+                       "GetCurrentConsoleFontEx");
+       if (pGetCurrentConsoleFontEx) {
+               CONSOLE_FONT_INFOEX cfi;
+               cfi.cbSize = sizeof(cfi);
+               if (pGetCurrentConsoleFontEx(console, 0, &cfi))
+                       fontFamily = cfi.FontFamily;
+       } else {
+               /* pre-Vista: check default console font in registry */
+               HKEY hkey;
+               if (ERROR_SUCCESS == RegOpenKeyExA(HKEY_CURRENT_USER, "Console",
+                               0, KEY_READ, &hkey)) {
+                       DWORD size = sizeof(fontFamily);
+                       RegQueryValueExA(hkey, "FontFamily", NULL, NULL,
+                                       (LPVOID) &fontFamily, &size);
+                       RegCloseKey(hkey);
+               }
+       }
+
+       if (!(fontFamily & TMPF_TRUETYPE)) {
+               const wchar_t *msg = L"\nWarning: Your console font probably "
+                       L"doesn\'t support Unicode. If you experience strange "
+                       L"characters in the output, consider switching to a "
+                       L"TrueType font such as Consolas!\n";
+               DWORD dummy;
+               WriteConsoleW(console, msg, wcslen(msg), &dummy, NULL);
+       }
+}
 
-static void init(void)
+static int is_console(int fd)
 {
        CONSOLE_SCREEN_BUFFER_INFO sbi;
+       HANDLE hcon;
 
        static int initialized = 0;
-       if (initialized)
-               return;
 
-       console = GetStdHandle(STD_OUTPUT_HANDLE);
-       if (console == INVALID_HANDLE_VALUE)
-               console = NULL;
+       /* get OS handle of the file descriptor */
+       hcon = (HANDLE) _get_osfhandle(fd);
+       if (hcon == INVALID_HANDLE_VALUE)
+               return 0;
 
-       if (!console)
-               return;
+       /* check if its a device (i.e. console, printer, serial port) */
+       if (GetFileType(hcon) != FILE_TYPE_CHAR)
+               return 0;
 
-       GetConsoleScreenBufferInfo(console, &sbi);
-       attr = plain_attr = sbi.wAttributes;
-       negative = 0;
+       /* check if its a handle to a console output screen buffer */
+       if (!GetConsoleScreenBufferInfo(hcon, &sbi))
+               return 0;
+
+       /* initialize attributes */
+       if (!initialized) {
+               console = hcon;
+               attr = plain_attr = sbi.wAttributes;
+               negative = 0;
+               initialized = 1;
+       }
 
-       initialized = 1;
+       return 1;
 }
 
+#define BUFFER_SIZE 4096
+#define MAX_PARAMS 16
+
+static void write_console(unsigned char *str, size_t len)
+{
+       /* only called from console_thread, so a static buffer will do */
+       static wchar_t wbuf[2 * BUFFER_SIZE + 1];
+       DWORD dummy;
+
+       /* convert utf-8 to utf-16 */
+       int wlen = xutftowcsn(wbuf, (char*) str, ARRAY_SIZE(wbuf), len);
+
+       /* write directly to console */
+       WriteConsoleW(console, wbuf, wlen, &dummy, NULL);
+
+       /* remember if non-ascii characters are printed */
+       if (wlen != len)
+               non_ascii_used = 1;
+}
 
 #define FOREGROUND_ALL (FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE)
 #define BACKGROUND_ALL (BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE)
@@ -90,18 +170,13 @@ static void erase_in_line(void)
                &dummy);
 }
 
-
-static const char *set_attr(const char *str)
+static void set_attr(char func, const int *params, int paramlen)
 {
-       const char *func;
-       size_t len = strspn(str, "0123456789;");
-       func = str + len;
-
-       switch (*func) {
+       int i;
+       switch (func) {
        case 'm':
-               do {
-                       long val = strtol(str, (char **)&str, 10);
-                       switch (val) {
+               for (i = 0; i < paramlen; i++) {
+                       switch (params[i]) {
                        case 0: /* reset */
                                attr = plain_attr;
                                negative = 0;
@@ -224,9 +299,7 @@ static const char *set_attr(const char *str)
                                /* Unsupported code */
                                break;
                        }
-                       str++;
-               } while (*(str-1) == ';');
-
+               }
                set_console_attr();
                break;
        case 'K':
@@ -236,122 +309,271 @@ static const char *set_attr(const char *str)
                /* Unsupported code */
                break;
        }
-
-       return func + 1;
 }
 
-static int ansi_emulate(const char *str, FILE *stream)
+enum {
+       TEXT = 0, ESCAPE = 033, BRACKET = '['
+};
+
+static DWORD WINAPI console_thread(LPVOID unused)
 {
-       int rv = 0;
-       const char *pos = str;
-
-       while (*pos) {
-               pos = strstr(str, "\033[");
-               if (pos) {
-                       size_t len = pos - str;
-
-                       if (len) {
-                               size_t out_len = fwrite(str, 1, len, stream);
-                               rv += out_len;
-                               if (out_len < len)
-                                       return rv;
+       unsigned char buffer[BUFFER_SIZE];
+       DWORD bytes;
+       int start, end = 0, c, parampos = 0, state = TEXT;
+       int params[MAX_PARAMS];
+
+       while (1) {
+               /* read next chunk of bytes from the pipe */
+               if (!ReadFile(hread, buffer + end, BUFFER_SIZE - end, &bytes,
+                               NULL)) {
+                       /* exit if pipe has been closed or disconnected */
+                       if (GetLastError() == ERROR_PIPE_NOT_CONNECTED ||
+                                       GetLastError() == ERROR_BROKEN_PIPE)
+                               break;
+                       /* ignore other errors */
+                       continue;
+               }
+
+               /* scan the bytes and handle ANSI control codes */
+               bytes += end;
+               start = end = 0;
+               while (end < bytes) {
+                       c = buffer[end++];
+                       switch (state) {
+                       case TEXT:
+                               if (c == ESCAPE) {
+                                       /* print text seen so far */
+                                       if (end - 1 > start)
+                                               write_console(buffer + start,
+                                                       end - 1 - start);
+
+                                       /* then start parsing escape sequence */
+                                       start = end - 1;
+                                       memset(params, 0, sizeof(params));
+                                       parampos = 0;
+                                       state = ESCAPE;
+                               }
+                               break;
+
+                       case ESCAPE:
+                               /* continue if "\033[", otherwise bail out */
+                               state = (c == BRACKET) ? BRACKET : TEXT;
+                               break;
+
+                       case BRACKET:
+                               /* parse [0-9;]* into array of parameters */
+                               if (c >= '0' && c <= '9') {
+                                       params[parampos] *= 10;
+                                       params[parampos] += c - '0';
+                               } else if (c == ';') {
+                                       /*
+                                        * next parameter, bail out if out of
+                                        * bounds
+                                        */
+                                       parampos++;
+                                       if (parampos >= MAX_PARAMS)
+                                               state = TEXT;
+                               } else {
+                                       /*
+                                        * end of escape sequence, change
+                                        * console attributes
+                                        */
+                                       set_attr(c, params, parampos + 1);
+                                       start = end;
+                                       state = TEXT;
+                               }
+                               break;
                        }
+               }
 
-                       str = pos + 2;
-                       rv += 2;
+               /* print remaining text unless parsing an escape sequence */
+               if (state == TEXT && end > start) {
+                       /* check for incomplete UTF-8 sequences and fix end */
+                       if (buffer[end - 1] >= 0x80) {
+                               if (buffer[end -1] >= 0xc0)
+                                       end--;
+                               else if (end - 1 > start &&
+                                               buffer[end - 2] >= 0xe0)
+                                       end -= 2;
+                               else if (end - 2 > start &&
+                                               buffer[end - 3] >= 0xf0)
+                                       end -= 3;
+                       }
 
-                       fflush(stream);
+                       /* print remaining complete UTF-8 sequences */
+                       if (end > start)
+                               write_console(buffer + start, end - start);
 
-                       pos = set_attr(str);
-                       rv += pos - str;
-                       str = pos;
+                       /* move remaining bytes to the front */
+                       if (end < bytes)
+                               memmove(buffer, buffer + end, bytes - end);
+                       end = bytes - end;
                } else {
-                       rv += strlen(str);
-                       fputs(str, stream);
-                       return rv;
+                       /* all data has been consumed, mark buffer empty */
+                       end = 0;
                }
        }
-       return rv;
+
+       /* check if the console font supports unicode */
+       warn_if_raster_font();
+
+       CloseHandle(hread);
+       return 0;
 }
 
-int winansi_fputs(const char *str, FILE *stream)
+static void winansi_exit(void)
 {
-       int rv;
-
-       if (!isatty(fileno(stream)))
-               return fputs(str, stream);
+       /* flush all streams */
+       _flushall();
 
-       init();
+       /* signal console thread to exit */
+       FlushFileBuffers(hwrite);
+       DisconnectNamedPipe(hwrite);
 
-       if (!console)
-               return fputs(str, stream);
+       /* wait for console thread to copy remaining data */
+       WaitForSingleObject(hthread, INFINITE);
 
-       rv = ansi_emulate(str, stream);
+       /* cleanup handles... */
+       CloseHandle(hwrite);
+       CloseHandle(hthread);
+}
 
-       if (rv >= 0)
-               return 0;
-       else
-               return EOF;
+static void die_lasterr(const char *fmt, ...)
+{
+       va_list params;
+       va_start(params, fmt);
+       errno = err_win_to_posix(GetLastError());
+       die_errno(fmt, params);
+       va_end(params);
 }
 
-static int winansi_vfprintf(FILE *stream, const char *format, va_list list)
+static HANDLE duplicate_handle(HANDLE hnd)
 {
-       int len, rv;
-       char small_buf[256];
-       char *buf = small_buf;
-       va_list cp;
+       HANDLE hresult, hproc = GetCurrentProcess();
+       if (!DuplicateHandle(hproc, hnd, hproc, &hresult, 0, TRUE,
+                       DUPLICATE_SAME_ACCESS))
+               die_lasterr("DuplicateHandle(%li) failed", (long) hnd);
+       return hresult;
+}
 
-       if (!isatty(fileno(stream)))
-               goto abort;
 
-       init();
+/*
+ * Make MSVCRT's internal file descriptor control structure accessible
+ * so that we can tweak OS handles and flags directly (we need MSVCRT
+ * to treat our pipe handle as if it were a console).
+ *
+ * We assume that the ioinfo structure (exposed by MSVCRT.dll via
+ * __pioinfo) starts with the OS handle and the flags. The exact size
+ * varies between MSVCRT versions, so we try different sizes until
+ * toggling the FDEV bit of _pioinfo(1)->osflags is reflected in
+ * isatty(1).
+ */
+typedef struct {
+       HANDLE osfhnd;
+       char osflags;
+} ioinfo;
 
-       if (!console)
-               goto abort;
+extern __declspec(dllimport) ioinfo *__pioinfo[];
 
-       va_copy(cp, list);
-       len = vsnprintf(small_buf, sizeof(small_buf), format, cp);
-       va_end(cp);
+static size_t sizeof_ioinfo = 0;
 
-       if (len > sizeof(small_buf) - 1) {
-               buf = malloc(len + 1);
-               if (!buf)
-                       goto abort;
+#define IOINFO_L2E 5
+#define IOINFO_ARRAY_ELTS (1 << IOINFO_L2E)
 
-               len = vsnprintf(buf, len + 1, format, list);
-       }
+#define FDEV  0x40
 
-       rv = ansi_emulate(buf, stream);
+static inline ioinfo* _pioinfo(int fd)
+{
+       return (ioinfo*)((char*)__pioinfo[fd >> IOINFO_L2E] +
+                       (fd & (IOINFO_ARRAY_ELTS - 1)) * sizeof_ioinfo);
+}
 
-       if (buf != small_buf)
-               free(buf);
-       return rv;
+static int init_sizeof_ioinfo()
+{
+       int istty, wastty;
+       /* don't init twice */
+       if (sizeof_ioinfo)
+               return sizeof_ioinfo >= 256;
+
+       sizeof_ioinfo = sizeof(ioinfo);
+       wastty = isatty(1);
+       while (sizeof_ioinfo < 256) {
+               /* toggle FDEV flag, check isatty, then toggle back */
+               _pioinfo(1)->osflags ^= FDEV;
+               istty = isatty(1);
+               _pioinfo(1)->osflags ^= FDEV;
+               /* return if we found the correct size */
+               if (istty != wastty)
+                       return 0;
+               sizeof_ioinfo += sizeof(void*);
+       }
+       error("Tweaking file descriptors doesn't work with this MSVCRT.dll");
+       return 1;
+}
 
-abort:
-       rv = vfprintf(stream, format, list);
-       return rv;
+static HANDLE swap_osfhnd(int fd, HANDLE new_handle)
+{
+       ioinfo *pioinfo;
+       HANDLE old_handle;
+
+       /* init ioinfo size if we haven't done so */
+       if (init_sizeof_ioinfo())
+               return INVALID_HANDLE_VALUE;
+
+       /* get ioinfo pointer and change the handles */
+       pioinfo = _pioinfo(fd);
+       old_handle = pioinfo->osfhnd;
+       pioinfo->osfhnd = new_handle;
+       return old_handle;
 }
 
-int winansi_fprintf(FILE *stream, const char *format, ...)
+void winansi_init(void)
 {
-       va_list list;
-       int rv;
+       int con1, con2;
+       char name[32];
 
-       va_start(list, format);
-       rv = winansi_vfprintf(stream, format, list);
-       va_end(list);
+       /* check if either stdout or stderr is a console output screen buffer */
+       con1 = is_console(1);
+       con2 = is_console(2);
+       if (!con1 && !con2)
+               return;
 
-       return rv;
+       /* create a named pipe to communicate with the console thread */
+       sprintf(name, "\\\\.\\pipe\\winansi%lu", GetCurrentProcessId());
+       hwrite = CreateNamedPipe(name, PIPE_ACCESS_OUTBOUND,
+               PIPE_TYPE_BYTE | PIPE_WAIT, 1, BUFFER_SIZE, 0, 0, NULL);
+       if (hwrite == INVALID_HANDLE_VALUE)
+               die_lasterr("CreateNamedPipe failed");
+
+       hread = CreateFile(name, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
+       if (hread == INVALID_HANDLE_VALUE)
+               die_lasterr("CreateFile for named pipe failed");
+
+       /* start console spool thread on the pipe's read end */
+       hthread = CreateThread(NULL, 0, console_thread, NULL, 0, NULL);
+       if (hthread == INVALID_HANDLE_VALUE)
+               die_lasterr("CreateThread(console_thread) failed");
+
+       /* schedule cleanup routine */
+       if (atexit(winansi_exit))
+               die_errno("atexit(winansi_exit) failed");
+
+       /* redirect stdout / stderr to the pipe */
+       if (con1)
+               hconsole1 = swap_osfhnd(1, duplicate_handle(hwrite));
+       if (con2)
+               hconsole2 = swap_osfhnd(2, duplicate_handle(hwrite));
 }
 
-int winansi_printf(const char *format, ...)
+/*
+ * Returns the real console handle if stdout / stderr is a pipe redirecting
+ * to the console. Allows spawn / exec to pass the console to the next process.
+ */
+HANDLE winansi_get_osfhandle(int fd)
 {
-       va_list list;
-       int rv;
-
-       va_start(list, format);
-       rv = winansi_vfprintf(stdout, format, list);
-       va_end(list);
-
-       return rv;
+       HANDLE hnd = (HANDLE) _get_osfhandle(fd);
+       if ((fd == 1 || fd == 2) && isatty(fd)
+           && GetFileType(hnd) == FILE_TYPE_PIPE)
+               return (fd == 1) ? hconsole1 : hconsole2;
+       return hnd;
 }