From 5b6a1d8c8e580a55a967faa5735966766b048244 Mon Sep 17 00:00:00 2001 From: YO4 Date: Wed, 11 Sep 2024 23:18:33 +0900 Subject: [PATCH] get working again on windows Use ReadConsoleOutputCharacterW instead of ReadConsoleOutputW - ReadConsoleOutputW reads multicolum character wrong (probably when raster font used) Propery terminate commandline in launch() wait_startup_message() and assert_screen from vterm.rb 415a438 --- lib/yamatanooroti/windows.rb | 136 ++++++++++++++++------------------- 1 file changed, 61 insertions(+), 75 deletions(-) diff --git a/lib/yamatanooroti/windows.rb b/lib/yamatanooroti/windows.rb index 7cc7343..2bf098d 100644 --- a/lib/yamatanooroti/windows.rb +++ b/lib/yamatanooroti/windows.rb @@ -148,11 +148,6 @@ module Yamatanooroti::WindowsDefinition STD_ERROR_HANDLE = -12 ATTACH_PARENT_PROCESS = -1 KEY_EVENT = 0x0001 - CT_CTYPE3 = 0x04 - C3_HIRAGANA = 0x0020 - C3_HALFWIDTH = 0x0040 - C3_FULLWIDTH = 0x0080 - C3_IDEOGRAPH = 0x0100 TH32CS_SNAPPROCESS = 0x00000002 PROCESS_ALL_ACCESS = 0x001FFFFF SW_HIDE = 0 @@ -183,8 +178,8 @@ module Yamatanooroti::WindowsDefinition extern 'SHORT VkKeyScanW(WCHAR);', :stdcall # UINT MapVirtualKeyW(UINT uCode, UINT uMapType); extern 'UINT MapVirtualKeyW(UINT, UINT);', :stdcall - # BOOL ReadConsoleOutputW(HANDLE hConsoleOutput, PCHAR_INFO lpBuffer, COORD dwBufferSize, COORD dwBufferCoord, PSMALL_RECT lpReadRegion); - extern 'BOOL ReadConsoleOutputW(HANDLE, PCHAR_INFO, COORD, COORD, PSMALL_RECT);', :stdcall + # BOOL WINAPI ReadConsoleOutputCharacterW(HANDLE hConsoleOutput, LPWSTR lpCharacter, DWORD nLength, COORD dwReadCoord, LPDWORD lpNumberOfCharsRead); + extern 'BOOL ReadConsoleOutputCharacterW(HANDLE, LPWSTR, DWORD, COORD, LPDWORD);', :stdcall # BOOL WINAPI GetConsoleScreenBufferInfo(HANDLE hConsoleOutput, PCONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo); extern 'BOOL GetConsoleScreenBufferInfo(HANDLE, PCONSOLE_SCREEN_BUFFER_INFO);', :stdcall # BOOL WINAPI GetCurrentConsoleFontEx(HANDLE hConsoleOutput, BOOL bMaximumWindow, PCONSOLE_FONT_INFOEX lpConsoleCurrentFontEx); @@ -213,8 +208,6 @@ module Yamatanooroti::WindowsDefinition extern 'int MultiByteToWideChar(UINT, DWORD, LPCSTR, int, LPWSTR, int);', :stdcall # int WideCharToMultiByte(UINT CodePage, DWORD dwFlags, _In_NLS_string_(cchWideChar)LPCWCH lpWideCharStr, int cchWideChar, LPSTR lpMultiByteStr, int cbMultiByte, LPCCH lpDefaultChar, LPBOOL lpUsedDefaultChar); extern 'int WideCharToMultiByte(UINT, DWORD, LPCWCH, int, LPSTR, int, LPCCH, LPBOOL);', :stdcall - #BOOL GetStringTypeW(DWORD dwInfoType, LPCWCH lpSrcStr, int cchSrc, LPWORD lpCharType); - extern 'BOOL GetStringTypeW(DWORD, LPCWCH, int, LPWORD);', :stdcall typealias 'LPTSTR', 'void*' typealias 'HLOCAL', 'HANDLE' @@ -310,25 +303,6 @@ module Yamatanooroti::WindowsTestCaseModule converted_str end - private def full_width?(c) - return false if c.nil? or c.empty? - wc = mb2wc(c) - type = Fiddle::Pointer.malloc(Fiddle::SIZEOF_WORD, DL::FREE) - DL.GetStringTypeW(DL::CT_CTYPE3, wc, wc.bytesize, type) - char_type = type[0, Fiddle::SIZEOF_WORD].unpack('S').first - if char_type.anybits?(DL::C3_FULLWIDTH) - true - elsif char_type.anybits?(DL::C3_HALFWIDTH) - false - elsif char_type.anybits?(DL::C3_HIRAGANA) - true - elsif char_type.anybits?(DL::C3_IDEOGRAPH) - true - else - false - end - end - private def quote_command_arg(arg) if not arg.match?(/[ \t"]/) # No quotation needed. @@ -359,7 +333,7 @@ module Yamatanooroti::WindowsTestCaseModule end private def launch(command) - command = %Q{cmd.exe /q /c "#{command}"} + command = "#{command}\0" converted_command = mb2wc(command) @pi = DL::PROCESS_INFORMATION.malloc (@pi.to_ptr + 0)[0, DL::PROCESS_INFORMATION.size] = "\x00" * DL::PROCESS_INFORMATION.size @@ -479,82 +453,94 @@ def write(str) end def close - sleep @wait + sleep 0.3 # read first before kill the console process including output @result = retrieve_screen free_resources end - private def retrieve_screen - char_info_matrix = Fiddle::Pointer.to_ptr("\x00" * (DL::CHAR_INFO.size * (@height * @width))) - region = DL::SMALL_RECT.malloc - region.Left = 0 - region.Top = 0 - region.Right = @width - 1 - region.Bottom = @height - 1 - r = DL.ReadConsoleOutputW(@output_handle, char_info_matrix, @height * 65536 + @width, 0, region) - error_message(r, "ReadConsoleOutputW") - screen = [] - prev_c = nil - @height.times do |y| - line = +'' - @width.times do |x| - index = @width * y + x - char_info = DL::CHAR_INFO.new(char_info_matrix + DL::CHAR_INFO.size * index) - mb = [char_info.UnicodeChar].pack('U') - if prev_c == mb and full_width?(mb) - prev_c = nil - else - line << mb - prev_c = mb - end - end - screen << line.gsub(/ *$/, '') + def retrieve_screen + buffer_chars = @width * 8 + buffer = Fiddle::Pointer.malloc(Fiddle::SIZEOF_SHORT * buffer_chars, DL::FREE) + n = Fiddle::Pointer[0] + lines = (0...@height).map do |y| + r = DL.ReadConsoleOutputCharacterW(@output_handle, buffer, @width, y << 16, -n) + error_message(r, "ReadConsoleOutputCharacterW") + r == 0 ? "" : wc2mb(buffer[0, n.to_i * 2]).gsub(/ *$/, "") end - screen + lines end def result @result || retrieve_screen end + private def assert_screen_with_proc(check_proc, assert_block, convert_proc = :itself.to_proc) + retry_until = Time.now + @timeout + while Time.now < retry_until + #break unless try_sync + + break if check_proc.call(convert_proc.call(result)) + sleep @wait + end + sleep @wait + assert_block.call(convert_proc.call(result)) + end + def assert_screen(expected_lines, message = nil) - actual_lines = result - actual_string = actual_lines.join("\n").sub(/\n*\z/, "\n") + lines_to_string = ->(lines) { lines.join("\n").sub(/\n*\z/, "\n") } case expected_lines when Array - assert_equal(expected_lines, actual_lines, message) + assert_screen_with_proc( + ->(a) { expected_lines == a }, + ->(a) { assert_equal(expected_lines, a, message) } + ) when String - assert_equal(expected_lines, actual_string, message) + assert_screen_with_proc( + ->(a) { expected_lines == a }, + ->(a) { assert_equal(expected_lines, a, message) }, + lines_to_string + ) when Regexp - assert_match(expected_lines, actual_string, message) + assert_screen_with_proc( + ->(a) { expected_lines.match?(a) }, + ->(a) { assert_match(expected_lines, a, message) }, + lines_to_string + ) end end - def start_terminal(height, width, command, wait: 1, startup_message: nil) - @height = height - @width = width + def start_terminal(height, width, command, wait: 0.2, timeout: 2, startup_message: nil) + @timeout = timeout @wait = wait @result = nil + + @height = height + @width = width setup_console(height, width) + system("cmd /c cls") launch(command.map{ |c| quote_command_arg(c) }.join(' ')) + case startup_message when String - check_startup_message = ->(message) { - message.start_with?( - startup_message.each_char.each_slice(width).map(&:join).join("\0").gsub(/ *\0/, "\n") - ) - } + wait_startup_message { |message| message.start_with?(startup_message) } when Regexp - check_startup_message = ->(message) { startup_message.match?(message) } + wait_startup_message { |message| startup_message.match?(message) } end - if check_startup_message - loop do - screen = retrieve_screen.join("\n").sub(/\n*\z/, "\n") - break if check_startup_message.(screen) - sleep @wait + end + + private def wait_startup_message + wait_until = Time.now + @timeout + chunks = +'' + loop do + wait = wait_until - Time.now + if wait.negative? + raise "Startup message didn't arrive within timeout: #{chunks.inspect}" end + + chunks = retrieve_screen.join("\n").sub(/\n*\z/, "\n") + break if yield chunks end end end