diff --git a/lib/puppet/util/colors.rb b/lib/puppet/util/colors.rb index 622c3b583..ba8a3933b 100644 --- a/lib/puppet/util/colors.rb +++ b/lib/puppet/util/colors.rb @@ -1,175 +1,202 @@ require 'puppet/util/platform' module Puppet::Util::Colors BLACK = {:console => "\e[0;30m", :html => "color: #FFA0A0" } RED = {:console => "\e[0;31m", :html => "color: #FFA0A0" } GREEN = {:console => "\e[0;32m", :html => "color: #00CD00" } YELLOW = {:console => "\e[0;33m", :html => "color: #FFFF60" } BLUE = {:console => "\e[0;34m", :html => "color: #80A0FF" } MAGENTA = {:console => "\e[0;35m", :html => "color: #FFA500" } CYAN = {:console => "\e[0;36m", :html => "color: #40FFFF" } WHITE = {:console => "\e[0;37m", :html => "color: #FFFFFF" } HBLACK = {:console => "\e[1;30m", :html => "color: #FFA0A0" } HRED = {:console => "\e[1;31m", :html => "color: #FFA0A0" } HGREEN = {:console => "\e[1;32m", :html => "color: #00CD00" } HYELLOW = {:console => "\e[1;33m", :html => "color: #FFFF60" } HBLUE = {:console => "\e[1;34m", :html => "color: #80A0FF" } HMAGENTA = {:console => "\e[1;35m", :html => "color: #FFA500" } HCYAN = {:console => "\e[1;36m", :html => "color: #40FFFF" } HWHITE = {:console => "\e[1;37m", :html => "color: #FFFFFF" } BG_RED = {:console => "\e[0;41m", :html => "background: #FFA0A0"} BG_GREEN = {:console => "\e[0;42m", :html => "background: #00CD00"} BG_YELLOW = {:console => "\e[0;43m", :html => "background: #FFFF60"} BG_BLUE = {:console => "\e[0;44m", :html => "background: #80A0FF"} BG_MAGENTA = {:console => "\e[0;45m", :html => "background: #FFA500"} BG_CYAN = {:console => "\e[0;46m", :html => "background: #40FFFF"} BG_WHITE = {:console => "\e[0;47m", :html => "background: #FFFFFF"} BG_HRED = {:console => "\e[1;41m", :html => "background: #FFA0A0"} BG_HGREEN = {:console => "\e[1;42m", :html => "background: #00CD00"} BG_HYELLOW = {:console => "\e[1;43m", :html => "background: #FFFF60"} BG_HBLUE = {:console => "\e[1;44m", :html => "background: #80A0FF"} BG_HMAGENTA = {:console => "\e[1;45m", :html => "background: #FFA500"} BG_HCYAN = {:console => "\e[1;46m", :html => "background: #40FFFF"} BG_HWHITE = {:console => "\e[1;47m", :html => "background: #FFFFFF"} RESET = {:console => "\e[0m", :html => "" } Colormap = { :debug => WHITE, :info => GREEN, :notice => CYAN, :warning => YELLOW, :err => HMAGENTA, :alert => RED, :emerg => HRED, :crit => HRED, :black => BLACK, :red => RED, :green => GREEN, :yellow => YELLOW, :blue => BLUE, :magenta => MAGENTA, :cyan => CYAN, :white => WHITE, :hblack => HBLACK, :hred => HRED, :hgreen => HGREEN, :hyellow => HYELLOW, :hblue => HBLUE, :hmagenta => HMAGENTA, :hcyan => HCYAN, :hwhite => HWHITE, :bg_red => BG_RED, :bg_green => BG_GREEN, :bg_yellow => BG_YELLOW, :bg_blue => BG_BLUE, :bg_magenta => BG_MAGENTA, :bg_cyan => BG_CYAN, :bg_white => BG_WHITE, :bg_hred => BG_HRED, :bg_hgreen => BG_HGREEN, :bg_hyellow => BG_HYELLOW, :bg_hblue => BG_HBLUE, :bg_hmagenta => BG_HMAGENTA, :bg_hcyan => BG_HCYAN, :bg_hwhite => BG_HWHITE, :reset => { :console => "\e[m", :html => "" } } # We define console_has_color? at load time since it's checking the # underlying platform which will not change, and we don't want to perform # the check every time we use logging if Puppet::Util::Platform.windows? # We're on windows, need win32console for color to work begin - require 'Win32API' + require 'ffi' require 'win32console' - require 'puppet/util/windows/string' # The win32console gem uses ANSI functions for writing to the console # which doesn't work for unicode strings, e.g. module tool. Ruby 1.9 # does the same thing, but doesn't account for ANSI escape sequences class WideConsole < Win32::Console - WriteConsole = Win32API.new( "kernel32", "WriteConsoleW", ['l', 'p', 'l', 'p', 'p'], 'l' ) - WriteConsoleOutputCharacter = Win32API.new( "kernel32", "WriteConsoleOutputCharacterW", ['l', 'p', 'l', 'l', 'p'], 'l' ) + extend FFI::Library + + # http://msdn.microsoft.com/en-us/library/windows/desktop/ms687401(v=vs.85).aspx + # BOOL WINAPI WriteConsole( + # _In_ HANDLE hConsoleOutput, + # _In_ const VOID *lpBuffer, + # _In_ DWORD nNumberOfCharsToWrite, + # _Out_ LPDWORD lpNumberOfCharsWritten, + # _Reserved_ LPVOID lpReserved + # ); + ffi_lib :kernel32 + attach_function_private :WriteConsoleW, + [:handle, :lpcwstr, :dword, :lpdword, :lpvoid], :win32_bool + + # typedef struct _COORD { + # SHORT X; + # SHORT Y; + # } COORD, *PCOORD; + class COORD < FFI::Struct + layout :X, :short, + :Y, :short + end + + # http://msdn.microsoft.com/en-us/library/windows/desktop/ms687410(v=vs.85).aspx + # BOOL WINAPI WriteConsoleOutputCharacter( + # _In_ HANDLE hConsoleOutput, + # _In_ LPCTSTR lpCharacter, + # _In_ DWORD nLength, + # _In_ COORD dwWriteCoord, + # _Out_ LPDWORD lpNumberOfCharsWritten + # ); + ffi_lib :kernel32 + attach_function_private :WriteConsoleOutputCharacterW, + [:handle, :lpcwstr, :dword, COORD, :lpdword], :win32_bool def initialize(t = nil) super(t) end def WriteChar(str, col, row) - dwWriteCoord = (row << 16) + col - lpNumberOfCharsWritten = ' ' * 4 - utf16, nChars = string_encode(str) - WriteConsoleOutputCharacter.call(@handle, utf16, nChars, dwWriteCoord, lpNumberOfCharsWritten) - lpNumberOfCharsWritten.unpack('L') - end + writeCoord = COORD.new() + writeCoord[:X] = row + writeCoord[:Y] = col - def Write(str) - written = 0.chr * 4 - reserved = 0.chr * 4 - utf16, nChars = string_encode(str) - WriteConsole.call(@handle, utf16, nChars, written, reserved) + numberOfCharsWritten_ptr = FFI::MemoryPointer.new(:dword, 1) + WriteConsoleOutputCharacterW(@handle, FFI::MemoryPointer.from_string_to_wide_string(str), + str.length, writeCoord, numberOfCharsWritten_ptr) + numberOfCharsWritten_ptr.read_dword end - def string_encode(str) - wstr = Puppet::Util::Windows::String.wide_string(str) - [wstr, wstr.length - 1] + def Write(str) + WriteConsoleW(@handle, FFI::MemoryPointer.from_string_to_wide_string(str), + str.length, FFI::MemoryPointer.new(:dword, 1), FFI::MemoryPointer::NULL) end end # Override the win32console's IO class so we can supply # our own Console class class WideIO < Win32::Console::ANSI::IO def initialize(fd_std = :stdout) super(fd_std) handle = FD_STD_MAP[fd_std][1] @Out = WideConsole.new(handle) end end $stdout = WideIO.new(:stdout) $stderr = WideIO.new(:stderr) rescue LoadError def console_has_color? false end else def console_has_color? true end end else # On a posix system we can just enable it def console_has_color? true end end def colorize(color, str) case Puppet[:color] when true, :ansi, "ansi", "yes" if console_has_color? console_color(color, str) else str end when :html, "html" html_color(color, str) else str end end def console_color(color, str) Colormap[color][:console] + str.gsub(RESET[:console], Colormap[color][:console]) + RESET[:console] end def html_color(color, str) span = '' % Colormap[color][:html] "#{span}%s" % str.gsub(//, "\\0#{span}") end end diff --git a/lib/puppet/util/windows/api_types.rb b/lib/puppet/util/windows/api_types.rb index 45f8ef9a1..afa986e0c 100644 --- a/lib/puppet/util/windows/api_types.rb +++ b/lib/puppet/util/windows/api_types.rb @@ -1,102 +1,103 @@ require 'ffi' require 'puppet/util/windows/string' module Puppet::Util::Windows::APITypes module ::FFI WIN32_FALSE = 0 end module ::FFI::Library # Wrapper method for attach_function + private def attach_function_private(*args) attach_function(*args) private args[0] end end class ::FFI::Pointer NULL_HANDLE = 0 end class ::FFI::MemoryPointer def self.from_string_to_wide_string(str) str = Puppet::Util::Windows::String.wide_string(str) ptr = FFI::MemoryPointer.new(:byte, str.bytesize) # uchar here is synonymous with byte ptr.put_array_of_uchar(0, str.bytes.to_a) ptr end def read_win32_bool # BOOL is always a 32-bit integer in Win32 # some Win32 APIs return 1 for true, while others are non-0 read_int32 != FFI::WIN32_FALSE end alias_method :read_dword, :read_uint32 def read_handle type_size == 4 ? read_uint32 : read_uint64 end def read_wide_string(char_length) # char_length is number of wide chars (typically excluding NULLs), *not* bytes str = get_bytes(0, char_length * 2).force_encoding('UTF-16LE') str.encode(Encoding.default_external) end alias_method :write_dword, :write_uint32 end # FFI Types # https://github.com/ffi/ffi/wiki/Types # Windows - Common Data Types # http://msdn.microsoft.com/en-us/library/cc230309.aspx # Windows Data Types # http://msdn.microsoft.com/en-us/library/windows/desktop/aa383751(v=vs.85).aspx FFI.typedef :uint16, :word FFI.typedef :uint32, :dword # uintptr_t is defined in an FFI conf as platform specific, either # ulong_long on x64 or just ulong on x86 FFI.typedef :uintptr_t, :handle # buffer_inout is similar to pointer (platform specific), but optimized for buffers FFI.typedef :buffer_inout, :lpwstr # buffer_in is similar to pointer (platform specific), but optimized for CONST read only buffers FFI.typedef :buffer_in, :lpcwstr # string is also similar to pointer, but should be used for const char * # NOTE that this is not wide, useful only for A suffixed functions FFI.typedef :string, :lpcstr # pointer in FFI is platform specific # NOTE: for API calls with reserved lpvoid parameters, pass a FFI::Pointer::NULL FFI.typedef :pointer, :lpvoid FFI.typedef :pointer, :lpword FFI.typedef :pointer, :lpdword FFI.typedef :pointer, :pdword FFI.typedef :pointer, :phandle FFI.typedef :pointer, :ulong_ptr FFI.typedef :pointer, :pbool # any time LONG / ULONG is in a win32 API definition DO NOT USE platform specific width # which is what FFI uses by default # instead create new aliases for these very special cases # NOTE: not a good idea to redefine FFI :ulong since other typedefs may rely on it FFI.typedef :uint32, :win32_ulong FFI.typedef :int32, :win32_long # FFI bool can be only 1 byte at times, # Win32 BOOL is a signed int, and is always 4 bytes, even on x64 # http://blogs.msdn.com/b/oldnewthing/archive/2011/03/28/10146459.aspx FFI.typedef :int32, :win32_bool - # NOTE: FFI already defines ushort as a 16-bit unsigned like this: + # NOTE: FFI already defines (u)short as a 16-bit (un)signed like this: # FFI.typedef :uint16, :ushort + # FFI.typedef :int16, :short # 8 bits per byte FFI.typedef :uchar, :byte end diff --git a/spec/unit/util/colors_spec.rb b/spec/unit/util/colors_spec.rb index 7407b628b..f114894da 100755 --- a/spec/unit/util/colors_spec.rb +++ b/spec/unit/util/colors_spec.rb @@ -1,83 +1,69 @@ #!/usr/bin/env ruby require 'spec_helper' describe Puppet::Util::Colors do include Puppet::Util::Colors let (:message) { 'a message' } let (:color) { :black } let (:subject) { self } describe ".console_color" do it { should respond_to :console_color } it "should generate ANSI escape sequences" do subject.console_color(color, message).should == "\e[0;30m#{message}\e[0m" end end describe ".html_color" do it { should respond_to :html_color } it "should generate an HTML span element and style attribute" do subject.html_color(color, message).should =~ /#{message}<\/span>/ end end describe ".colorize" do it { should respond_to :colorize } context "ansicolor supported" do before :each do subject.stubs(:console_has_color?).returns(true) end it "should colorize console output" do Puppet[:color] = true subject.expects(:console_color).with(color, message) subject.colorize(:black, message) end it "should not colorize unknown color schemes" do Puppet[:color] = :thisisanunknownscheme subject.colorize(:black, message).should == message end end context "ansicolor not supported" do before :each do subject.stubs(:console_has_color?).returns(false) end it "should not colorize console output" do Puppet[:color] = true subject.expects(:console_color).never subject.colorize(:black, message).should == message end it "should colorize html output" do Puppet[:color] = :html subject.expects(:html_color).with(color, message) subject.colorize(color, message) end end end - - describe "on Windows", :if => Puppet.features.microsoft_windows? do - it "expects a trailing embedded NULL character in the wide string" do - message = "hello" - - console = Puppet::Util::Colors::WideConsole.new - wstr, nchars = console.string_encode(message) - - expect(nchars).to eq(message.length) - - expect(wstr.length).to eq(nchars + 1) - expect(wstr[-1].ord).to be_zero - end - end end