From 596cd5c95d2a85df1598a72cd211104beb7cf19f Mon Sep 17 00:00:00 2001 From: "iannucci@chromium.org" Date: Mon, 4 Apr 2016 21:34:39 +0000 Subject: [PATCH] Fix coloring madness in depot_tools. 'setup_color' now contains logic to correctly detect: * cmd * cmd pipe * msys bash * msys pipe * cmd running inside msys bash (git-command) * cmd pipe running inside msys bash (git-command > outfile) R=brucedawson@chromium.org, dnj@chromium.org BUG=600049 Review URL: https://codereview.chromium.org/1851283002 git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@299682 0039d316-1c4b-4281-b951-d872f2087c98 --- depot-tools-auth.py | 5 +-- gclient.py | 6 +-- git_cl.py | 7 ++-- git_common.py | 5 ++- git_map_branches.py | 11 +++--- setup_color.py | 94 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 112 insertions(+), 16 deletions(-) create mode 100755 setup_color.py diff --git a/depot-tools-auth.py b/depot-tools-auth.py index 87ec1ccf0..853eef4cf 100755 --- a/depot-tools-auth.py +++ b/depot-tools-auth.py @@ -16,9 +16,8 @@ import optparse import sys import os -from third_party import colorama - import auth +import setup_color import subcommand __version__ = '1.0' @@ -95,7 +94,7 @@ def main(argv): if __name__ == '__main__': - colorama.init(wrap="TERM" not in os.environ) + setup_color.init() try: sys.exit(main(sys.argv[1:])) except KeyboardInterrupt: diff --git a/gclient.py b/gclient.py index aae6783f4..5a40321f1 100755 --- a/gclient.py +++ b/gclient.py @@ -102,7 +102,7 @@ import git_cache from third_party.repo.progress import Progress import subcommand import subprocess2 -from third_party import colorama +import setup_color CHROMIUM_SRC_URL = 'https://chromium.googlesource.com/chromium/src.git' @@ -1511,7 +1511,7 @@ been automagically updated. The previous version is available at %s.old. revision_overrides = self._EnforceRevisions() pm = None # Disable progress for non-tty stdout. - if (sys.stdout.isatty() and not self._options.verbose and progress): + if (setup_color.IS_TTY and not self._options.verbose and progress): if command in ('update', 'revert'): pm = Progress('Syncing projects', 1) elif command == 'recurse': @@ -2303,7 +2303,7 @@ def main(argv): return 2 fix_encoding.fix_encoding() disable_buffering() - colorama.init(wrap="TERM" not in os.environ) + setup_color.init() dispatcher = subcommand.CommandDispatcher(__name__) try: return dispatcher.execute(OptionParser(), argv) diff --git a/git_cl.py b/git_cl.py index 4c6bb10ce..49891fcf5 100755 --- a/git_cl.py +++ b/git_cl.py @@ -45,6 +45,7 @@ from luci_hacks import trigger_luci_job as luci_trigger import clang_format import commit_queue import dart_format +import setup_color import fix_encoding import gclient_utils import gerrit_util @@ -2481,7 +2482,7 @@ def CMDstatus(parser, args): issue_url, status = branch_statuses.pop(branch) color = color_for_status(status) reset = Fore.RESET - if not sys.stdout.isatty(): + if not setup_color.IS_TTY: color = '' reset = '' status_str = '(%s)' % status if status else '' @@ -4181,7 +4182,7 @@ def CMDtry_results(parser, args): group.add_option( "--print-master", action='store_true', help="print master name as well.") group.add_option( - "--color", action='store_true', default=sys.stdout.isatty(), + "--color", action='store_true', default=setup_color.IS_TTY, help="force color output, useful when piping output.") group.add_option( "--buildbucket-host", default='cr-buildbucket.appspot.com', @@ -4615,7 +4616,7 @@ if __name__ == '__main__': # These affect sys.stdout so do it outside of main() to simplify mocks in # unit testing. fix_encoding.fix_encoding() - colorama.init(wrap="TERM" not in os.environ) + setup_color.init() try: sys.exit(main(sys.argv[1:])) except KeyboardInterrupt: diff --git a/git_common.py b/git_common.py index 949ba4695..bef628e40 100644 --- a/git_common.py +++ b/git_common.py @@ -22,6 +22,7 @@ import functools import logging import os import re +import setup_color import shutil import signal import sys @@ -281,7 +282,7 @@ def once(function): ## Git functions -def blame(filename, revision=None, porcelain=False, *args): +def blame(filename, revision=None, porcelain=False, *_args): command = ['blame'] if porcelain: command.append('-p') @@ -576,7 +577,7 @@ def less(): # pragma: no cover Automatically checks if sys.stdout is a non-TTY stream. If so, it avoids running less and just yields sys.stdout. """ - if not sys.stdout.isatty(): + if not setup_color.IS_TTY: yield sys.stdout return diff --git a/git_map_branches.py b/git_map_branches.py index 50b403d4c..016a0330f 100755 --- a/git_map_branches.py +++ b/git_map_branches.py @@ -28,16 +28,17 @@ Branches are colorized as follows: import argparse import collections import os -import sys import subprocess2 - -from third_party import colorama -from third_party.colorama import Fore, Style +import sys from git_common import current_branch, upstream, tags, get_branches_info from git_common import get_git_version, MIN_UPSTREAM_TRACK_GIT_VERSION, hash_one from git_common import run +import setup_color + +from third_party.colorama import Fore, Style + DEFAULT_SEPARATOR = ' ' * 4 @@ -272,7 +273,7 @@ class BranchMapper(object): def main(argv): - colorama.init(wrap="TERM" not in os.environ) + setup_color.init() if get_git_version() < MIN_UPSTREAM_TRACK_GIT_VERSION: print >> sys.stderr, ( 'This tool will not show all tracking information for git version ' diff --git a/setup_color.py b/setup_color.py new file mode 100755 index 000000000..5baa88014 --- /dev/null +++ b/setup_color.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python +# Copyright (c) 2016 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import os +import sys +from third_party import colorama + +IS_TTY = None +OUT_TYPE = 'unknown' + +def init(): + # should_wrap instructs colorama to wrap stdout/stderr with an ASNI colorcode + # interpreter that converts them to SetConsoleTextAttribute calls. This only + # should be True in cases where we're connected to cmd.exe's console. Setting + # this to True on non-windows systems has no effect. + should_wrap = False + global IS_TTY, OUT_TYPE + IS_TTY = sys.stdout.isatty() + if IS_TTY: + # Yay! We detected a console in the normal way. It doesn't really matter + # if it's windows or not, we win. + OUT_TYPE = 'console' + should_wrap = True + elif sys.platform.startswith('win'): + # assume this is some sort of file + OUT_TYPE = 'file (win)' + + import msvcrt + import ctypes + h = msvcrt.get_osfhandle(sys.stdout.fileno()) + # h is the win32 HANDLE for stdout. + ftype = ctypes.windll.kernel32.GetFileType(h) + if ftype == 2: # FILE_TYPE_CHAR + # This is a normal cmd console, but we'll only get here if we're running + # inside a `git command` which is actually git->bash->command. Not sure + # why isatty doesn't detect this case. + OUT_TYPE = 'console (cmd via msys)' + IS_TTY = True + should_wrap = True + elif ftype == 3: # FILE_TYPE_PIPE + OUT_TYPE = 'pipe (win)' + # This is some kind of pipe on windows. This could either be a real pipe + # or this could be msys using a pipe to emulate a pty. We use the same + # algorithm that msys-git uses to determine if it's connected to a pty or + # not. + + # This function and the structures are defined in the MSDN documentation + # using the same names. + def NT_SUCCESS(status): + # The first two bits of status are the severity. The success + # severities are 0 and 1, and the !success severities are 2 and 3. + # Therefore since ctypes interprets the default restype of the call + # to be an 'C int' (which is guaranteed to be signed 32 bits), All + # success codes are positive, and all !success codes are negative. + return status >= 0 + + class UNICODE_STRING(ctypes.Structure): + _fields_ = [('Length', ctypes.c_ushort), + ('MaximumLength', ctypes.c_ushort), + ('Buffer', ctypes.c_wchar_p)] + + class OBJECT_NAME_INFORMATION(ctypes.Structure): + _fields_ = [('Name', UNICODE_STRING), + ('NameBuffer', ctypes.c_wchar_p)] + + buf = ctypes.create_string_buffer('\0', 1024) + # Ask NT what the name of the object our stdout HANDLE is. It would be + # possible to use GetFileInformationByHandleEx, but it's only available + # on Vista+. If you're reading this in 2017 or later, feel free to + # refactor this out. + # + # The '1' here is ObjectNameInformation + if NT_SUCCESS(ctypes.windll.ntdll.NtQueryObject(h, 1, buf, len(buf)-2, + None)): + out = OBJECT_NAME_INFORMATION.from_buffer(buf) + name = out.Name.Buffer.split('\\')[-1] + IS_TTY = name.startswith('msys-') and '-pty' in name + if IS_TTY: + OUT_TYPE = 'bash (msys)' + else: + # A normal file, or an unknown file type. + pass + else: + # This is non-windows, so we trust isatty. + OUT_TYPE = 'pipe or file' + + colorama.init(wrap=should_wrap) + +if __name__ == '__main__': + init() + print 'IS_TTY:', IS_TTY + print 'OUT_TYPE:', OUT_TYPE