From 7ca028b11eb6567e6f3839902ec1d5edbcdedf8c Mon Sep 17 00:00:00 2001 From: YO4 Date: Mon, 8 Apr 2024 22:35:13 +0900 Subject: [PATCH 01/76] windows: modify for ci in windows CI enviconments (Github Actions, Appveyor, CircleCI), it seems to standard handles not valid. use conin$/conout$ explicitly. --- lib/yamatanooroti/windows.rb | 227 +++++++++++++++++++++++++---------- 1 file changed, 163 insertions(+), 64 deletions(-) diff --git a/lib/yamatanooroti/windows.rb b/lib/yamatanooroti/windows.rb index d0f1d3e..0f96e2e 100644 --- a/lib/yamatanooroti/windows.rb +++ b/lib/yamatanooroti/windows.rb @@ -32,6 +32,7 @@ module Yamatanooroti::WindowsDefinition typealias 'LPWORD', 'void*' typealias 'ULONG_PTR', 'ULONG*' typealias 'LONG', 'int' + typealias 'HLOCAL', 'HANDLE' Fiddle::SIZEOF_HANDLE = Fiddle::SIZEOF_LONG Fiddle::SIZEOF_HPCON = Fiddle::SIZEOF_LONG @@ -146,6 +147,11 @@ module Yamatanooroti::WindowsDefinition STD_INPUT_HANDLE = -10 STD_OUTPUT_HANDLE = -11 STD_ERROR_HANDLE = -12 + STARTF_USESHOWWINDOW = 1 + CREATE_NEW_CONSOLE = 0x10 + CREATE_NEW_PROCESS_GROUP = 0x200 + CREATE_UNICODE_ENVIRONMENT = 0x400 + CREATE_NO_WINDOW = 0x08000000 ATTACH_PARENT_PROCESS = -1 KEY_EVENT = 0x0001 TH32CS_SNAPPROCESS = 0x00000002 @@ -178,6 +184,8 @@ module Yamatanooroti::WindowsDefinition extern 'SHORT VkKeyScanW(WCHAR);', :stdcall # UINT MapVirtualKeyW(UINT uCode, UINT uMapType); extern 'UINT MapVirtualKeyW(UINT, UINT);', :stdcall + # BOOL GetNumberOfConsoleInputEvents(HANDLE hConsoleInput, LPDWORD lpcNumberOfEvents); + extern 'BOOL GetNumberOfConsoleInputEvents(HANDLE hConsoleInput, LPDWORD lpcNumberOfEvents);', :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); @@ -209,15 +217,24 @@ module Yamatanooroti::WindowsDefinition # 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 - typealias 'LPTSTR', 'void*' - typealias 'HLOCAL', 'HANDLE' - extern 'DWORD FormatMessage(DWORD dwFlags, LPCVOID lpSource, DWORD dwMessageId, DWORD dwLanguageId, LPTSTR lpBuffer, DWORD nSize, va_list *Arguments);', :stdcall + # HANDLE CreateFileA(LPCSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile); + extern 'HANDLE CreateFileA(LPCSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE);', :stdcall + GENERIC_READ = 0x80000000 + GENERIC_WRITE = 0x40000000 + FILE_SHARE_READ = 0x00000001 + FILE_SHARE_WRITE = 0x00000002 + OPEN_EXISTING = 3 + INVALID_HANDLE_VALUE = 0xffffffff + + # DWORD FormatMessageW(DWORD dwFlags, LPCVOID lpSource, DWORD dwMessageId, DWORD dwLanguageId, LPWSTR lpBuffer, DWORD nSize, va_list *Arguments); + extern 'DWORD FormatMessageW(DWORD dwFlags, LPCVOID lpSource, DWORD dwMessageId, DWORD dwLanguageId, LPWSTR lpBuffer, DWORD nSize, va_list *Arguments);', :stdcall + # HLOCAL LocalFree(HLOCAL hMem); extern 'HLOCAL LocalFree(HLOCAL hMem);', :stdcall - extern 'DWORD GetLastError();', :stdcall FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100 FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000 LANG_NEUTRAL = 0x00 SUBLANG_DEFAULT = 0x01 + extern 'int GetSystemMetrics(int);', :stdcall SM_CXMIN = 28 SM_CYMIN = 29 @@ -226,18 +243,91 @@ module Yamatanooroti::WindowsDefinition module Yamatanooroti::WindowsTestCaseModule DL = Yamatanooroti::WindowsDefinition + private def attach_terminal + conin = conout = nil + r = DL.FreeConsole() + error_message(r, "FreeConsole") + r = DL.AttachConsole(@console_process_info.dwProcessId) + # this can be fail while new process is starting + # error_message(r, 'AttachConsole') + return nil if r.zero? + conin = DL.CreateFileA( + "conin$", + DL::GENERIC_READ | DL::GENERIC_WRITE, + DL::FILE_SHARE_READ | DL::FILE_SHARE_WRITE, + nil, + DL::OPEN_EXISTING, + 0, + 0 + ) + error_message(conin.to_i == DL::INVALID_HANDLE_VALUE ? 0 : 1, "conin$") + return nil if conin.to_i == DL::INVALID_HANDLE_VALUE + conout = DL.CreateFileA( + "conout$", + DL::GENERIC_READ | DL::GENERIC_WRITE, + DL::FILE_SHARE_READ | DL::FILE_SHARE_WRITE, + nil, + DL::OPEN_EXISTING, + 0, + 0 + ) + error_message(conout.to_i == DL::INVALID_HANDLE_VALUE ? 0 : 1, "conout$") + return nil if conout.to_i == DL::INVALID_HANDLE_VALUE + yield(conin.to_i, conout.to_i) + ensure + if conin != nil && conin.to_i != DL::INVALID_HANDLE_VALUE + r = DL.CloseHandle(conin) + error_message(r, "CloseHandle") + end + if conout != nil && conout.to_i != DL::INVALID_HANDLE_VALUE + r = DL.CloseHandle(conout) + error_message(r, "CloseHandle") + end + r = DL.FreeConsole() + error_message(r, "FreeConsole") + r = DL.AttachConsole(DL::ATTACH_PARENT_PROCESS) + error_message(r, 'AttachConsole') + end + private def setup_console(height, width) + command = %q[ruby.exe --disable=gems -e sleep"] # 'DO NOTHING JUST STAY THERE' CONSOLE KEEPING PROCESS + converted_command = mb2wc("#{command}\x00") + @console_process_info = DL::PROCESS_INFORMATION.malloc + @console_process_info.to_ptr[0, DL::PROCESS_INFORMATION.size] = "\x00".b * DL::PROCESS_INFORMATION.size + startup_info = DL::STARTUPINFOW.malloc + (startup_info.to_ptr + 0)[0, DL::STARTUPINFOW.size] = "\x00".b * DL::STARTUPINFOW.size + startup_info.cb = DL::STARTUPINFOW.size + if not ENV['YAMATANOOROTI_SHOW_WINDOW'] + startup_info.dwFlags = DL::STARTF_USESHOWWINDOW + startup_info.wShowWindow = DL::SW_HIDE + end + + r = DL.CreateProcessW( + Fiddle::NULL, converted_command, + Fiddle::NULL, Fiddle::NULL, + 0, + DL::CREATE_NEW_CONSOLE | DL::CREATE_UNICODE_ENVIRONMENT, + Fiddle::NULL, Fiddle::NULL, + startup_info, @console_process_info + ) + error_message(r, 'CreateProcessW') + + # wait for console startup complete + 8.times do |n| + break if attach_terminal { true } + sleep 0.02 * 2**n + end - r = DL.FreeConsole - error_message(r, 'FreeConsole') - r = DL.AllocConsole - error_message(r, 'AllocConsole') - @output_handle = DL.GetStdHandle(DL::STD_OUTPUT_HANDLE) + attach_terminal do |conin, conout| + change_console_size(conout, height, width) + end + end + def change_console_size(handle, height, width) font = DL::CONSOLE_FONT_INFOEX.malloc font.cbSize = DL::CONSOLE_FONT_INFOEX.size - r = DL.GetCurrentConsoleFontEx(@output_handle, 0, font) + r = DL.GetCurrentConsoleFontEx(handle, 0, font) error_message(r, 'GetCurrentConsoleFontEx') fontsize = (font.dwFontSize & 0xffff0000) / 65536 fontwidth = font.dwFontSize & 0xffff @@ -245,7 +335,7 @@ module Yamatanooroti::WindowsTestCaseModule newwidth = fontwidth csbi = DL::CONSOLE_SCREEN_BUFFER_INFO.malloc - r = DL.GetConsoleScreenBufferInfo(@output_handle, csbi) + r = DL.GetConsoleScreenBufferInfo(handle, csbi) error_message(r, 'GetConsoleScreenBufferInfo') if (width < (csbi.Right - csbi.Left + 1) / 4) @@ -258,7 +348,7 @@ module Yamatanooroti::WindowsTestCaseModule end font.dwFontSize = newsize * 65536 + newwidth - r = DL.SetCurrentConsoleFontEx(@output_handle, 0, font) + r = DL.SetCurrentConsoleFontEx(handle, 0, font) error_message(r, 'SetCurrentConsoleFontEx') rect = DL::SMALL_RECT.malloc @@ -266,27 +356,21 @@ module Yamatanooroti::WindowsTestCaseModule rect.Top = 0 rect.Right = width - 1 rect.Bottom = height - 1 - r = DL.SetConsoleWindowInfo(@output_handle, 1, rect) + r = DL.SetConsoleWindowInfo(handle, 1, rect) error_message(r, 'SetConsoleWindowInfo') -# size = DL.GetSystemMetrics(DL::SM_CYMIN) * 65536 + DL.GetSystemMetrics(DL::SM_CXMIN) -# r = DL.SetConsoleScreenBufferSize(@output_handle, size) -# error_message(r, 'SetConsoleScreenBufferSize') - csbi = DL::CONSOLE_SCREEN_BUFFER_INFO.malloc - r = DL.GetConsoleScreenBufferInfo(@output_handle, csbi) + r = DL.GetConsoleScreenBufferInfo(handle, csbi) error_message(r, 'GetConsoleScreenBufferInfo') size = height * 65536 + width - r = DL.SetConsoleScreenBufferSize(@output_handle, size) + r = DL.SetConsoleScreenBufferSize(handle, size) error_message(r, "SetConsoleScreenBufferSize " \ - "(#{width} #{height}) " \ - "(#{csbi.Right - csbi.Left + 1} #{csbi.Bottom - csbi.Top + 1}) " \ - "(#{csbi.dwSize & 65535} #{csbi.dwSize / 65536}) " \ - "(#{csbi.Left} #{csbi.Top}) " \ - "(#{csbi.Right} #{csbi.Bottom})") - r = DL.ShowWindow(DL.GetConsoleWindow(), DL::SW_HIDE) - error_message(r, 'ShowWindow') + "(#{height} #{width}) " \ + "(#{csbi.Bottom - csbi.Top + 1} #{csbi.Right - csbi.Left + 1}) " \ + "(#{csbi.dwSize / 65536} #{csbi.dwSize & 65535}) " \ + "(#{csbi.Top} #{csbi.Left}) " \ + "(#{csbi.Bottom} #{csbi.Right})") end private def mb2wc(str) @@ -304,7 +388,7 @@ module Yamatanooroti::WindowsTestCaseModule end private def quote_command_arg(arg) - if not arg.match?(/[ \t"]/) + if not arg.match?(/[ \t"<>|()]/) # No quotation needed. return arg end @@ -333,28 +417,18 @@ module Yamatanooroti::WindowsTestCaseModule end private def launch(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 - @startup_info_ex = DL::STARTUPINFOW.malloc - (@startup_info_ex.to_ptr + 0)[0, DL::STARTUPINFOW.size] = "\x00" * DL::STARTUPINFOW.size - r = DL.CreateProcessW( - Fiddle::NULL, converted_command, - Fiddle::NULL, Fiddle::NULL, 0, 0, Fiddle::NULL, Fiddle::NULL, - @startup_info_ex, @pi - ) - error_message(r, 'CreateProcessW') + attach_terminal do + pid = spawn(command, {in: ["conin$", File::RDWR | File::BINARY], out: ["conout$", File::RDWR | File::BINARY], err: STDERR}) + @pi = Process.detach(pid) + end sleep @wait - rescue => e - pp e end private def error_message(r, method_name) return if not r.zero? - err = DL.GetLastError + err = Fiddle.win32_last_error string = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP) - DL.FormatMessage( + n = DL.FormatMessageW( DL::FORMAT_MESSAGE_ALLOCATE_BUFFER | DL::FORMAT_MESSAGE_FROM_SYSTEM, Fiddle::NULL, err, @@ -363,12 +437,15 @@ module Yamatanooroti::WindowsTestCaseModule 0, Fiddle::NULL ) - log "ERROR(#{method_name}): #{err.to_s}: #{string.ptr.to_s}" - DL.LocalFree(string) + if n > 0 + str = wc2mb(string.ptr[0, n * 2]) + LocalFree(string) + $stderr.puts "ERROR(#{method_name}): #{err.to_s}: #{str}" + end end private def log(str) - puts str + $stderr.puts str open('aaa', 'a') do |fp| fp.puts str end @@ -394,8 +471,18 @@ def write(str) set_input_record(r, c, false, control_key_state) end written_size = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DWORD, DL::FREE) - r = DL.WriteConsoleInputW(DL.GetStdHandle(DL::STD_INPUT_HANDLE), records, str.size * 2, written_size) - error_message(r, 'WriteConsoleInput') + attach_terminal do |conin, conout| + r = DL.WriteConsoleInputW(conin, records, str.size * 2, written_size) + error_message(r, 'WriteConsoleInput') + + n = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DWORD, DL::FREE) + loop do + sleep 0.02 + r = DL.GetNumberOfConsoleInputEvents(conin, n) + error_message(r, 'GetNumberOfConsoleInputEvents') + break if n.to_str.unpack1("L") <= 1 # key up record still remains + end + end end private def set_input_record(r, c, key_down, control_key_state) @@ -433,10 +520,10 @@ def write(str) #r = DL.TerminateThread(@pi.hThread, 0) #error_message(r, "TerminateThread") #sleep @wait - r = DL.FreeConsole() + #r = DL.FreeConsole() #error_message(r, "FreeConsole") - r = DL.AttachConsole(DL::ATTACH_PARENT_PROCESS) - error_message(r, 'AttachConsole') + #r = DL.AttachConsole(DL::ATTACH_PARENT_PROCESS) + #error_message(r, 'AttachConsole') end private def kill_process_tree(process_table, pid) @@ -446,7 +533,7 @@ def write(str) h_proc = DL.OpenProcess(DL::PROCESS_ALL_ACCESS, 0, pid) if (h_proc) r = DL.TerminateProcess(h_proc, 0) - error_message(r, "TerminateProcess") + # error_message(r, "TerminateProcess") r = DL.CloseHandle(h_proc) error_message(r, "CloseHandle") end @@ -463,11 +550,17 @@ def close private 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(/ *$/, "") + n = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DWORD, DL::FREE) + lines = attach_terminal do |conin, conout| + (0...@height).map do |y| + r = DL.ReadConsoleOutputCharacterW(conout, buffer, @width, y << 16, n) + error_message(r, "ReadConsoleOutputCharacterW") + if r != 0 + wc2mb(buffer[0, n.to_str.unpack1("L") * 2]).gsub(/ *$/, "") + else + "" + end + end end lines end @@ -496,26 +589,32 @@ def start_terminal(height, width, command, wait: 0.01, timeout: 2, startup_messa 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 - break if yield retrieve_screen.join("\n").sub(/\n*\z/, "\n") + chunks = retrieve_screen.join("\n").sub(/\n*\z/, "\n") + break if yield chunks sleep @wait end end private def retryable_screen_assertion_with_proc(check_proc, assert_proc, convert_proc = :itself.to_proc) retry_until = Time.now + @timeout - while Time.now < retry_until - break if @result - - break if check_proc.call(convert_proc.call(retrieve_screen)) - sleep @wait + screen = if @result + convert_proc.call(@result) + else + loop do + screen = convert_proc.call(retrieve_screen) + break screen if Time.now >= retry_until + break screen if check_proc.call(screen) + sleep @wait + end end - assert_proc.call(convert_proc.call(@result || retrieve_screen)) + assert_proc.call(screen) end def assert_screen(expected_lines, message = nil) From 871f9c2198513de6694d91874e81a3b8d2c6e844 Mon Sep 17 00:00:00 2001 From: YO4 Date: Tue, 9 Apr 2024 00:26:49 +0900 Subject: [PATCH 02/76] windows: change console codepage if needed on console device, gets() returns console codepage string. --- bin/simple_repl | 0 lib/yamatanooroti/windows.rb | 12 +++++++++++- test/yamatanooroti/test_multiplatform.rb | 12 ++++++++++++ test/yamatanooroti/test_windows.rb | 20 ++++++++++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) mode change 100755 => 100644 bin/simple_repl diff --git a/bin/simple_repl b/bin/simple_repl old mode 100755 new mode 100644 diff --git a/lib/yamatanooroti/windows.rb b/lib/yamatanooroti/windows.rb index 0f96e2e..e0f1541 100644 --- a/lib/yamatanooroti/windows.rb +++ b/lib/yamatanooroti/windows.rb @@ -424,6 +424,14 @@ def change_console_size(handle, height, width) sleep @wait end + private def setup_cp(cp) + @codepage_success_p = attach_terminal { system("chcp #{Integer(cp)} > NUL") } + end + + private def codepage_success? + @codepage_success_p + end + private def error_message(r, method_name) return if not r.zero? err = Fiddle.win32_last_error @@ -569,14 +577,16 @@ def result @result || retrieve_screen end - def start_terminal(height, width, command, wait: 0.01, timeout: 2, startup_message: nil) + def start_terminal(height, width, command, wait: 0.01, timeout: 2, startup_message: nil, codepage: nil) @timeout = timeout @wait = wait @result = nil + @codepage_success_p @height = height @width = width setup_console(height, width) + setup_cp(codepage) if codepage launch(command.map{ |c| quote_command_arg(c) }.join(' ')) case startup_message diff --git a/test/yamatanooroti/test_multiplatform.rb b/test/yamatanooroti/test_multiplatform.rb index dbcd1af..a596318 100644 --- a/test/yamatanooroti/test_multiplatform.rb +++ b/test/yamatanooroti/test_multiplatform.rb @@ -52,8 +52,19 @@ def test_auto_wrap prompt> EOC end +end + +class Yamatanooroti::TestMultiplatformMultiByte < Yamatanooroti::TestCase + def setup + if Yamatanooroti.win? + start_terminal(5, 30, ['ruby', 'bin/simple_repl'], startup_message: 'prompt>', codepage: 932) + else + start_terminal(5, 30, ['ruby', 'bin/simple_repl'], startup_message: 'prompt>') + end + end def test_fullwidth + omit "multibyte char not supported by env" if Yamatanooroti.win? and !codepage_success? write(":あ\n") close assert_screen(/=> :あ\nprompt>/) @@ -61,6 +72,7 @@ def test_fullwidth end def test_two_fullwidth + omit "multibyte char not supported by env" if Yamatanooroti.win? and !codepage_success? write(":あい\n") close assert_screen(/=> :あい\nprompt>/) diff --git a/test/yamatanooroti/test_windows.rb b/test/yamatanooroti/test_windows.rb index b037651..b83c61f 100644 --- a/test/yamatanooroti/test_windows.rb +++ b/test/yamatanooroti/test_windows.rb @@ -13,3 +13,23 @@ def test_load end end end + +class Yamatanooroti::TestWindowsCodepage < Yamatanooroti::TestCase + if Yamatanooroti.win? + def test_codepage_932 + start_terminal(5, 30, ['ruby', '-e', 'puts(Encoding.find(%Q[locale]).name)'], codepage: 932) + sleep 0.5 + close + omit "codepage 932 not supported" if !codepage_success? + assert_equal(['Windows-31J', '', '', '', ''], result) + end + + def test_codepage_437 + start_terminal(5, 30, ['ruby', '-e', 'puts(Encoding.find(%Q[locale]).name)'], codepage: 437) + sleep 0.5 + close + omit "codepage 437 not supported" if !codepage_success? + assert_equal(['IBM437', '', '', '', ''], result) + end + end +end From c91e83a05f79535bdf28201406640d79bbf5489c Mon Sep 17 00:00:00 2001 From: YO4 Date: Sun, 14 Apr 2024 21:11:49 +0900 Subject: [PATCH 03/76] report exception properly even if that occurs in the target process --- lib/yamatanooroti/windows.rb | 71 ++++++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 8 deletions(-) diff --git a/lib/yamatanooroti/windows.rb b/lib/yamatanooroti/windows.rb index e0f1541..7eaf970 100644 --- a/lib/yamatanooroti/windows.rb +++ b/lib/yamatanooroti/windows.rb @@ -1,4 +1,5 @@ require 'test/unit' +require 'stringio' require 'fiddle/import' require 'fiddle/types' @@ -244,6 +245,9 @@ module Yamatanooroti::WindowsTestCaseModule DL = Yamatanooroti::WindowsDefinition private def attach_terminal + stderr = $stderr + $stderr = StringIO.new + conin = conout = nil r = DL.FreeConsole() error_message(r, "FreeConsole") @@ -274,6 +278,7 @@ module Yamatanooroti::WindowsTestCaseModule error_message(conout.to_i == DL::INVALID_HANDLE_VALUE ? 0 : 1, "conout$") return nil if conout.to_i == DL::INVALID_HANDLE_VALUE yield(conin.to_i, conout.to_i) + rescue => evar ensure if conin != nil && conin.to_i != DL::INVALID_HANDLE_VALUE r = DL.CloseHandle(conin) @@ -287,6 +292,9 @@ module Yamatanooroti::WindowsTestCaseModule error_message(r, "FreeConsole") r = DL.AttachConsole(DL::ATTACH_PARENT_PROCESS) error_message(r, 'AttachConsole') + stderr.write $stderr.read + $stderr = stderr + raise evar if evar end private def setup_console(height, width) @@ -416,12 +424,56 @@ def change_console_size(handle, height, width) result.reverse end + class SubProcess + def initialize(command) + @errin, err = IO.pipe + @pid = spawn(command, {in: ["conin$", File::RDWR | File::BINARY], out: ["conout$", File::RDWR | File::BINARY], err: err}) + err.close + @closed = false + @status = nil + @q = Thread::Queue.new + @t = Thread.new do + err = @errin.gets + @q << err if err + end + end + + def closed? + @closed ||= !(@status = Process.wait2(@pid, Process::WNOHANG)).nil? + end + + private def consume(buffer) + while !@q.empty? + buffer << @q.shift + end + end + + def ensure_close + @errin.close if !@errin.closed? + end + + def sync + buffer = "" + if closed? + @t.kill + @t.join + consume(buffer) + rest = "".b + while ((str = @errin.read_nonblock(1024, exception: false)).is_a?(String)) do + rest << str + end + buffer << rest.force_encoding(Encoding.default_external) << "\n" if rest != "" + else + consume(buffer) + end + $stderr.write buffer if buffer != "" + end + end + private def launch(command) attach_terminal do - pid = spawn(command, {in: ["conin$", File::RDWR | File::BINARY], out: ["conout$", File::RDWR | File::BINARY], err: STDERR}) - @pi = Process.detach(pid) + SubProcess.new(command) end - sleep @wait end private def setup_cp(cp) @@ -454,9 +506,6 @@ def change_console_size(handle, height, width) private def log(str) $stderr.puts str - open('aaa', 'a') do |fp| - fp.puts str - end end def write(str) @@ -489,6 +538,8 @@ def write(str) r = DL.GetNumberOfConsoleInputEvents(conin, n) error_message(r, 'GetNumberOfConsoleInputEvents') break if n.to_str.unpack1("L") <= 1 # key up record still remains + @target.sync + break if @target.closed? end end end @@ -548,11 +599,14 @@ def write(str) end def close - sleep 0.3 + @target.sync + sleep @wait if !@target.closed? # read first before kill the console process including output @result = retrieve_screen free_resources + @target.sync + @target.ensure_close end private def retrieve_screen @@ -587,7 +641,7 @@ def start_terminal(height, width, command, wait: 0.01, timeout: 2, startup_messa @width = width setup_console(height, width) setup_cp(codepage) if codepage - launch(command.map{ |c| quote_command_arg(c) }.join(' ')) + @target = launch(command.map{ |c| quote_command_arg(c) }.join(' ')) case startup_message when String @@ -606,6 +660,7 @@ def start_terminal(height, width, command, wait: 0.01, timeout: 2, startup_messa raise "Startup message didn't arrive within timeout: #{chunks.inspect}" end + @target.sync chunks = retrieve_screen.join("\n").sub(/\n*\z/, "\n") break if yield chunks sleep @wait From 6a13318b288dec5e72c43b907bb3a6fd089f8b60 Mon Sep 17 00:00:00 2001 From: YO4 Date: Mon, 15 Apr 2024 23:57:40 +0900 Subject: [PATCH 04/76] change terminal resizing mechanism SetConsoleScreenBufferSize has a minimum buffer width limit by window width (pixels) derived from the font size. SetConsoleScreenBufferInfoEx do not seem to have that limitation. so to use SetConsoleScreenBufferInfoEx can avoid font size tweaking. --- lib/yamatanooroti/windows.rb | 181 +++++++++++++++++++---------------- 1 file changed, 101 insertions(+), 80 deletions(-) diff --git a/lib/yamatanooroti/windows.rb b/lib/yamatanooroti/windows.rb index 7eaf970..21b9233 100644 --- a/lib/yamatanooroti/windows.rb +++ b/lib/yamatanooroti/windows.rb @@ -57,14 +57,27 @@ module Yamatanooroti::WindowsDefinition typealias 'PSMALL_RECT', 'SMALL_RECT*' CONSOLE_SCREEN_BUFFER_INFO = struct [ - 'COORD dwSize', - 'COORD dwCursorPosition', + 'SHORT dwSize_X', 'SHORT dwSize_Y', # 'COORD dwSize', + 'SHORT dwCursorPosition_X', 'SHORT dwCursorPosition_Y', #'COORD dwCursorPosition', 'WORD wAttributes', 'SHORT Left', 'SHORT Top', 'SHORT Right', 'SHORT Bottom', # 'SMALL_RECT srWindow', 'SHORT MaxWidth', 'SHORT MaxHeight' # 'COORD dwMaximumWindowSize' ] typealias 'PCONSOLE_SCREEN_BUFFER_INFO', 'CONSOLE_SCREEN_BUFFER_INFO*' + typealias 'COLORREF', 'DWORD' + CONSOLE_SCREEN_BUFFER_INFOEX = struct [ + 'ULONG cbSize', + 'SHORT dwSize_X', 'SHORT dwSize_Y', # 'COORD dwSize', + 'SHORT dwCursorPosition_X', 'SHORT dwCursorPosition_Y', #'COORD dwCursorPosition', + 'WORD wAttributes', + 'SHORT Left', 'SHORT Top', 'SHORT Right', 'SHORT Bottom', # 'SMALL_RECT srWindow', + 'SHORT MaxWidth', 'SHORT MaxHeight', # 'COORD dwMaximumWindowSize', + 'BOOL bFullScreenSupported', + 'COLORREF ColorTable[16]' + ] + typealias 'PCONSOLE_SCREEN_BUFFER_INFOEX', 'CONSOLE_SCREEN_BUFFER_INFOEX*' + SECURITY_ATTRIBUTES = struct [ 'DWORD nLength', 'LPVOID lpSecurityDescriptor', @@ -135,19 +148,6 @@ module Yamatanooroti::WindowsDefinition ] typealias 'LPPROCESSENTRY32W', 'PROCESSENTRY32W*' - CONSOLE_FONT_INFOEX = struct [ - 'ULONG cbSize', - 'DWORD nFont', - 'DWORD32 dwFontSize', - 'UINT FontFamily', - 'UINT FontWeight', - 'WCHAR FaceName[32]' - ] - typealias 'PCONSOLE_FONT_INFOEX', 'CONSOLE_FONT_INFOEX*' - - STD_INPUT_HANDLE = -10 - STD_OUTPUT_HANDLE = -11 - STD_ERROR_HANDLE = -12 STARTF_USESHOWWINDOW = 1 CREATE_NEW_CONSOLE = 0x10 CREATE_NEW_PROCESS_GROUP = 0x200 @@ -158,6 +158,7 @@ module Yamatanooroti::WindowsDefinition TH32CS_SNAPPROCESS = 0x00000002 PROCESS_ALL_ACCESS = 0x001FFFFF SW_HIDE = 0 + SW_SHOWNOACTIVE = 4 LEFT_ALT_PRESSED = 0x0002 # HANDLE GetStdHandle(DWORD nStdHandle); @@ -175,8 +176,6 @@ module Yamatanooroti::WindowsDefinition extern 'BOOL ShowWindow(HWND hWnd,int nCmdShow);', :stdcall # HWND WINAPI GetConsoleWindow(void); extern 'HWND GetConsoleWindow(void);', :stdcall - # BOOL WINAPI SetConsoleScreenBufferSize(HANDLE hConsoleOutput, COORD dwSize); - extern 'BOOL SetConsoleScreenBufferSize(HANDLE, COORD);', :stdcall # BOOL WINAPI SetConsoleWindowInfo(HANDLE hConsoleOutput, BOOL bAbsolute, const SMALL_RECT *lpConsoleWindow); extern 'BOOL SetConsoleWindowInfo(HANDLE, BOOL, PSMALL_RECT);', :stdcall # BOOL WriteConsoleInputW(HANDLE hConsoleInput, const INPUT_RECORD *lpBuffer, DWORD nLength, LPDWORD lpNumberOfEventsWritten); @@ -191,10 +190,10 @@ module Yamatanooroti::WindowsDefinition 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); - extern 'BOOL GetCurrentConsoleFontEx(HANDLE, BOOL, PCONSOLE_FONT_INFOEX);', :stdcall - # BOOL WINAPI SetCurrentConsoleFontEx(HANDLE hConsoleOutput, BOOL bMaximumWindow, PCONSOLE_FONT_INFOEX lpConsoleCurrentFontEx); - extern 'BOOL SetCurrentConsoleFontEx(HANDLE, BOOL, PCONSOLE_FONT_INFOEX);', :stdcall + # BOOL WINAPI GetConsoleScreenBufferInfoEx(HANDLE hConsoleOutput, PCONSOLE_SCREEN_BUFFER_INFOEX lpConsoleScreenBufferInfoEx); + extern 'BOOL GetConsoleScreenBufferInfoEx(HANDLE, PCONSOLE_SCREEN_BUFFER_INFOEX);', :stdcall + # BOOL WINAPI SetConsoleScreenBufferInfoEx(HANDLE hConsoleOutput, PCONSOLE_SCREEN_BUFFER_INFOEX lpConsoleScreenBufferInfoEx); + extern 'BOOL SetConsoleScreenBufferInfoEx(HANDLE, PCONSOLE_SCREEN_BUFFER_INFOEX);', :stdcall # BOOL CreateProcessW(LPCWSTR lpApplicationName, LPWSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation); extern 'BOOL CreateProcessW(LPCWSTR lpApplicationName, LPWSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation);', :stdcall @@ -233,12 +232,72 @@ module Yamatanooroti::WindowsDefinition extern 'HLOCAL LocalFree(HLOCAL hMem);', :stdcall FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100 FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000 - LANG_NEUTRAL = 0x00 - SUBLANG_DEFAULT = 0x01 - extern 'int GetSystemMetrics(int);', :stdcall - SM_CXMIN = 28 - SM_CYMIN = 29 + private def error_message(err, method_name) + string = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP) + FormatMessage( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, + Fiddle::NULL, + err, + 0x0, + string, + 0, + Fiddle::NULL + ) + str = string.ptr.to_s + LocalFree(string) + str.force_encoding("Windows-31J") + puts "ERROR(#{method_name}): #{err.to_s}: #{string.ptr.to_s.force_encoding("locale")}" + end + + def get_console_screen_buffer_info(handle) + csbi = CONSOLE_SCREEN_BUFFER_INFO.malloc + r = GetConsoleScreenBufferInfo(handle, csbi) + puts error_message(r, 'GetConsoleScreenBufferInfo') if r == 0 + r == 0 ? nil : csbi + end + + def set_console_screen_buffer_info_ex(handle, h, w, buffer_height) + csbi = CONSOLE_SCREEN_BUFFER_INFOEX.malloc + csbi.cbSize = CONSOLE_SCREEN_BUFFER_INFOEX.size + r = GetConsoleScreenBufferInfoEx(handle, csbi) + error_message(r, 'GetConsoleScreenBufferSize') if r == 0 + csbi.dwSize_X = w + csbi.dwSize_Y = buffer_height + csbi.Left = 0 + csbi.Right = w - 1 + csbi.Top = [csbi.Top, buffer_height - h].min + csbi.Bottom = csbi.Top + h - 1 + r = SetConsoleScreenBufferInfoEx(handle, csbi) + puts error_message(r, 'SetConsoleScreenBufferInfoEx') if r == 0 + return r != 0 + end + + def set_console_window_info(handle, h, w) + rect = SMALL_RECT.malloc + rect.Left = 0 + rect.Top = 0 + rect.Right = w - 1 + rect.Bottom = h - 1 + r = SetConsoleWindowInfo(handle, 1, rect) + puts error_message(r, 'SetConsoleWindowInfo') if r == 0 + return r != 0 + end + + def set_console_window_size(handle, h, w) + # expand buffer size to keep scrolled away lines + buffer_h = h + 100 + + r = set_console_screen_buffer_info_ex(handle, h, w, buffer_h) + return false unless r + + r = set_console_window_info(handle, h, w) + return false unless r + + return true + end + + extend self end module Yamatanooroti::WindowsTestCaseModule @@ -305,7 +364,10 @@ module Yamatanooroti::WindowsTestCaseModule startup_info = DL::STARTUPINFOW.malloc (startup_info.to_ptr + 0)[0, DL::STARTUPINFOW.size] = "\x00".b * DL::STARTUPINFOW.size startup_info.cb = DL::STARTUPINFOW.size - if not ENV['YAMATANOOROTI_SHOW_WINDOW'] + if ENV['YAMATANOOROTI_SHOW_WINDOW'] + startup_info.dwFlags = DL::STARTF_USESHOWWINDOW + startup_info.wShowWindow = DL::SW_SHOWNOACTIVE + else startup_info.dwFlags = DL::STARTF_USESHOWWINDOW startup_info.wShowWindow = DL::SW_HIDE end @@ -327,58 +389,8 @@ module Yamatanooroti::WindowsTestCaseModule end attach_terminal do |conin, conout| - change_console_size(conout, height, width) - end - end - - def change_console_size(handle, height, width) - font = DL::CONSOLE_FONT_INFOEX.malloc - font.cbSize = DL::CONSOLE_FONT_INFOEX.size - - r = DL.GetCurrentConsoleFontEx(handle, 0, font) - error_message(r, 'GetCurrentConsoleFontEx') - fontsize = (font.dwFontSize & 0xffff0000) / 65536 - fontwidth = font.dwFontSize & 0xffff - newsize = fontsize - newwidth = fontwidth - - csbi = DL::CONSOLE_SCREEN_BUFFER_INFO.malloc - r = DL.GetConsoleScreenBufferInfo(handle, csbi) - error_message(r, 'GetConsoleScreenBufferInfo') - - if (width < (csbi.Right - csbi.Left + 1) / 4) - newsize = fontsize * (csbi.Right - csbi.Left + 1) / width - newwidth = fontwidth * (csbi.Right - csbi.Left + 1) / width + DL.set_console_window_size(conout, height, width) end - if newsize * height > fontsize * csbi.MaxHeight - newsize = fontsize * csbi.MaxHeight / height - newwidth = fontwidth * newsize / fontsize - end - - font.dwFontSize = newsize * 65536 + newwidth - r = DL.SetCurrentConsoleFontEx(handle, 0, font) - error_message(r, 'SetCurrentConsoleFontEx') - - rect = DL::SMALL_RECT.malloc - rect.Left = 0 - rect.Top = 0 - rect.Right = width - 1 - rect.Bottom = height - 1 - r = DL.SetConsoleWindowInfo(handle, 1, rect) - error_message(r, 'SetConsoleWindowInfo') - - csbi = DL::CONSOLE_SCREEN_BUFFER_INFO.malloc - r = DL.GetConsoleScreenBufferInfo(handle, csbi) - error_message(r, 'GetConsoleScreenBufferInfo') - - size = height * 65536 + width - r = DL.SetConsoleScreenBufferSize(handle, size) - error_message(r, "SetConsoleScreenBufferSize " \ - "(#{height} #{width}) " \ - "(#{csbi.Bottom - csbi.Top + 1} #{csbi.Right - csbi.Left + 1}) " \ - "(#{csbi.dwSize / 65536} #{csbi.dwSize & 65535}) " \ - "(#{csbi.Top} #{csbi.Left}) " \ - "(#{csbi.Bottom} #{csbi.Right})") end private def mb2wc(str) @@ -609,12 +621,21 @@ def close @target.ensure_close end - private def retrieve_screen + private def retrieve_screen(top_of_buffer: false) + top, bottom = attach_terminal do |conin, conout| + csbi = DL.get_console_screen_buffer_info(conout) + if top_of_buffer + [0, csbi.Bottom] + else + [csbi.Top, csbi.Bottom] + end + end + buffer_chars = @width * 8 buffer = Fiddle::Pointer.malloc(Fiddle::SIZEOF_SHORT * buffer_chars, DL::FREE) n = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DWORD, DL::FREE) lines = attach_terminal do |conin, conout| - (0...@height).map do |y| + (top..bottom).map do |y| r = DL.ReadConsoleOutputCharacterW(conout, buffer, @width, y << 16, n) error_message(r, "ReadConsoleOutputCharacterW") if r != 0 From ced37b2711757ce212ddbc399aac4c3f27000183 Mon Sep 17 00:00:00 2001 From: YO4 Date: Wed, 10 Apr 2024 23:19:43 +0900 Subject: [PATCH 05/76] use taskkill.exe to close child processes --- lib/yamatanooroti/windows.rb | 90 ++++++------------------------------ 1 file changed, 15 insertions(+), 75 deletions(-) diff --git a/lib/yamatanooroti/windows.rb b/lib/yamatanooroti/windows.rb index 21b9233..6f8ccf2 100644 --- a/lib/yamatanooroti/windows.rb +++ b/lib/yamatanooroti/windows.rb @@ -5,7 +5,7 @@ module Yamatanooroti::WindowsDefinition extend Fiddle::Importer - dlload 'kernel32.dll', 'psapi.dll', 'user32.dll' + dlload 'kernel32.dll', 'user32.dll' include Fiddle::Win32Types FREE = Fiddle::Function.new(Fiddle::RUBY_FREE, [Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOID) @@ -128,26 +128,6 @@ module Yamatanooroti::WindowsDefinition 'DWORD dwControlKeyState' ] - CHAR_INFO = struct [ - 'WCHAR UnicodeChar', - 'WORD Attributes' - ] - typealias 'PCHAR_INFO', 'CHAR_INFO*' - - PROCESSENTRY32W = struct [ - 'DWORD dwSize', - 'DWORD cntUsage', - 'DWORD th32ProcessID', - 'ULONG_PTR th32DefaultHeapID', - 'DWORD th32ModuleID', - 'DWORD cntThreads', - 'DWORD th32ParentProcessID', - 'LONG pcPriClassBase', - 'DWORD dwFlags', - 'WCHAR szExeFile[260]' - ] - typealias 'LPPROCESSENTRY32W', 'PROCESSENTRY32W*' - STARTF_USESHOWWINDOW = 1 CREATE_NEW_CONSOLE = 0x10 CREATE_NEW_PROCESS_GROUP = 0x200 @@ -155,8 +135,6 @@ module Yamatanooroti::WindowsDefinition CREATE_NO_WINDOW = 0x08000000 ATTACH_PARENT_PROCESS = -1 KEY_EVENT = 0x0001 - TH32CS_SNAPPROCESS = 0x00000002 - PROCESS_ALL_ACCESS = 0x001FFFFF SW_HIDE = 0 SW_SHOWNOACTIVE = 4 LEFT_ALT_PRESSED = 0x0002 @@ -197,20 +175,6 @@ module Yamatanooroti::WindowsDefinition # BOOL CreateProcessW(LPCWSTR lpApplicationName, LPWSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation); extern 'BOOL CreateProcessW(LPCWSTR lpApplicationName, LPWSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation);', :stdcall - # HANDLE CreateToolhelp32Snapshot(DWORD dwFlags, DWORD th32ProcessID); - extern 'HANDLE CreateToolhelp32Snapshot(DWORD, DWORD);', :stdcall - # BOOL Process32First(HANDLE hSnapshot, LPPROCESSENTRY32W lppe); - extern 'BOOL Process32FirstW(HANDLE, LPPROCESSENTRY32W);', :stdcall - # BOOL Process32Next(HANDLE hSnapshot, LPPROCESSENTRY32 lppe); - extern 'BOOL Process32NextW(HANDLE, LPPROCESSENTRY32W);', :stdcall - # DWORD GetCurrentProcessId(); - extern 'DWORD GetCurrentProcessId();', :stdcall - # HANDLE OpenProcess(DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId); - extern 'HANDLE OpenProcess(DWORD, BOOL, DWORD);', :stdcall - # BOOL TerminateProcess(HANDLE hProcess, UINT uExitCode); - extern 'BOOL TerminateProcess(HANDLE, UINT);', :stdcall - #BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode); - extern 'BOOL TerminateThread(HANDLE, DWORD);', :stdcall # int MultiByteToWideChar(UINT CodePage, DWORD dwFlags, LPCSTR lpMultiByteStr, int cbMultiByte, LPWSTR lpWideCharStr, int cchWideChar); extern 'int MultiByteToWideChar(UINT, DWORD, LPCSTR, int, LPWSTR, int);', :stdcall @@ -445,8 +409,13 @@ def initialize(command) @status = nil @q = Thread::Queue.new @t = Thread.new do - err = @errin.gets - @q << err if err + begin + err = @errin.gets + @q << err if err + rescue IOError + # target process already terminated + next + end end end @@ -572,42 +541,12 @@ def write(str) end private def free_resources - h_snap = DL.CreateToolhelp32Snapshot(DL::TH32CS_SNAPPROCESS, 0) - pe = DL::PROCESSENTRY32W.malloc - (pe.to_ptr + 0)[0, DL::PROCESSENTRY32W.size] = "\x00" * DL::PROCESSENTRY32W.size - pe.dwSize = DL::PROCESSENTRY32W.size - r = DL.Process32FirstW(h_snap, pe) - error_message(r, "Process32First") - process_table = {} - loop do - #log "a #{pe.th32ParentProcessID.inspect} -> #{pe.th32ProcessID.inspect} #{wc2mb(pe.szExeFile.pack('S260')).unpack('Z*').pack('Z*')}" - process_table[pe.th32ParentProcessID] ||= [] - process_table[pe.th32ParentProcessID] << pe.th32ProcessID - break if DL.Process32NextW(h_snap, pe).zero? - end - process_table[DL.GetCurrentProcessId].each do |child_pid| - kill_process_tree(process_table, child_pid) - end - #r = DL.TerminateThread(@pi.hThread, 0) - #error_message(r, "TerminateThread") - #sleep @wait - #r = DL.FreeConsole() - #error_message(r, "FreeConsole") - #r = DL.AttachConsole(DL::ATTACH_PARENT_PROCESS) - #error_message(r, 'AttachConsole') - end - - private def kill_process_tree(process_table, pid) - process_table[pid]&.each do |child_pid| - kill_process_tree(process_table, child_pid) - end - h_proc = DL.OpenProcess(DL::PROCESS_ALL_ACCESS, 0, pid) - if (h_proc) - r = DL.TerminateProcess(h_proc, 0) - # error_message(r, "TerminateProcess") - r = DL.CloseHandle(h_proc) - error_message(r, "CloseHandle") - end + system("taskkill.exe", "/PID", "#{@pid}", {[:out, :err] => "NUL"}) + system("taskkill.exe", "/PID", "#{@console_process_info.dwProcessId}", {[:out, :err] => "NUL"}) + r = DL.CloseHandle(@console_process_info.hProcess) + error_message(r, "CloseHandle(hProcess)") + r = DL.CloseHandle(@console_process_info.hThread) + error_message(r, "CloseHandle(hThread)") end def close @@ -694,6 +633,7 @@ def start_terminal(height, width, command, wait: 0.01, timeout: 2, startup_messa convert_proc.call(@result) else loop do + @target.sync screen = convert_proc.call(retrieve_screen) break screen if Time.now >= retry_until break screen if check_proc.call(screen) From 065edb4b931ce58f93f91e59ee1652520338899f Mon Sep 17 00:00:00 2001 From: YO4 Date: Thu, 3 Oct 2024 23:20:42 +0900 Subject: [PATCH 06/76] more WindowsDefinition --- lib/yamatanooroti/windows.rb | 359 +++++++++++++++++------------------ 1 file changed, 173 insertions(+), 186 deletions(-) diff --git a/lib/yamatanooroti/windows.rb b/lib/yamatanooroti/windows.rb index 6f8ccf2..8d021c9 100644 --- a/lib/yamatanooroti/windows.rb +++ b/lib/yamatanooroti/windows.rb @@ -11,20 +11,13 @@ module Yamatanooroti::WindowsDefinition FREE = Fiddle::Function.new(Fiddle::RUBY_FREE, [Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOID) typealias 'SHORT', 'short' - typealias 'HPCON', 'HANDLE' typealias 'HWND', 'HANDLE' - typealias 'HRESULT', 'HANDLE' typealias 'LPVOID', 'void*' - typealias 'SIZE_T', 'size_t' typealias 'LPWSTR', 'void*' typealias 'LPBYTE', 'void*' typealias 'LPCWSTR', 'void*' - typealias 'LPPROC_THREAD_ATTRIBUTE_LIST', 'void*' - typealias 'PSIZE_T', 'void*' - typealias 'DWORD_PTR', 'void*' typealias 'LPCVOID', 'void*' typealias 'LPDWORD', 'void*' - typealias 'LPOVERLAPPED', 'void*' typealias 'WCHAR', 'unsigned short' typealias 'LPCWCH', 'void*' typealias 'LPSTR', 'void*' @@ -35,9 +28,6 @@ module Yamatanooroti::WindowsDefinition typealias 'LONG', 'int' typealias 'HLOCAL', 'HANDLE' - Fiddle::SIZEOF_HANDLE = Fiddle::SIZEOF_LONG - Fiddle::SIZEOF_HPCON = Fiddle::SIZEOF_LONG - Fiddle::SIZEOF_HRESULT = Fiddle::SIZEOF_LONG Fiddle::SIZEOF_DWORD = Fiddle::SIZEOF_LONG Fiddle::SIZEOF_WORD = Fiddle::SIZEOF_SHORT @@ -53,7 +43,6 @@ module Yamatanooroti::WindowsDefinition 'SHORT Right', 'SHORT Bottom' ] - typealias 'SMALL_RECT*', 'DWORD64*' typealias 'PSMALL_RECT', 'SMALL_RECT*' CONSOLE_SCREEN_BUFFER_INFO = struct [ @@ -83,7 +72,6 @@ module Yamatanooroti::WindowsDefinition 'LPVOID lpSecurityDescriptor', 'BOOL bInheritHandle' ] - typealias 'PSECURITY_ATTRIBUTES', 'SECURITY_ATTRIBUTES*' typealias 'LPSECURITY_ATTRIBUTES', 'SECURITY_ATTRIBUTES*' STARTUPINFOW = struct [ @@ -114,7 +102,6 @@ module Yamatanooroti::WindowsDefinition 'DWORD dwProcessId', 'DWORD dwThreadId' ] - typealias 'PPROCESS_INFORMATION', 'PROCESS_INFORMATION*' typealias 'LPPROCESS_INFORMATION', 'PROCESS_INFORMATION*' INPUT_RECORD_WITH_KEY_EVENT = struct [ @@ -139,19 +126,13 @@ module Yamatanooroti::WindowsDefinition SW_SHOWNOACTIVE = 4 LEFT_ALT_PRESSED = 0x0002 - # HANDLE GetStdHandle(DWORD nStdHandle); - extern 'HANDLE GetStdHandle(DWORD);', :stdcall # BOOL CloseHandle(HANDLE hObject); extern 'BOOL CloseHandle(HANDLE);', :stdcall # BOOL FreeConsole(void); extern 'BOOL FreeConsole(void);', :stdcall - # BOOL AllocConsole(void); - extern 'BOOL AllocConsole(void);', :stdcall # BOOL AttachConsole(DWORD dwProcessId); extern 'BOOL AttachConsole(DWORD);', :stdcall - # BOOL ShowWindow(HWND hWnd, int nCmdShow); - extern 'BOOL ShowWindow(HWND hWnd,int nCmdShow);', :stdcall # HWND WINAPI GetConsoleWindow(void); extern 'HWND GetConsoleWindow(void);', :stdcall # BOOL WINAPI SetConsoleWindowInfo(HANDLE hConsoleOutput, BOOL bAbsolute, const SMALL_RECT *lpConsoleWindow); @@ -197,9 +178,11 @@ module Yamatanooroti::WindowsDefinition FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100 FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000 - private def error_message(err, method_name) - string = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP) - FormatMessage( + private def error_message(r, method_name, exception: true) + return if not r.zero? + err = Fiddle.win32_last_error + string = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP, FREE) + n = FormatMessageW( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, Fiddle::NULL, err, @@ -208,24 +191,30 @@ module Yamatanooroti::WindowsDefinition 0, Fiddle::NULL ) - str = string.ptr.to_s - LocalFree(string) - str.force_encoding("Windows-31J") - puts "ERROR(#{method_name}): #{err.to_s}: #{string.ptr.to_s.force_encoding("locale")}" + if n > 0 + str = wc2mb(string.ptr[0, n * 2]) + LocalFree(string) + msg = "ERROR(#{method_name}): #{err.to_s}: #{str}" + end + if exception + raise msg + else + $stderr.puts msg + end end def get_console_screen_buffer_info(handle) - csbi = CONSOLE_SCREEN_BUFFER_INFO.malloc + csbi = CONSOLE_SCREEN_BUFFER_INFO.malloc(FREE) r = GetConsoleScreenBufferInfo(handle, csbi) - puts error_message(r, 'GetConsoleScreenBufferInfo') if r == 0 - r == 0 ? nil : csbi + error_message(r, 'GetConsoleScreenBufferInfo') + return csbi end def set_console_screen_buffer_info_ex(handle, h, w, buffer_height) - csbi = CONSOLE_SCREEN_BUFFER_INFOEX.malloc + csbi = CONSOLE_SCREEN_BUFFER_INFOEX.malloc(FREE) csbi.cbSize = CONSOLE_SCREEN_BUFFER_INFOEX.size r = GetConsoleScreenBufferInfoEx(handle, csbi) - error_message(r, 'GetConsoleScreenBufferSize') if r == 0 + error_message(r, 'GetConsoleScreenBufferSize') csbi.dwSize_X = w csbi.dwSize_Y = buffer_height csbi.Left = 0 @@ -233,18 +222,18 @@ def set_console_screen_buffer_info_ex(handle, h, w, buffer_height) csbi.Top = [csbi.Top, buffer_height - h].min csbi.Bottom = csbi.Top + h - 1 r = SetConsoleScreenBufferInfoEx(handle, csbi) - puts error_message(r, 'SetConsoleScreenBufferInfoEx') if r == 0 + error_message(r, 'SetConsoleScreenBufferInfoEx') return r != 0 end def set_console_window_info(handle, h, w) - rect = SMALL_RECT.malloc + rect = SMALL_RECT.malloc(FREE) rect.Left = 0 rect.Top = 0 rect.Right = w - 1 rect.Bottom = h - 1 r = SetConsoleWindowInfo(handle, 1, rect) - puts error_message(r, 'SetConsoleWindowInfo') if r == 0 + error_message(r, 'SetConsoleWindowInfo') return r != 0 end @@ -261,95 +250,159 @@ def set_console_window_size(handle, h, w) return true end + def create_console_file_handle(name) + fh = CreateFileA( + name, + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nil, + OPEN_EXISTING, + 0, + 0 + ) + fh = [fh].pack("J").unpack1("J") + error_message(0, name) if fh == INVALID_HANDLE_VALUE + fh + end + + def close_handle(handle) + r = CloseHandle(handle) + error_message(r, "CloseHandle") + return r != 0 + end + + def free_console + r = FreeConsole() + error_message(r, "FreeConsole") + return r != 0 + end + + def attach_console(pid = ATTACH_PARENT_PROCESS, maybe_fail: false) + r = AttachConsole(pid) + error_message(r, 'AttachConsole') unless maybe_fail + return r != 0 + end + + def create_console(command) + converted_command = mb2wc("#{command}\0") + console_process_info = PROCESS_INFORMATION.malloc(FREE) + console_process_info.to_ptr[0, PROCESS_INFORMATION.size] = "\0".b * PROCESS_INFORMATION.size + startup_info = STARTUPINFOW.malloc(FREE) + startup_info.to_ptr[0, STARTUPINFOW.size] = "\0".b * STARTUPINFOW.size + startup_info.cb = STARTUPINFOW.size + if false + startup_info.dwFlags = STARTF_USESHOWWINDOW + startup_info.wShowWindow = SW_SHOWNOACTIVE + else + startup_info.dwFlags = STARTF_USESHOWWINDOW + startup_info.wShowWindow = SW_HIDE + end + + r = CreateProcessW( + Fiddle::NULL, converted_command, + Fiddle::NULL, Fiddle::NULL, + 0, + CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT, + Fiddle::NULL, Fiddle::NULL, + startup_info, console_process_info + ) + error_message(r, 'CreateProcessW') + console_process_info + end + + def mb2wc(str) + size = MultiByteToWideChar(65001, 0, str, str.bytesize, '', 0) + converted_str = "\x00".b * (size * 2) + MultiByteToWideChar(65001, 0, str, str.bytesize, converted_str, size) + converted_str + end + + def wc2mb(str) + size = WideCharToMultiByte(65001, 0, str, str.bytesize / 2, '', 0, 0, 0) + converted_str = "\x00".b * size + WideCharToMultiByte(65001, 0, str, str.bytesize / 2, converted_str, converted_str.bytesize, 0, 0) + converted_str.force_encoding("UTF-8") + end + + def read_console_output(handle, row, width) + buffer_chars = width * 8 + buffer = "\0".b * Fiddle::SIZEOF_SHORT * buffer_chars + n = "\0".b * Fiddle::SIZEOF_DWORD + r = ReadConsoleOutputCharacterW(handle, buffer, width, row << 16, n) + error_message(r, "ReadConsoleOutputCharacterW") + return wc2mb(buffer[0, n.unpack1("L") * 2]).gsub(/ *$/, "") + end + + def set_input_record(r, code) + r.EventType = KEY_EVENT + # r.bKeyDown = 1 + r.wRepeatCount = 1 + r.dwControlKeyState = code < 0 ? LEFT_ALT_PRESSED : 0 + code = code.abs + r.wVirtualKeyCode = VkKeyScanW(code) + r.wVirtualScanCode = MapVirtualKeyW(code, 0) + r.UnicodeChar = code + return r + end + + def write_console_input(handle, records, n) + written = "\0".b * Fiddle::SIZEOF_DWORD + r = WriteConsoleInputW(handle, records, n, written) + error_message(r, 'WriteConsoleInput') + return written.unpack1('L') + end + + def get_number_of_console_input_events(handle) + n = "\0".b * Fiddle::SIZEOF_DWORD + r = GetNumberOfConsoleInputEvents(handle, n) + error_message(r, 'GetNumberOfConsoleInputEvents') + return n.unpack1('L') + end + extend self end module Yamatanooroti::WindowsTestCaseModule DL = Yamatanooroti::WindowsDefinition - private def attach_terminal + private def attach_terminal(open = true) stderr = $stderr $stderr = StringIO.new conin = conout = nil - r = DL.FreeConsole() - error_message(r, "FreeConsole") - r = DL.AttachConsole(@console_process_info.dwProcessId) + DL.free_console # this can be fail while new process is starting - # error_message(r, 'AttachConsole') - return nil if r.zero? - conin = DL.CreateFileA( - "conin$", - DL::GENERIC_READ | DL::GENERIC_WRITE, - DL::FILE_SHARE_READ | DL::FILE_SHARE_WRITE, - nil, - DL::OPEN_EXISTING, - 0, - 0 - ) - error_message(conin.to_i == DL::INVALID_HANDLE_VALUE ? 0 : 1, "conin$") - return nil if conin.to_i == DL::INVALID_HANDLE_VALUE - conout = DL.CreateFileA( - "conout$", - DL::GENERIC_READ | DL::GENERIC_WRITE, - DL::FILE_SHARE_READ | DL::FILE_SHARE_WRITE, - nil, - DL::OPEN_EXISTING, - 0, - 0 - ) - error_message(conout.to_i == DL::INVALID_HANDLE_VALUE ? 0 : 1, "conout$") - return nil if conout.to_i == DL::INVALID_HANDLE_VALUE - yield(conin.to_i, conout.to_i) + r = DL.attach_console(@console_process_info.dwProcessId, maybe_fail: true) + return nil unless r + + if open + conin = DL.create_console_file_handle("conin$") + return nil if conin == DL::INVALID_HANDLE_VALUE + + conout = DL.create_console_file_handle("conout$") + return nil if conout == DL::INVALID_HANDLE_VALUE + end + + yield(conin, conout) rescue => evar ensure - if conin != nil && conin.to_i != DL::INVALID_HANDLE_VALUE - r = DL.CloseHandle(conin) - error_message(r, "CloseHandle") - end - if conout != nil && conout.to_i != DL::INVALID_HANDLE_VALUE - r = DL.CloseHandle(conout) - error_message(r, "CloseHandle") - end - r = DL.FreeConsole() - error_message(r, "FreeConsole") - r = DL.AttachConsole(DL::ATTACH_PARENT_PROCESS) - error_message(r, 'AttachConsole') - stderr.write $stderr.read + DL.close_handle(conin) if conin && conin != DL::INVALID_HANDLE_VALUE + DL.close_handle(conout) if conout && conout != DL::INVALID_HANDLE_VALUE + DL.free_console + DL.attach_console + stderr.write $stderr.string $stderr = stderr raise evar if evar end private def setup_console(height, width) - command = %q[ruby.exe --disable=gems -e sleep"] # 'DO NOTHING JUST STAY THERE' CONSOLE KEEPING PROCESS - converted_command = mb2wc("#{command}\x00") - @console_process_info = DL::PROCESS_INFORMATION.malloc - @console_process_info.to_ptr[0, DL::PROCESS_INFORMATION.size] = "\x00".b * DL::PROCESS_INFORMATION.size - startup_info = DL::STARTUPINFOW.malloc - (startup_info.to_ptr + 0)[0, DL::STARTUPINFOW.size] = "\x00".b * DL::STARTUPINFOW.size - startup_info.cb = DL::STARTUPINFOW.size - if ENV['YAMATANOOROTI_SHOW_WINDOW'] - startup_info.dwFlags = DL::STARTF_USESHOWWINDOW - startup_info.wShowWindow = DL::SW_SHOWNOACTIVE - else - startup_info.dwFlags = DL::STARTF_USESHOWWINDOW - startup_info.wShowWindow = DL::SW_HIDE - end - - r = DL.CreateProcessW( - Fiddle::NULL, converted_command, - Fiddle::NULL, Fiddle::NULL, - 0, - DL::CREATE_NEW_CONSOLE | DL::CREATE_UNICODE_ENVIRONMENT, - Fiddle::NULL, Fiddle::NULL, - startup_info, @console_process_info - ) - error_message(r, 'CreateProcessW') + command = %q[ruby.exe --disable=gems -e sleep"] # console keeping process + @console_process_info = DL.create_console(command) # wait for console startup complete 8.times do |n| break if attach_terminal { true } - sleep 0.02 * 2**n + sleep 0.01 * 2**n end attach_terminal do |conin, conout| @@ -357,20 +410,6 @@ module Yamatanooroti::WindowsTestCaseModule end end - private def mb2wc(str) - size = DL.MultiByteToWideChar(65001, 0, str, str.bytesize, '', 0) - converted_str = String.new("\x00" * (size * 2), encoding: 'ASCII-8BIT') - DL.MultiByteToWideChar(65001, 0, str, str.bytesize, converted_str, size) - converted_str - end - - private def wc2mb(str) - size = DL.WideCharToMultiByte(65001, 0, str, str.bytesize / 2, '', 0, 0, 0) - converted_str = "\x00" * size - DL.WideCharToMultiByte(65001, 0, str, str.bytesize / 2, converted_str, converted_str.bytesize, 0, 0) - converted_str - end - private def quote_command_arg(arg) if not arg.match?(/[ \t"<>|()]/) # No quotation needed. @@ -465,88 +504,42 @@ def sync @codepage_success_p end - private def error_message(r, method_name) - return if not r.zero? - err = Fiddle.win32_last_error - string = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP) - n = DL.FormatMessageW( - DL::FORMAT_MESSAGE_ALLOCATE_BUFFER | DL::FORMAT_MESSAGE_FROM_SYSTEM, - Fiddle::NULL, - err, - 0x0, - string, - 0, - Fiddle::NULL - ) - if n > 0 - str = wc2mb(string.ptr[0, n * 2]) - LocalFree(string) - $stderr.puts "ERROR(#{method_name}): #{err.to_s}: #{str}" - end - end - - private def log(str) - $stderr.puts str - end - def write(str) - sleep @wait - records = Fiddle::Pointer.malloc(DL::INPUT_RECORD_WITH_KEY_EVENT.size * str.size * 2, DL::FREE) - str.chars.each_with_index do |c, i| + codes = str.chars.map do |c| c = "\r" if c == "\n" byte = c.getbyte(0) if c.bytesize == 1 and byte.allbits?(0x80) # with Meta key - c = (byte ^ 0x80).chr - control_key_state = DL::LEFT_ALT_PRESSED + [-(byte ^ 0x80)] else - control_key_state = 0 + DL.mb2wc(c).unpack("S*") end - record_index = i * 2 - r = DL::INPUT_RECORD_WITH_KEY_EVENT.new(records + DL::INPUT_RECORD_WITH_KEY_EVENT.size * record_index) - set_input_record(r, c, true, control_key_state) - record_index = i * 2 + 1 - r = DL::INPUT_RECORD_WITH_KEY_EVENT.new(records + DL::INPUT_RECORD_WITH_KEY_EVENT.size * record_index) - set_input_record(r, c, false, control_key_state) + end.flatten + record = DL::INPUT_RECORD_WITH_KEY_EVENT.malloc(DL::FREE) + records = codes.reduce("".b) do |records, code| + DL.set_input_record(record, code) + record.bKeyDown = 1 + records << record.to_ptr.to_str + record.bKeyDown = 0 + records << record.to_ptr.to_str end - written_size = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DWORD, DL::FREE) attach_terminal do |conin, conout| - r = DL.WriteConsoleInputW(conin, records, str.size * 2, written_size) - error_message(r, 'WriteConsoleInput') - - n = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DWORD, DL::FREE) + DL.write_console_input(conin, records, codes.size * 2) loop do - sleep 0.02 - r = DL.GetNumberOfConsoleInputEvents(conin, n) - error_message(r, 'GetNumberOfConsoleInputEvents') - break if n.to_str.unpack1("L") <= 1 # key up record still remains + sleep @wait + n = DL.get_number_of_console_input_events(conin) + break if n <= 1 # maybe keyup event still be there + break if n.nil? @target.sync break if @target.closed? end end end - private def set_input_record(r, c, key_down, control_key_state) - begin - code = c.unpack('U').first - rescue ArgumentError - code = c.bytes.first - end - r.EventType = DL::KEY_EVENT - r.bKeyDown = key_down ? 1 : 0 - r.wRepeatCount = 1 - r.wVirtualKeyCode = DL.VkKeyScanW(code) - r.wVirtualScanCode = DL.MapVirtualKeyW(code, 0) - r.UnicodeChar = code - r.dwControlKeyState = control_key_state - end - private def free_resources system("taskkill.exe", "/PID", "#{@pid}", {[:out, :err] => "NUL"}) system("taskkill.exe", "/PID", "#{@console_process_info.dwProcessId}", {[:out, :err] => "NUL"}) - r = DL.CloseHandle(@console_process_info.hProcess) - error_message(r, "CloseHandle(hProcess)") - r = DL.CloseHandle(@console_process_info.hThread) - error_message(r, "CloseHandle(hThread)") + DL.close_handle(@console_process_info.hProcess) + DL.close_handle(@console_process_info.hThread) end def close @@ -575,13 +568,7 @@ def close n = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DWORD, DL::FREE) lines = attach_terminal do |conin, conout| (top..bottom).map do |y| - r = DL.ReadConsoleOutputCharacterW(conout, buffer, @width, y << 16, n) - error_message(r, "ReadConsoleOutputCharacterW") - if r != 0 - wc2mb(buffer[0, n.to_str.unpack1("L") * 2]).gsub(/ *$/, "") - else - "" - end + DL.read_console_output(conout, y, @width) || "" end end lines From ade0febd1fd7bd82a9fe31cce13b2e81b07302e7 Mon Sep 17 00:00:00 2001 From: YO4 Date: Sun, 6 Oct 2024 00:32:49 +0900 Subject: [PATCH 07/76] prepare windows/windows-definition.rb --- lib/yamatanooroti/{windows.rb => windows/windows-definition.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/yamatanooroti/{windows.rb => windows/windows-definition.rb} (100%) diff --git a/lib/yamatanooroti/windows.rb b/lib/yamatanooroti/windows/windows-definition.rb similarity index 100% rename from lib/yamatanooroti/windows.rb rename to lib/yamatanooroti/windows/windows-definition.rb From 81e681947f5cb42da17768f44ac5261bdf6dbf15 Mon Sep 17 00:00:00 2001 From: YO4 Date: Sun, 6 Oct 2024 00:32:50 +0900 Subject: [PATCH 08/76] prepare windows/conhost.rb --- lib/yamatanooroti/{windows.rb => windows/conhost.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/yamatanooroti/{windows.rb => windows/conhost.rb} (100%) diff --git a/lib/yamatanooroti/windows.rb b/lib/yamatanooroti/windows/conhost.rb similarity index 100% rename from lib/yamatanooroti/windows.rb rename to lib/yamatanooroti/windows/conhost.rb From 9835ae80d509f1c7bfc57d0e5cdb186cb0692009 Mon Sep 17 00:00:00 2001 From: YO4 Date: Sun, 6 Oct 2024 00:32:50 +0900 Subject: [PATCH 09/76] prepare windows/windows.rb --- lib/yamatanooroti/{ => windows}/windows.rb | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/yamatanooroti/{ => windows}/windows.rb (100%) diff --git a/lib/yamatanooroti/windows.rb b/lib/yamatanooroti/windows/windows.rb similarity index 100% rename from lib/yamatanooroti/windows.rb rename to lib/yamatanooroti/windows/windows.rb From df5c8b915faa7f413d1f3aee7042c58b8f94e580 Mon Sep 17 00:00:00 2001 From: YO4 Date: Sun, 6 Oct 2024 00:35:51 +0900 Subject: [PATCH 10/76] split windows.rb and prepareing to support Windows Terminal --- lib/yamatanooroti/windows.rb | 591 +--------------- lib/yamatanooroti/windows/conhost.rb | 651 +----------------- .../windows/windows-definition.rb | 317 +-------- lib/yamatanooroti/windows/windows.rb | 537 +-------------- 4 files changed, 75 insertions(+), 2021 deletions(-) diff --git a/lib/yamatanooroti/windows.rb b/lib/yamatanooroti/windows.rb index 8d021c9..74086b8 100644 --- a/lib/yamatanooroti/windows.rb +++ b/lib/yamatanooroti/windows.rb @@ -1,594 +1,33 @@ require 'test/unit' -require 'stringio' -require 'fiddle/import' -require 'fiddle/types' - -module Yamatanooroti::WindowsDefinition - extend Fiddle::Importer - dlload 'kernel32.dll', 'user32.dll' - include Fiddle::Win32Types - - FREE = Fiddle::Function.new(Fiddle::RUBY_FREE, [Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOID) - - typealias 'SHORT', 'short' - typealias 'HWND', 'HANDLE' - typealias 'LPVOID', 'void*' - typealias 'LPWSTR', 'void*' - typealias 'LPBYTE', 'void*' - typealias 'LPCWSTR', 'void*' - typealias 'LPCVOID', 'void*' - typealias 'LPDWORD', 'void*' - typealias 'WCHAR', 'unsigned short' - typealias 'LPCWCH', 'void*' - typealias 'LPSTR', 'void*' - typealias 'LPCCH', 'void*' - typealias 'LPBOOL', 'void*' - typealias 'LPWORD', 'void*' - typealias 'ULONG_PTR', 'ULONG*' - typealias 'LONG', 'int' - typealias 'HLOCAL', 'HANDLE' - - Fiddle::SIZEOF_DWORD = Fiddle::SIZEOF_LONG - Fiddle::SIZEOF_WORD = Fiddle::SIZEOF_SHORT - - COORD = struct [ - 'SHORT X', - 'SHORT Y' - ] - typealias 'COORD', 'DWORD32' - - SMALL_RECT = struct [ - 'SHORT Left', - 'SHORT Top', - 'SHORT Right', - 'SHORT Bottom' - ] - typealias 'PSMALL_RECT', 'SMALL_RECT*' - - CONSOLE_SCREEN_BUFFER_INFO = struct [ - 'SHORT dwSize_X', 'SHORT dwSize_Y', # 'COORD dwSize', - 'SHORT dwCursorPosition_X', 'SHORT dwCursorPosition_Y', #'COORD dwCursorPosition', - 'WORD wAttributes', - 'SHORT Left', 'SHORT Top', 'SHORT Right', 'SHORT Bottom', # 'SMALL_RECT srWindow', - 'SHORT MaxWidth', 'SHORT MaxHeight' # 'COORD dwMaximumWindowSize' - ] - typealias 'PCONSOLE_SCREEN_BUFFER_INFO', 'CONSOLE_SCREEN_BUFFER_INFO*' - - typealias 'COLORREF', 'DWORD' - CONSOLE_SCREEN_BUFFER_INFOEX = struct [ - 'ULONG cbSize', - 'SHORT dwSize_X', 'SHORT dwSize_Y', # 'COORD dwSize', - 'SHORT dwCursorPosition_X', 'SHORT dwCursorPosition_Y', #'COORD dwCursorPosition', - 'WORD wAttributes', - 'SHORT Left', 'SHORT Top', 'SHORT Right', 'SHORT Bottom', # 'SMALL_RECT srWindow', - 'SHORT MaxWidth', 'SHORT MaxHeight', # 'COORD dwMaximumWindowSize', - 'BOOL bFullScreenSupported', - 'COLORREF ColorTable[16]' - ] - typealias 'PCONSOLE_SCREEN_BUFFER_INFOEX', 'CONSOLE_SCREEN_BUFFER_INFOEX*' - - SECURITY_ATTRIBUTES = struct [ - 'DWORD nLength', - 'LPVOID lpSecurityDescriptor', - 'BOOL bInheritHandle' - ] - typealias 'LPSECURITY_ATTRIBUTES', 'SECURITY_ATTRIBUTES*' - - STARTUPINFOW = struct [ - 'DWORD cb', - 'LPWSTR lpReserved', - 'LPWSTR lpDesktop', - 'LPWSTR lpTitle', - 'DWORD dwX', - 'DWORD dwY', - 'DWORD dwXSize', - 'DWORD dwYSize', - 'DWORD dwXCountChars', - 'DWORD dwYCountChars', - 'DWORD dwFillAttribute', - 'DWORD dwFlags', - 'WORD wShowWindow', - 'WORD cbReserved2', - 'LPBYTE lpReserved2', - 'HANDLE hStdInput', - 'HANDLE hStdOutput', - 'HANDLE hStdError' - ] - typealias 'LPSTARTUPINFOW', 'STARTUPINFOW*' - - PROCESS_INFORMATION = struct [ - 'HANDLE hProcess', - 'HANDLE hThread', - 'DWORD dwProcessId', - 'DWORD dwThreadId' - ] - typealias 'LPPROCESS_INFORMATION', 'PROCESS_INFORMATION*' - - INPUT_RECORD_WITH_KEY_EVENT = struct [ - 'WORD EventType', - 'BOOL bKeyDown', - 'WORD wRepeatCount', - 'WORD wVirtualKeyCode', - 'WORD wVirtualScanCode', - 'WCHAR UnicodeChar', - ## union 'CHAR AsciiChar', - 'DWORD dwControlKeyState' - ] - - STARTF_USESHOWWINDOW = 1 - CREATE_NEW_CONSOLE = 0x10 - CREATE_NEW_PROCESS_GROUP = 0x200 - CREATE_UNICODE_ENVIRONMENT = 0x400 - CREATE_NO_WINDOW = 0x08000000 - ATTACH_PARENT_PROCESS = -1 - KEY_EVENT = 0x0001 - SW_HIDE = 0 - SW_SHOWNOACTIVE = 4 - LEFT_ALT_PRESSED = 0x0002 - - # BOOL CloseHandle(HANDLE hObject); - extern 'BOOL CloseHandle(HANDLE);', :stdcall - - # BOOL FreeConsole(void); - extern 'BOOL FreeConsole(void);', :stdcall - # BOOL AttachConsole(DWORD dwProcessId); - extern 'BOOL AttachConsole(DWORD);', :stdcall - # HWND WINAPI GetConsoleWindow(void); - extern 'HWND GetConsoleWindow(void);', :stdcall - # BOOL WINAPI SetConsoleWindowInfo(HANDLE hConsoleOutput, BOOL bAbsolute, const SMALL_RECT *lpConsoleWindow); - extern 'BOOL SetConsoleWindowInfo(HANDLE, BOOL, PSMALL_RECT);', :stdcall - # BOOL WriteConsoleInputW(HANDLE hConsoleInput, const INPUT_RECORD *lpBuffer, DWORD nLength, LPDWORD lpNumberOfEventsWritten); - extern 'BOOL WriteConsoleInputW(HANDLE, const INPUT_RECORD*, DWORD, LPDWORD);', :stdcall - # SHORT VkKeyScanW(WCHAR ch); - extern 'SHORT VkKeyScanW(WCHAR);', :stdcall - # UINT MapVirtualKeyW(UINT uCode, UINT uMapType); - extern 'UINT MapVirtualKeyW(UINT, UINT);', :stdcall - # BOOL GetNumberOfConsoleInputEvents(HANDLE hConsoleInput, LPDWORD lpcNumberOfEvents); - extern 'BOOL GetNumberOfConsoleInputEvents(HANDLE hConsoleInput, LPDWORD lpcNumberOfEvents);', :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 GetConsoleScreenBufferInfoEx(HANDLE hConsoleOutput, PCONSOLE_SCREEN_BUFFER_INFOEX lpConsoleScreenBufferInfoEx); - extern 'BOOL GetConsoleScreenBufferInfoEx(HANDLE, PCONSOLE_SCREEN_BUFFER_INFOEX);', :stdcall - # BOOL WINAPI SetConsoleScreenBufferInfoEx(HANDLE hConsoleOutput, PCONSOLE_SCREEN_BUFFER_INFOEX lpConsoleScreenBufferInfoEx); - extern 'BOOL SetConsoleScreenBufferInfoEx(HANDLE, PCONSOLE_SCREEN_BUFFER_INFOEX);', :stdcall - - # BOOL CreateProcessW(LPCWSTR lpApplicationName, LPWSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation); - extern 'BOOL CreateProcessW(LPCWSTR lpApplicationName, LPWSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation);', :stdcall - - # int MultiByteToWideChar(UINT CodePage, DWORD dwFlags, LPCSTR lpMultiByteStr, int cbMultiByte, LPWSTR lpWideCharStr, int cchWideChar); - 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 - - # HANDLE CreateFileA(LPCSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile); - extern 'HANDLE CreateFileA(LPCSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE);', :stdcall - GENERIC_READ = 0x80000000 - GENERIC_WRITE = 0x40000000 - FILE_SHARE_READ = 0x00000001 - FILE_SHARE_WRITE = 0x00000002 - OPEN_EXISTING = 3 - INVALID_HANDLE_VALUE = 0xffffffff - - # DWORD FormatMessageW(DWORD dwFlags, LPCVOID lpSource, DWORD dwMessageId, DWORD dwLanguageId, LPWSTR lpBuffer, DWORD nSize, va_list *Arguments); - extern 'DWORD FormatMessageW(DWORD dwFlags, LPCVOID lpSource, DWORD dwMessageId, DWORD dwLanguageId, LPWSTR lpBuffer, DWORD nSize, va_list *Arguments);', :stdcall - # HLOCAL LocalFree(HLOCAL hMem); - extern 'HLOCAL LocalFree(HLOCAL hMem);', :stdcall - FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100 - FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000 - - private def error_message(r, method_name, exception: true) - return if not r.zero? - err = Fiddle.win32_last_error - string = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP, FREE) - n = FormatMessageW( - FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, - Fiddle::NULL, - err, - 0x0, - string, - 0, - Fiddle::NULL - ) - if n > 0 - str = wc2mb(string.ptr[0, n * 2]) - LocalFree(string) - msg = "ERROR(#{method_name}): #{err.to_s}: #{str}" - end - if exception - raise msg - else - $stderr.puts msg - end - end - - def get_console_screen_buffer_info(handle) - csbi = CONSOLE_SCREEN_BUFFER_INFO.malloc(FREE) - r = GetConsoleScreenBufferInfo(handle, csbi) - error_message(r, 'GetConsoleScreenBufferInfo') - return csbi - end - - def set_console_screen_buffer_info_ex(handle, h, w, buffer_height) - csbi = CONSOLE_SCREEN_BUFFER_INFOEX.malloc(FREE) - csbi.cbSize = CONSOLE_SCREEN_BUFFER_INFOEX.size - r = GetConsoleScreenBufferInfoEx(handle, csbi) - error_message(r, 'GetConsoleScreenBufferSize') - csbi.dwSize_X = w - csbi.dwSize_Y = buffer_height - csbi.Left = 0 - csbi.Right = w - 1 - csbi.Top = [csbi.Top, buffer_height - h].min - csbi.Bottom = csbi.Top + h - 1 - r = SetConsoleScreenBufferInfoEx(handle, csbi) - error_message(r, 'SetConsoleScreenBufferInfoEx') - return r != 0 - end - - def set_console_window_info(handle, h, w) - rect = SMALL_RECT.malloc(FREE) - rect.Left = 0 - rect.Top = 0 - rect.Right = w - 1 - rect.Bottom = h - 1 - r = SetConsoleWindowInfo(handle, 1, rect) - error_message(r, 'SetConsoleWindowInfo') - return r != 0 - end - - def set_console_window_size(handle, h, w) - # expand buffer size to keep scrolled away lines - buffer_h = h + 100 - - r = set_console_screen_buffer_info_ex(handle, h, w, buffer_h) - return false unless r - - r = set_console_window_info(handle, h, w) - return false unless r - - return true - end - - def create_console_file_handle(name) - fh = CreateFileA( - name, - GENERIC_READ | GENERIC_WRITE, - FILE_SHARE_READ | FILE_SHARE_WRITE, - nil, - OPEN_EXISTING, - 0, - 0 - ) - fh = [fh].pack("J").unpack1("J") - error_message(0, name) if fh == INVALID_HANDLE_VALUE - fh - end - - def close_handle(handle) - r = CloseHandle(handle) - error_message(r, "CloseHandle") - return r != 0 - end - - def free_console - r = FreeConsole() - error_message(r, "FreeConsole") - return r != 0 - end - - def attach_console(pid = ATTACH_PARENT_PROCESS, maybe_fail: false) - r = AttachConsole(pid) - error_message(r, 'AttachConsole') unless maybe_fail - return r != 0 - end - - def create_console(command) - converted_command = mb2wc("#{command}\0") - console_process_info = PROCESS_INFORMATION.malloc(FREE) - console_process_info.to_ptr[0, PROCESS_INFORMATION.size] = "\0".b * PROCESS_INFORMATION.size - startup_info = STARTUPINFOW.malloc(FREE) - startup_info.to_ptr[0, STARTUPINFOW.size] = "\0".b * STARTUPINFOW.size - startup_info.cb = STARTUPINFOW.size - if false - startup_info.dwFlags = STARTF_USESHOWWINDOW - startup_info.wShowWindow = SW_SHOWNOACTIVE - else - startup_info.dwFlags = STARTF_USESHOWWINDOW - startup_info.wShowWindow = SW_HIDE - end - - r = CreateProcessW( - Fiddle::NULL, converted_command, - Fiddle::NULL, Fiddle::NULL, - 0, - CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT, - Fiddle::NULL, Fiddle::NULL, - startup_info, console_process_info - ) - error_message(r, 'CreateProcessW') - console_process_info - end - - def mb2wc(str) - size = MultiByteToWideChar(65001, 0, str, str.bytesize, '', 0) - converted_str = "\x00".b * (size * 2) - MultiByteToWideChar(65001, 0, str, str.bytesize, converted_str, size) - converted_str - end - - def wc2mb(str) - size = WideCharToMultiByte(65001, 0, str, str.bytesize / 2, '', 0, 0, 0) - converted_str = "\x00".b * size - WideCharToMultiByte(65001, 0, str, str.bytesize / 2, converted_str, converted_str.bytesize, 0, 0) - converted_str.force_encoding("UTF-8") - end - - def read_console_output(handle, row, width) - buffer_chars = width * 8 - buffer = "\0".b * Fiddle::SIZEOF_SHORT * buffer_chars - n = "\0".b * Fiddle::SIZEOF_DWORD - r = ReadConsoleOutputCharacterW(handle, buffer, width, row << 16, n) - error_message(r, "ReadConsoleOutputCharacterW") - return wc2mb(buffer[0, n.unpack1("L") * 2]).gsub(/ *$/, "") - end - - def set_input_record(r, code) - r.EventType = KEY_EVENT - # r.bKeyDown = 1 - r.wRepeatCount = 1 - r.dwControlKeyState = code < 0 ? LEFT_ALT_PRESSED : 0 - code = code.abs - r.wVirtualKeyCode = VkKeyScanW(code) - r.wVirtualScanCode = MapVirtualKeyW(code, 0) - r.UnicodeChar = code - return r - end - - def write_console_input(handle, records, n) - written = "\0".b * Fiddle::SIZEOF_DWORD - r = WriteConsoleInputW(handle, records, n, written) - error_message(r, 'WriteConsoleInput') - return written.unpack1('L') - end - - def get_number_of_console_input_events(handle) - n = "\0".b * Fiddle::SIZEOF_DWORD - r = GetNumberOfConsoleInputEvents(handle, n) - error_message(r, 'GetNumberOfConsoleInputEvents') - return n.unpack1('L') - end - - extend self -end +require_relative 'windows/windows-definition' +require_relative 'windows/windows' +require_relative 'windows/conhost' module Yamatanooroti::WindowsTestCaseModule - DL = Yamatanooroti::WindowsDefinition - - private def attach_terminal(open = true) - stderr = $stderr - $stderr = StringIO.new - - conin = conout = nil - DL.free_console - # this can be fail while new process is starting - r = DL.attach_console(@console_process_info.dwProcessId, maybe_fail: true) - return nil unless r - - if open - conin = DL.create_console_file_handle("conin$") - return nil if conin == DL::INVALID_HANDLE_VALUE - - conout = DL.create_console_file_handle("conout$") - return nil if conout == DL::INVALID_HANDLE_VALUE - end - - yield(conin, conout) - rescue => evar - ensure - DL.close_handle(conin) if conin && conin != DL::INVALID_HANDLE_VALUE - DL.close_handle(conout) if conout && conout != DL::INVALID_HANDLE_VALUE - DL.free_console - DL.attach_console - stderr.write $stderr.string - $stderr = stderr - raise evar if evar - end - - private def setup_console(height, width) - command = %q[ruby.exe --disable=gems -e sleep"] # console keeping process - @console_process_info = DL.create_console(command) - - # wait for console startup complete - 8.times do |n| - break if attach_terminal { true } - sleep 0.01 * 2**n - end - - attach_terminal do |conin, conout| - DL.set_console_window_size(conout, height, width) - end - end - - private def quote_command_arg(arg) - if not arg.match?(/[ \t"<>|()]/) - # No quotation needed. - return arg - end - - if not arg.match?(/["\\]/) - # No embedded double quotes or backlashes, so I can just wrap quote - # marks around the whole thing. - return %{"#{arg}"} - end - - quote_hit = true - result = +'"' - arg.chars.reverse.each do |c| - result << c - if quote_hit and c == '\\' - result << '\\' - elsif c == '"' - quote_hit = true - result << '\\' - else - quote_hit = false - end - end - result << '"' - result.reverse - end - - class SubProcess - def initialize(command) - @errin, err = IO.pipe - @pid = spawn(command, {in: ["conin$", File::RDWR | File::BINARY], out: ["conout$", File::RDWR | File::BINARY], err: err}) - err.close - @closed = false - @status = nil - @q = Thread::Queue.new - @t = Thread.new do - begin - err = @errin.gets - @q << err if err - rescue IOError - # target process already terminated - next - end - end - end - - def closed? - @closed ||= !(@status = Process.wait2(@pid, Process::WNOHANG)).nil? - end - - private def consume(buffer) - while !@q.empty? - buffer << @q.shift - end - end - - def ensure_close - @errin.close if !@errin.closed? - end - - def sync - buffer = "" - if closed? - @t.kill - @t.join - consume(buffer) - rest = "".b - while ((str = @errin.read_nonblock(1024, exception: false)).is_a?(String)) do - rest << str - end - buffer << rest.force_encoding(Encoding.default_external) << "\n" if rest != "" - else - consume(buffer) - end - $stderr.write buffer if buffer != "" - end - end - - private def launch(command) - attach_terminal do - SubProcess.new(command) - end - end - - private def setup_cp(cp) - @codepage_success_p = attach_terminal { system("chcp #{Integer(cp)} > NUL") } - end - - private def codepage_success? - @codepage_success_p - end - def write(str) - codes = str.chars.map do |c| - c = "\r" if c == "\n" - byte = c.getbyte(0) - if c.bytesize == 1 and byte.allbits?(0x80) # with Meta key - [-(byte ^ 0x80)] - else - DL.mb2wc(c).unpack("S*") - end - end.flatten - record = DL::INPUT_RECORD_WITH_KEY_EVENT.malloc(DL::FREE) - records = codes.reduce("".b) do |records, code| - DL.set_input_record(record, code) - record.bKeyDown = 1 - records << record.to_ptr.to_str - record.bKeyDown = 0 - records << record.to_ptr.to_str - end - attach_terminal do |conin, conout| - DL.write_console_input(conin, records, codes.size * 2) - loop do - sleep @wait - n = DL.get_number_of_console_input_events(conin) - break if n <= 1 # maybe keyup event still be there - break if n.nil? - @target.sync - break if @target.closed? - end - end - end - - private def free_resources - system("taskkill.exe", "/PID", "#{@pid}", {[:out, :err] => "NUL"}) - system("taskkill.exe", "/PID", "#{@console_process_info.dwProcessId}", {[:out, :err] => "NUL"}) - DL.close_handle(@console_process_info.hProcess) - DL.close_handle(@console_process_info.hThread) + @terminal.write(str) end def close - @target.sync - sleep @wait if !@target.closed? - # read first before kill the console process including output - @result = retrieve_screen - - free_resources - @target.sync - @target.ensure_close + @terminal.close end - private def retrieve_screen(top_of_buffer: false) - top, bottom = attach_terminal do |conin, conout| - csbi = DL.get_console_screen_buffer_info(conout) - if top_of_buffer - [0, csbi.Bottom] - else - [csbi.Top, csbi.Bottom] - end - end - - buffer_chars = @width * 8 - buffer = Fiddle::Pointer.malloc(Fiddle::SIZEOF_SHORT * buffer_chars, DL::FREE) - n = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DWORD, DL::FREE) - lines = attach_terminal do |conin, conout| - (top..bottom).map do |y| - DL.read_console_output(conout, y, @width) || "" - end - end - lines + def result + @terminal.result end - def result - @result || retrieve_screen + def codepage_success? + @terminal.codepage_success? end def start_terminal(height, width, command, wait: 0.01, timeout: 2, startup_message: nil, codepage: nil) @timeout = timeout @wait = wait @result = nil - @codepage_success_p - @height = height - @width = width - setup_console(height, width) - setup_cp(codepage) if codepage - @target = launch(command.map{ |c| quote_command_arg(c) }.join(' ')) + @terminal = Yamatanooroti::ConhostTerm.setup_console(height, width, @wait) + @terminal.setup_cp(codepage) if codepage + @terminal.launch(command) case startup_message when String @@ -607,8 +46,7 @@ def start_terminal(height, width, command, wait: 0.01, timeout: 2, startup_messa raise "Startup message didn't arrive within timeout: #{chunks.inspect}" end - @target.sync - chunks = retrieve_screen.join("\n").sub(/\n*\z/, "\n") + chunks = @terminal.retrieve_screen.join("\n").sub(/\n*\z/, "\n") break if yield chunks sleep @wait end @@ -620,8 +58,7 @@ def start_terminal(height, width, command, wait: 0.01, timeout: 2, startup_messa convert_proc.call(@result) else loop do - @target.sync - screen = convert_proc.call(retrieve_screen) + screen = convert_proc.call(@terminal.retrieve_screen) break screen if Time.now >= retry_until break screen if check_proc.call(screen) sleep @wait diff --git a/lib/yamatanooroti/windows/conhost.rb b/lib/yamatanooroti/windows/conhost.rb index 8d021c9..674d68b 100644 --- a/lib/yamatanooroti/windows/conhost.rb +++ b/lib/yamatanooroti/windows/conhost.rb @@ -1,403 +1,16 @@ -require 'test/unit' -require 'stringio' -require 'fiddle/import' -require 'fiddle/types' +class Yamatanooroti::ConhostTerm + include Yamatanooroti::WindowsTermMixin -module Yamatanooroti::WindowsDefinition - extend Fiddle::Importer - dlload 'kernel32.dll', 'user32.dll' - include Fiddle::Win32Types - - FREE = Fiddle::Function.new(Fiddle::RUBY_FREE, [Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOID) - - typealias 'SHORT', 'short' - typealias 'HWND', 'HANDLE' - typealias 'LPVOID', 'void*' - typealias 'LPWSTR', 'void*' - typealias 'LPBYTE', 'void*' - typealias 'LPCWSTR', 'void*' - typealias 'LPCVOID', 'void*' - typealias 'LPDWORD', 'void*' - typealias 'WCHAR', 'unsigned short' - typealias 'LPCWCH', 'void*' - typealias 'LPSTR', 'void*' - typealias 'LPCCH', 'void*' - typealias 'LPBOOL', 'void*' - typealias 'LPWORD', 'void*' - typealias 'ULONG_PTR', 'ULONG*' - typealias 'LONG', 'int' - typealias 'HLOCAL', 'HANDLE' - - Fiddle::SIZEOF_DWORD = Fiddle::SIZEOF_LONG - Fiddle::SIZEOF_WORD = Fiddle::SIZEOF_SHORT - - COORD = struct [ - 'SHORT X', - 'SHORT Y' - ] - typealias 'COORD', 'DWORD32' - - SMALL_RECT = struct [ - 'SHORT Left', - 'SHORT Top', - 'SHORT Right', - 'SHORT Bottom' - ] - typealias 'PSMALL_RECT', 'SMALL_RECT*' - - CONSOLE_SCREEN_BUFFER_INFO = struct [ - 'SHORT dwSize_X', 'SHORT dwSize_Y', # 'COORD dwSize', - 'SHORT dwCursorPosition_X', 'SHORT dwCursorPosition_Y', #'COORD dwCursorPosition', - 'WORD wAttributes', - 'SHORT Left', 'SHORT Top', 'SHORT Right', 'SHORT Bottom', # 'SMALL_RECT srWindow', - 'SHORT MaxWidth', 'SHORT MaxHeight' # 'COORD dwMaximumWindowSize' - ] - typealias 'PCONSOLE_SCREEN_BUFFER_INFO', 'CONSOLE_SCREEN_BUFFER_INFO*' - - typealias 'COLORREF', 'DWORD' - CONSOLE_SCREEN_BUFFER_INFOEX = struct [ - 'ULONG cbSize', - 'SHORT dwSize_X', 'SHORT dwSize_Y', # 'COORD dwSize', - 'SHORT dwCursorPosition_X', 'SHORT dwCursorPosition_Y', #'COORD dwCursorPosition', - 'WORD wAttributes', - 'SHORT Left', 'SHORT Top', 'SHORT Right', 'SHORT Bottom', # 'SMALL_RECT srWindow', - 'SHORT MaxWidth', 'SHORT MaxHeight', # 'COORD dwMaximumWindowSize', - 'BOOL bFullScreenSupported', - 'COLORREF ColorTable[16]' - ] - typealias 'PCONSOLE_SCREEN_BUFFER_INFOEX', 'CONSOLE_SCREEN_BUFFER_INFOEX*' - - SECURITY_ATTRIBUTES = struct [ - 'DWORD nLength', - 'LPVOID lpSecurityDescriptor', - 'BOOL bInheritHandle' - ] - typealias 'LPSECURITY_ATTRIBUTES', 'SECURITY_ATTRIBUTES*' - - STARTUPINFOW = struct [ - 'DWORD cb', - 'LPWSTR lpReserved', - 'LPWSTR lpDesktop', - 'LPWSTR lpTitle', - 'DWORD dwX', - 'DWORD dwY', - 'DWORD dwXSize', - 'DWORD dwYSize', - 'DWORD dwXCountChars', - 'DWORD dwYCountChars', - 'DWORD dwFillAttribute', - 'DWORD dwFlags', - 'WORD wShowWindow', - 'WORD cbReserved2', - 'LPBYTE lpReserved2', - 'HANDLE hStdInput', - 'HANDLE hStdOutput', - 'HANDLE hStdError' - ] - typealias 'LPSTARTUPINFOW', 'STARTUPINFOW*' - - PROCESS_INFORMATION = struct [ - 'HANDLE hProcess', - 'HANDLE hThread', - 'DWORD dwProcessId', - 'DWORD dwThreadId' - ] - typealias 'LPPROCESS_INFORMATION', 'PROCESS_INFORMATION*' - - INPUT_RECORD_WITH_KEY_EVENT = struct [ - 'WORD EventType', - 'BOOL bKeyDown', - 'WORD wRepeatCount', - 'WORD wVirtualKeyCode', - 'WORD wVirtualScanCode', - 'WCHAR UnicodeChar', - ## union 'CHAR AsciiChar', - 'DWORD dwControlKeyState' - ] - - STARTF_USESHOWWINDOW = 1 - CREATE_NEW_CONSOLE = 0x10 - CREATE_NEW_PROCESS_GROUP = 0x200 - CREATE_UNICODE_ENVIRONMENT = 0x400 - CREATE_NO_WINDOW = 0x08000000 - ATTACH_PARENT_PROCESS = -1 - KEY_EVENT = 0x0001 - SW_HIDE = 0 - SW_SHOWNOACTIVE = 4 - LEFT_ALT_PRESSED = 0x0002 - - # BOOL CloseHandle(HANDLE hObject); - extern 'BOOL CloseHandle(HANDLE);', :stdcall - - # BOOL FreeConsole(void); - extern 'BOOL FreeConsole(void);', :stdcall - # BOOL AttachConsole(DWORD dwProcessId); - extern 'BOOL AttachConsole(DWORD);', :stdcall - # HWND WINAPI GetConsoleWindow(void); - extern 'HWND GetConsoleWindow(void);', :stdcall - # BOOL WINAPI SetConsoleWindowInfo(HANDLE hConsoleOutput, BOOL bAbsolute, const SMALL_RECT *lpConsoleWindow); - extern 'BOOL SetConsoleWindowInfo(HANDLE, BOOL, PSMALL_RECT);', :stdcall - # BOOL WriteConsoleInputW(HANDLE hConsoleInput, const INPUT_RECORD *lpBuffer, DWORD nLength, LPDWORD lpNumberOfEventsWritten); - extern 'BOOL WriteConsoleInputW(HANDLE, const INPUT_RECORD*, DWORD, LPDWORD);', :stdcall - # SHORT VkKeyScanW(WCHAR ch); - extern 'SHORT VkKeyScanW(WCHAR);', :stdcall - # UINT MapVirtualKeyW(UINT uCode, UINT uMapType); - extern 'UINT MapVirtualKeyW(UINT, UINT);', :stdcall - # BOOL GetNumberOfConsoleInputEvents(HANDLE hConsoleInput, LPDWORD lpcNumberOfEvents); - extern 'BOOL GetNumberOfConsoleInputEvents(HANDLE hConsoleInput, LPDWORD lpcNumberOfEvents);', :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 GetConsoleScreenBufferInfoEx(HANDLE hConsoleOutput, PCONSOLE_SCREEN_BUFFER_INFOEX lpConsoleScreenBufferInfoEx); - extern 'BOOL GetConsoleScreenBufferInfoEx(HANDLE, PCONSOLE_SCREEN_BUFFER_INFOEX);', :stdcall - # BOOL WINAPI SetConsoleScreenBufferInfoEx(HANDLE hConsoleOutput, PCONSOLE_SCREEN_BUFFER_INFOEX lpConsoleScreenBufferInfoEx); - extern 'BOOL SetConsoleScreenBufferInfoEx(HANDLE, PCONSOLE_SCREEN_BUFFER_INFOEX);', :stdcall - - # BOOL CreateProcessW(LPCWSTR lpApplicationName, LPWSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation); - extern 'BOOL CreateProcessW(LPCWSTR lpApplicationName, LPWSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation);', :stdcall - - # int MultiByteToWideChar(UINT CodePage, DWORD dwFlags, LPCSTR lpMultiByteStr, int cbMultiByte, LPWSTR lpWideCharStr, int cchWideChar); - 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 - - # HANDLE CreateFileA(LPCSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile); - extern 'HANDLE CreateFileA(LPCSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE);', :stdcall - GENERIC_READ = 0x80000000 - GENERIC_WRITE = 0x40000000 - FILE_SHARE_READ = 0x00000001 - FILE_SHARE_WRITE = 0x00000002 - OPEN_EXISTING = 3 - INVALID_HANDLE_VALUE = 0xffffffff - - # DWORD FormatMessageW(DWORD dwFlags, LPCVOID lpSource, DWORD dwMessageId, DWORD dwLanguageId, LPWSTR lpBuffer, DWORD nSize, va_list *Arguments); - extern 'DWORD FormatMessageW(DWORD dwFlags, LPCVOID lpSource, DWORD dwMessageId, DWORD dwLanguageId, LPWSTR lpBuffer, DWORD nSize, va_list *Arguments);', :stdcall - # HLOCAL LocalFree(HLOCAL hMem); - extern 'HLOCAL LocalFree(HLOCAL hMem);', :stdcall - FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100 - FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000 - - private def error_message(r, method_name, exception: true) - return if not r.zero? - err = Fiddle.win32_last_error - string = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP, FREE) - n = FormatMessageW( - FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, - Fiddle::NULL, - err, - 0x0, - string, - 0, - Fiddle::NULL - ) - if n > 0 - str = wc2mb(string.ptr[0, n * 2]) - LocalFree(string) - msg = "ERROR(#{method_name}): #{err.to_s}: #{str}" - end - if exception - raise msg - else - $stderr.puts msg - end - end - - def get_console_screen_buffer_info(handle) - csbi = CONSOLE_SCREEN_BUFFER_INFO.malloc(FREE) - r = GetConsoleScreenBufferInfo(handle, csbi) - error_message(r, 'GetConsoleScreenBufferInfo') - return csbi - end - - def set_console_screen_buffer_info_ex(handle, h, w, buffer_height) - csbi = CONSOLE_SCREEN_BUFFER_INFOEX.malloc(FREE) - csbi.cbSize = CONSOLE_SCREEN_BUFFER_INFOEX.size - r = GetConsoleScreenBufferInfoEx(handle, csbi) - error_message(r, 'GetConsoleScreenBufferSize') - csbi.dwSize_X = w - csbi.dwSize_Y = buffer_height - csbi.Left = 0 - csbi.Right = w - 1 - csbi.Top = [csbi.Top, buffer_height - h].min - csbi.Bottom = csbi.Top + h - 1 - r = SetConsoleScreenBufferInfoEx(handle, csbi) - error_message(r, 'SetConsoleScreenBufferInfoEx') - return r != 0 - end - - def set_console_window_info(handle, h, w) - rect = SMALL_RECT.malloc(FREE) - rect.Left = 0 - rect.Top = 0 - rect.Right = w - 1 - rect.Bottom = h - 1 - r = SetConsoleWindowInfo(handle, 1, rect) - error_message(r, 'SetConsoleWindowInfo') - return r != 0 - end - - def set_console_window_size(handle, h, w) - # expand buffer size to keep scrolled away lines - buffer_h = h + 100 - - r = set_console_screen_buffer_info_ex(handle, h, w, buffer_h) - return false unless r - - r = set_console_window_info(handle, h, w) - return false unless r - - return true + def self.setup_console(height, width, wait) + new(height, width, wait) end - def create_console_file_handle(name) - fh = CreateFileA( - name, - GENERIC_READ | GENERIC_WRITE, - FILE_SHARE_READ | FILE_SHARE_WRITE, - nil, - OPEN_EXISTING, - 0, - 0 - ) - fh = [fh].pack("J").unpack1("J") - error_message(0, name) if fh == INVALID_HANDLE_VALUE - fh - end - - def close_handle(handle) - r = CloseHandle(handle) - error_message(r, "CloseHandle") - return r != 0 - end - - def free_console - r = FreeConsole() - error_message(r, "FreeConsole") - return r != 0 - end - - def attach_console(pid = ATTACH_PARENT_PROCESS, maybe_fail: false) - r = AttachConsole(pid) - error_message(r, 'AttachConsole') unless maybe_fail - return r != 0 - end - - def create_console(command) - converted_command = mb2wc("#{command}\0") - console_process_info = PROCESS_INFORMATION.malloc(FREE) - console_process_info.to_ptr[0, PROCESS_INFORMATION.size] = "\0".b * PROCESS_INFORMATION.size - startup_info = STARTUPINFOW.malloc(FREE) - startup_info.to_ptr[0, STARTUPINFOW.size] = "\0".b * STARTUPINFOW.size - startup_info.cb = STARTUPINFOW.size - if false - startup_info.dwFlags = STARTF_USESHOWWINDOW - startup_info.wShowWindow = SW_SHOWNOACTIVE - else - startup_info.dwFlags = STARTF_USESHOWWINDOW - startup_info.wShowWindow = SW_HIDE - end - - r = CreateProcessW( - Fiddle::NULL, converted_command, - Fiddle::NULL, Fiddle::NULL, - 0, - CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT, - Fiddle::NULL, Fiddle::NULL, - startup_info, console_process_info - ) - error_message(r, 'CreateProcessW') - console_process_info - end - - def mb2wc(str) - size = MultiByteToWideChar(65001, 0, str, str.bytesize, '', 0) - converted_str = "\x00".b * (size * 2) - MultiByteToWideChar(65001, 0, str, str.bytesize, converted_str, size) - converted_str - end - - def wc2mb(str) - size = WideCharToMultiByte(65001, 0, str, str.bytesize / 2, '', 0, 0, 0) - converted_str = "\x00".b * size - WideCharToMultiByte(65001, 0, str, str.bytesize / 2, converted_str, converted_str.bytesize, 0, 0) - converted_str.force_encoding("UTF-8") - end - - def read_console_output(handle, row, width) - buffer_chars = width * 8 - buffer = "\0".b * Fiddle::SIZEOF_SHORT * buffer_chars - n = "\0".b * Fiddle::SIZEOF_DWORD - r = ReadConsoleOutputCharacterW(handle, buffer, width, row << 16, n) - error_message(r, "ReadConsoleOutputCharacterW") - return wc2mb(buffer[0, n.unpack1("L") * 2]).gsub(/ *$/, "") - end - - def set_input_record(r, code) - r.EventType = KEY_EVENT - # r.bKeyDown = 1 - r.wRepeatCount = 1 - r.dwControlKeyState = code < 0 ? LEFT_ALT_PRESSED : 0 - code = code.abs - r.wVirtualKeyCode = VkKeyScanW(code) - r.wVirtualScanCode = MapVirtualKeyW(code, 0) - r.UnicodeChar = code - return r - end - - def write_console_input(handle, records, n) - written = "\0".b * Fiddle::SIZEOF_DWORD - r = WriteConsoleInputW(handle, records, n, written) - error_message(r, 'WriteConsoleInput') - return written.unpack1('L') - end - - def get_number_of_console_input_events(handle) - n = "\0".b * Fiddle::SIZEOF_DWORD - r = GetNumberOfConsoleInputEvents(handle, n) - error_message(r, 'GetNumberOfConsoleInputEvents') - return n.unpack1('L') - end - - extend self -end - -module Yamatanooroti::WindowsTestCaseModule - DL = Yamatanooroti::WindowsDefinition - - private def attach_terminal(open = true) - stderr = $stderr - $stderr = StringIO.new - - conin = conout = nil - DL.free_console - # this can be fail while new process is starting - r = DL.attach_console(@console_process_info.dwProcessId, maybe_fail: true) - return nil unless r - - if open - conin = DL.create_console_file_handle("conin$") - return nil if conin == DL::INVALID_HANDLE_VALUE - - conout = DL.create_console_file_handle("conout$") - return nil if conout == DL::INVALID_HANDLE_VALUE - end - - yield(conin, conout) - rescue => evar - ensure - DL.close_handle(conin) if conin && conin != DL::INVALID_HANDLE_VALUE - DL.close_handle(conout) if conout && conout != DL::INVALID_HANDLE_VALUE - DL.free_console - DL.attach_console - stderr.write $stderr.string - $stderr = stderr - raise evar if evar - end + def initialize(height, width, wait) + @wait = wait + @result = nil + @codepage_success_p = nil - private def setup_console(height, width) - command = %q[ruby.exe --disable=gems -e sleep"] # console keeping process - @console_process_info = DL.create_console(command) + @console_process_id = DL.create_console(CONSOLE_KEEPING_COMMAND) # wait for console startup complete 8.times do |n| @@ -410,250 +23,12 @@ module Yamatanooroti::WindowsTestCaseModule end end - private def quote_command_arg(arg) - if not arg.match?(/[ \t"<>|()]/) - # No quotation needed. - return arg - end - - if not arg.match?(/["\\]/) - # No embedded double quotes or backlashes, so I can just wrap quote - # marks around the whole thing. - return %{"#{arg}"} - end - - quote_hit = true - result = +'"' - arg.chars.reverse.each do |c| - result << c - if quote_hit and c == '\\' - result << '\\' - elsif c == '"' - quote_hit = true - result << '\\' - else - quote_hit = false - end - end - result << '"' - result.reverse - end - - class SubProcess - def initialize(command) - @errin, err = IO.pipe - @pid = spawn(command, {in: ["conin$", File::RDWR | File::BINARY], out: ["conout$", File::RDWR | File::BINARY], err: err}) - err.close - @closed = false - @status = nil - @q = Thread::Queue.new - @t = Thread.new do - begin - err = @errin.gets - @q << err if err - rescue IOError - # target process already terminated - next - end - end - end - - def closed? - @closed ||= !(@status = Process.wait2(@pid, Process::WNOHANG)).nil? - end - - private def consume(buffer) - while !@q.empty? - buffer << @q.shift - end - end - - def ensure_close - @errin.close if !@errin.closed? - end - - def sync - buffer = "" - if closed? - @t.kill - @t.join - consume(buffer) - rest = "".b - while ((str = @errin.read_nonblock(1024, exception: false)).is_a?(String)) do - rest << str - end - buffer << rest.force_encoding(Encoding.default_external) << "\n" if rest != "" - else - consume(buffer) - end - $stderr.write buffer if buffer != "" - end - end - - private def launch(command) - attach_terminal do - SubProcess.new(command) - end - end - - private def setup_cp(cp) - @codepage_success_p = attach_terminal { system("chcp #{Integer(cp)} > NUL") } - end - - private def codepage_success? - @codepage_success_p - end - - def write(str) - codes = str.chars.map do |c| - c = "\r" if c == "\n" - byte = c.getbyte(0) - if c.bytesize == 1 and byte.allbits?(0x80) # with Meta key - [-(byte ^ 0x80)] - else - DL.mb2wc(c).unpack("S*") - end - end.flatten - record = DL::INPUT_RECORD_WITH_KEY_EVENT.malloc(DL::FREE) - records = codes.reduce("".b) do |records, code| - DL.set_input_record(record, code) - record.bKeyDown = 1 - records << record.to_ptr.to_str - record.bKeyDown = 0 - records << record.to_ptr.to_str - end - attach_terminal do |conin, conout| - DL.write_console_input(conin, records, codes.size * 2) - loop do - sleep @wait - n = DL.get_number_of_console_input_events(conin) - break if n <= 1 # maybe keyup event still be there - break if n.nil? - @target.sync - break if @target.closed? - end - end - end - - private def free_resources - system("taskkill.exe", "/PID", "#{@pid}", {[:out, :err] => "NUL"}) - system("taskkill.exe", "/PID", "#{@console_process_info.dwProcessId}", {[:out, :err] => "NUL"}) - DL.close_handle(@console_process_info.hProcess) - DL.close_handle(@console_process_info.hThread) - end - def close - @target.sync - sleep @wait if !@target.closed? - # read first before kill the console process including output + @target.close @result = retrieve_screen - - free_resources - @target.sync - @target.ensure_close - end - - private def retrieve_screen(top_of_buffer: false) - top, bottom = attach_terminal do |conin, conout| - csbi = DL.get_console_screen_buffer_info(conout) - if top_of_buffer - [0, csbi.Bottom] - else - [csbi.Top, csbi.Bottom] - end + begin + Process.kill("KILL", @console_process_id) + rescue Errno::ESRCH # No such process end - - buffer_chars = @width * 8 - buffer = Fiddle::Pointer.malloc(Fiddle::SIZEOF_SHORT * buffer_chars, DL::FREE) - n = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DWORD, DL::FREE) - lines = attach_terminal do |conin, conout| - (top..bottom).map do |y| - DL.read_console_output(conout, y, @width) || "" - end - end - lines - end - - def result - @result || retrieve_screen end - - def start_terminal(height, width, command, wait: 0.01, timeout: 2, startup_message: nil, codepage: nil) - @timeout = timeout - @wait = wait - @result = nil - @codepage_success_p - - @height = height - @width = width - setup_console(height, width) - setup_cp(codepage) if codepage - @target = launch(command.map{ |c| quote_command_arg(c) }.join(' ')) - - case startup_message - when String - wait_startup_message { |message| message.start_with?(startup_message) } - when Regexp - wait_startup_message { |message| startup_message.match?(message) } - end - 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 - - @target.sync - chunks = retrieve_screen.join("\n").sub(/\n*\z/, "\n") - break if yield chunks - sleep @wait - end - end - - private def retryable_screen_assertion_with_proc(check_proc, assert_proc, convert_proc = :itself.to_proc) - retry_until = Time.now + @timeout - screen = if @result - convert_proc.call(@result) - else - loop do - @target.sync - screen = convert_proc.call(retrieve_screen) - break screen if Time.now >= retry_until - break screen if check_proc.call(screen) - sleep @wait - end - end - assert_proc.call(screen) - end - - def assert_screen(expected_lines, message = nil) - lines_to_string = ->(lines) { lines.join("\n").sub(/\n*\z/, "\n") } - case expected_lines - when Array - retryable_screen_assertion_with_proc( - ->(actual) { expected_lines == actual }, - ->(actual) { assert_equal(expected_lines, actual, message) } - ) - when String - retryable_screen_assertion_with_proc( - ->(actual) { expected_lines == actual }, - ->(actual) { assert_equal(expected_lines, actual, message) }, - lines_to_string - ) - when Regexp - retryable_screen_assertion_with_proc( - ->(actual) { expected_lines.match?(actual) }, - ->(actual) { assert_match(expected_lines, actual, message) }, - lines_to_string - ) - end - end -end - -class Yamatanooroti::WindowsTestCase < Test::Unit::TestCase - include Yamatanooroti::WindowsTestCaseModule end diff --git a/lib/yamatanooroti/windows/windows-definition.rb b/lib/yamatanooroti/windows/windows-definition.rb index 8d021c9..363e5ee 100644 --- a/lib/yamatanooroti/windows/windows-definition.rb +++ b/lib/yamatanooroti/windows/windows-definition.rb @@ -1,5 +1,3 @@ -require 'test/unit' -require 'stringio' require 'fiddle/import' require 'fiddle/types' @@ -307,7 +305,9 @@ def create_console(command) startup_info, console_process_info ) error_message(r, 'CreateProcessW') - console_process_info + close_handle(console_process_info.hProcess) + close_handle(console_process_info.hThread) + return console_process_info.dwProcessId end def mb2wc(str) @@ -325,12 +325,11 @@ def wc2mb(str) end def read_console_output(handle, row, width) - buffer_chars = width * 8 - buffer = "\0".b * Fiddle::SIZEOF_SHORT * buffer_chars - n = "\0".b * Fiddle::SIZEOF_DWORD + buffer = Fiddle::Pointer.malloc(Fiddle::SIZEOF_SHORT * width, FREE) + n = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DWORD, FREE) r = ReadConsoleOutputCharacterW(handle, buffer, width, row << 16, n) error_message(r, "ReadConsoleOutputCharacterW") - return wc2mb(buffer[0, n.unpack1("L") * 2]).gsub(/ *$/, "") + return wc2mb(buffer[0, n.to_str.unpack1("L") * 2]).gsub(/ *$/, "") end def set_input_record(r, code) @@ -346,314 +345,18 @@ def set_input_record(r, code) end def write_console_input(handle, records, n) - written = "\0".b * Fiddle::SIZEOF_DWORD + written = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DWORD, FREE) r = WriteConsoleInputW(handle, records, n, written) error_message(r, 'WriteConsoleInput') - return written.unpack1('L') + return written.to_str.unpack1('L') end def get_number_of_console_input_events(handle) - n = "\0".b * Fiddle::SIZEOF_DWORD + n = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DWORD, FREE) r = GetNumberOfConsoleInputEvents(handle, n) error_message(r, 'GetNumberOfConsoleInputEvents') - return n.unpack1('L') + return n.to_str.unpack1('L') end extend self end - -module Yamatanooroti::WindowsTestCaseModule - DL = Yamatanooroti::WindowsDefinition - - private def attach_terminal(open = true) - stderr = $stderr - $stderr = StringIO.new - - conin = conout = nil - DL.free_console - # this can be fail while new process is starting - r = DL.attach_console(@console_process_info.dwProcessId, maybe_fail: true) - return nil unless r - - if open - conin = DL.create_console_file_handle("conin$") - return nil if conin == DL::INVALID_HANDLE_VALUE - - conout = DL.create_console_file_handle("conout$") - return nil if conout == DL::INVALID_HANDLE_VALUE - end - - yield(conin, conout) - rescue => evar - ensure - DL.close_handle(conin) if conin && conin != DL::INVALID_HANDLE_VALUE - DL.close_handle(conout) if conout && conout != DL::INVALID_HANDLE_VALUE - DL.free_console - DL.attach_console - stderr.write $stderr.string - $stderr = stderr - raise evar if evar - end - - private def setup_console(height, width) - command = %q[ruby.exe --disable=gems -e sleep"] # console keeping process - @console_process_info = DL.create_console(command) - - # wait for console startup complete - 8.times do |n| - break if attach_terminal { true } - sleep 0.01 * 2**n - end - - attach_terminal do |conin, conout| - DL.set_console_window_size(conout, height, width) - end - end - - private def quote_command_arg(arg) - if not arg.match?(/[ \t"<>|()]/) - # No quotation needed. - return arg - end - - if not arg.match?(/["\\]/) - # No embedded double quotes or backlashes, so I can just wrap quote - # marks around the whole thing. - return %{"#{arg}"} - end - - quote_hit = true - result = +'"' - arg.chars.reverse.each do |c| - result << c - if quote_hit and c == '\\' - result << '\\' - elsif c == '"' - quote_hit = true - result << '\\' - else - quote_hit = false - end - end - result << '"' - result.reverse - end - - class SubProcess - def initialize(command) - @errin, err = IO.pipe - @pid = spawn(command, {in: ["conin$", File::RDWR | File::BINARY], out: ["conout$", File::RDWR | File::BINARY], err: err}) - err.close - @closed = false - @status = nil - @q = Thread::Queue.new - @t = Thread.new do - begin - err = @errin.gets - @q << err if err - rescue IOError - # target process already terminated - next - end - end - end - - def closed? - @closed ||= !(@status = Process.wait2(@pid, Process::WNOHANG)).nil? - end - - private def consume(buffer) - while !@q.empty? - buffer << @q.shift - end - end - - def ensure_close - @errin.close if !@errin.closed? - end - - def sync - buffer = "" - if closed? - @t.kill - @t.join - consume(buffer) - rest = "".b - while ((str = @errin.read_nonblock(1024, exception: false)).is_a?(String)) do - rest << str - end - buffer << rest.force_encoding(Encoding.default_external) << "\n" if rest != "" - else - consume(buffer) - end - $stderr.write buffer if buffer != "" - end - end - - private def launch(command) - attach_terminal do - SubProcess.new(command) - end - end - - private def setup_cp(cp) - @codepage_success_p = attach_terminal { system("chcp #{Integer(cp)} > NUL") } - end - - private def codepage_success? - @codepage_success_p - end - - def write(str) - codes = str.chars.map do |c| - c = "\r" if c == "\n" - byte = c.getbyte(0) - if c.bytesize == 1 and byte.allbits?(0x80) # with Meta key - [-(byte ^ 0x80)] - else - DL.mb2wc(c).unpack("S*") - end - end.flatten - record = DL::INPUT_RECORD_WITH_KEY_EVENT.malloc(DL::FREE) - records = codes.reduce("".b) do |records, code| - DL.set_input_record(record, code) - record.bKeyDown = 1 - records << record.to_ptr.to_str - record.bKeyDown = 0 - records << record.to_ptr.to_str - end - attach_terminal do |conin, conout| - DL.write_console_input(conin, records, codes.size * 2) - loop do - sleep @wait - n = DL.get_number_of_console_input_events(conin) - break if n <= 1 # maybe keyup event still be there - break if n.nil? - @target.sync - break if @target.closed? - end - end - end - - private def free_resources - system("taskkill.exe", "/PID", "#{@pid}", {[:out, :err] => "NUL"}) - system("taskkill.exe", "/PID", "#{@console_process_info.dwProcessId}", {[:out, :err] => "NUL"}) - DL.close_handle(@console_process_info.hProcess) - DL.close_handle(@console_process_info.hThread) - end - - def close - @target.sync - sleep @wait if !@target.closed? - # read first before kill the console process including output - @result = retrieve_screen - - free_resources - @target.sync - @target.ensure_close - end - - private def retrieve_screen(top_of_buffer: false) - top, bottom = attach_terminal do |conin, conout| - csbi = DL.get_console_screen_buffer_info(conout) - if top_of_buffer - [0, csbi.Bottom] - else - [csbi.Top, csbi.Bottom] - end - end - - buffer_chars = @width * 8 - buffer = Fiddle::Pointer.malloc(Fiddle::SIZEOF_SHORT * buffer_chars, DL::FREE) - n = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DWORD, DL::FREE) - lines = attach_terminal do |conin, conout| - (top..bottom).map do |y| - DL.read_console_output(conout, y, @width) || "" - end - end - lines - end - - def result - @result || retrieve_screen - end - - def start_terminal(height, width, command, wait: 0.01, timeout: 2, startup_message: nil, codepage: nil) - @timeout = timeout - @wait = wait - @result = nil - @codepage_success_p - - @height = height - @width = width - setup_console(height, width) - setup_cp(codepage) if codepage - @target = launch(command.map{ |c| quote_command_arg(c) }.join(' ')) - - case startup_message - when String - wait_startup_message { |message| message.start_with?(startup_message) } - when Regexp - wait_startup_message { |message| startup_message.match?(message) } - end - 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 - - @target.sync - chunks = retrieve_screen.join("\n").sub(/\n*\z/, "\n") - break if yield chunks - sleep @wait - end - end - - private def retryable_screen_assertion_with_proc(check_proc, assert_proc, convert_proc = :itself.to_proc) - retry_until = Time.now + @timeout - screen = if @result - convert_proc.call(@result) - else - loop do - @target.sync - screen = convert_proc.call(retrieve_screen) - break screen if Time.now >= retry_until - break screen if check_proc.call(screen) - sleep @wait - end - end - assert_proc.call(screen) - end - - def assert_screen(expected_lines, message = nil) - lines_to_string = ->(lines) { lines.join("\n").sub(/\n*\z/, "\n") } - case expected_lines - when Array - retryable_screen_assertion_with_proc( - ->(actual) { expected_lines == actual }, - ->(actual) { assert_equal(expected_lines, actual, message) } - ) - when String - retryable_screen_assertion_with_proc( - ->(actual) { expected_lines == actual }, - ->(actual) { assert_equal(expected_lines, actual, message) }, - lines_to_string - ) - when Regexp - retryable_screen_assertion_with_proc( - ->(actual) { expected_lines.match?(actual) }, - ->(actual) { assert_match(expected_lines, actual, message) }, - lines_to_string - ) - end - end -end - -class Yamatanooroti::WindowsTestCase < Test::Unit::TestCase - include Yamatanooroti::WindowsTestCaseModule -end diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index 8d021c9..dace932 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -1,370 +1,11 @@ -require 'test/unit' require 'stringio' -require 'fiddle/import' -require 'fiddle/types' -module Yamatanooroti::WindowsDefinition - extend Fiddle::Importer - dlload 'kernel32.dll', 'user32.dll' - include Fiddle::Win32Types - - FREE = Fiddle::Function.new(Fiddle::RUBY_FREE, [Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOID) - - typealias 'SHORT', 'short' - typealias 'HWND', 'HANDLE' - typealias 'LPVOID', 'void*' - typealias 'LPWSTR', 'void*' - typealias 'LPBYTE', 'void*' - typealias 'LPCWSTR', 'void*' - typealias 'LPCVOID', 'void*' - typealias 'LPDWORD', 'void*' - typealias 'WCHAR', 'unsigned short' - typealias 'LPCWCH', 'void*' - typealias 'LPSTR', 'void*' - typealias 'LPCCH', 'void*' - typealias 'LPBOOL', 'void*' - typealias 'LPWORD', 'void*' - typealias 'ULONG_PTR', 'ULONG*' - typealias 'LONG', 'int' - typealias 'HLOCAL', 'HANDLE' - - Fiddle::SIZEOF_DWORD = Fiddle::SIZEOF_LONG - Fiddle::SIZEOF_WORD = Fiddle::SIZEOF_SHORT - - COORD = struct [ - 'SHORT X', - 'SHORT Y' - ] - typealias 'COORD', 'DWORD32' - - SMALL_RECT = struct [ - 'SHORT Left', - 'SHORT Top', - 'SHORT Right', - 'SHORT Bottom' - ] - typealias 'PSMALL_RECT', 'SMALL_RECT*' - - CONSOLE_SCREEN_BUFFER_INFO = struct [ - 'SHORT dwSize_X', 'SHORT dwSize_Y', # 'COORD dwSize', - 'SHORT dwCursorPosition_X', 'SHORT dwCursorPosition_Y', #'COORD dwCursorPosition', - 'WORD wAttributes', - 'SHORT Left', 'SHORT Top', 'SHORT Right', 'SHORT Bottom', # 'SMALL_RECT srWindow', - 'SHORT MaxWidth', 'SHORT MaxHeight' # 'COORD dwMaximumWindowSize' - ] - typealias 'PCONSOLE_SCREEN_BUFFER_INFO', 'CONSOLE_SCREEN_BUFFER_INFO*' - - typealias 'COLORREF', 'DWORD' - CONSOLE_SCREEN_BUFFER_INFOEX = struct [ - 'ULONG cbSize', - 'SHORT dwSize_X', 'SHORT dwSize_Y', # 'COORD dwSize', - 'SHORT dwCursorPosition_X', 'SHORT dwCursorPosition_Y', #'COORD dwCursorPosition', - 'WORD wAttributes', - 'SHORT Left', 'SHORT Top', 'SHORT Right', 'SHORT Bottom', # 'SMALL_RECT srWindow', - 'SHORT MaxWidth', 'SHORT MaxHeight', # 'COORD dwMaximumWindowSize', - 'BOOL bFullScreenSupported', - 'COLORREF ColorTable[16]' - ] - typealias 'PCONSOLE_SCREEN_BUFFER_INFOEX', 'CONSOLE_SCREEN_BUFFER_INFOEX*' - - SECURITY_ATTRIBUTES = struct [ - 'DWORD nLength', - 'LPVOID lpSecurityDescriptor', - 'BOOL bInheritHandle' - ] - typealias 'LPSECURITY_ATTRIBUTES', 'SECURITY_ATTRIBUTES*' - - STARTUPINFOW = struct [ - 'DWORD cb', - 'LPWSTR lpReserved', - 'LPWSTR lpDesktop', - 'LPWSTR lpTitle', - 'DWORD dwX', - 'DWORD dwY', - 'DWORD dwXSize', - 'DWORD dwYSize', - 'DWORD dwXCountChars', - 'DWORD dwYCountChars', - 'DWORD dwFillAttribute', - 'DWORD dwFlags', - 'WORD wShowWindow', - 'WORD cbReserved2', - 'LPBYTE lpReserved2', - 'HANDLE hStdInput', - 'HANDLE hStdOutput', - 'HANDLE hStdError' - ] - typealias 'LPSTARTUPINFOW', 'STARTUPINFOW*' - - PROCESS_INFORMATION = struct [ - 'HANDLE hProcess', - 'HANDLE hThread', - 'DWORD dwProcessId', - 'DWORD dwThreadId' - ] - typealias 'LPPROCESS_INFORMATION', 'PROCESS_INFORMATION*' - - INPUT_RECORD_WITH_KEY_EVENT = struct [ - 'WORD EventType', - 'BOOL bKeyDown', - 'WORD wRepeatCount', - 'WORD wVirtualKeyCode', - 'WORD wVirtualScanCode', - 'WCHAR UnicodeChar', - ## union 'CHAR AsciiChar', - 'DWORD dwControlKeyState' - ] - - STARTF_USESHOWWINDOW = 1 - CREATE_NEW_CONSOLE = 0x10 - CREATE_NEW_PROCESS_GROUP = 0x200 - CREATE_UNICODE_ENVIRONMENT = 0x400 - CREATE_NO_WINDOW = 0x08000000 - ATTACH_PARENT_PROCESS = -1 - KEY_EVENT = 0x0001 - SW_HIDE = 0 - SW_SHOWNOACTIVE = 4 - LEFT_ALT_PRESSED = 0x0002 - - # BOOL CloseHandle(HANDLE hObject); - extern 'BOOL CloseHandle(HANDLE);', :stdcall - - # BOOL FreeConsole(void); - extern 'BOOL FreeConsole(void);', :stdcall - # BOOL AttachConsole(DWORD dwProcessId); - extern 'BOOL AttachConsole(DWORD);', :stdcall - # HWND WINAPI GetConsoleWindow(void); - extern 'HWND GetConsoleWindow(void);', :stdcall - # BOOL WINAPI SetConsoleWindowInfo(HANDLE hConsoleOutput, BOOL bAbsolute, const SMALL_RECT *lpConsoleWindow); - extern 'BOOL SetConsoleWindowInfo(HANDLE, BOOL, PSMALL_RECT);', :stdcall - # BOOL WriteConsoleInputW(HANDLE hConsoleInput, const INPUT_RECORD *lpBuffer, DWORD nLength, LPDWORD lpNumberOfEventsWritten); - extern 'BOOL WriteConsoleInputW(HANDLE, const INPUT_RECORD*, DWORD, LPDWORD);', :stdcall - # SHORT VkKeyScanW(WCHAR ch); - extern 'SHORT VkKeyScanW(WCHAR);', :stdcall - # UINT MapVirtualKeyW(UINT uCode, UINT uMapType); - extern 'UINT MapVirtualKeyW(UINT, UINT);', :stdcall - # BOOL GetNumberOfConsoleInputEvents(HANDLE hConsoleInput, LPDWORD lpcNumberOfEvents); - extern 'BOOL GetNumberOfConsoleInputEvents(HANDLE hConsoleInput, LPDWORD lpcNumberOfEvents);', :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 GetConsoleScreenBufferInfoEx(HANDLE hConsoleOutput, PCONSOLE_SCREEN_BUFFER_INFOEX lpConsoleScreenBufferInfoEx); - extern 'BOOL GetConsoleScreenBufferInfoEx(HANDLE, PCONSOLE_SCREEN_BUFFER_INFOEX);', :stdcall - # BOOL WINAPI SetConsoleScreenBufferInfoEx(HANDLE hConsoleOutput, PCONSOLE_SCREEN_BUFFER_INFOEX lpConsoleScreenBufferInfoEx); - extern 'BOOL SetConsoleScreenBufferInfoEx(HANDLE, PCONSOLE_SCREEN_BUFFER_INFOEX);', :stdcall - - # BOOL CreateProcessW(LPCWSTR lpApplicationName, LPWSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation); - extern 'BOOL CreateProcessW(LPCWSTR lpApplicationName, LPWSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation);', :stdcall - - # int MultiByteToWideChar(UINT CodePage, DWORD dwFlags, LPCSTR lpMultiByteStr, int cbMultiByte, LPWSTR lpWideCharStr, int cchWideChar); - 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 - - # HANDLE CreateFileA(LPCSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile); - extern 'HANDLE CreateFileA(LPCSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE);', :stdcall - GENERIC_READ = 0x80000000 - GENERIC_WRITE = 0x40000000 - FILE_SHARE_READ = 0x00000001 - FILE_SHARE_WRITE = 0x00000002 - OPEN_EXISTING = 3 - INVALID_HANDLE_VALUE = 0xffffffff - - # DWORD FormatMessageW(DWORD dwFlags, LPCVOID lpSource, DWORD dwMessageId, DWORD dwLanguageId, LPWSTR lpBuffer, DWORD nSize, va_list *Arguments); - extern 'DWORD FormatMessageW(DWORD dwFlags, LPCVOID lpSource, DWORD dwMessageId, DWORD dwLanguageId, LPWSTR lpBuffer, DWORD nSize, va_list *Arguments);', :stdcall - # HLOCAL LocalFree(HLOCAL hMem); - extern 'HLOCAL LocalFree(HLOCAL hMem);', :stdcall - FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100 - FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000 - - private def error_message(r, method_name, exception: true) - return if not r.zero? - err = Fiddle.win32_last_error - string = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP, FREE) - n = FormatMessageW( - FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, - Fiddle::NULL, - err, - 0x0, - string, - 0, - Fiddle::NULL - ) - if n > 0 - str = wc2mb(string.ptr[0, n * 2]) - LocalFree(string) - msg = "ERROR(#{method_name}): #{err.to_s}: #{str}" - end - if exception - raise msg - else - $stderr.puts msg - end - end - - def get_console_screen_buffer_info(handle) - csbi = CONSOLE_SCREEN_BUFFER_INFO.malloc(FREE) - r = GetConsoleScreenBufferInfo(handle, csbi) - error_message(r, 'GetConsoleScreenBufferInfo') - return csbi - end - - def set_console_screen_buffer_info_ex(handle, h, w, buffer_height) - csbi = CONSOLE_SCREEN_BUFFER_INFOEX.malloc(FREE) - csbi.cbSize = CONSOLE_SCREEN_BUFFER_INFOEX.size - r = GetConsoleScreenBufferInfoEx(handle, csbi) - error_message(r, 'GetConsoleScreenBufferSize') - csbi.dwSize_X = w - csbi.dwSize_Y = buffer_height - csbi.Left = 0 - csbi.Right = w - 1 - csbi.Top = [csbi.Top, buffer_height - h].min - csbi.Bottom = csbi.Top + h - 1 - r = SetConsoleScreenBufferInfoEx(handle, csbi) - error_message(r, 'SetConsoleScreenBufferInfoEx') - return r != 0 - end - - def set_console_window_info(handle, h, w) - rect = SMALL_RECT.malloc(FREE) - rect.Left = 0 - rect.Top = 0 - rect.Right = w - 1 - rect.Bottom = h - 1 - r = SetConsoleWindowInfo(handle, 1, rect) - error_message(r, 'SetConsoleWindowInfo') - return r != 0 - end - - def set_console_window_size(handle, h, w) - # expand buffer size to keep scrolled away lines - buffer_h = h + 100 - - r = set_console_screen_buffer_info_ex(handle, h, w, buffer_h) - return false unless r - - r = set_console_window_info(handle, h, w) - return false unless r - - return true - end - - def create_console_file_handle(name) - fh = CreateFileA( - name, - GENERIC_READ | GENERIC_WRITE, - FILE_SHARE_READ | FILE_SHARE_WRITE, - nil, - OPEN_EXISTING, - 0, - 0 - ) - fh = [fh].pack("J").unpack1("J") - error_message(0, name) if fh == INVALID_HANDLE_VALUE - fh - end - - def close_handle(handle) - r = CloseHandle(handle) - error_message(r, "CloseHandle") - return r != 0 - end - - def free_console - r = FreeConsole() - error_message(r, "FreeConsole") - return r != 0 - end - - def attach_console(pid = ATTACH_PARENT_PROCESS, maybe_fail: false) - r = AttachConsole(pid) - error_message(r, 'AttachConsole') unless maybe_fail - return r != 0 - end - - def create_console(command) - converted_command = mb2wc("#{command}\0") - console_process_info = PROCESS_INFORMATION.malloc(FREE) - console_process_info.to_ptr[0, PROCESS_INFORMATION.size] = "\0".b * PROCESS_INFORMATION.size - startup_info = STARTUPINFOW.malloc(FREE) - startup_info.to_ptr[0, STARTUPINFOW.size] = "\0".b * STARTUPINFOW.size - startup_info.cb = STARTUPINFOW.size - if false - startup_info.dwFlags = STARTF_USESHOWWINDOW - startup_info.wShowWindow = SW_SHOWNOACTIVE - else - startup_info.dwFlags = STARTF_USESHOWWINDOW - startup_info.wShowWindow = SW_HIDE - end - - r = CreateProcessW( - Fiddle::NULL, converted_command, - Fiddle::NULL, Fiddle::NULL, - 0, - CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT, - Fiddle::NULL, Fiddle::NULL, - startup_info, console_process_info - ) - error_message(r, 'CreateProcessW') - console_process_info - end - - def mb2wc(str) - size = MultiByteToWideChar(65001, 0, str, str.bytesize, '', 0) - converted_str = "\x00".b * (size * 2) - MultiByteToWideChar(65001, 0, str, str.bytesize, converted_str, size) - converted_str - end - - def wc2mb(str) - size = WideCharToMultiByte(65001, 0, str, str.bytesize / 2, '', 0, 0, 0) - converted_str = "\x00".b * size - WideCharToMultiByte(65001, 0, str, str.bytesize / 2, converted_str, converted_str.bytesize, 0, 0) - converted_str.force_encoding("UTF-8") - end - - def read_console_output(handle, row, width) - buffer_chars = width * 8 - buffer = "\0".b * Fiddle::SIZEOF_SHORT * buffer_chars - n = "\0".b * Fiddle::SIZEOF_DWORD - r = ReadConsoleOutputCharacterW(handle, buffer, width, row << 16, n) - error_message(r, "ReadConsoleOutputCharacterW") - return wc2mb(buffer[0, n.unpack1("L") * 2]).gsub(/ *$/, "") - end - - def set_input_record(r, code) - r.EventType = KEY_EVENT - # r.bKeyDown = 1 - r.wRepeatCount = 1 - r.dwControlKeyState = code < 0 ? LEFT_ALT_PRESSED : 0 - code = code.abs - r.wVirtualKeyCode = VkKeyScanW(code) - r.wVirtualScanCode = MapVirtualKeyW(code, 0) - r.UnicodeChar = code - return r - end - - def write_console_input(handle, records, n) - written = "\0".b * Fiddle::SIZEOF_DWORD - r = WriteConsoleInputW(handle, records, n, written) - error_message(r, 'WriteConsoleInput') - return written.unpack1('L') - end - - def get_number_of_console_input_events(handle) - n = "\0".b * Fiddle::SIZEOF_DWORD - r = GetNumberOfConsoleInputEvents(handle, n) - error_message(r, 'GetNumberOfConsoleInputEvents') - return n.unpack1('L') - end - - extend self -end - -module Yamatanooroti::WindowsTestCaseModule +module Yamatanooroti::WindowsTermMixin DL = Yamatanooroti::WindowsDefinition + CONSOLE_KEEPING_COMMAND = %q[ruby.exe --disable=gems -e sleep] + CONSOLE_MARKING_COMMAND = %q[findstr.exe yamatanooroti] + private def attach_terminal(open = true) stderr = $stderr $stderr = StringIO.new @@ -372,7 +13,7 @@ module Yamatanooroti::WindowsTestCaseModule conin = conout = nil DL.free_console # this can be fail while new process is starting - r = DL.attach_console(@console_process_info.dwProcessId, maybe_fail: true) + r = DL.attach_console(@console_process_id, maybe_fail: true) return nil unless r if open @@ -395,21 +36,6 @@ module Yamatanooroti::WindowsTestCaseModule raise evar if evar end - private def setup_console(height, width) - command = %q[ruby.exe --disable=gems -e sleep"] # console keeping process - @console_process_info = DL.create_console(command) - - # wait for console startup complete - 8.times do |n| - break if attach_terminal { true } - sleep 0.01 * 2**n - end - - attach_terminal do |conin, conout| - DL.set_console_window_size(conout, height, width) - end - end - private def quote_command_arg(arg) if not arg.match?(/[ \t"<>|()]/) # No quotation needed. @@ -443,6 +69,7 @@ class SubProcess def initialize(command) @errin, err = IO.pipe @pid = spawn(command, {in: ["conin$", File::RDWR | File::BINARY], out: ["conout$", File::RDWR | File::BINARY], err: err}) + @mon = Process.detach(@pid) err.close @closed = false @status = nil @@ -458,8 +85,20 @@ def initialize(command) end end + def close + unless closed? + begin + Process.kill("KILL", @pid) + rescue Errno::ESRCH # No such process + end + @status = @mon.join.value + sync + @errin.close + end + end + def closed? - @closed ||= !(@status = Process.wait2(@pid, Process::WNOHANG)).nil? + !@mon.alive? end private def consume(buffer) @@ -468,21 +107,19 @@ def closed? end end - def ensure_close - @errin.close if !@errin.closed? - end - def sync buffer = "" if closed? - @t.kill - @t.join - consume(buffer) - rest = "".b - while ((str = @errin.read_nonblock(1024, exception: false)).is_a?(String)) do - rest << str + if !@errin.closed? + @t.kill + @t.join + consume(buffer) + rest = "".b + while ((str = @errin.read_nonblock(1024, exception: false)).is_a?(String)) do + rest << str + end + buffer << rest.force_encoding(Encoding.default_external) << "\n" if rest != "" end - buffer << rest.force_encoding(Encoding.default_external) << "\n" if rest != "" else consume(buffer) end @@ -490,17 +127,17 @@ def sync end end - private def launch(command) + def launch(command) attach_terminal do - SubProcess.new(command) + @target = SubProcess.new(command.map{ |c| quote_command_arg(c) }.join(' ')) end end - private def setup_cp(cp) + def setup_cp(cp) @codepage_success_p = attach_terminal { system("chcp #{Integer(cp)} > NUL") } end - private def codepage_success? + def codepage_success? @codepage_success_p end @@ -535,40 +172,21 @@ def write(str) end end - private def free_resources - system("taskkill.exe", "/PID", "#{@pid}", {[:out, :err] => "NUL"}) - system("taskkill.exe", "/PID", "#{@console_process_info.dwProcessId}", {[:out, :err] => "NUL"}) - DL.close_handle(@console_process_info.hProcess) - DL.close_handle(@console_process_info.hThread) - end - - def close - @target.sync - sleep @wait if !@target.closed? - # read first before kill the console process including output - @result = retrieve_screen - - free_resources + def retrieve_screen(top_of_buffer: false) + return @result if @result @target.sync - @target.ensure_close - end - - private def retrieve_screen(top_of_buffer: false) - top, bottom = attach_terminal do |conin, conout| + top, bottom, width = attach_terminal do |conin, conout| csbi = DL.get_console_screen_buffer_info(conout) if top_of_buffer - [0, csbi.Bottom] + [0, csbi.Bottom, csbi.Right - csbi.Left + 1] else - [csbi.Top, csbi.Bottom] + [csbi.Top, csbi.Bottom, csbi.Right - csbi.Left + 1] end end - buffer_chars = @width * 8 - buffer = Fiddle::Pointer.malloc(Fiddle::SIZEOF_SHORT * buffer_chars, DL::FREE) - n = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DWORD, DL::FREE) lines = attach_terminal do |conin, conout| (top..bottom).map do |y| - DL.read_console_output(conout, y, @width) || "" + DL.read_console_output(conout, y, width) || "" end end lines @@ -577,83 +195,4 @@ def close def result @result || retrieve_screen end - - def start_terminal(height, width, command, wait: 0.01, timeout: 2, startup_message: nil, codepage: nil) - @timeout = timeout - @wait = wait - @result = nil - @codepage_success_p - - @height = height - @width = width - setup_console(height, width) - setup_cp(codepage) if codepage - @target = launch(command.map{ |c| quote_command_arg(c) }.join(' ')) - - case startup_message - when String - wait_startup_message { |message| message.start_with?(startup_message) } - when Regexp - wait_startup_message { |message| startup_message.match?(message) } - end - 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 - - @target.sync - chunks = retrieve_screen.join("\n").sub(/\n*\z/, "\n") - break if yield chunks - sleep @wait - end - end - - private def retryable_screen_assertion_with_proc(check_proc, assert_proc, convert_proc = :itself.to_proc) - retry_until = Time.now + @timeout - screen = if @result - convert_proc.call(@result) - else - loop do - @target.sync - screen = convert_proc.call(retrieve_screen) - break screen if Time.now >= retry_until - break screen if check_proc.call(screen) - sleep @wait - end - end - assert_proc.call(screen) - end - - def assert_screen(expected_lines, message = nil) - lines_to_string = ->(lines) { lines.join("\n").sub(/\n*\z/, "\n") } - case expected_lines - when Array - retryable_screen_assertion_with_proc( - ->(actual) { expected_lines == actual }, - ->(actual) { assert_equal(expected_lines, actual, message) } - ) - when String - retryable_screen_assertion_with_proc( - ->(actual) { expected_lines == actual }, - ->(actual) { assert_equal(expected_lines, actual, message) }, - lines_to_string - ) - when Regexp - retryable_screen_assertion_with_proc( - ->(actual) { expected_lines.match?(actual) }, - ->(actual) { assert_match(expected_lines, actual, message) }, - lines_to_string - ) - end - end -end - -class Yamatanooroti::WindowsTestCase < Test::Unit::TestCase - include Yamatanooroti::WindowsTestCaseModule end From 837a91a6f771cbff9edfc742c649395979356ef0 Mon Sep 17 00:00:00 2001 From: YO4 Date: Tue, 8 Oct 2024 00:43:41 +0900 Subject: [PATCH 11/76] cleanup process propery when interrupted(Ctrl+C) AttachConsole(), FreeConsole() ignores C runtime interrupt handler. If not addressed, the process will not catch Ctrl+C and will die immediately. Not identical to normal Ctrl+C behavior, but at least a minimal stop action can be performed. --- lib/yamatanooroti/windows/conhost.rb | 10 ++- .../windows/windows-definition.rb | 65 ++++++++++++++++--- lib/yamatanooroti/windows/windows.rb | 20 +++++- test/yamatanooroti/test_windows.rb | 14 ++-- 4 files changed, 87 insertions(+), 22 deletions(-) diff --git a/lib/yamatanooroti/windows/conhost.rb b/lib/yamatanooroti/windows/conhost.rb index 674d68b..f61e533 100644 --- a/lib/yamatanooroti/windows/conhost.rb +++ b/lib/yamatanooroti/windows/conhost.rb @@ -24,11 +24,15 @@ def initialize(height, width, wait) end def close - @target.close - @result = retrieve_screen + if @target && !@target.closed? + @target.close + end + @result ||= retrieve_screen if !DL.interrupted? begin - Process.kill("KILL", @console_process_id) + Process.kill("KILL", @console_process_id) if @console_process_id rescue Errno::ESRCH # No such process + ensure + @console_process_id = nil end end end diff --git a/lib/yamatanooroti/windows/windows-definition.rb b/lib/yamatanooroti/windows/windows-definition.rb index 363e5ee..ea53d06 100644 --- a/lib/yamatanooroti/windows/windows-definition.rb +++ b/lib/yamatanooroti/windows/windows-definition.rb @@ -176,6 +176,9 @@ module Yamatanooroti::WindowsDefinition FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100 FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000 + # BOOL WINAPI SetConsoleCtrlHandler(PHANDLER_ROUTINE HandlerRoutine, BOOL Add); + extern 'BOOL SetConsoleCtrlHandler(void *, BOOL Add);', :stdcall + private def error_message(r, method_name, exception: true) return if not r.zero? err = Fiddle.win32_last_error @@ -296,15 +299,17 @@ def create_console(command) startup_info.wShowWindow = SW_HIDE end - r = CreateProcessW( - Fiddle::NULL, converted_command, - Fiddle::NULL, Fiddle::NULL, - 0, - CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT, - Fiddle::NULL, Fiddle::NULL, - startup_info, console_process_info - ) - error_message(r, 'CreateProcessW') + restore_console_control_handler do + r = CreateProcessW( + Fiddle::NULL, converted_command, + Fiddle::NULL, Fiddle::NULL, + 0, + CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT, + Fiddle::NULL, Fiddle::NULL, + startup_info, console_process_info + ) + error_message(r, 'CreateProcessW') + end close_handle(console_process_info.hProcess) close_handle(console_process_info.hThread) return console_process_info.dwProcessId @@ -358,5 +363,47 @@ def get_number_of_console_input_events(handle) return n.to_str.unpack1('L') end + # Ctrl+C trap support + # FreeConsole(), AttachConsole() clears console control handlers. + # Make matter worse, C runtime does not provide the function to restore that handlers. + # Yamatanooroti will ignore Ctrl+C and monitors the end of decoy process. + + def self.ignore_console_control_handler + SetConsoleCtrlHandler(0, 1) + end + + def self.restore_console_control_handler(&block) + SetConsoleCtrlHandler(0, 0) + if block_given? + yield + SetConsoleCtrlHandler(0, 1) + end + end + + @interrupt_monitor_pid = spawn("ruby --disable=gems -e sleep", [:out, :err] => "NUL") + @interrupt_monitor = Process.detach(@interrupt_monitor_pid) + ignore_console_control_handler + @interrupted_p = nil + + def self.interrupted? + @interrupted_p || + unless @interrupt_monitor.alive? + @interrupted_p = (@interrupt_monitor.value.exitstatus == 3) + end + end + + def self.at_exit + if @interrupt_monitor.alive? + begin + Process.kill("KILL", @interrupt_monitor_pid) + rescue Errno::ESRCH # No such process + end + end + end + + Test::Unit.at_exit do + self.at_exit + end + extend self end diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index dace932..efa4075 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -11,6 +11,7 @@ module Yamatanooroti::WindowsTermMixin $stderr = StringIO.new conin = conout = nil + check_interrupt DL.free_console # this can be fail while new process is starting r = DL.attach_console(@console_process_id, maybe_fail: true) @@ -68,7 +69,9 @@ module Yamatanooroti::WindowsTermMixin class SubProcess def initialize(command) @errin, err = IO.pipe - @pid = spawn(command, {in: ["conin$", File::RDWR | File::BINARY], out: ["conout$", File::RDWR | File::BINARY], err: err}) + DL.restore_console_control_handler do + @pid = spawn(command, {in: ["conin$", File::RDWR | File::BINARY], out: ["conout$", File::RDWR | File::BINARY], err: err}) + end @mon = Process.detach(@pid) err.close @closed = false @@ -91,7 +94,7 @@ def close Process.kill("KILL", @pid) rescue Errno::ESRCH # No such process end - @status = @mon.join.value + @status = @mon.join.value.exitstatus sync @errin.close end @@ -128,6 +131,7 @@ def sync end def launch(command) + check_interrupt attach_terminal do @target = SubProcess.new(command.map{ |c| quote_command_arg(c) }.join(' ')) end @@ -142,6 +146,7 @@ def codepage_success? end def write(str) + check_interrupt codes = str.chars.map do |c| c = "\r" if c == "\n" byte = c.getbyte(0) @@ -174,6 +179,7 @@ def write(str) def retrieve_screen(top_of_buffer: false) return @result if @result + check_interrupt @target.sync top, bottom, width = attach_terminal do |conin, conout| csbi = DL.get_console_screen_buffer_info(conout) @@ -195,4 +201,14 @@ def retrieve_screen(top_of_buffer: false) def result @result || retrieve_screen end + + def check_interrupt + raise_interrupt if DL.interrupted? + end + + def raise_interrupt + close + DL.at_exit + raise Interrupt + end end diff --git a/test/yamatanooroti/test_windows.rb b/test/yamatanooroti/test_windows.rb index b83c61f..539c90c 100644 --- a/test/yamatanooroti/test_windows.rb +++ b/test/yamatanooroti/test_windows.rb @@ -17,19 +17,17 @@ def test_load class Yamatanooroti::TestWindowsCodepage < Yamatanooroti::TestCase if Yamatanooroti.win? def test_codepage_932 - start_terminal(5, 30, ['ruby', '-e', 'puts(Encoding.find(%Q[locale]).name)'], codepage: 932) - sleep 0.5 - close + start_terminal(5, 30, ['ruby', '-e', 'puts(%Q!Encoding:#{Encoding.find(%Q[locale]).name}!)'], startup_message: 'Encoding:', codepage: 932) omit "codepage 932 not supported" if !codepage_success? - assert_equal(['Windows-31J', '', '', '', ''], result) + assert_equal(['Encoding:Windows-31J', '', '', '', ''], result) + close end def test_codepage_437 - start_terminal(5, 30, ['ruby', '-e', 'puts(Encoding.find(%Q[locale]).name)'], codepage: 437) - sleep 0.5 - close + start_terminal(5, 30, ['ruby', '-e', 'puts(%Q!Encoding:#{Encoding.find(%Q[locale]).name}!)'], startup_message: 'Encoding:', codepage: 437) omit "codepage 437 not supported" if !codepage_success? - assert_equal(['IBM437', '', '', '', ''], result) + assert_equal(['Encoding:IBM437', '', '', '', ''], result) + close end end end From d5b84efe22286b6b05de78176b91c00bd4671036 Mon Sep 17 00:00:00 2001 From: YO4 Date: Tue, 8 Oct 2024 19:52:05 +0900 Subject: [PATCH 12/76] Enhance TESTOPTS for windows conhost selection and others new TESTOPTS --wait=seconds --timeout=seconds --windows=CONSOLE_TYPE --[no-]show-console --[no-]close-console[=COND] --- lib/yamatanooroti.rb | 1 + lib/yamatanooroti/options.rb | 60 ++++++++++++++++++ lib/yamatanooroti/vterm.rb | 6 +- lib/yamatanooroti/windows.rb | 19 ++++-- lib/yamatanooroti/windows/conhost.rb | 6 ++ .../windows/windows-definition.rb | 5 +- lib/yamatanooroti/windows/windows.rb | 61 ++++++++++++++++++- 7 files changed, 148 insertions(+), 10 deletions(-) create mode 100644 lib/yamatanooroti/options.rb diff --git a/lib/yamatanooroti.rb b/lib/yamatanooroti.rb index f9c4367..ba6af92 100644 --- a/lib/yamatanooroti.rb +++ b/lib/yamatanooroti.rb @@ -1,4 +1,5 @@ require 'test/unit' +require_relative 'yamatanooroti/options' class Yamatanooroti def self.load_vterm diff --git a/lib/yamatanooroti/options.rb b/lib/yamatanooroti/options.rb new file mode 100644 index 0000000..800b481 --- /dev/null +++ b/lib/yamatanooroti/options.rb @@ -0,0 +1,60 @@ +class Yamatanooroti + module Options + options = class << self + attr_reader :default_wait, :default_timeout, :windows, :show_console, :close_console + end + + Accessor = Module.new do |mod| + options.each do |name| + mod.define_method name do + Yamatanooroti::Options.public_send(name) + end + end + end + + @default_wait = 0.01 + @default_timeout = 2.0 + @show_console = false + @close_console = :always + + CONSOLE_TYPES = [:conhost, :"legacy-conhost"] + CLOSE_WHEN = [:always, :pass, :never] + + ::Test::Unit::AutoRunner.setup_option do |autorunner, o| + + o.on_tail("yamatanooroti options") + o.on_tail("--wait=#{@default_wait}", Float, + "Specify yamatanooroti wait time in seconds.") do |seconds| + @default_wait = seconds + end + + o.on_tail("--timeout=#{@default_timeout}", Float, + "Specify yamatanooroti timeout in seconds.") do |seconds| + @default_timeout = seconds + end + + o.on_tail("windows specific yamatanooroti options") + + o.on_tail("--windows=TYPE", CONSOLE_TYPES, + "Specify console type", + "(#{autorunner.keyword_display(CONSOLE_TYPES)})") do |type| + @windows = type + end + + o.on_tail("--[no-]show-console", + "Show test ongoing console.") do |show| + @show_console = show + end + + o.on_tail("--[no-]close-console[=COND]", CLOSE_WHEN, + "Close test target console when COND met", + "(#{autorunner.keyword_display(CLOSE_WHEN)})") do |cond| + @close_console = (cond.nil? ? :always : cond) || :never + end + end + end + + def self.options + Options + end +end diff --git a/lib/yamatanooroti/vterm.rb b/lib/yamatanooroti/vterm.rb index 7c29fe2..4329d12 100644 --- a/lib/yamatanooroti/vterm.rb +++ b/lib/yamatanooroti/vterm.rb @@ -5,9 +5,9 @@ require 'io/wait' module Yamatanooroti::VTermTestCaseModule - def start_terminal(height, width, command, wait: 0.01, timeout: 2, startup_message: nil) - @timeout = timeout - @wait = wait + def start_terminal(height, width, command, wait: nil, timeout: nil, startup_message: nil) + @timeout = timeout || Yamatanooroti.options.default_timeout + @wait = wait || Yamatanooroti.options.default_wait @result = nil @pty_output, @pty_input, @pid = PTY.spawn(*command) diff --git a/lib/yamatanooroti/windows.rb b/lib/yamatanooroti/windows.rb index 74086b8..d936a2f 100644 --- a/lib/yamatanooroti/windows.rb +++ b/lib/yamatanooroti/windows.rb @@ -20,11 +20,10 @@ def codepage_success? @terminal.codepage_success? end - def start_terminal(height, width, command, wait: 0.01, timeout: 2, startup_message: nil, codepage: nil) - @timeout = timeout - @wait = wait + def start_terminal(height, width, command, wait: nil, timeout: nil, startup_message: nil, codepage: nil) + @timeout = timeout || Yamatanooroti.options.default_timeout + @wait = wait || Yamatanooroti.options.default_wait @result = nil - @terminal = Yamatanooroti::ConhostTerm.setup_console(height, width, @wait) @terminal.setup_cp(codepage) if codepage @terminal.launch(command) @@ -89,6 +88,18 @@ def assert_screen(expected_lines, message = nil) ) end end + + def self.included(cls) + cls.instance_exec do + teardown do + if !Yamatanooroti.options.show_console || + Yamatanooroti.options.close_console == :always || + Yamatanooroti.options.close_console == :pass && passed? + @terminal.close_console + end + end + end + end end class Yamatanooroti::WindowsTestCase < Test::Unit::TestCase diff --git a/lib/yamatanooroti/windows/conhost.rb b/lib/yamatanooroti/windows/conhost.rb index f61e533..e2edec9 100644 --- a/lib/yamatanooroti/windows/conhost.rb +++ b/lib/yamatanooroti/windows/conhost.rb @@ -28,6 +28,12 @@ def close @target.close end @result ||= retrieve_screen if !DL.interrupted? + end + + def close_console + if @target && !@target.closed? + @target.close + end begin Process.kill("KILL", @console_process_id) if @console_process_id rescue Errno::ESRCH # No such process diff --git a/lib/yamatanooroti/windows/windows-definition.rb b/lib/yamatanooroti/windows/windows-definition.rb index ea53d06..50bed21 100644 --- a/lib/yamatanooroti/windows/windows-definition.rb +++ b/lib/yamatanooroti/windows/windows-definition.rb @@ -122,6 +122,7 @@ module Yamatanooroti::WindowsDefinition KEY_EVENT = 0x0001 SW_HIDE = 0 SW_SHOWNOACTIVE = 4 + SW_SHOWMINNOACTIVE = 7 LEFT_ALT_PRESSED = 0x0002 # BOOL CloseHandle(HANDLE hObject); @@ -291,12 +292,12 @@ def create_console(command) startup_info = STARTUPINFOW.malloc(FREE) startup_info.to_ptr[0, STARTUPINFOW.size] = "\0".b * STARTUPINFOW.size startup_info.cb = STARTUPINFOW.size - if false + if Yamatanooroti.options.show_console startup_info.dwFlags = STARTF_USESHOWWINDOW startup_info.wShowWindow = SW_SHOWNOACTIVE else startup_info.dwFlags = STARTF_USESHOWWINDOW - startup_info.wShowWindow = SW_HIDE + startup_info.wShowWindow = Yamatanooroti.options.windows.to_s == "legacy-conhost" ? SW_SHOWMINNOACTIVE : SW_HIDE end restore_console_control_handler do diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index efa4075..53daeb4 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -1,4 +1,63 @@ require 'stringio' +require 'win32/registry' + +module Yamatanooroti::WindowsConsoleSettings + DelegationConsoleSetting = { + conhost: "{B23D10C0-E52E-411E-9D5B-C09FDF709C7D}", + terminal: "{2EACA947-7F5F-4CFA-BA87-8F7FBEEFBE69}", + preview: "{06EC847C-C0A5-46B8-92CB-7C92F6E35CD5}", + }.freeze + DelegationTerminalSetting = { + conhost: "{B23D10C0-E52E-411E-9D5B-C09FDF709C7D}", + terminal: "{E12CFF52-A866-4C77-9A90-F570A7AA2C6B}", + preview: "{86633F1F-6454-40EC-89CE-DA4EBA977EE2}", + }.freeze + + begin + Win32::Registry::HKEY_CURRENT_USER.open('Console') do |reg| + @orig_conhost = reg['ForceV2'] + end + rescue Win32::Registry::Error + end + begin + Win32::Registry::HKEY_CURRENT_USER.open('Console\%%Startup') do |reg| + @orig_console = reg['DelegationConsole'] + @orig_terminal = reg['DelegationTerminal'] + end + rescue Win32::Registry::Error + end + + Test::Unit.at_start do + case Yamatanooroti.options.windows.to_s + when "conhost" + Win32::Registry::HKEY_CURRENT_USER.open('Console', Win32::Registry::KEY_WRITE) do |reg| + reg['ForceV2', Win32::Registry::REG_DWORD] = 1 + end + Win32::Registry::HKEY_CURRENT_USER.open('Console\%%Startup', Win32::Registry::KEY_WRITE) do |reg| + reg['DelegationConsole', Win32::Registry::REG_SZ] = DelegationConsoleSetting[:conhost] + reg['DelegationTerminal', Win32::Registry::REG_SZ] = DelegationTerminalSetting[:conhost] + end if @orig_console && @orig_terminal + when "legacy-conhost" + Win32::Registry::HKEY_CURRENT_USER.open('Console', Win32::Registry::KEY_WRITE) do |reg| + reg['ForceV2', Win32::Registry::REG_DWORD] = 0 + end + Win32::Registry::HKEY_CURRENT_USER.open('Console\%%Startup', Win32::Registry::KEY_WRITE) do |reg| + reg['DelegationConsole', Win32::Registry::REG_SZ] = DelegationConsoleSetting[:conhost] + reg['DelegationTerminal', Win32::Registry::REG_SZ] = DelegationTerminalSetting[:conhost] + end if @orig_console && @orig_terminal + end + end + + Test::Unit.at_exit do + Win32::Registry::HKEY_CURRENT_USER.open('Console', Win32::Registry::KEY_WRITE) do |reg| + reg['ForceV2', Win32::Registry::REG_DWORD] = @orig_conhost + end if @orig_conhost + Win32::Registry::HKEY_CURRENT_USER.open('Console\%%Startup', Win32::Registry::KEY_WRITE) do |reg| + reg['DelegationConsole', Win32::Registry::REG_SZ] = @orig_console + reg['DelegationTerminal', Win32::Registry::REG_SZ] = @orig_terminal + end if @orig_console && @orig_terminal + end +end module Yamatanooroti::WindowsTermMixin DL = Yamatanooroti::WindowsDefinition @@ -207,7 +266,7 @@ def check_interrupt end def raise_interrupt - close + close_console DL.at_exit raise Interrupt end From 16085f77a1f113b33c96e247138c22d50a7e6471 Mon Sep 17 00:00:00 2001 From: YO4 Date: Tue, 8 Oct 2024 23:06:11 +0900 Subject: [PATCH 13/76] attr_reader returns nil on old rubies --- lib/yamatanooroti/options.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/yamatanooroti/options.rb b/lib/yamatanooroti/options.rb index 800b481..8080e96 100644 --- a/lib/yamatanooroti/options.rb +++ b/lib/yamatanooroti/options.rb @@ -1,7 +1,8 @@ class Yamatanooroti module Options - options = class << self - attr_reader :default_wait, :default_timeout, :windows, :show_console, :close_console + options = [:default_wait, :default_timeout, :windows, :show_console, :close_console] + self.singleton_class.instance_eval do + attr_reader(*options) end Accessor = Module.new do |mod| From a7c978f468630b8e24e625983490f19438c8d91d Mon Sep 17 00:00:00 2001 From: YO4 Date: Tue, 8 Oct 2024 22:36:49 +0900 Subject: [PATCH 14/76] report windows console selection --- lib/yamatanooroti/options.rb | 1 + lib/yamatanooroti/windows/windows.rb | 2 ++ 2 files changed, 3 insertions(+) diff --git a/lib/yamatanooroti/options.rb b/lib/yamatanooroti/options.rb index 8080e96..d96e725 100644 --- a/lib/yamatanooroti/options.rb +++ b/lib/yamatanooroti/options.rb @@ -15,6 +15,7 @@ module Options @default_wait = 0.01 @default_timeout = 2.0 + @windows = :conhost @show_console = false @close_console = :always diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index 53daeb4..29dc957 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -30,6 +30,7 @@ module Yamatanooroti::WindowsConsoleSettings Test::Unit.at_start do case Yamatanooroti.options.windows.to_s when "conhost" + puts "use conhost(classic, conhostV2) for windows console" Win32::Registry::HKEY_CURRENT_USER.open('Console', Win32::Registry::KEY_WRITE) do |reg| reg['ForceV2', Win32::Registry::REG_DWORD] = 1 end @@ -38,6 +39,7 @@ module Yamatanooroti::WindowsConsoleSettings reg['DelegationTerminal', Win32::Registry::REG_SZ] = DelegationTerminalSetting[:conhost] end if @orig_console && @orig_terminal when "legacy-conhost" + puts "use conhost(legacy, conhostV1) for windows console" Win32::Registry::HKEY_CURRENT_USER.open('Console', Win32::Registry::KEY_WRITE) do |reg| reg['ForceV2', Win32::Registry::REG_DWORD] = 0 end From f12c2243b76448e774479afe19c24d43b74851a8 Mon Sep 17 00:00:00 2001 From: YO4 Date: Tue, 8 Oct 2024 22:50:01 +0900 Subject: [PATCH 15/76] ci for windows We still support only command prompt window (conhost.exe). Windows Terminal support is planned. Consider legacy-conhost support to be meaningful even without Windows Terminal support, since it can alternatively check operation on older versions of Windows. --- .github/workflows/y.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/y.yml b/.github/workflows/y.yml index 8faf1a8..48498b0 100644 --- a/.github/workflows/y.yml +++ b/.github/workflows/y.yml @@ -42,3 +42,24 @@ jobs: run: | bundle install bundle exec rake test + windows-conhost: + name: >- + ${{ matrix.os }} ${{ matrix.ruby }} ${{ matrix.console }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ windows-2019, windows-2022 ] + ruby: [ 3.3, mingw ] + console: [ conhost, legacy-conhost ] + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + - name: rake test + run: | + rake test TESTOPTS="--windows=${{ matrix.console }}" From aec68144366334d41b434c3ddbe52aad96a87068 Mon Sep 17 00:00:00 2001 From: YO4 Date: Wed, 9 Oct 2024 00:19:06 +0900 Subject: [PATCH 16/76] Add minimal TESTOPTS instructions in README.md --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 99fa145..ea9fe05 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,14 @@ Tasks: TOP => default => test (See full trace by running task with --trace) ``` +### Commandline Options + +Yamatanooroti provides some additional TESTOPTS options. + +This is more important when running on Windows because of the type of console to be tested is specified in TESTOPTS. + +Please see ```rake TESTOPTS="-h"```. + ### Advanced Usage If you want to specify vterm environment that needs vterm gem, you can use `Yamatanooroti::VTermTestCase`: From 4558f8499c4b47271b03691375ff202d108685b8 Mon Sep 17 00:00:00 2001 From: YO4 Date: Wed, 9 Oct 2024 21:26:05 +0900 Subject: [PATCH 17/76] add test windows console selection function working propery --- .github/workflows/y.yml | 2 +- lib/yamatanooroti/windows.rb | 4 ++ .../windows/windows-definition.rb | 18 ++++++++ lib/yamatanooroti/windows/windows.rb | 44 +++++++++++++++++++ test/yamatanooroti/test_windows.rb | 7 ++- 5 files changed, 73 insertions(+), 2 deletions(-) diff --git a/.github/workflows/y.yml b/.github/workflows/y.yml index 48498b0..680e92e 100644 --- a/.github/workflows/y.yml +++ b/.github/workflows/y.yml @@ -62,4 +62,4 @@ jobs: ruby-version: ${{ matrix.ruby }} - name: rake test run: | - rake test TESTOPTS="--windows=${{ matrix.console }}" + rake test TESTOPTS="-v --windows=${{ matrix.console }}" diff --git a/lib/yamatanooroti/windows.rb b/lib/yamatanooroti/windows.rb index d936a2f..e16b171 100644 --- a/lib/yamatanooroti/windows.rb +++ b/lib/yamatanooroti/windows.rb @@ -20,6 +20,10 @@ def codepage_success? @terminal.codepage_success? end + def identify + @terminal.identify + end + def start_terminal(height, width, command, wait: nil, timeout: nil, startup_message: nil, codepage: nil) @timeout = timeout || Yamatanooroti.options.default_timeout @wait = wait || Yamatanooroti.options.default_wait diff --git a/lib/yamatanooroti/windows/windows-definition.rb b/lib/yamatanooroti/windows/windows-definition.rb index 50bed21..3ec2536 100644 --- a/lib/yamatanooroti/windows/windows-definition.rb +++ b/lib/yamatanooroti/windows/windows-definition.rb @@ -124,6 +124,7 @@ module Yamatanooroti::WindowsDefinition SW_SHOWNOACTIVE = 4 SW_SHOWMINNOACTIVE = 7 LEFT_ALT_PRESSED = 0x0002 + ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 # BOOL CloseHandle(HANDLE hObject); extern 'BOOL CloseHandle(HANDLE);', :stdcall @@ -152,6 +153,11 @@ module Yamatanooroti::WindowsDefinition extern 'BOOL GetConsoleScreenBufferInfoEx(HANDLE, PCONSOLE_SCREEN_BUFFER_INFOEX);', :stdcall # BOOL WINAPI SetConsoleScreenBufferInfoEx(HANDLE hConsoleOutput, PCONSOLE_SCREEN_BUFFER_INFOEX lpConsoleScreenBufferInfoEx); extern 'BOOL SetConsoleScreenBufferInfoEx(HANDLE, PCONSOLE_SCREEN_BUFFER_INFOEX);', :stdcall + # BOOL WINAPI GetConsoleMode(HANDLE hConsoleHandle, LPDWORD lpMode); + extern 'BOOL GetConsoleMode(HANDLE, LPDWORD);', :stdcall + # BOOL WINAPI SetConsoleMode(HANDLE hConsoleHandle, DWORD dwMode); + extern 'BOOL SetConsoleMode(HANDLE, DWORD);', :stdcall + # BOOL CreateProcessW(LPCWSTR lpApplicationName, LPWSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation); extern 'BOOL CreateProcessW(LPCWSTR lpApplicationName, LPWSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation);', :stdcall @@ -364,6 +370,18 @@ def get_number_of_console_input_events(handle) return n.to_str.unpack1('L') end + def get_console_mode(handle) + mode = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DWORD, FREE) + mode[0, Fiddle::SIZEOF_DWORD] = "\0".b * Fiddle::SIZEOF_DWORD + GetConsoleMode(handle, mode) + # error_message(r, 'GetConsoleMode') # may be fail + mode.to_str.unpack1('L') + end + + def set_console_mode(handle, mode) + 0 != SetConsoleMode(handle, mode) + end + # Ctrl+C trap support # FreeConsole(), AttachConsole() clears console control handlers. # Make matter worse, C runtime does not provide the function to restore that handlers. diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index 29dc957..714bf7c 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -263,6 +263,50 @@ def result @result || retrieve_screen end + # identify windows console + # conhost(legacy) + # compatible with older windows + # lacks newer features (VT sequence support) + # conhost(classic) + # conhost with supports VT + # terminal + # fully VT support + # focused on modern features over compatibility + # can't access screen buffer outside of view + # change winsize using win32api + def identify + attach_terminal do |conin, conout| + orig_mode = DL.get_console_mode(conout) + DL.set_console_mode(conout, orig_mode ^ DL::ENABLE_VIRTUAL_TERMINAL_PROCESSING) + alt_mode = DL.get_console_mode(conout) + if ((orig_mode | alt_mode) & DL::ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0 + # consolemode unchanged, ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0 + return :"legacy-conhost" + end + DL.set_console_mode(conout, orig_mode) + + orig_buffer_info = DL.get_console_screen_buffer_info(conout) + view_w = orig_buffer_info.dwSize_X + view_h = orig_buffer_info.Bottom - orig_buffer_info.Top + 1 + buffer_height = orig_buffer_info.dwSize_Y + if buffer_height != view_h + # buffer size != view size + return :conhost + end + + DL.set_console_screen_buffer_info_ex(conout, view_h, view_w, buffer_height + 1) + alt_buffer_info = DL.get_console_screen_buffer_info(conout) + if alt_buffer_info.dwSize_Y == buffer_height + 1 + # now screen buffer size can be diffrent to view size + DL.set_console_screen_buffer_info_ex(conout, view_h, view_w, buffer_height) + return :conhost + else + DL.set_console_window_info(handle, view_h, view_w) + return :terminal + end + end + end + def check_interrupt raise_interrupt if DL.interrupted? end diff --git a/test/yamatanooroti/test_windows.rb b/test/yamatanooroti/test_windows.rb index 539c90c..7957eff 100644 --- a/test/yamatanooroti/test_windows.rb +++ b/test/yamatanooroti/test_windows.rb @@ -14,8 +14,13 @@ def test_load end end -class Yamatanooroti::TestWindowsCodepage < Yamatanooroti::TestCase +class Yamatanooroti::TestWindowsSpecific < Yamatanooroti::TestCase if Yamatanooroti.win? + def test_console_selection + start_terminal(5, 30, ['ruby', 'bin/simple_repl'], startup_message: 'prompt>') + assert_equal(Yamatanooroti.options.windows, identify) + end + def test_codepage_932 start_terminal(5, 30, ['ruby', '-e', 'puts(%Q!Encoding:#{Encoding.find(%Q[locale]).name}!)'], startup_message: 'Encoding:', codepage: 932) omit "codepage 932 not supported" if !codepage_success? From 30e8bc9592dd6064a74342f9cc5928bb5aa65a89 Mon Sep 17 00:00:00 2001 From: YO4 Date: Thu, 10 Oct 2024 20:27:13 +0900 Subject: [PATCH 18/76] @terminal can be nil When testcase is omitted, @terminal will be nil --- lib/yamatanooroti/windows.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/yamatanooroti/windows.rb b/lib/yamatanooroti/windows.rb index e16b171..b75f149 100644 --- a/lib/yamatanooroti/windows.rb +++ b/lib/yamatanooroti/windows.rb @@ -99,7 +99,7 @@ def self.included(cls) if !Yamatanooroti.options.show_console || Yamatanooroti.options.close_console == :always || Yamatanooroti.options.close_console == :pass && passed? - @terminal.close_console + @terminal&.close_console end end end From f0ee26b8fd832b786e4fd7a7c8c145fb4ae73687 Mon Sep 17 00:00:00 2001 From: YO4 Date: Thu, 10 Oct 2024 21:09:51 +0900 Subject: [PATCH 19/76] support startup_message with narrow window size In narrow console window, start up message shows like below: ``` +-----+ |Multi| |line | |REPL.| |promp| |t> | | | ``` When start_tarminal() with ```startup_message: 'Multiline REPL.'```, vterm.rb recieves startup message as stream, so window size is not matter. windows.rb can't recieve startup message as stream, only can retreive_screen(). retreive_screen recieves like: ```["Multi", "line", "REPL.", "promp", "t>", "", ...]``` This patch splits startup_message into every width characters to allow for correct comparisons. If there are wide widths or complex characters, they will fail to divide accurately. So it is not perfect. Nevertheless, the situation can be mitigated. --- lib/yamatanooroti/windows.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/yamatanooroti/windows.rb b/lib/yamatanooroti/windows.rb index b75f149..93c0da1 100644 --- a/lib/yamatanooroti/windows.rb +++ b/lib/yamatanooroti/windows.rb @@ -34,7 +34,7 @@ def start_terminal(height, width, command, wait: nil, timeout: nil, startup_mess case startup_message when String - wait_startup_message { |message| message.start_with?(startup_message) } + wait_startup_message { |message| message.start_with?(startup_message.chars.each_slice(width).map { |l| l.join.rstrip }.join("\n")) } when Regexp wait_startup_message { |message| startup_message.match?(message) } end From d5ae8acc06aabd72c3a93402aab4fb8594c993e9 Mon Sep 17 00:00:00 2001 From: YO4 Date: Sat, 12 Oct 2024 22:34:10 +0900 Subject: [PATCH 20/76] close subprocess propery even when calling setup_terminal() several times from one testcase test_simple_dialog_at_right_edge at reline/test/reline/yamatanooroti/test_rendering.rb --- lib/yamatanooroti/windows.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/yamatanooroti/windows.rb b/lib/yamatanooroti/windows.rb index 93c0da1..84d976b 100644 --- a/lib/yamatanooroti/windows.rb +++ b/lib/yamatanooroti/windows.rb @@ -28,6 +28,11 @@ def start_terminal(height, width, command, wait: nil, timeout: nil, startup_mess @timeout = timeout || Yamatanooroti.options.default_timeout @wait = wait || Yamatanooroti.options.default_wait @result = nil + if @terminal + if !Yamatanooroti.options.show_console || Yamatanooroti.options.close_console != :never + @terminal.close_console + end + end @terminal = Yamatanooroti::ConhostTerm.setup_console(height, width, @wait) @terminal.setup_cp(codepage) if codepage @terminal.launch(command) From ae4416b6ee4da68b3a679228969a408fea0d701a Mon Sep 17 00:00:00 2001 From: YO4 Date: Thu, 10 Oct 2024 00:15:33 +0900 Subject: [PATCH 21/76] add TESTOPTS windows terminal settings --- lib/yamatanooroti/options.rb | 116 +++++++++++++++++++++++++-- lib/yamatanooroti/windows/windows.rb | 20 ++++- test/yamatanooroti/test_windows.rb | 2 +- 3 files changed, 126 insertions(+), 12 deletions(-) diff --git a/lib/yamatanooroti/options.rb b/lib/yamatanooroti/options.rb index d96e725..c3124bc 100644 --- a/lib/yamatanooroti/options.rb +++ b/lib/yamatanooroti/options.rb @@ -1,6 +1,27 @@ class Yamatanooroti module Options - options = [:default_wait, :default_timeout, :windows, :show_console, :close_console] + options = [ + :default_wait, + :default_timeout, + + # windows console selection + :windows, + + # true if conhost(classic) or conhost(legacy) + :conhost, + + # true if windows terminal + :terminal, + + # windows terminal download/extract dir + :terminal_workdir, + + # show console window on windows + :show_console, + + # conditional close console window on windows + :close_console, + ] self.singleton_class.instance_eval do attr_reader(*options) end @@ -16,10 +37,71 @@ module Options @default_wait = 0.01 @default_timeout = 2.0 @windows = :conhost - @show_console = false + @conhost = true + @terminal = false + @terminal_workdir = nil + @show_console = nil @close_console = :always - CONSOLE_TYPES = [:conhost, :"legacy-conhost"] + module WindowsTerminal + ALIAS = { + stable: :"1.21", + preview: :"1.22preview" + } + RELEASES = { + "1.22preview": { + url: "https://github.com/microsoft/terminal/releases/download/v1.22.2702.0/Microsoft.WindowsTerminalPreview_1.22.2702.0_x64.zip", + sha256: "CE8EED54D120775F31E3572A76EF5AE461B9E2D8887AB5DFF2F1859E24F4CE0B" + }, + "1.21": { + url: "https://github.com/microsoft/terminal/releases/download/v1.21.2701.0/Microsoft.WindowsTerminal_1.21.2701.0_x64.zip", + sha256: "2F712872ED7F552763F3776EA7A823C9E7413CFD5EC65B88E95162E93ACEF899" + }, + "1.21preview": { + url: "https://github.com/microsoft/terminal/releases/download/v1.21.1772.0/Microsoft.WindowsTerminalPreview_1.21.1772.0_x64.zip", + sha256: "6AA37175E2B09170829A39DAF3357D4B88A3965C3C529A45B1B0781B8F3425F0" + }, + "1.20": { + url: "https://github.com/microsoft/terminal/releases/download/v1.20.11781.0/Microsoft.WindowsTerminal_1.20.11781.0_x64.zip", + sha256: "B7A6046903CE33D75250DA7E40AD2929E51703AB66E9C3A0B02A839C2E868FEC" + }, + "1.19": { + url: "https://github.com/microsoft/terminal/releases/download/v1.19.11213.0/Microsoft.WindowsTerminal_1.19.11213.0_x64.zip", + sha256: "E32D7E72F8490AD94174708BB0B420E1EF4467B92F442D40DFAFDF42202A16A7" + }, + "1.18": { + url: "https://github.com/microsoft/terminal/releases/download/v1.18.10301.0/Microsoft.WindowsTerminal_1.18.10301.0_x64.zip", + sha256: "38B0E38B545D9C61F1B4214EA3EC6117C0EC348FEB18476D04ECEFB4D7DA723D" + }, + # v1.17 : the first windows terminal supports portable mode + "1.17": { + url: "https://github.com/microsoft/terminal/releases/download/v1.17.11461.0/Microsoft.WindowsTerminal_1.17.11461.0_x64.zip", + sha256: "F2B1539649D17752888D7944F97D6372F8D48EB1CEB024501DF8D8E9D3352F25" + }, + } + @wt_path = nil + + # `curl -L -O -s -w "%{url_effective}" https://aka.ms/terminal-canary-zip-x64` + # `curl --head -w "%header{Location}\n%header{Content-Length} %header{Last-Modified}" https://aka.ms/terminal-canary-zip-x64` + # `curl --head -L -w "%header{Location}\n%header{Content-Length} %header{Last-Modified}" https://aka.ms/terminal-canary-zip-x64` + # `curl --head -L -w "%{url_effective}\n%header{Location}\n%header{Content-Length}\n%header{Last-Modified}" https://aka.ms/terminal-canary-zip-x64` + + def self.interpret(name) + if ALIAS.has_key?(name) + name = ALIAS[name] + end + if RELEASES.has_key?(name) + return name.to_s + elsif name == :canary + return name + end + raise "bug! #{name} is unknows" + end + end + + CONHOST_TYPES = [:conhost, :"legacy-conhost"] + TERMINAL_TYPES = [:stable, :preview, :canary] + TERMINAL_VERSIONS = WindowsTerminal::RELEASES.keys CLOSE_WHEN = [:always, :pass, :never] ::Test::Unit::AutoRunner.setup_option do |autorunner, o| @@ -37,15 +119,35 @@ module Options o.on_tail("windows specific yamatanooroti options") - o.on_tail("--windows=TYPE", CONSOLE_TYPES, + o.on_tail("--windows=TYPE", CONHOST_TYPES + TERMINAL_TYPES + TERMINAL_VERSIONS, "Specify console type", - "(#{autorunner.keyword_display(CONSOLE_TYPES)})") do |type| - @windows = type + "(#{autorunner.keyword_display(CONHOST_TYPES + TERMINAL_TYPES)})", + "(#{TERMINAL_VERSIONS.sort.join(", ")})") do |type| + @conhost = CONHOST_TYPES.include?(type) + @terminal = !@conhost + @windows = @conhost ? type : WindowsTerminal.interpret(type) + if @terminal + if @show_console == false + puts "Windows Terminal is always visible. --no-show-console is ignored." + end + @show_console = true + end + end + + o.on_tail("--wt-dir=DIR", String, + "Specify Windows Terminal working dir.", + "Automatically determined if not specified and treated temporary.", + "DIR is treaded permanent if specified and download files are remains.") do |dir| + @terminal_workdir = dir end o.on_tail("--[no-]show-console", "Show test ongoing console.") do |show| - @show_console = show + if show == false and @terminal + puts "Windows Terminal is always visible. --no-show-console is ignored." + else + @show_console = show + end end o.on_tail("--[no-]close-console[=COND]", CLOSE_WHEN, diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index 714bf7c..9275e47 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -28,8 +28,8 @@ module Yamatanooroti::WindowsConsoleSettings end Test::Unit.at_start do - case Yamatanooroti.options.windows.to_s - when "conhost" + case Yamatanooroti.options.windows + when :conhost puts "use conhost(classic, conhostV2) for windows console" Win32::Registry::HKEY_CURRENT_USER.open('Console', Win32::Registry::KEY_WRITE) do |reg| reg['ForceV2', Win32::Registry::REG_DWORD] = 1 @@ -38,7 +38,7 @@ module Yamatanooroti::WindowsConsoleSettings reg['DelegationConsole', Win32::Registry::REG_SZ] = DelegationConsoleSetting[:conhost] reg['DelegationTerminal', Win32::Registry::REG_SZ] = DelegationTerminalSetting[:conhost] end if @orig_console && @orig_terminal - when "legacy-conhost" + when :"legacy-conhost" puts "use conhost(legacy, conhostV1) for windows console" Win32::Registry::HKEY_CURRENT_USER.open('Console', Win32::Registry::KEY_WRITE) do |reg| reg['ForceV2', Win32::Registry::REG_DWORD] = 0 @@ -47,6 +47,10 @@ module Yamatanooroti::WindowsConsoleSettings reg['DelegationConsole', Win32::Registry::REG_SZ] = DelegationConsoleSetting[:conhost] reg['DelegationTerminal', Win32::Registry::REG_SZ] = DelegationTerminalSetting[:conhost] end if @orig_console && @orig_terminal + when :canary + prepare_terminal_canary + else + prepare_terminal_portable end end @@ -59,6 +63,14 @@ module Yamatanooroti::WindowsConsoleSettings reg['DelegationTerminal', Win32::Registry::REG_SZ] = @orig_terminal end if @orig_console && @orig_terminal end + + def self.prepare_terminal_canary + puts "TODO: prepare_terminal_canary" + end + + def self.prepare_terminal_portable + puts "TODO: prepare_terminal_portable" + end end module Yamatanooroti::WindowsTermMixin @@ -301,7 +313,7 @@ def identify DL.set_console_screen_buffer_info_ex(conout, view_h, view_w, buffer_height) return :conhost else - DL.set_console_window_info(handle, view_h, view_w) + DL.set_console_window_info(conout, view_h, view_w) return :terminal end end diff --git a/test/yamatanooroti/test_windows.rb b/test/yamatanooroti/test_windows.rb index 7957eff..47914e2 100644 --- a/test/yamatanooroti/test_windows.rb +++ b/test/yamatanooroti/test_windows.rb @@ -18,7 +18,7 @@ class Yamatanooroti::TestWindowsSpecific < Yamatanooroti::TestCase if Yamatanooroti.win? def test_console_selection start_terminal(5, 30, ['ruby', 'bin/simple_repl'], startup_message: 'prompt>') - assert_equal(Yamatanooroti.options.windows, identify) + assert_equal(Yamatanooroti.options.terminal ? :terminal : Yamatanooroti.options.windows, identify) end def test_codepage_932 From 31a14306feb5560e771697bc30315a7a87b2ba8c Mon Sep 17 00:00:00 2001 From: YO4 Date: Thu, 10 Oct 2024 01:33:27 +0900 Subject: [PATCH 22/76] download windows terminal zip --- lib/yamatanooroti/windows/windows.rb | 63 +++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index 9275e47..146fb01 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -1,5 +1,9 @@ require 'stringio' require 'win32/registry' +require 'tmpdir' +require 'fileutils' +require 'uri' +require 'digest/sha2' module Yamatanooroti::WindowsConsoleSettings DelegationConsoleSetting = { @@ -64,12 +68,67 @@ module Yamatanooroti::WindowsConsoleSettings end if @orig_console && @orig_terminal end + def self.tmpdir + if Yamatanooroti.options.terminal_workdir + dir = Yamatanooroti.options.terminal_workdir + FileUtils.mkdir_p(dir) + else + dir = nil + Thread.new do + Thread.current.abort_on_exception = true + Dir.tmpdir do |tmpdir| + dir = tmpdir + p dir + sleep + end + end + Thread.pass while dir == nil + end + return dir + end + def self.prepare_terminal_canary - puts "TODO: prepare_terminal_canary" + header = `curl --head -sS -o NUL -L -w "%{url_effective}\n%header{ETag}\n%header{Content-Length}\n%header{Last-Modified}" https://aka.ms/terminal-canary-zip-x64` + url, etag, length, timestamp = *header.lines.map(&:chomp) + dir = tmpdir() + name = File.basename(URI.parse(url).path) + path = File.join(dir, "canary", etag, name) + if File.exist?(path) + if File.size(path) == length.to_i + puts "use existing #{path}" + return path + else + FileUtils.remove_entry(path) + end + else + if Dir.empty(dir) + puts "removing old canary zip" + Dir.entries.each { |olddir| FileUtils.remove_entry(olddir) } + end + end + FileUtils.mkdir_p(File.dirname(path)) + system "curl #{$stdin.isatty ? "" : "-sS "}-L -o #{path} https://aka.ms/terminal-canary-zip-x64" + return path end def self.prepare_terminal_portable - puts "TODO: prepare_terminal_portable" + releases = Yamatanooroti::Options::WindowsTerminal::RELEASES + url = releases[Yamatanooroti.options.windows.to_sym][:url] + sha256 = releases[Yamatanooroti.options.windows.to_sym][:sha256] + dir = tmpdir() + name = File.basename(URI.parse(url).path) + path = File.join(dir, Yamatanooroti.options.windows, name) + if File.exist?(path) + if Digest::SHA256.new.file(path).hexdigest.upcase == sha256 + puts "use existing #{path}" + return path + else + FileUtils.remove_entry(path) + end + end + FileUtils.mkdir_p(File.dirname(path)) + system "curl #{$stdin.isatty ? "" : "-sS "}-L -o #{path} #{url}" + return path end end From 875ebf2b9562e0b36b5592a30f48069d5128b614 Mon Sep 17 00:00:00 2001 From: YO4 Date: Sun, 13 Oct 2024 23:34:17 +0900 Subject: [PATCH 23/76] extract windows terminal zip --- lib/yamatanooroti/windows/windows.rb | 78 ++++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 11 deletions(-) diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index 146fb01..70b982e 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -17,6 +17,14 @@ module Yamatanooroti::WindowsConsoleSettings preview: "{86633F1F-6454-40EC-89CE-DA4EBA977EE2}", }.freeze + def self.wt_exe + @wt_exe + end + + def self.wt_wait + 0 + end + begin Win32::Registry::HKEY_CURRENT_USER.open('Console') do |reg| @orig_conhost = reg['ForceV2'] @@ -52,9 +60,9 @@ module Yamatanooroti::WindowsConsoleSettings reg['DelegationTerminal', Win32::Registry::REG_SZ] = DelegationTerminalSetting[:conhost] end if @orig_console && @orig_terminal when :canary - prepare_terminal_canary + @wt_exe = extract_terminal(prepare_terminal_canary) else - prepare_terminal_portable + @wt_exe = extract_terminal(prepare_terminal_portable) end end @@ -69,6 +77,7 @@ module Yamatanooroti::WindowsConsoleSettings end def self.tmpdir + @tmpdir if @tmpdir if Yamatanooroti.options.terminal_workdir dir = Yamatanooroti.options.terminal_workdir FileUtils.mkdir_p(dir) @@ -84,15 +93,61 @@ def self.tmpdir end Thread.pass while dir == nil end - return dir + return @tmpdir = dir + end + + def self.extract_terminal(path) + tar = File.join(ENV['SystemRoot'], "system32", "tar.exe") + extract_dir = File.join(tmpdir, "wt") + FileUtils.remove_entry(extract_dir) if File.exist?(extract_dir) + FileUtils.mkdir_p(extract_dir) + puts "extracting #{File.basename(path)}" + system tar, "xf", path, "-C", extract_dir + wt = Dir["**/wt.exe", base: extract_dir] + raise "not found wt.exe. aborted." if wt.size < 1 + raise "found wt.exe #{wt.size} times unexpectedly. aborted." if wt.size > 1 + wt = File.join(extract_dir, wt[0]) + wt_dir = File.dirname(wt) + portable_mark = File.join(wt_dir, ".portable") + open(portable_mark, "w") { |f| f.puts } unless File.exist?(portable_mark) + settings = File.join(wt_dir, "settings", "settings.json") + FileUtils.mkdir_p(File.dirname(settings)) + open(settings, "wb") do |settings| + settings.write <<~'JSON' + { + "defaultProfile": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", + "disableAnimations": true, + "profiles": + { + "defaults": + { + "bellStyle": "none", + "closeOnExit": "always" + }, + "list": + [ + { + "commandline": "%SystemRoot%\\System32\\cmd.exe", + "guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", + "name": "cmd.exe" + } + ] + }, + "warning.confirmCloseAllTabs": false, + "warning.largePaste": false, + "warning.multiLinePaste": false + } + JSON + end + wt end def self.prepare_terminal_canary - header = `curl --head -sS -o NUL -L -w "%{url_effective}\n%header{ETag}\n%header{Content-Length}\n%header{Last-Modified}" https://aka.ms/terminal-canary-zip-x64` + dir = tmpdir + header = `curl --head -sS -o #{tmpdir}/header -L -w "%{url_effective}\n%header{ETag}\n%header{Content-Length}\n%header{Last-Modified}" https://aka.ms/terminal-canary-zip-x64` url, etag, length, timestamp = *header.lines.map(&:chomp) - dir = tmpdir() name = File.basename(URI.parse(url).path) - path = File.join(dir, "canary", etag, name) + path = File.join(dir, "wt_dists", "canary", etag, name) if File.exist?(path) if File.size(path) == length.to_i puts "use existing #{path}" @@ -101,23 +156,23 @@ def self.prepare_terminal_canary FileUtils.remove_entry(path) end else - if Dir.empty(dir) + if Dir.empty?(dir) puts "removing old canary zip" Dir.entries.each { |olddir| FileUtils.remove_entry(olddir) } end end FileUtils.mkdir_p(File.dirname(path)) system "curl #{$stdin.isatty ? "" : "-sS "}-L -o #{path} https://aka.ms/terminal-canary-zip-x64" - return path + path end def self.prepare_terminal_portable releases = Yamatanooroti::Options::WindowsTerminal::RELEASES url = releases[Yamatanooroti.options.windows.to_sym][:url] sha256 = releases[Yamatanooroti.options.windows.to_sym][:sha256] - dir = tmpdir() + dir = tmpdir name = File.basename(URI.parse(url).path) - path = File.join(dir, Yamatanooroti.options.windows, name) + path = File.join(dir, "wt_dists", Yamatanooroti.options.windows, name) if File.exist?(path) if Digest::SHA256.new.file(path).hexdigest.upcase == sha256 puts "use existing #{path}" @@ -128,7 +183,8 @@ def self.prepare_terminal_portable end FileUtils.mkdir_p(File.dirname(path)) system "curl #{$stdin.isatty ? "" : "-sS "}-L -o #{path} #{url}" - return path + raise "not match windows terminal distribution zip sha256" unless Digest::SHA256.new.file(path).hexdigest.upcase == sha256 + path end end From 5a9886c549b9e5d581b63f463d57c3f99022d5ac Mon Sep 17 00:00:00 2001 From: YO4 Date: Mon, 14 Oct 2024 01:21:50 +0900 Subject: [PATCH 24/76] support windows terminal --- lib/yamatanooroti/options.rb | 23 ++-- lib/yamatanooroti/windows.rb | 7 +- lib/yamatanooroti/windows/terminal.rb | 170 ++++++++++++++++++++++++++ 3 files changed, 184 insertions(+), 16 deletions(-) create mode 100644 lib/yamatanooroti/windows/terminal.rb diff --git a/lib/yamatanooroti/options.rb b/lib/yamatanooroti/options.rb index c3124bc..900e52c 100644 --- a/lib/yamatanooroti/options.rb +++ b/lib/yamatanooroti/options.rb @@ -34,15 +34,6 @@ module Options end end - @default_wait = 0.01 - @default_timeout = 2.0 - @windows = :conhost - @conhost = true - @terminal = false - @terminal_workdir = nil - @show_console = nil - @close_console = :always - module WindowsTerminal ALIAS = { stable: :"1.21", @@ -79,12 +70,6 @@ module WindowsTerminal sha256: "F2B1539649D17752888D7944F97D6372F8D48EB1CEB024501DF8D8E9D3352F25" }, } - @wt_path = nil - - # `curl -L -O -s -w "%{url_effective}" https://aka.ms/terminal-canary-zip-x64` - # `curl --head -w "%header{Location}\n%header{Content-Length} %header{Last-Modified}" https://aka.ms/terminal-canary-zip-x64` - # `curl --head -L -w "%header{Location}\n%header{Content-Length} %header{Last-Modified}" https://aka.ms/terminal-canary-zip-x64` - # `curl --head -L -w "%{url_effective}\n%header{Location}\n%header{Content-Length}\n%header{Last-Modified}" https://aka.ms/terminal-canary-zip-x64` def self.interpret(name) if ALIAS.has_key?(name) @@ -105,6 +90,14 @@ def self.interpret(name) CLOSE_WHEN = [:always, :pass, :never] ::Test::Unit::AutoRunner.setup_option do |autorunner, o| + @default_wait = 0.01 + @default_timeout = 2.0 + @windows = Yamatanooroti.win? ? :conhost : nil + @conhost = true + @terminal = false + @terminal_workdir = nil + @show_console = nil + @close_console = :always o.on_tail("yamatanooroti options") o.on_tail("--wait=#{@default_wait}", Float, diff --git a/lib/yamatanooroti/windows.rb b/lib/yamatanooroti/windows.rb index 84d976b..706928f 100644 --- a/lib/yamatanooroti/windows.rb +++ b/lib/yamatanooroti/windows.rb @@ -2,6 +2,7 @@ require_relative 'windows/windows-definition' require_relative 'windows/windows' require_relative 'windows/conhost' +require_relative 'windows/terminal' module Yamatanooroti::WindowsTestCaseModule def write(str) @@ -33,7 +34,11 @@ def start_terminal(height, width, command, wait: nil, timeout: nil, startup_mess @terminal.close_console end end - @terminal = Yamatanooroti::ConhostTerm.setup_console(height, width, @wait) + if Yamatanooroti.options.conhost + @terminal = Yamatanooroti::ConhostTerm.setup_console(height, width, @wait) + else + @terminal = Yamatanooroti::WindowsTerminalTerm.setup_console(height, width, @wait, @timeout) + end @terminal.setup_cp(codepage) if codepage @terminal.launch(command) diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb new file mode 100644 index 0000000..8962f44 --- /dev/null +++ b/lib/yamatanooroti/windows/terminal.rb @@ -0,0 +1,170 @@ +class Yamatanooroti::WindowsTerminalTerm + include Yamatanooroti::WindowsTermMixin + + @@count = 0 + + def get_size + attach_terminal do |conin, conout| + csbi = DL.get_console_screen_buffer_info(conout) + [csbi.Bottom + 1, csbi.Right + 1] + end + end + + def do_tasklist(filter) + list = `tasklist /FI "#{filter}"`.lines + if list.length != 4 + return nil + end + pid_start = list[2].index(/ \K=/) + list[3][pid_start..-1].to_i + end + + def pid_from_imagename(name) + do_tasklist("IMAGENAME eq #{name}") + end + + def pid_from_windowtitle(name) + do_tasklist("WINDOWTITLE eq #{name}") + end + + private def invoke_wt_process(command, marker, timeout) + spawn(command) + # wait for create console process complete + wait_until = Time.now + timeout + marker_pid = loop do + pid = pid_from_imagename(marker) + break pid if pid + raise "Windows Terminal marker process detection failed." if wait_until < Time.now + sleep @wait + end + @console_process_id = marker_pid + + # wait for console startup complete + 8.times do |n| + break if attach_terminal { true } + sleep 0.01 * 2**n + end + + keeper_pid = attach_terminal do + spawn(CONSOLE_KEEPING_COMMAND) + end + @console_process_id = keeper_pid + + # wait for console keeping process startup complete + 8.times do |n| + break if attach_terminal { true } + sleep 0.01 * 2**n + end + + Process.kill(:KILL, marker_pid) + return keeper_pid + end + + def new_wt(rows, cols, timeout) + marker_command = CONSOLE_MARKING_COMMAND + + @wt_id = "yamaoro#{Process.pid}##{@@count}" + @@count += 1 + command = "#{Yamatanooroti::WindowsConsoleSettings.wt_exe} -w #{@wt_id} --size #{cols},#{rows} nt --title #{@wt_id} #{marker_command}" + + return invoke_wt_process(command, marker_command.split(" ").first, timeout) + end + + def split_pane(div = 0.5, timeout) + marker_command = CONSOLE_MARKING_COMMAND + + command = "#{Yamatanooroti::WindowsConsoleSettings.wt_exe} -w #{@wt_id} sp -V --title #{@wt_id} -s #{div} #{marker_command}" + return invoke_wt_process(command, marker_command.split(" ").first, timeout) + end + + def close_pane + Process.kill(:KILL, @console_process_id) + @console_process_id = @terminal_process_id + end + + @@minimum_width = nil + @@div_to_width = {} + @@width_to_div = {} + + def self.setup_console(height, width, wait, timeout) + if @@minimum_width.nil? || @@minimum_width <= width + wt = self.new(height, width, wait, timeout) + end + if wt + size = wt.get_size + if size == [height, width] + return wt + else + @@minimum_width = size[1] + wt.close_console + end + end + min_w = @@minimum_width + expanded_size = min_w + 30 + wt = self.new(height, expanded_size, wait, timeout) + div = @@width_to_div[width] + div ||= (width * 98 + (min_w - width) * 9) / (expanded_size - 5) + loop do + w = dw = @@div_to_width[div] + unless w + wt.split_pane(div/100.0, timeout) + size = wt.get_size + w = @@div_to_width[div] = size[1] + end + if w == width + wt.split_pane(div/100.0, timeout) if dw + @@width_to_div[width] = div + return wt + else + unless dw + wt.close_pane + sleep Yamatanooroti::WindowsConsoleSettings.wt_wait + end + if w > width + div -= 1 + if div <= 0 + raise "Could not set Windows Terminal to size #{[height, width]}" + end + else + div += 1 + if div >= 100 + raise "Could not set Windows Terminal to size #{[height, width]}" + end + end + end + end + end + + def initialize(height, width, wait, timeout) + @wait = wait + @result = nil + @codepage_success_p = nil + + @terminal_process_id = new_wt(height, width, timeout) + end + + def close + if @target && !@target.closed? + @target.close + end + @result ||= retrieve_screen if !DL.interrupted? + end + + def close_console + if @target && !@target.closed? + @target.close + end + begin + Process.kill("KILL", @console_process_id) if @console_process_id + rescue Errno::ESRCH # No such process + ensure + begin + if @console_process_id != @terminal_process_id + Process.kill("KILL", @terminal_process_id) + end + rescue Errno::ESRCH # No such process + end + @console_process_id = @terminal_process_id = nil + end + end +end From a9191f9bcfd64b685a6e6e749ea4ab10fa58d57a Mon Sep 17 00:00:00 2001 From: YO4 Date: Mon, 14 Oct 2024 02:05:55 +0900 Subject: [PATCH 25/76] expand invoke timeout --- lib/yamatanooroti/windows/terminal.rb | 2 +- lib/yamatanooroti/windows/windows.rb | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb index 8962f44..be1ffc0 100644 --- a/lib/yamatanooroti/windows/terminal.rb +++ b/lib/yamatanooroti/windows/terminal.rb @@ -30,7 +30,7 @@ def pid_from_windowtitle(name) private def invoke_wt_process(command, marker, timeout) spawn(command) # wait for create console process complete - wait_until = Time.now + timeout + wait_until = Time.now + timeout + 3 # 2sec timeout seems to be too short marker_pid = loop do pid = pid_from_imagename(marker) break pid if pid diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index 70b982e..f1fcaed 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -139,6 +139,7 @@ def self.extract_terminal(path) } JSON end + puts "use #{wt} for windows console" wt end From ec56b4d0e48b8593bc7a8015b7107f04837fef12 Mon Sep 17 00:00:00 2001 From: YO4 Date: Mon, 14 Oct 2024 01:33:40 +0900 Subject: [PATCH 26/76] add windows terminal to ci --- .github/workflows/y.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/y.yml b/.github/workflows/y.yml index 680e92e..ef85472 100644 --- a/.github/workflows/y.yml +++ b/.github/workflows/y.yml @@ -42,15 +42,23 @@ jobs: run: | bundle install bundle exec rake test - windows-conhost: + windows-yamatanooroti: name: >- ${{ matrix.os }} ${{ matrix.ruby }} ${{ matrix.console }} runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ windows-2019, windows-2022 ] ruby: [ 3.3, mingw ] - console: [ conhost, legacy-conhost ] + console: [ conhost, legacy-conhost, 1.21, 1.22preview, canary ] + exclude: + - console: 1.21 + os: windows-2019 + - console: 1.22preview + os: windows-2019 + - console: canary + os: windows-2019 defaults: run: shell: bash @@ -62,4 +70,4 @@ jobs: ruby-version: ${{ matrix.ruby }} - name: rake test run: | - rake test TESTOPTS="-v --windows=${{ matrix.console }}" + rake test TESTOPTS="-v --wt_dir=./tmp --windows=${{ matrix.console }}" From 6c85a8e514a05bc8037b401a5b8cde532387e431 Mon Sep 17 00:00:00 2001 From: YO4 Date: Mon, 14 Oct 2024 02:38:08 +0900 Subject: [PATCH 27/76] allow close() to be called after close_console() If ```def teardown; close; end``` defined in testcase, close() occurs after close_console() which triggered by teardown block in WindowsTestcaseModule --- lib/yamatanooroti/windows/conhost.rb | 2 +- lib/yamatanooroti/windows/terminal.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/yamatanooroti/windows/conhost.rb b/lib/yamatanooroti/windows/conhost.rb index e2edec9..7ab017e 100644 --- a/lib/yamatanooroti/windows/conhost.rb +++ b/lib/yamatanooroti/windows/conhost.rb @@ -27,7 +27,7 @@ def close if @target && !@target.closed? @target.close end - @result ||= retrieve_screen if !DL.interrupted? + @result ||= retrieve_screen if !DL.interrupted? && @console_process_id end def close_console diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb index be1ffc0..19fd500 100644 --- a/lib/yamatanooroti/windows/terminal.rb +++ b/lib/yamatanooroti/windows/terminal.rb @@ -147,7 +147,7 @@ def close if @target && !@target.closed? @target.close end - @result ||= retrieve_screen if !DL.interrupted? + @result ||= retrieve_screen if !DL.interrupted? && @console_process_id end def close_console From 9f46da4c209c9ac950c1f5536d0b104ed5e614ab Mon Sep 17 00:00:00 2001 From: YO4 Date: Tue, 15 Oct 2024 20:44:43 +0900 Subject: [PATCH 28/76] prevent to child process handle leak This leak causes problems in the test_yamatanooroti of reline. To wait process terminate with timeout, use Process.detach and Thread#join(timeout) --- lib/yamatanooroti/windows/terminal.rb | 66 +++++++++++++++++---------- 1 file changed, 41 insertions(+), 25 deletions(-) diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb index 19fd500..44a73d1 100644 --- a/lib/yamatanooroti/windows/terminal.rb +++ b/lib/yamatanooroti/windows/terminal.rb @@ -2,6 +2,30 @@ class Yamatanooroti::WindowsTerminalTerm include Yamatanooroti::WindowsTermMixin @@count = 0 + @@cradle = {} + + def call_spawn(command) + pid = spawn(command) + if t = Process.detach(pid) + @@cradle[pid] = t + end + pid + end + + def kill_and_wait(pid) + return unless pid + t = @@cradle[pid] + begin + Process.kill(:KILL, pid) + rescue Errno::ESRCH # No such process + end + if t + if t.join(@timeout) == nil + puts "Caution: process #{pid} does not terminate in #{@timeout} seconds." + end + @@cradle.delete(pid) + end + end def get_size attach_terminal do |conin, conout| @@ -27,10 +51,10 @@ def pid_from_windowtitle(name) do_tasklist("WINDOWTITLE eq #{name}") end - private def invoke_wt_process(command, marker, timeout) - spawn(command) + private def invoke_wt_process(command, marker) + call_spawn(command) # wait for create console process complete - wait_until = Time.now + timeout + 3 # 2sec timeout seems to be too short + wait_until = Time.now + @timeout + 3 # 2sec timeout seems to be too short marker_pid = loop do pid = pid_from_imagename(marker) break pid if pid @@ -46,7 +70,7 @@ def pid_from_windowtitle(name) end keeper_pid = attach_terminal do - spawn(CONSOLE_KEEPING_COMMAND) + call_spawn(CONSOLE_KEEPING_COMMAND) end @console_process_id = keeper_pid @@ -56,29 +80,29 @@ def pid_from_windowtitle(name) sleep 0.01 * 2**n end - Process.kill(:KILL, marker_pid) + kill_and_wait(marker_pid) return keeper_pid end - def new_wt(rows, cols, timeout) + def new_wt(rows, cols) marker_command = CONSOLE_MARKING_COMMAND @wt_id = "yamaoro#{Process.pid}##{@@count}" @@count += 1 command = "#{Yamatanooroti::WindowsConsoleSettings.wt_exe} -w #{@wt_id} --size #{cols},#{rows} nt --title #{@wt_id} #{marker_command}" - return invoke_wt_process(command, marker_command.split(" ").first, timeout) + return invoke_wt_process(command, marker_command.split(" ").first) end - def split_pane(div = 0.5, timeout) + def split_pane(div = 0.5) marker_command = CONSOLE_MARKING_COMMAND command = "#{Yamatanooroti::WindowsConsoleSettings.wt_exe} -w #{@wt_id} sp -V --title #{@wt_id} -s #{div} #{marker_command}" - return invoke_wt_process(command, marker_command.split(" ").first, timeout) + return invoke_wt_process(command, marker_command.split(" ").first) end def close_pane - Process.kill(:KILL, @console_process_id) + kill_and_wait(@console_process_id) @console_process_id = @terminal_process_id end @@ -107,12 +131,12 @@ def self.setup_console(height, width, wait, timeout) loop do w = dw = @@div_to_width[div] unless w - wt.split_pane(div/100.0, timeout) + wt.split_pane(div/100.0) size = wt.get_size w = @@div_to_width[div] = size[1] end if w == width - wt.split_pane(div/100.0, timeout) if dw + wt.split_pane(div/100.0) if dw @@width_to_div[width] = div return wt else @@ -137,10 +161,11 @@ def self.setup_console(height, width, wait, timeout) def initialize(height, width, wait, timeout) @wait = wait + @timeout = timeout @result = nil @codepage_success_p = nil - @terminal_process_id = new_wt(height, width, timeout) + @terminal_process_id = new_wt(height, width) end def close @@ -154,17 +179,8 @@ def close_console if @target && !@target.closed? @target.close end - begin - Process.kill("KILL", @console_process_id) if @console_process_id - rescue Errno::ESRCH # No such process - ensure - begin - if @console_process_id != @terminal_process_id - Process.kill("KILL", @terminal_process_id) - end - rescue Errno::ESRCH # No such process - end - @console_process_id = @terminal_process_id = nil - end + kill_and_wait(@console_process_id) if @console_process_id + kill_and_wait(@terminal_process_id) if @console_process_id != @terminal_process_id + @console_process_id = @terminal_process_id = nil end end From 745b886ea730a87d6017629f3bd6879e2df5fd06 Mon Sep 17 00:00:00 2001 From: YO4 Date: Tue, 15 Oct 2024 22:14:51 +0900 Subject: [PATCH 29/76] use GeneraConsoleCtrlEvent to simulate Ctrl+C for irb test_yamatanooroti --- .../windows/windows-definition.rb | 9 +++++++ lib/yamatanooroti/windows/windows.rb | 26 ++++++++++++++++--- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/lib/yamatanooroti/windows/windows-definition.rb b/lib/yamatanooroti/windows/windows-definition.rb index 3ec2536..c8a3768 100644 --- a/lib/yamatanooroti/windows/windows-definition.rb +++ b/lib/yamatanooroti/windows/windows-definition.rb @@ -124,6 +124,7 @@ module Yamatanooroti::WindowsDefinition SW_SHOWNOACTIVE = 4 SW_SHOWMINNOACTIVE = 7 LEFT_ALT_PRESSED = 0x0002 + ENABLE_PROCESSED_INPUT = 0x0001 ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 # BOOL CloseHandle(HANDLE hObject); @@ -185,6 +186,8 @@ module Yamatanooroti::WindowsDefinition # BOOL WINAPI SetConsoleCtrlHandler(PHANDLER_ROUTINE HandlerRoutine, BOOL Add); extern 'BOOL SetConsoleCtrlHandler(void *, BOOL Add);', :stdcall + # BOOL WINAPI GenerateConsoleCtrlEvent(DWORD dwCtrlEvent, DWORD dwProcessGroupId); + extern 'BOOL GenerateConsoleCtrlEvent(DWORD, DWORD);', :stdcall private def error_message(r, method_name, exception: true) return if not r.zero? @@ -382,6 +385,12 @@ def set_console_mode(handle, mode) 0 != SetConsoleMode(handle, mode) end + def generate_console_ctrl_event(event, pgrp) + r = GenerateConsoleCtrlEvent(event, pgrp) + error_message(r, 'GenerateConsoleCtrlEvent') + return r != 0 + end + # Ctrl+C trap support # FreeConsole(), AttachConsole() clears console control handlers. # Make matter worse, C runtime does not provide the function to restore that handlers. diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index f1fcaed..27a35ad 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -192,7 +192,7 @@ def self.prepare_terminal_portable module Yamatanooroti::WindowsTermMixin DL = Yamatanooroti::WindowsDefinition - CONSOLE_KEEPING_COMMAND = %q[ruby.exe --disable=gems -e sleep] + CONSOLE_KEEPING_COMMAND = %q[ruby.exe --disable=gems -e "Signal.trap(:INT, nil); sleep"] CONSOLE_MARKING_COMMAND = %q[findstr.exe yamatanooroti] private def attach_terminal(open = true) @@ -321,20 +321,20 @@ def sync def launch(command) check_interrupt - attach_terminal do + attach_terminal(false) do @target = SubProcess.new(command.map{ |c| quote_command_arg(c) }.join(' ')) end end def setup_cp(cp) - @codepage_success_p = attach_terminal { system("chcp #{Integer(cp)} > NUL") } + @codepage_success_p = attach_terminal(false) { system("chcp #{Integer(cp)} > NUL") } end def codepage_success? @codepage_success_p end - def write(str) + def do_write(str) check_interrupt codes = str.chars.map do |c| c = "\r" if c == "\n" @@ -366,6 +366,24 @@ def write(str) end end + def write(str) + mode = attach_terminal { |conin, conout| DL.get_console_mode(conin) } + if 0 == (mode & DL::ENABLE_PROCESSED_INPUT) + do_write(str) + else + str.dup.force_encoding(Encoding::ASCII_8BIT).split(/(\C-c)/).each do |chunk| + if chunk == "\C-c" + attach_terminal(false) do + # generate Ctrl+C event to process on same console + DL.generate_console_ctrl_event(0, 0) + end + else + do_write(chunk.force_encoding(str.encoding)) if chunk != "" + end + end + end + end + def retrieve_screen(top_of_buffer: false) return @result if @result check_interrupt From 6495f7f8b0c73e024d50b7b7671fe282c49d0fed Mon Sep 17 00:00:00 2001 From: YO4 Date: Tue, 15 Oct 2024 22:26:13 +0900 Subject: [PATCH 30/76] kill process recursively Process.kill faster than using taskkill.exe, but taskkill.exe can be expected to kill process recursively more robust. test_yamatanooroti of irb requires this. --- lib/yamatanooroti/windows/windows.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index 27a35ad..c0092af 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -279,10 +279,7 @@ def initialize(command) def close unless closed? - begin - Process.kill("KILL", @pid) - rescue Errno::ESRCH # No such process - end + system("taskkill /PID #{@pid} /F /T", {[:out, :err] => "NUL"}) @status = @mon.join.value.exitstatus sync @errin.close From 5087550ad2be35d5e920e5d19be4a490b4bac1d0 Mon Sep 17 00:00:00 2001 From: YO4 Date: Thu, 17 Oct 2024 00:54:14 +0900 Subject: [PATCH 31/76] add timeout to wait terminal wakeup --- lib/yamatanooroti/windows/terminal.rb | 30 +++++++++++++++------------ 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb index 44a73d1..3554d9a 100644 --- a/lib/yamatanooroti/windows/terminal.rb +++ b/lib/yamatanooroti/windows/terminal.rb @@ -51,22 +51,27 @@ def pid_from_windowtitle(name) do_tasklist("WINDOWTITLE eq #{name}") end + private def with_timeout(timeout_message, timeout = @timeout, &block) + wait_until = Time.now + timeout + loop do + result = block.call + break result if result + raise timeout_message if wait_until < Time.now + sleep @wait + end + end + private def invoke_wt_process(command, marker) call_spawn(command) - # wait for create console process complete - wait_until = Time.now + @timeout + 3 # 2sec timeout seems to be too short - marker_pid = loop do - pid = pid_from_imagename(marker) - break pid if pid - raise "Windows Terminal marker process detection failed." if wait_until < Time.now - sleep @wait + # default timeout seems to be too short + marker_pid = with_timeout("Windows Terminal marker process detection failed.", @timeout + 5) do + pid_from_imagename(marker) end @console_process_id = marker_pid # wait for console startup complete - 8.times do |n| - break if attach_terminal { true } - sleep 0.01 * 2**n + with_timeout("Console process startup timed out.") do + attach_terminal { true } end keeper_pid = attach_terminal do @@ -75,9 +80,8 @@ def pid_from_windowtitle(name) @console_process_id = keeper_pid # wait for console keeping process startup complete - 8.times do |n| - break if attach_terminal { true } - sleep 0.01 * 2**n + with_timeout("Console process startup timed out.") do + attach_terminal { true } end kill_and_wait(marker_pid) From 808d5c4fd837997e23a72122d256f69956526a0b Mon Sep 17 00:00:00 2001 From: YO4 Date: Thu, 17 Oct 2024 00:57:51 +0900 Subject: [PATCH 32/76] always check terminal size --- lib/yamatanooroti/windows/terminal.rb | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb index 3554d9a..c961c85 100644 --- a/lib/yamatanooroti/windows/terminal.rb +++ b/lib/yamatanooroti/windows/terminal.rb @@ -133,21 +133,16 @@ def self.setup_console(height, width, wait, timeout) div = @@width_to_div[width] div ||= (width * 98 + (min_w - width) * 9) / (expanded_size - 5) loop do - w = dw = @@div_to_width[div] - unless w - wt.split_pane(div/100.0) - size = wt.get_size - w = @@div_to_width[div] = size[1] - end + wt.split_pane(div/100.0) + sleep Yamatanooroti::WindowsConsoleSettings.wt_wait + size = wt.get_size + w = size[1] if w == width - wt.split_pane(div/100.0) if dw @@width_to_div[width] = div return wt else - unless dw - wt.close_pane - sleep Yamatanooroti::WindowsConsoleSettings.wt_wait - end + wt.close_pane + sleep Yamatanooroti::WindowsConsoleSettings.wt_wait if w > width div -= 1 if div <= 0 From f47d625887664466317869115c7a21fdd9a713fd Mon Sep 17 00:00:00 2001 From: YO4 Date: Thu, 17 Oct 2024 00:58:30 +0900 Subject: [PATCH 33/76] fix tmpdir when --wt-dir not specified --- lib/yamatanooroti/windows/windows.rb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index c0092af..dc301fc 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -77,18 +77,20 @@ def self.wt_wait end def self.tmpdir - @tmpdir if @tmpdir + return @tmpdir if @tmpdir + dir = nil if Yamatanooroti.options.terminal_workdir dir = Yamatanooroti.options.terminal_workdir FileUtils.mkdir_p(dir) else - dir = nil - Thread.new do + @tmpdir_t = Thread.new do Thread.current.abort_on_exception = true - Dir.tmpdir do |tmpdir| + Dir.mktmpdir do |tmpdir| dir = tmpdir p dir sleep + ensure + sleep 0.5 # wait for terminate windows terminal end end Thread.pass while dir == nil @@ -148,7 +150,7 @@ def self.prepare_terminal_canary header = `curl --head -sS -o #{tmpdir}/header -L -w "%{url_effective}\n%header{ETag}\n%header{Content-Length}\n%header{Last-Modified}" https://aka.ms/terminal-canary-zip-x64` url, etag, length, timestamp = *header.lines.map(&:chomp) name = File.basename(URI.parse(url).path) - path = File.join(dir, "wt_dists", "canary", etag, name) + path = File.join(dir, "wt_dists", "canary", etag.delete('"'), name) if File.exist?(path) if File.size(path) == length.to_i puts "use existing #{path}" From 9f205598fec5cd54404ff0d07359e03d5d058ee0 Mon Sep 17 00:00:00 2001 From: YO4 Date: Thu, 17 Oct 2024 01:36:43 +0900 Subject: [PATCH 34/76] report tasklist when marker process detection failed --- lib/yamatanooroti/windows/terminal.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb index c961c85..e39b44c 100644 --- a/lib/yamatanooroti/windows/terminal.rb +++ b/lib/yamatanooroti/windows/terminal.rb @@ -64,8 +64,13 @@ def pid_from_windowtitle(name) private def invoke_wt_process(command, marker) call_spawn(command) # default timeout seems to be too short - marker_pid = with_timeout("Windows Terminal marker process detection failed.", @timeout + 5) do - pid_from_imagename(marker) + begin + marker_pid = with_timeout("Windows Terminal marker process detection failed.", @timeout + 5) do + pid_from_imagename(marker) + end + rescue => e + system "tasklist /FI \"SESSION ge 0\"" + raise e end @console_process_id = marker_pid From 669b5da10bcca5afbcd0df0fbbba814798ca9619 Mon Sep 17 00:00:00 2001 From: YO4 Date: Thu, 17 Oct 2024 01:54:31 +0900 Subject: [PATCH 35/76] more debug --- lib/yamatanooroti/windows/terminal.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb index e39b44c..ea36a6f 100644 --- a/lib/yamatanooroti/windows/terminal.rb +++ b/lib/yamatanooroti/windows/terminal.rb @@ -69,7 +69,7 @@ def pid_from_windowtitle(name) pid_from_imagename(marker) end rescue => e - system "tasklist /FI \"SESSION ge 0\"" + puts `tasklist /FI "SESSION ge 0"` raise e end @console_process_id = marker_pid From 8904d9d81b2974b71de7471be297a1bcbdf99bac Mon Sep 17 00:00:00 2001 From: YO4 Date: Thu, 17 Oct 2024 22:56:08 +0900 Subject: [PATCH 36/76] shrink windows terminal font size It seems to maximum window size of WT limited. Mitigating it. --- lib/yamatanooroti/windows/windows.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index dc301fc..a360caa 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -124,7 +124,11 @@ def self.extract_terminal(path) "defaults": { "bellStyle": "none", - "closeOnExit": "always" + "closeOnExit": "always", + "font": + { + "size": 9 + } }, "list": [ From d3dd5ffbc350d5582723ff84ae310d5b8e4dad4d Mon Sep 17 00:00:00 2001 From: YO4 Date: Thu, 17 Oct 2024 23:42:20 +0900 Subject: [PATCH 37/76] repl with ```ruby windows-definition.rb``` for win32api operation experiment --- .../windows/windows-definition.rb | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/lib/yamatanooroti/windows/windows-definition.rb b/lib/yamatanooroti/windows/windows-definition.rb index c8a3768..e8e7f5c 100644 --- a/lib/yamatanooroti/windows/windows-definition.rb +++ b/lib/yamatanooroti/windows/windows-definition.rb @@ -1,5 +1,6 @@ require 'fiddle/import' require 'fiddle/types' +class Yamatanooroti; end module Yamatanooroti::WindowsDefinition extend Fiddle::Importer @@ -113,6 +114,10 @@ module Yamatanooroti::WindowsDefinition 'DWORD dwControlKeyState' ] + STD_INPUT_HANDLE = -10 + STD_OUTPUT_HANDLE = -11 + STD_ERROR_HANDLE = -12 + STARTF_USESHOWWINDOW = 1 CREATE_NEW_CONSOLE = 0x10 CREATE_NEW_PROCESS_GROUP = 0x200 @@ -127,6 +132,8 @@ module Yamatanooroti::WindowsDefinition ENABLE_PROCESSED_INPUT = 0x0001 ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 + # HANDLE GetStdHandle(DWORD nStdHandle); + extern 'HANDLE GetStdHandle(DWORD);', :stdcall # BOOL CloseHandle(HANDLE hObject); extern 'BOOL CloseHandle(HANDLE);', :stdcall @@ -325,6 +332,12 @@ def create_console(command) return console_process_info.dwProcessId end + def get_std_handle(stdhandle) + fh = GetStdHandle(stdhandle) + error_message(0, name) if fh == INVALID_HANDLE_VALUE + fh + end + def mb2wc(str) size = MultiByteToWideChar(65001, 0, str, str.bytesize, '', 0) converted_str = "\x00".b * (size * 2) @@ -429,9 +442,32 @@ def self.at_exit end end - Test::Unit.at_exit do - self.at_exit + if Object.const_defined?(:Test) && Test.const_defined?(:Unit) + Test::Unit.at_exit do + Yamatanooroti::WindowsDefinition.at_exit + end end extend self end + +if __FILE__ == $0 + class Yamatanooroti + class << self + attr_reader :options + end + @options = Object.new + class << @options + attr_accessor :show_console, :windows + end + options.show_console = true + options.windows = :conhost + end + + DL = Yamatanooroti::WindowsDefinition + cin = DL.get_std_handle(DL::STD_INPUT_HANDLE) + cout = DL.get_std_handle(DL::STD_OUTPUT_HANDLE) + cerr = DL.get_std_handle(DL::STD_ERROR_HANDLE) + + binding.irb +end From c6231d4efb7d56063e8bc5e0f00d8cf4a154684c Mon Sep 17 00:00:00 2001 From: YO4 Date: Fri, 18 Oct 2024 00:25:56 +0900 Subject: [PATCH 38/76] windows terminal supports --no-show option --- lib/yamatanooroti/options.rb | 16 +++------------- lib/yamatanooroti/windows/terminal.rb | 2 +- lib/yamatanooroti/windows/windows-definition.rb | 2 +- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/lib/yamatanooroti/options.rb b/lib/yamatanooroti/options.rb index 900e52c..ce5f175 100644 --- a/lib/yamatanooroti/options.rb +++ b/lib/yamatanooroti/options.rb @@ -119,28 +119,18 @@ def self.interpret(name) @conhost = CONHOST_TYPES.include?(type) @terminal = !@conhost @windows = @conhost ? type : WindowsTerminal.interpret(type) - if @terminal - if @show_console == false - puts "Windows Terminal is always visible. --no-show-console is ignored." - end - @show_console = true - end end o.on_tail("--wt-dir=DIR", String, "Specify Windows Terminal working dir.", - "Automatically determined if not specified and treated temporary.", - "DIR is treaded permanent if specified and download files are remains.") do |dir| + "Automatically determined if not specified.", + "DIR is treaded permanent if specified so download files are remains there.") do |dir| @terminal_workdir = dir end o.on_tail("--[no-]show-console", "Show test ongoing console.") do |show| - if show == false and @terminal - puts "Windows Terminal is always visible. --no-show-console is ignored." - else - @show_console = show - end + @show_console = show end o.on_tail("--[no-]close-console[=COND]", CLOSE_WHEN, diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb index ea36a6f..a46f623 100644 --- a/lib/yamatanooroti/windows/terminal.rb +++ b/lib/yamatanooroti/windows/terminal.rb @@ -62,7 +62,7 @@ def pid_from_windowtitle(name) end private def invoke_wt_process(command, marker) - call_spawn(command) + DL.create_console(command) # default timeout seems to be too short begin marker_pid = with_timeout("Windows Terminal marker process detection failed.", @timeout + 5) do diff --git a/lib/yamatanooroti/windows/windows-definition.rb b/lib/yamatanooroti/windows/windows-definition.rb index e8e7f5c..572b566 100644 --- a/lib/yamatanooroti/windows/windows-definition.rb +++ b/lib/yamatanooroti/windows/windows-definition.rb @@ -313,7 +313,7 @@ def create_console(command) startup_info.wShowWindow = SW_SHOWNOACTIVE else startup_info.dwFlags = STARTF_USESHOWWINDOW - startup_info.wShowWindow = Yamatanooroti.options.windows.to_s == "legacy-conhost" ? SW_SHOWMINNOACTIVE : SW_HIDE + startup_info.wShowWindow = Yamatanooroti.options.windows.to_s != "conhost" ? SW_SHOWMINNOACTIVE : SW_HIDE end restore_console_control_handler do From 003507cf87d99f37ec84e02b38321fe15fda0765 Mon Sep 17 00:00:00 2001 From: YO4 Date: Thu, 17 Oct 2024 23:05:46 +0900 Subject: [PATCH 39/76] propery check codepage changed --- .../windows/windows-definition.rb | 31 +++++++++++++++++-- lib/yamatanooroti/windows/windows.rb | 5 ++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/lib/yamatanooroti/windows/windows-definition.rb b/lib/yamatanooroti/windows/windows-definition.rb index 572b566..60f1e35 100644 --- a/lib/yamatanooroti/windows/windows-definition.rb +++ b/lib/yamatanooroti/windows/windows-definition.rb @@ -165,7 +165,14 @@ module Yamatanooroti::WindowsDefinition extern 'BOOL GetConsoleMode(HANDLE, LPDWORD);', :stdcall # BOOL WINAPI SetConsoleMode(HANDLE hConsoleHandle, DWORD dwMode); extern 'BOOL SetConsoleMode(HANDLE, DWORD);', :stdcall - + # BOOL WINAPI SetConsoleCP(UINT wCodePageID); + extern 'BOOL SetConsoleCP(UINT);', :stdcall + # BOOL WINAPI SetConsoleOutputCP(UINT wCodePageID); + extern 'BOOL SetConsoleOutputCP(UINT);', :stdcall + # UINT WINAPI GetConsoleCP(void); + extern 'UINT GetConsoleCP(void);', :stdcall + # UINT WINAPI GetConsoleOutputCP(void); + extern 'UINT GetConsoleOutputCP(void);', :stdcall # BOOL CreateProcessW(LPCWSTR lpApplicationName, LPWSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation); extern 'BOOL CreateProcessW(LPCWSTR lpApplicationName, LPWSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation);', :stdcall @@ -389,8 +396,8 @@ def get_number_of_console_input_events(handle) def get_console_mode(handle) mode = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DWORD, FREE) mode[0, Fiddle::SIZEOF_DWORD] = "\0".b * Fiddle::SIZEOF_DWORD - GetConsoleMode(handle, mode) - # error_message(r, 'GetConsoleMode') # may be fail + r = GetConsoleMode(handle, mode) + error_message(r, 'GetConsoleMode', exception: false) mode.to_str.unpack1('L') end @@ -398,6 +405,24 @@ def set_console_mode(handle, mode) 0 != SetConsoleMode(handle, mode) end + def set_console_codepage(cp) + r = SetConsoleCP(cp) + error_message(r, 'SetConsoleCP', exception: false) + end + + def set_console_output_codepage(cp) + r = SetConsoleOutputCP(cp) + error_message(r, 'SetConsoleOutputCP', exception: false) + end + + def get_console_codepage() + GetConsoleCP() + end + + def get_console_output_codepage() + GetConsoleOutputCP() + end + def generate_console_ctrl_event(event, pgrp) r = GenerateConsoleCtrlEvent(event, pgrp) error_message(r, 'GenerateConsoleCtrlEvent') diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index a360caa..a4e155b 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -330,7 +330,10 @@ def launch(command) end def setup_cp(cp) - @codepage_success_p = attach_terminal(false) { system("chcp #{Integer(cp)} > NUL") } + @codepage_success_p = attach_terminal(false) do + system("chcp #{Integer(cp)} > NUL") + DL.get_console_codepage() == cp && DL.get_console_output_codepage() == cp + end end def codepage_success? From b6aebec4ec91c488c6c23ba084b326cec4bc7342 Mon Sep 17 00:00:00 2001 From: YO4 Date: Fri, 18 Oct 2024 02:13:06 +0900 Subject: [PATCH 40/76] diagnose WT min/max size --- lib/yamatanooroti/windows/terminal.rb | 12 ++++++++++++ lib/yamatanooroti/windows/windows.rb | 3 +++ 2 files changed, 15 insertions(+) diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb index a46f623..89015f6 100644 --- a/lib/yamatanooroti/windows/terminal.rb +++ b/lib/yamatanooroti/windows/terminal.rb @@ -172,6 +172,18 @@ def initialize(height, width, wait, timeout) @terminal_process_id = new_wt(height, width) end + def self.diagnose_size_capability + wt = self.new(999, 999, 0.01, 5.0) + @@max_size = wt.get_size + @@max_size = [[@@max_size[0], 60].min, [@@max_size[1], 200].min] + puts @@max_size.then { "Windows Terminal maximum size: rows: #{_1}, columns: #{_2}" } + wt.close_console + wt = self.new(2, 2, 0.01, 5.0) + @@min_size = wt.get_size + puts @@min_size.then { "Windows Terminal smallest size: rows: #{_1}, columns: #{_2}" } + wt.close_console + end + def close if @target && !@target.closed? @target.close diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index a4e155b..5c31db8 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -64,6 +64,9 @@ def self.wt_wait else @wt_exe = extract_terminal(prepare_terminal_portable) end + if @wt_exe + Yamatanooroti::WindowsTerminalTerm.diagnose_size_capability + end end Test::Unit.at_exit do From b819910af41f094fce5dda9cf77390cbbd015775 Mon Sep 17 00:00:00 2001 From: YO4 Date: Fri, 18 Oct 2024 02:25:36 +0900 Subject: [PATCH 41/76] puts WT size if failed --- lib/yamatanooroti/windows.rb | 2 +- lib/yamatanooroti/windows/conhost.rb | 2 +- lib/yamatanooroti/windows/terminal.rb | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/yamatanooroti/windows.rb b/lib/yamatanooroti/windows.rb index 706928f..b8236d5 100644 --- a/lib/yamatanooroti/windows.rb +++ b/lib/yamatanooroti/windows.rb @@ -109,7 +109,7 @@ def self.included(cls) if !Yamatanooroti.options.show_console || Yamatanooroti.options.close_console == :always || Yamatanooroti.options.close_console == :pass && passed? - @terminal&.close_console + @terminal&.close_console(passed?) end end end diff --git a/lib/yamatanooroti/windows/conhost.rb b/lib/yamatanooroti/windows/conhost.rb index 7ab017e..5daf417 100644 --- a/lib/yamatanooroti/windows/conhost.rb +++ b/lib/yamatanooroti/windows/conhost.rb @@ -30,7 +30,7 @@ def close @result ||= retrieve_screen if !DL.interrupted? && @console_process_id end - def close_console + def close_console(passed = nil) if @target && !@target.closed? @target.close end diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb index 89015f6..23ab73f 100644 --- a/lib/yamatanooroti/windows/terminal.rb +++ b/lib/yamatanooroti/windows/terminal.rb @@ -191,10 +191,11 @@ def close @result ||= retrieve_screen if !DL.interrupted? && @console_process_id end - def close_console + def close_console(passed = nil) if @target && !@target.closed? @target.close end + puts get_size.then { "Windows Terminal max size: rows: #{_1}, columns: #{_2}" } if @terminal_process_id && passed == false kill_and_wait(@console_process_id) if @console_process_id kill_and_wait(@terminal_process_id) if @console_process_id != @terminal_process_id @console_process_id = @terminal_process_id = nil From a977f743f86a7982af757e90d48213cb67293b0a Mon Sep 17 00:00:00 2001 From: YO4 Date: Fri, 18 Oct 2024 01:57:49 +0900 Subject: [PATCH 42/76] windows terminal split pane calculation change font size change and swap-pane --- lib/yamatanooroti/windows/terminal.rb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb index 23ab73f..e03227e 100644 --- a/lib/yamatanooroti/windows/terminal.rb +++ b/lib/yamatanooroti/windows/terminal.rb @@ -106,7 +106,7 @@ def new_wt(rows, cols) def split_pane(div = 0.5) marker_command = CONSOLE_MARKING_COMMAND - command = "#{Yamatanooroti::WindowsConsoleSettings.wt_exe} -w #{@wt_id} sp -V --title #{@wt_id} -s #{div} #{marker_command}" + command = "#{Yamatanooroti::WindowsConsoleSettings.wt_exe} -w #{@wt_id} sp -V --title #{@wt_id} -s #{div} #{marker_command}; swap-pane previous" return invoke_wt_process(command, marker_command.split(" ").first) end @@ -133,22 +133,24 @@ def self.setup_console(height, width, wait, timeout) end end min_w = @@minimum_width - expanded_size = min_w + 30 + #expanded_size = min_w + 30 # for default font size + expanded_size = 101 wt = self.new(height, expanded_size, wait, timeout) div = @@width_to_div[width] - div ||= (width * 98 + (min_w - width) * 9) / (expanded_size - 5) + #div ||= (width * 98 + (min_w - width) * 9) / (expanded_size - 5) # for default font size + div ||= (expanded_size - width) * 84 / expanded_size + 8 loop do wt.split_pane(div/100.0) sleep Yamatanooroti::WindowsConsoleSettings.wt_wait size = wt.get_size w = size[1] + @@width_to_div[w] = div if w == width - @@width_to_div[width] = div return wt else wt.close_pane sleep Yamatanooroti::WindowsConsoleSettings.wt_wait - if w > width + if w < width div -= 1 if div <= 0 raise "Could not set Windows Terminal to size #{[height, width]}" From 59e2ed6cfc65cc658ec048254fe9f1fb9a7d5c1f Mon Sep 17 00:00:00 2001 From: YO4 Date: Fri, 18 Oct 2024 23:17:36 +0900 Subject: [PATCH 43/76] Change WT size changing strategy "wt.exe --size c,r" will sometimes fail and results diffrent size window. Introduce console size setting new strategy, one big window split vertical/horizontal to fit required size. This makes open and close windows less often. TESTOPTS="--show --close=pass" makes failing window leaved as console tab. --- lib/yamatanooroti/windows.rb | 14 +- lib/yamatanooroti/windows/conhost.rb | 28 +-- lib/yamatanooroti/windows/terminal.rb | 189 ++++++++++++------ .../windows/windows-definition.rb | 29 ++- lib/yamatanooroti/windows/windows.rb | 20 +- 5 files changed, 186 insertions(+), 94 deletions(-) diff --git a/lib/yamatanooroti/windows.rb b/lib/yamatanooroti/windows.rb index b8236d5..e51ed25 100644 --- a/lib/yamatanooroti/windows.rb +++ b/lib/yamatanooroti/windows.rb @@ -35,9 +35,9 @@ def start_terminal(height, width, command, wait: nil, timeout: nil, startup_mess end end if Yamatanooroti.options.conhost - @terminal = Yamatanooroti::ConhostTerm.setup_console(height, width, @wait) + @terminal = Yamatanooroti::ConhostTerm.setup_console(height, width, @wait, name) else - @terminal = Yamatanooroti::WindowsTerminalTerm.setup_console(height, width, @wait, @timeout) + @terminal = Yamatanooroti::WindowsTerminalTerm.setup_console(height, width, @wait, @timeout, name) end @terminal.setup_cp(codepage) if codepage @terminal.launch(command) @@ -106,11 +106,11 @@ def assert_screen(expected_lines, message = nil) def self.included(cls) cls.instance_exec do teardown do - if !Yamatanooroti.options.show_console || - Yamatanooroti.options.close_console == :always || - Yamatanooroti.options.close_console == :pass && passed? - @terminal&.close_console(passed?) - end + @terminal&.close_console( + !Yamatanooroti.options.show_console || + Yamatanooroti.options.close_console == :always || + Yamatanooroti.options.close_console == :pass && passed? + ) end end end diff --git a/lib/yamatanooroti/windows/conhost.rb b/lib/yamatanooroti/windows/conhost.rb index 5daf417..71f766c 100644 --- a/lib/yamatanooroti/windows/conhost.rb +++ b/lib/yamatanooroti/windows/conhost.rb @@ -1,16 +1,16 @@ class Yamatanooroti::ConhostTerm include Yamatanooroti::WindowsTermMixin - def self.setup_console(height, width, wait) - new(height, width, wait) + def self.setup_console(height, width, wait, name) + new(height, width, wait, name) end - def initialize(height, width, wait) + def initialize(height, width, wait, name) @wait = wait @result = nil @codepage_success_p = nil - @console_process_id = DL.create_console(CONSOLE_KEEPING_COMMAND) + @console_process_id = DL.create_console(CONSOLE_KEEPING_COMMAND.sub("NAME", name)) # wait for console startup complete 8.times do |n| @@ -30,15 +30,17 @@ def close @result ||= retrieve_screen if !DL.interrupted? && @console_process_id end - def close_console(passed = nil) - if @target && !@target.closed? - @target.close - end - begin - Process.kill("KILL", @console_process_id) if @console_process_id - rescue Errno::ESRCH # No such process - ensure - @console_process_id = nil + def close_console(need_to_close = true) + if (need_to_close) + if @target && !@target.closed? + @target.close + end + begin + Process.kill("KILL", @console_process_id) if @console_process_id + rescue Errno::ESRCH # No such process + ensure + @console_process_id = nil + end end end end diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb index e03227e..a1a7c67 100644 --- a/lib/yamatanooroti/windows/terminal.rb +++ b/lib/yamatanooroti/windows/terminal.rb @@ -61,7 +61,7 @@ def pid_from_windowtitle(name) end end - private def invoke_wt_process(command, marker) + private def invoke_wt_process(command, marker, keeper_name) DL.create_console(command) # default timeout seems to be too short begin @@ -69,7 +69,7 @@ def pid_from_windowtitle(name) pid_from_imagename(marker) end rescue => e - puts `tasklist /FI "SESSION ge 0"` + # puts `tasklist /FI "SESSION ge 0"` raise e end @console_process_id = marker_pid @@ -80,7 +80,7 @@ def pid_from_windowtitle(name) end keeper_pid = attach_terminal do - call_spawn(CONSOLE_KEEPING_COMMAND) + call_spawn(CONSOLE_KEEPING_COMMAND.sub("NAME", keeper_name)) end @console_process_id = keeper_pid @@ -100,78 +100,137 @@ def new_wt(rows, cols) @@count += 1 command = "#{Yamatanooroti::WindowsConsoleSettings.wt_exe} -w #{@wt_id} --size #{cols},#{rows} nt --title #{@wt_id} #{marker_command}" - return invoke_wt_process(command, marker_command.split(" ").first) + return invoke_wt_process(command, marker_command.split(" ").first, "new_wt") end - def split_pane(div = 0.5) + def split_pane(div = 0.5, splitter: :v, name: nil) marker_command = CONSOLE_MARKING_COMMAND - command = "#{Yamatanooroti::WindowsConsoleSettings.wt_exe} -w #{@wt_id} sp -V --title #{@wt_id} -s #{div} #{marker_command}; swap-pane previous" - return invoke_wt_process(command, marker_command.split(" ").first) + command = "#{Yamatanooroti::WindowsConsoleSettings.wt_exe} " \ + "-w #{@wt_id} " \ + "move-focus first; " \ + "sp #{splitter == :v ? "-V" : "-H"} "\ + "--title #{name || @wt_id} " \ + "-s #{div} " \ + "#{marker_command}" + pid = invoke_wt_process(command, marker_command.split(" ").first, "split_pane") + @process_ids.push pid + @console_process_id = @process_ids[0] end - def close_pane - kill_and_wait(@console_process_id) - @console_process_id = @terminal_process_id + def move_focus(direction) + command = "#{Yamatanooroti::WindowsConsoleSettings.wt_exe} " \ + "-w #{@wt_id} " \ + "move-pane #{direction}" + system(command) end - @@minimum_width = nil - @@div_to_width = {} - @@width_to_div = {} + def new_tab + marker_command = CONSOLE_MARKING_COMMAND - def self.setup_console(height, width, wait, timeout) - if @@minimum_width.nil? || @@minimum_width <= width - wt = self.new(height, width, wait, timeout) - end - if wt - size = wt.get_size - if size == [height, width] - return wt - else - @@minimum_width = size[1] - wt.close_console - end + command = "#{Yamatanooroti::WindowsConsoleSettings.wt_exe} " \ + "-w #{@wt_id} " \ + "new-tab " \ + "#{marker_command}" + invoke_wt_process(command, marker_command.split(" ").first, "new_tab") + end + + def close_pane + kill_and_wait(@process_ids.pop) + end + + class List + def initialize(total) + @total = total + @div_to_x = {} + @x_to_div = {} end - min_w = @@minimum_width - #expanded_size = min_w + 30 # for default font size - expanded_size = 101 - wt = self.new(height, expanded_size, wait, timeout) - div = @@width_to_div[width] - #div ||= (width * 98 + (min_w - width) * 9) / (expanded_size - 5) # for default font size - div ||= (expanded_size - width) * 84 / expanded_size + 8 - loop do - wt.split_pane(div/100.0) - sleep Yamatanooroti::WindowsConsoleSettings.wt_wait - size = wt.get_size - w = size[1] - @@width_to_div[w] = div - if w == width - return wt - else - wt.close_pane - sleep Yamatanooroti::WindowsConsoleSettings.wt_wait - if w < width + + def search_div(x, &block) + denominator = 100 + div, denominator = @x_to_div[x] if @x_to_div[x] + div ||= (@total - x) * (denominator * 95 / 100) / @total + denominator * 4 / 100 + loop do + # STDOUT.write "target: #{x} total: #{@total} div: #{div.to_f / denominator}" + result = block.call(div.to_f / denominator) + # puts " result: #{result}" + @div_to_x[div.to_f / denominator] = result + @x_to_div[x] = [div, denominator] + return result if result == x + if result < x div -= 1 - if div <= 0 - raise "Could not set Windows Terminal to size #{[height, width]}" + return nil if div <= 0 + if @div_to_x[div.to_f / denominator] && @div_to_x[div.to_f / denominator] != x + div = div * 10 + 5 + denominator *= 10 end else div += 1 - if div >= 100 - raise "Could not set Windows Terminal to size #{[height, width]}" + return nil if div >= denominator + if @div_to_x[div.to_f / denominator] && @div_to_x[div.to_f / denominator] != x + div = div * 10 - 5 + denominator *= 10 end end end end end + @@mother_wt = nil + @@div_to_width = {} + @@width_to_div = {} + + # raise "Could not set Windows Terminal to size #{[height, width]}" + + def self.setup_console(height, width, wait, timeout, name) + if @@mother_wt == nil + @@mother_wt = self.new(*@@max_size, wait, timeout) + @@hsplit_info = List.new(@@max_size[0]) + @@vsplit_info = List.new(@@max_size[1]) + end + mother_height = @@max_size[0] + mother_width = @@max_size[1] + + if height > mother_height + raise "console height #{height} grater than maximum(#{mother_height})" + end + if width > mother_width + raise "console width #{width} grater than maximum(#{mother_width})" + end + + if height != mother_height + result_h = @@hsplit_info.search_div(height) do |div| + @@mother_wt.split_pane(div, splitter: :h, name: name) + @@mother_wt.move_focus("first") + h = @@mother_wt.get_size[0] + @@mother_wt.close_pane if h != height + h + end + raise "console height deviding to #{height} failed" if !result_h + end + + if width != mother_width + result_w = @@vsplit_info.search_div(width) do |div| + @@mother_wt.split_pane(div, splitter: :v, name: name) + @@mother_wt.move_focus("first") + @@mother_wt.get_size[1] + w = @@mother_wt.get_size[1] + @@mother_wt.close_pane if w != width + w + end + raise "console widtht deviding to #{width} failed" if !result_w + end + + return @@mother_wt + end + def initialize(height, width, wait, timeout) @wait = wait @timeout = timeout @result = nil @codepage_success_p = nil - @terminal_process_id = new_wt(height, width) + @process_ids = [new_wt(height, width)] end def self.diagnose_size_capability @@ -193,13 +252,31 @@ def close @result ||= retrieve_screen if !DL.interrupted? && @console_process_id end - def close_console(passed = nil) - if @target && !@target.closed? - @target.close + def close_console(need_to_close = true) + nt = new_tab() + if need_to_close && @process_ids + if @target && !@target.closed? + @target.close + end + while @process_ids.size > 0 + kill_and_wait(@process_ids.pop) + end + end + @process_ids = [@console_process_id = nt] + @result = nil + end + + def close! + if @process_ids + while @process_ids.size > 0 + kill_and_wait(@process_ids.pop) + end end - puts get_size.then { "Windows Terminal max size: rows: #{_1}, columns: #{_2}" } if @terminal_process_id && passed == false - kill_and_wait(@console_process_id) if @console_process_id - kill_and_wait(@terminal_process_id) if @console_process_id != @terminal_process_id - @console_process_id = @terminal_process_id = nil + @@mother_wt = nil + @process_ids = @console_process_id = nil + end + + Test::Unit.at_exit do + @@mother_wt&.close! end end diff --git a/lib/yamatanooroti/windows/windows-definition.rb b/lib/yamatanooroti/windows/windows-definition.rb index 60f1e35..e00a44c 100644 --- a/lib/yamatanooroti/windows/windows-definition.rb +++ b/lib/yamatanooroti/windows/windows-definition.rb @@ -128,6 +128,7 @@ module Yamatanooroti::WindowsDefinition SW_HIDE = 0 SW_SHOWNOACTIVE = 4 SW_SHOWMINNOACTIVE = 7 + SW_SHOWNA = 8 LEFT_ALT_PRESSED = 0x0002 ENABLE_PROCESSED_INPUT = 0x0001 ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 @@ -308,6 +309,21 @@ def attach_console(pid = ATTACH_PARENT_PROCESS, maybe_fail: false) return r != 0 end + SHOWWINDOW_MAP = { + conhost: { + show: SW_SHOWNOACTIVE, + hide: SW_HIDE, + }, + "legacy-conhost": { + show: SW_SHOWNOACTIVE, + hide: SW_SHOWMINNOACTIVE, + }, + "terminal": { + show: SW_SHOWNOACTIVE, + hide: SW_HIDE, + } + } + def create_console(command) converted_command = mb2wc("#{command}\0") console_process_info = PROCESS_INFORMATION.malloc(FREE) @@ -315,13 +331,10 @@ def create_console(command) startup_info = STARTUPINFOW.malloc(FREE) startup_info.to_ptr[0, STARTUPINFOW.size] = "\0".b * STARTUPINFOW.size startup_info.cb = STARTUPINFOW.size - if Yamatanooroti.options.show_console - startup_info.dwFlags = STARTF_USESHOWWINDOW - startup_info.wShowWindow = SW_SHOWNOACTIVE - else - startup_info.dwFlags = STARTF_USESHOWWINDOW - startup_info.wShowWindow = Yamatanooroti.options.windows.to_s != "conhost" ? SW_SHOWMINNOACTIVE : SW_HIDE - end + startup_info.dwFlags = STARTF_USESHOWWINDOW + startup_info.wShowWindow = + (SHOWWINDOW_MAP[Yamatanooroti.options.windows] || SHOWWINDOW_MAP[:terminal]) + .fetch(Yamatanooroti.options.show_console ? :show : :hide) restore_console_control_handler do r = CreateProcessW( @@ -446,7 +459,7 @@ def self.restore_console_control_handler(&block) end end - @interrupt_monitor_pid = spawn("ruby --disable=gems -e sleep", [:out, :err] => "NUL") + @interrupt_monitor_pid = spawn("ruby --disable=gems -e sleep #InterruptMonitor", [:out, :err] => "NUL") @interrupt_monitor = Process.detach(@interrupt_monitor_pid) ignore_console_control_handler @interrupted_p = nil diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index 5c31db8..6e50ee3 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -90,7 +90,6 @@ def self.tmpdir Thread.current.abort_on_exception = true Dir.mktmpdir do |tmpdir| dir = tmpdir - p dir sleep ensure sleep 0.5 # wait for terminate windows terminal @@ -122,6 +121,7 @@ def self.extract_terminal(path) { "defaultProfile": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", "disableAnimations": true, + "minimizeToNotificationArea": true, "profiles": { "defaults": @@ -131,7 +131,9 @@ def self.extract_terminal(path) "font": { "size": 9 - } + }, + "padding": "0", + "scrollbarState": "always" }, "list": [ @@ -142,6 +144,7 @@ def self.extract_terminal(path) } ] }, + "showTabsInTitlebar": false, "warning.confirmCloseAllTabs": false, "warning.largePaste": false, "warning.multiLinePaste": false @@ -201,7 +204,7 @@ def self.prepare_terminal_portable module Yamatanooroti::WindowsTermMixin DL = Yamatanooroti::WindowsDefinition - CONSOLE_KEEPING_COMMAND = %q[ruby.exe --disable=gems -e "Signal.trap(:INT, nil); sleep"] + CONSOLE_KEEPING_COMMAND = %q[ruby.exe --disable=gems -e "Signal.trap(:INT, nil); sleep; #NAME"] CONSOLE_MARKING_COMMAND = %q[findstr.exe yamatanooroti] private def attach_terminal(open = true) @@ -397,21 +400,18 @@ def retrieve_screen(top_of_buffer: false) return @result if @result check_interrupt @target.sync - top, bottom, width = attach_terminal do |conin, conout| + attach_terminal do |conin, conout| csbi = DL.get_console_screen_buffer_info(conout) - if top_of_buffer + top, bottom, width = if top_of_buffer [0, csbi.Bottom, csbi.Right - csbi.Left + 1] else [csbi.Top, csbi.Bottom, csbi.Right - csbi.Left + 1] end - end - lines = attach_terminal do |conin, conout| - (top..bottom).map do |y| + return (top..bottom).map do |y| DL.read_console_output(conout, y, width) || "" end end - lines end def result @@ -467,7 +467,7 @@ def check_interrupt end def raise_interrupt - close_console + close! DL.at_exit raise Interrupt end From e9bb16e08c66428420adc833f8e35e58f6bca7d9 Mon Sep 17 00:00:00 2001 From: YO4 Date: Fri, 18 Oct 2024 23:23:20 +0900 Subject: [PATCH 44/76] revert windows terminal supports --no-show option When wt.exe manipulates existing console, that window is forced to be activated. --- lib/yamatanooroti/options.rb | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/yamatanooroti/options.rb b/lib/yamatanooroti/options.rb index ce5f175..900e52c 100644 --- a/lib/yamatanooroti/options.rb +++ b/lib/yamatanooroti/options.rb @@ -119,18 +119,28 @@ def self.interpret(name) @conhost = CONHOST_TYPES.include?(type) @terminal = !@conhost @windows = @conhost ? type : WindowsTerminal.interpret(type) + if @terminal + if @show_console == false + puts "Windows Terminal is always visible. --no-show-console is ignored." + end + @show_console = true + end end o.on_tail("--wt-dir=DIR", String, "Specify Windows Terminal working dir.", - "Automatically determined if not specified.", - "DIR is treaded permanent if specified so download files are remains there.") do |dir| + "Automatically determined if not specified and treated temporary.", + "DIR is treaded permanent if specified and download files are remains.") do |dir| @terminal_workdir = dir end o.on_tail("--[no-]show-console", "Show test ongoing console.") do |show| - @show_console = show + if show == false and @terminal + puts "Windows Terminal is always visible. --no-show-console is ignored." + else + @show_console = show + end end o.on_tail("--[no-]close-console[=COND]", CLOSE_WHEN, From c04f7149d501e26f7a3b554797394ae3d7bdc165 Mon Sep 17 00:00:00 2001 From: YO4 Date: Fri, 18 Oct 2024 23:49:35 +0900 Subject: [PATCH 45/76] Report actual window size --- lib/yamatanooroti/windows/terminal.rb | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb index a1a7c67..2b99635 100644 --- a/lib/yamatanooroti/windows/terminal.rb +++ b/lib/yamatanooroti/windows/terminal.rb @@ -235,14 +235,15 @@ def initialize(height, width, wait, timeout) def self.diagnose_size_capability wt = self.new(999, 999, 0.01, 5.0) - @@max_size = wt.get_size - @@max_size = [[@@max_size[0], 60].min, [@@max_size[1], 200].min] - puts @@max_size.then { "Windows Terminal maximum size: rows: #{_1}, columns: #{_2}" } - wt.close_console + max_size = wt.get_size + @@max_size = [[max_size[0], 60].min, [max_size[1], 200].min] + puts max_size.then { "Windows Terminal maximum size: rows: #{_1}, columns: #{_2}" } + wt.close! wt = self.new(2, 2, 0.01, 5.0) - @@min_size = wt.get_size - puts @@min_size.then { "Windows Terminal smallest size: rows: #{_1}, columns: #{_2}" } - wt.close_console + min_size = @@min_size = wt.get_size + puts min_size.then { "Windows Terminal smallest size: rows: #{_1}, columns: #{_2}" } + wt.close! + puts @@max_size.then { "Use test window size: rows: #{_1}, columns: #{_2}" } end def close From d0e9883a6d3bcf96dfcdbcd5d21eaf884ac84009 Mon Sep 17 00:00:00 2001 From: YO4 Date: Fri, 18 Oct 2024 23:53:05 +0900 Subject: [PATCH 46/76] Isolate Yamatanooroti::Options dependency from win32api REPL --- lib/yamatanooroti/windows/conhost.rb | 2 +- lib/yamatanooroti/windows/terminal.rb | 2 +- .../windows/windows-definition.rb | 19 +++---------------- lib/yamatanooroti/windows/windows.rb | 5 +++++ 4 files changed, 10 insertions(+), 18 deletions(-) diff --git a/lib/yamatanooroti/windows/conhost.rb b/lib/yamatanooroti/windows/conhost.rb index 71f766c..2c03dd0 100644 --- a/lib/yamatanooroti/windows/conhost.rb +++ b/lib/yamatanooroti/windows/conhost.rb @@ -10,7 +10,7 @@ def initialize(height, width, wait, name) @result = nil @codepage_success_p = nil - @console_process_id = DL.create_console(CONSOLE_KEEPING_COMMAND.sub("NAME", name)) + @console_process_id = DL.create_console(CONSOLE_KEEPING_COMMAND.sub("NAME", name), show_console_param()) # wait for console startup complete 8.times do |n| diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb index 2b99635..04f0838 100644 --- a/lib/yamatanooroti/windows/terminal.rb +++ b/lib/yamatanooroti/windows/terminal.rb @@ -62,7 +62,7 @@ def pid_from_windowtitle(name) end private def invoke_wt_process(command, marker, keeper_name) - DL.create_console(command) + DL.create_console(command, show_console_param()) # default timeout seems to be too short begin marker_pid = with_timeout("Windows Terminal marker process detection failed.", @timeout + 5) do diff --git a/lib/yamatanooroti/windows/windows-definition.rb b/lib/yamatanooroti/windows/windows-definition.rb index e00a44c..5fd7363 100644 --- a/lib/yamatanooroti/windows/windows-definition.rb +++ b/lib/yamatanooroti/windows/windows-definition.rb @@ -324,7 +324,7 @@ def attach_console(pid = ATTACH_PARENT_PROCESS, maybe_fail: false) } } - def create_console(command) + def create_console(command, show = SW_SHOWNORMAL) converted_command = mb2wc("#{command}\0") console_process_info = PROCESS_INFORMATION.malloc(FREE) console_process_info.to_ptr[0, PROCESS_INFORMATION.size] = "\0".b * PROCESS_INFORMATION.size @@ -332,9 +332,7 @@ def create_console(command) startup_info.to_ptr[0, STARTUPINFOW.size] = "\0".b * STARTUPINFOW.size startup_info.cb = STARTUPINFOW.size startup_info.dwFlags = STARTF_USESHOWWINDOW - startup_info.wShowWindow = - (SHOWWINDOW_MAP[Yamatanooroti.options.windows] || SHOWWINDOW_MAP[:terminal]) - .fetch(Yamatanooroti.options.show_console ? :show : :hide) + startup_info.wShowWindow = show restore_console_control_handler do r = CreateProcessW( @@ -490,22 +488,11 @@ def self.at_exit end if __FILE__ == $0 - class Yamatanooroti - class << self - attr_reader :options - end - @options = Object.new - class << @options - attr_accessor :show_console, :windows - end - options.show_console = true - options.windows = :conhost - end - DL = Yamatanooroti::WindowsDefinition cin = DL.get_std_handle(DL::STD_INPUT_HANDLE) cout = DL.get_std_handle(DL::STD_OUTPUT_HANDLE) cerr = DL.get_std_handle(DL::STD_ERROR_HANDLE) binding.irb + [cin, cout, cerr] end diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index 6e50ee3..bffe4c2 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -207,6 +207,11 @@ module Yamatanooroti::WindowsTermMixin CONSOLE_KEEPING_COMMAND = %q[ruby.exe --disable=gems -e "Signal.trap(:INT, nil); sleep; #NAME"] CONSOLE_MARKING_COMMAND = %q[findstr.exe yamatanooroti] + private def show_console_param + map = DL::SHOWWINDOW_MAP[Yamatanooroti.options.windows] || DL::SHOWWINDOW_MAP[:terminal] + map.fetch(Yamatanooroti.options.show_console ? :show : :hide) + end + private def attach_terminal(open = true) stderr = $stderr $stderr = StringIO.new From e1e2f3e82288dd87edfd77eecabc521b68d70387 Mon Sep 17 00:00:00 2001 From: YO4 Date: Sat, 19 Oct 2024 16:57:51 +0900 Subject: [PATCH 47/76] WIN32API error message generation fixed =============================================================================== Error: test_autowrap_in_the_middle_of_a_line(Reline::RenderingTest::TestWindows): TypeError: exception class/object expected C:/hostedtoolcache/windows/Ruby/3.3.5/x64/lib/ruby/gems/3.3.0/bundler/gems/yamatanooroti-d0e9883a6d3b/lib/yamatanooroti/windows/windows-definition.rb:226:in `raise' C:/hostedtoolcache/windows/Ruby/3.3.5/x64/lib/ruby/gems/3.3.0/bundler/gems/yamatanooroti-d0e9883a6d3b/lib/yamatanooroti/windows/windows-definition.rb:226:in `error_message' C:/hostedtoolcache/windows/Ruby/3.3.5/x64/lib/ruby/gems/3.3.0/bundler/gems/yamatanooroti-d0e9883a6d3b/lib/yamatanooroti/windows/windows-definition.rb:308:in `attach_console' --- lib/yamatanooroti/windows/windows-definition.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/yamatanooroti/windows/windows-definition.rb b/lib/yamatanooroti/windows/windows-definition.rb index 5fd7363..071d9e0 100644 --- a/lib/yamatanooroti/windows/windows-definition.rb +++ b/lib/yamatanooroti/windows/windows-definition.rb @@ -220,8 +220,11 @@ module Yamatanooroti::WindowsDefinition if n > 0 str = wc2mb(string.ptr[0, n * 2]) LocalFree(string) - msg = "ERROR(#{method_name}): #{err.to_s}: #{str}" + else + msgerr = Fiddle.win32_last_error + str = "error description unknow (FormatMessageW failed with #{msgerr})" end + msg = "ERROR(#{method_name}): #{err}: #{str}" if exception raise msg else From b5c4450f6a6a33e54860f5e18e92ff3bb2ce4007 Mon Sep 17 00:00:00 2001 From: YO4 Date: Sun, 20 Oct 2024 00:44:13 +0900 Subject: [PATCH 48/76] In normal case, an error in attach_terminal() should raise an exception --- lib/yamatanooroti/windows/conhost.rb | 2 +- lib/yamatanooroti/windows/terminal.rb | 10 +++++----- lib/yamatanooroti/windows/windows.rb | 14 ++++++-------- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/lib/yamatanooroti/windows/conhost.rb b/lib/yamatanooroti/windows/conhost.rb index 2c03dd0..0b3f05a 100644 --- a/lib/yamatanooroti/windows/conhost.rb +++ b/lib/yamatanooroti/windows/conhost.rb @@ -14,7 +14,7 @@ def initialize(height, width, wait, name) # wait for console startup complete 8.times do |n| - break if attach_terminal { true } + break if attach_terminal(open: false, exception: false) { true } sleep 0.01 * 2**n end diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb index 04f0838..96ee797 100644 --- a/lib/yamatanooroti/windows/terminal.rb +++ b/lib/yamatanooroti/windows/terminal.rb @@ -76,21 +76,22 @@ def pid_from_windowtitle(name) # wait for console startup complete with_timeout("Console process startup timed out.") do - attach_terminal { true } + attach_terminal(open: false, exception: false) { true } end - keeper_pid = attach_terminal do + keeper_pid = attach_terminal(open: false) do call_spawn(CONSOLE_KEEPING_COMMAND.sub("NAME", keeper_name)) end @console_process_id = keeper_pid # wait for console keeping process startup complete with_timeout("Console process startup timed out.") do - attach_terminal { true } + attach_terminal(open: false, exception: false) { true } end - kill_and_wait(marker_pid) return keeper_pid + ensure + kill_and_wait(marker_pid) if marker_pid end def new_wt(rows, cols) @@ -213,7 +214,6 @@ def self.setup_console(height, width, wait, timeout, name) result_w = @@vsplit_info.search_div(width) do |div| @@mother_wt.split_pane(div, splitter: :v, name: name) @@mother_wt.move_focus("first") - @@mother_wt.get_size[1] w = @@mother_wt.get_size[1] @@mother_wt.close_pane if w != width w diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index bffe4c2..4e6e28b 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -212,7 +212,7 @@ module Yamatanooroti::WindowsTermMixin map.fetch(Yamatanooroti.options.show_console ? :show : :hide) end - private def attach_terminal(open = true) + private def attach_terminal(open: true, exception: true) stderr = $stderr $stderr = StringIO.new @@ -220,15 +220,13 @@ module Yamatanooroti::WindowsTermMixin check_interrupt DL.free_console # this can be fail while new process is starting - r = DL.attach_console(@console_process_id, maybe_fail: true) + r = DL.attach_console(@console_process_id, maybe_fail: !exception) return nil unless r if open + # if error occurred, causes exception regardless of exception: false conin = DL.create_console_file_handle("conin$") - return nil if conin == DL::INVALID_HANDLE_VALUE - conout = DL.create_console_file_handle("conout$") - return nil if conout == DL::INVALID_HANDLE_VALUE end yield(conin, conout) @@ -335,13 +333,13 @@ def sync def launch(command) check_interrupt - attach_terminal(false) do + attach_terminal(open: false) do @target = SubProcess.new(command.map{ |c| quote_command_arg(c) }.join(' ')) end end def setup_cp(cp) - @codepage_success_p = attach_terminal(false) do + @codepage_success_p = attach_terminal(open: false) do system("chcp #{Integer(cp)} > NUL") DL.get_console_codepage() == cp && DL.get_console_output_codepage() == cp end @@ -390,7 +388,7 @@ def write(str) else str.dup.force_encoding(Encoding::ASCII_8BIT).split(/(\C-c)/).each do |chunk| if chunk == "\C-c" - attach_terminal(false) do + attach_terminal(open: false) do # generate Ctrl+C event to process on same console DL.generate_console_ctrl_event(0, 0) end From 4cbf2b8371da2712959ae14ce186db619794e1b7 Mon Sep 17 00:00:00 2001 From: YO4 Date: Sun, 20 Oct 2024 00:52:29 +0900 Subject: [PATCH 49/76] Avoid timing issues in test --- test/yamatanooroti/test_multiplatform.rb | 6 +++--- test/yamatanooroti/test_run_ruby.rb | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/test/yamatanooroti/test_multiplatform.rb b/test/yamatanooroti/test_multiplatform.rb index a596318..7c18652 100644 --- a/test/yamatanooroti/test_multiplatform.rb +++ b/test/yamatanooroti/test_multiplatform.rb @@ -27,14 +27,14 @@ def test_result_repeatedly end def test_assert_screen_retries - write("sleep 1\n") + write("sleep 1 && 1\n") assert_screen(/=> 1\nprompt>/) - assert_equal(['prompt> sleep 1', '=> 1', 'prompt>', '', ''], result) + assert_equal(['prompt> sleep 1 && 1', '=> 1', 'prompt>', '', ''], result) close end def test_assert_screen_timeout - write("sleep 3\n") + write("sleep 3 && 3\n") assert_raise do assert_screen(/=> 3\nprompt>/) end diff --git a/test/yamatanooroti/test_run_ruby.rb b/test/yamatanooroti/test_run_ruby.rb index 28d7b7d..c304957 100644 --- a/test/yamatanooroti/test_run_ruby.rb +++ b/test/yamatanooroti/test_run_ruby.rb @@ -4,11 +4,10 @@ class Yamatanooroti::TestRunRuby < Yamatanooroti::TestCase def test_winsize start_terminal(5, 30, ['ruby', '-rio/console', '-e', 'puts(IO.console.winsize.inspect)']) - sleep 0.5 - close assert_screen(<<~EOC) [5, 30] EOC + close end def test_wait_for_startup_message From 9a076a4a47f7f35cbf262083dbbac4cd3a85ecca Mon Sep 17 00:00:00 2001 From: YO4 Date: Sun, 20 Oct 2024 10:35:53 +0900 Subject: [PATCH 50/76] add a convenient method invoke_key in windows-definitions.rb REPL --- .../windows/windows-definition.rb | 24 +++++++++++++++++++ lib/yamatanooroti/windows/windows.rb | 20 ++-------------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/lib/yamatanooroti/windows/windows-definition.rb b/lib/yamatanooroti/windows/windows-definition.rb index 071d9e0..ef9fdcb 100644 --- a/lib/yamatanooroti/windows/windows-definition.rb +++ b/lib/yamatanooroti/windows/windows-definition.rb @@ -393,6 +393,27 @@ def set_input_record(r, code) return r end + def build_key_input_record(str) + codes = str.chars.map do |c| + c = "\r" if c == "\n" + byte = c.getbyte(0) + if c.bytesize == 1 and byte.allbits?(0x80) # with Meta key + [-(byte ^ 0x80)] + else + mb2wc(c).unpack("S*") + end + end.flatten + record = INPUT_RECORD_WITH_KEY_EVENT.malloc(FREE) + records = codes.reduce("".b) do |records, code| + set_input_record(record, code) + record.bKeyDown = 1 + records << record.to_ptr.to_str + record.bKeyDown = 0 + records << record.to_ptr.to_str + end + [records, codes.size * 2] + end + def write_console_input(handle, records, n) written = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DWORD, FREE) r = WriteConsoleInputW(handle, records, n, written) @@ -492,6 +513,9 @@ def self.at_exit if __FILE__ == $0 DL = Yamatanooroti::WindowsDefinition + def invoke_key(conin, str) + DL.write_console_input(conin, *DL.build_key_input_record(str)) + end cin = DL.get_std_handle(DL::STD_INPUT_HANDLE) cout = DL.get_std_handle(DL::STD_OUTPUT_HANDLE) cerr = DL.get_std_handle(DL::STD_ERROR_HANDLE) diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index 4e6e28b..b3a17e3 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -351,25 +351,9 @@ def codepage_success? def do_write(str) check_interrupt - codes = str.chars.map do |c| - c = "\r" if c == "\n" - byte = c.getbyte(0) - if c.bytesize == 1 and byte.allbits?(0x80) # with Meta key - [-(byte ^ 0x80)] - else - DL.mb2wc(c).unpack("S*") - end - end.flatten - record = DL::INPUT_RECORD_WITH_KEY_EVENT.malloc(DL::FREE) - records = codes.reduce("".b) do |records, code| - DL.set_input_record(record, code) - record.bKeyDown = 1 - records << record.to_ptr.to_str - record.bKeyDown = 0 - records << record.to_ptr.to_str - end + records, count = DL.build_key_input_record(str) attach_terminal do |conin, conout| - DL.write_console_input(conin, records, codes.size * 2) + DL.write_console_input(conin, records, count) loop do sleep @wait n = DL.get_number_of_console_input_events(conin) From 6b5eb189a566527b3ebe742a39791dd9cd137dcf Mon Sep 17 00:00:00 2001 From: YO4 Date: Sun, 20 Oct 2024 10:39:14 +0900 Subject: [PATCH 51/76] use various ruby versions on windows ci use bundler for windows too RUBY_VERSION < 3.0 requires newer fiddle RUBY_VERSION >= 3.4 requires fiddle in gemspec when using bundler --- .github/workflows/y.yml | 6 ++++-- yamatanooroti.gemspec | 10 ++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/y.yml b/.github/workflows/y.yml index ef85472..7535b5d 100644 --- a/.github/workflows/y.yml +++ b/.github/workflows/y.yml @@ -43,6 +43,7 @@ jobs: bundle install bundle exec rake test windows-yamatanooroti: + needs: ruby-versions name: >- ${{ matrix.os }} ${{ matrix.ruby }} ${{ matrix.console }} runs-on: ${{ matrix.os }} @@ -50,7 +51,7 @@ jobs: fail-fast: false matrix: os: [ windows-2019, windows-2022 ] - ruby: [ 3.3, mingw ] + ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} console: [ conhost, legacy-conhost, 1.21, 1.22preview, canary ] exclude: - console: 1.21 @@ -70,4 +71,5 @@ jobs: ruby-version: ${{ matrix.ruby }} - name: rake test run: | - rake test TESTOPTS="-v --wt_dir=./tmp --windows=${{ matrix.console }}" + bundle install + bundle exec rake test TESTOPTS="-v --wt_dir=./tmp --windows=${{ matrix.console }}" diff --git a/yamatanooroti.gemspec b/yamatanooroti.gemspec index 16972ad..b575570 100644 --- a/yamatanooroti.gemspec +++ b/yamatanooroti.gemspec @@ -24,4 +24,14 @@ Gem::Specification.new do |spec| spec.required_ruby_version = Gem::Requirement.new('>= 2.5') spec.add_dependency 'test-unit' + if Gem.win_platform? + spec.add_dependency 'fiddle', '>= 1.0.8' if + (RUBY_ENGINE == "ruby" && RUBY_VERSION >= '3.4') || + Gem::Version.new("1.0.8") > begin + require 'fiddle' + Gem::Version.new(Fiddle::VERSION) + rescue + Gem::Version.new("0.0.0") + end + end end From efcf1fb9a6ead23cdb5480c0254c3e6485336c26 Mon Sep 17 00:00:00 2001 From: YO4 Date: Sun, 20 Oct 2024 11:40:51 +0900 Subject: [PATCH 52/76] use newer reline if necessary to avoid an windows specific bug --- test/yamatanooroti/test_run_ruby.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/yamatanooroti/test_run_ruby.rb b/test/yamatanooroti/test_run_ruby.rb index c304957..7b8c1d4 100644 --- a/test/yamatanooroti/test_run_ruby.rb +++ b/test/yamatanooroti/test_run_ruby.rb @@ -32,7 +32,13 @@ def test_move_cursor_and_render def test_meta_key get_into_tmpdir - start_terminal(5, 30, ['ruby', '-rreline', '-e', 'Reline.readline(%{>>>})'], startup_message: />{3}/) + if !Yamatanooroti.win? || RUBY_VERSION > '2.6.99' + command = ['ruby', '-rreline', '-e', 'Reline.readline(%{>>>})'] + else + command = ['bundle', 'exec', 'ruby', '-e', 'require "reline"; Reline.readline(%{>>>})'] + # older ruby inboxed reline (0.1.5) has a windows specific bug. use newer reline + end + start_terminal(5, 30, command, startup_message: />{3}/) write('aaa ccc') write("\M-b") write('bbb ') From 72fe4fcaeae7b355a6c4ce50a7749c93ce329178 Mon Sep 17 00:00:00 2001 From: YO4 Date: Sun, 20 Oct 2024 12:53:36 +0900 Subject: [PATCH 53/76] fiddle of ruby 3.4 is bundled gem --- test/yamatanooroti/test_run_ruby.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/yamatanooroti/test_run_ruby.rb b/test/yamatanooroti/test_run_ruby.rb index 7b8c1d4..1554186 100644 --- a/test/yamatanooroti/test_run_ruby.rb +++ b/test/yamatanooroti/test_run_ruby.rb @@ -32,7 +32,7 @@ def test_move_cursor_and_render def test_meta_key get_into_tmpdir - if !Yamatanooroti.win? || RUBY_VERSION > '2.6.99' + if !Yamatanooroti.win? || (RUBY_VERSION > '2.6.99' && RUBY_VERSION < '3.4.0') command = ['ruby', '-rreline', '-e', 'Reline.readline(%{>>>})'] else command = ['bundle', 'exec', 'ruby', '-e', 'require "reline"; Reline.readline(%{>>>})'] From 217423903ff14e5719a1498b5ac8897bc3d33f21 Mon Sep 17 00:00:00 2001 From: YO4 Date: Sun, 20 Oct 2024 12:55:06 +0900 Subject: [PATCH 54/76] older windows ruby has multibyte character console input issues --- bin/simple_repl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bin/simple_repl b/bin/simple_repl index e4bb5c3..210e7be 100644 --- a/bin/simple_repl +++ b/bin/simple_repl @@ -3,5 +3,10 @@ loop { print 'prompt> ' result = gets + result.force_encoding('locale').encode!(Encoding.default_external) unless result.valid_encoding? puts "=> #{eval(result).inspect}" } + +# On older windows ruby versions(< 3.1 ?), STDIN.gets returns locale code bytes with default external encoding. https://bugs.ruby-lang.org/issues/18353 +# Not same versions(< 3.2 ?), STDIN.gets returns invalid bytes if input with leading DBCS characters. https://bugs.ruby-lang.org/issues/18588 +# Because of this, input key stroke must start with SBCS character to support older ruby versions. From ae4211ea75ab0c8ba9f7790b7862b0259a368671 Mon Sep 17 00:00:00 2001 From: YO4 Date: Sun, 20 Oct 2024 12:55:40 +0900 Subject: [PATCH 55/76] avoid a frozen string literal issue --- lib/yamatanooroti/windows/windows.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index b3a17e3..7e586f6 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -312,7 +312,7 @@ def closed? end def sync - buffer = "" + buffer = +"" if closed? if !@errin.closed? @t.kill From 621e63c3469ee7f6a2cf8824eabfa7347676c61a Mon Sep 17 00:00:00 2001 From: YO4 Date: Sun, 20 Oct 2024 12:57:23 +0900 Subject: [PATCH 56/76] mitigates the issue of close() immediately after write() --- lib/yamatanooroti/windows.rb | 6 +++--- lib/yamatanooroti/windows/conhost.rb | 15 +++++---------- lib/yamatanooroti/windows/terminal.rb | 8 +------- lib/yamatanooroti/windows/windows.rb | 19 +++++++++++++++++++ 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/lib/yamatanooroti/windows.rb b/lib/yamatanooroti/windows.rb index e51ed25..fdae3fd 100644 --- a/lib/yamatanooroti/windows.rb +++ b/lib/yamatanooroti/windows.rb @@ -10,7 +10,7 @@ def write(str) end def close - @terminal.close + @result = @terminal.close end def result @@ -35,7 +35,7 @@ def start_terminal(height, width, command, wait: nil, timeout: nil, startup_mess end end if Yamatanooroti.options.conhost - @terminal = Yamatanooroti::ConhostTerm.setup_console(height, width, @wait, name) + @terminal = Yamatanooroti::ConhostTerm.setup_console(height, width, @wait, @timeout, name) else @terminal = Yamatanooroti::WindowsTerminalTerm.setup_console(height, width, @wait, @timeout, name) end @@ -74,7 +74,7 @@ def start_terminal(height, width, command, wait: nil, timeout: nil, startup_mess screen = convert_proc.call(@terminal.retrieve_screen) break screen if Time.now >= retry_until break screen if check_proc.call(screen) - sleep @wait + @terminal.sleep_wait end end assert_proc.call(screen) diff --git a/lib/yamatanooroti/windows/conhost.rb b/lib/yamatanooroti/windows/conhost.rb index 0b3f05a..0f64646 100644 --- a/lib/yamatanooroti/windows/conhost.rb +++ b/lib/yamatanooroti/windows/conhost.rb @@ -1,14 +1,16 @@ class Yamatanooroti::ConhostTerm include Yamatanooroti::WindowsTermMixin - def self.setup_console(height, width, wait, name) - new(height, width, wait, name) + def self.setup_console(height, width, wait, timeout, name) + new(height, width, wait, timeout, name) end - def initialize(height, width, wait, name) + def initialize(height, width, wait, timeout, name) @wait = wait + @timeout = timeout @result = nil @codepage_success_p = nil + @wrote_and_not_yet_waited = false @console_process_id = DL.create_console(CONSOLE_KEEPING_COMMAND.sub("NAME", name), show_console_param()) @@ -23,13 +25,6 @@ def initialize(height, width, wait, name) end end - def close - if @target && !@target.closed? - @target.close - end - @result ||= retrieve_screen if !DL.interrupted? && @console_process_id - end - def close_console(need_to_close = true) if (need_to_close) if @target && !@target.closed? diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb index 96ee797..4a7e0ae 100644 --- a/lib/yamatanooroti/windows/terminal.rb +++ b/lib/yamatanooroti/windows/terminal.rb @@ -229,6 +229,7 @@ def initialize(height, width, wait, timeout) @timeout = timeout @result = nil @codepage_success_p = nil + @wrote_and_not_yet_waited = false @process_ids = [new_wt(height, width)] end @@ -246,13 +247,6 @@ def self.diagnose_size_capability puts @@max_size.then { "Use test window size: rows: #{_1}, columns: #{_2}" } end - def close - if @target && !@target.closed? - @target.close - end - @result ||= retrieve_screen if !DL.interrupted? && @console_process_id - end - def close_console(need_to_close = true) nt = new_tab() if need_to_close && @process_ids diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index 7e586f6..e274d1c 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -381,6 +381,7 @@ def write(str) end end end + @wrote_and_not_yet_waited = true end def retrieve_screen(top_of_buffer: false) @@ -405,6 +406,24 @@ def result @result || retrieve_screen end + def close + if !@result && @console_process_id + sleep @timeout if @wrote_and_not_yet_waited # wait a long. avoid write();close() sequence + end + if @target && !@target.closed? + @target.close + end + if !DL.interrupted? && @console_process_id + @result = retrieve_screen + end + @result ||= "" + end + + def sleep_wait + sleep @wait + @wrote_and_not_yet_waited = false + end + # identify windows console # conhost(legacy) # compatible with older windows From 3adce06647b9a673ba0a815f764d09ff4fa18f37 Mon Sep 17 00:00:00 2001 From: YO4 Date: Sun, 20 Oct 2024 13:15:18 +0900 Subject: [PATCH 57/76] support 2.6 syntax --- lib/yamatanooroti/windows/terminal.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb index 4a7e0ae..a9607ee 100644 --- a/lib/yamatanooroti/windows/terminal.rb +++ b/lib/yamatanooroti/windows/terminal.rb @@ -238,13 +238,13 @@ def self.diagnose_size_capability wt = self.new(999, 999, 0.01, 5.0) max_size = wt.get_size @@max_size = [[max_size[0], 60].min, [max_size[1], 200].min] - puts max_size.then { "Windows Terminal maximum size: rows: #{_1}, columns: #{_2}" } + puts max_size.then { |r, c| "Windows Terminal maximum size: rows: #{r}, columns: #{c}" } wt.close! wt = self.new(2, 2, 0.01, 5.0) min_size = @@min_size = wt.get_size - puts min_size.then { "Windows Terminal smallest size: rows: #{_1}, columns: #{_2}" } + puts min_size.then { |r, c| "Windows Terminal smallest size: rows: #{r}, columns: #{c}" } wt.close! - puts @@max_size.then { "Use test window size: rows: #{_1}, columns: #{_2}" } + puts @@max_size.then {|r, c| "Use test window size: rows: #{r}, columns: #{c}" } end def close_console(need_to_close = true) From 8a4ed713da28d2c2dd19c6451c83c54e951d1f46 Mon Sep 17 00:00:00 2001 From: YO4 Date: Sun, 20 Oct 2024 13:20:10 +0900 Subject: [PATCH 58/76] reduce windows matrix --- .github/workflows/y.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/y.yml b/.github/workflows/y.yml index 7535b5d..aad9d31 100644 --- a/.github/workflows/y.yml +++ b/.github/workflows/y.yml @@ -52,14 +52,20 @@ jobs: matrix: os: [ windows-2019, windows-2022 ] ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} - console: [ conhost, legacy-conhost, 1.21, 1.22preview, canary ] + console: [ conhost, legacy-conhost, stable ] exclude: - - console: 1.21 - os: windows-2019 - - console: 1.22preview - os: windows-2019 - - console: canary + - ruby: 2.5 + - console: stable os: windows-2019 + - console: legacy-conhost + os: windows-2022 + include: + - os: windows-2022 + ruby: 3.3 + console: preview + - os: windows-2022 + ruby: 3.3 + console: canary defaults: run: shell: bash From 3ec89d1a4821e704bfa91317bb73532a79ce6048 Mon Sep 17 00:00:00 2001 From: YO4 Date: Sun, 20 Oct 2024 14:47:16 +0900 Subject: [PATCH 59/76] move gem "fiddle" to gemfile --- Gemfile | 11 +++++++++++ yamatanooroti.gemspec | 10 ---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Gemfile b/Gemfile index fa59d53..cb9828c 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,17 @@ unless RbConfig::CONFIG['host_os'].match?(/mswin|msys|mingw|cygwin|bccwin|wince| gem "vterm", github: "ruby/vterm-gem" end +if Gem.win_platform? + gem "fiddle", '>= 1.0.8' if + (RUBY_ENGINE == "ruby" && RUBY_VERSION >= '3.4') || + Gem::Version.new("1.0.8") > begin + require 'fiddle' + Gem::Version.new(Fiddle::VERSION) + rescue + Gem::Version.new("0.0.0") + end +end + # Specify your gem's dependencies in reline.gemspec gemspec diff --git a/yamatanooroti.gemspec b/yamatanooroti.gemspec index b575570..16972ad 100644 --- a/yamatanooroti.gemspec +++ b/yamatanooroti.gemspec @@ -24,14 +24,4 @@ Gem::Specification.new do |spec| spec.required_ruby_version = Gem::Requirement.new('>= 2.5') spec.add_dependency 'test-unit' - if Gem.win_platform? - spec.add_dependency 'fiddle', '>= 1.0.8' if - (RUBY_ENGINE == "ruby" && RUBY_VERSION >= '3.4') || - Gem::Version.new("1.0.8") > begin - require 'fiddle' - Gem::Version.new(Fiddle::VERSION) - rescue - Gem::Version.new("0.0.0") - end - end end From 2561d295290471c067da7f5bad6dfb17eaf1ac25 Mon Sep 17 00:00:00 2001 From: YO4 Date: Sun, 20 Oct 2024 14:48:00 +0900 Subject: [PATCH 60/76] waiting before close condition fix and more precise message --- lib/yamatanooroti/windows.rb | 7 ++++--- lib/yamatanooroti/windows/conhost.rb | 1 + lib/yamatanooroti/windows/terminal.rb | 13 +++++++++---- lib/yamatanooroti/windows/windows.rb | 22 ++++++++++++---------- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/lib/yamatanooroti/windows.rb b/lib/yamatanooroti/windows.rb index fdae3fd..c18170a 100644 --- a/lib/yamatanooroti/windows.rb +++ b/lib/yamatanooroti/windows.rb @@ -35,9 +35,9 @@ def start_terminal(height, width, command, wait: nil, timeout: nil, startup_mess end end if Yamatanooroti.options.conhost - @terminal = Yamatanooroti::ConhostTerm.setup_console(height, width, @wait, @timeout, name) + @terminal = Yamatanooroti::ConhostTerm.setup_console(height, width, @wait, @timeout, local_name) else - @terminal = Yamatanooroti::WindowsTerminalTerm.setup_console(height, width, @wait, @timeout, name) + @terminal = Yamatanooroti::WindowsTerminalTerm.setup_console(height, width, @wait, @timeout, local_name) end @terminal.setup_cp(codepage) if codepage @terminal.launch(command) @@ -74,9 +74,10 @@ def start_terminal(height, width, command, wait: nil, timeout: nil, startup_mess screen = convert_proc.call(@terminal.retrieve_screen) break screen if Time.now >= retry_until break screen if check_proc.call(screen) - @terminal.sleep_wait + sleep @wait end end + @terminal.clear_need_wait_flag assert_proc.call(screen) end diff --git a/lib/yamatanooroti/windows/conhost.rb b/lib/yamatanooroti/windows/conhost.rb index 0f64646..8e0e6d5 100644 --- a/lib/yamatanooroti/windows/conhost.rb +++ b/lib/yamatanooroti/windows/conhost.rb @@ -8,6 +8,7 @@ def self.setup_console(height, width, wait, timeout, name) def initialize(height, width, wait, timeout, name) @wait = wait @timeout = timeout + @name = name @result = nil @codepage_success_p = nil @wrote_and_not_yet_waited = false diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb index a9607ee..dca357b 100644 --- a/lib/yamatanooroti/windows/terminal.rb +++ b/lib/yamatanooroti/windows/terminal.rb @@ -104,14 +104,14 @@ def new_wt(rows, cols) return invoke_wt_process(command, marker_command.split(" ").first, "new_wt") end - def split_pane(div = 0.5, splitter: :v, name: nil) + def split_pane(div = 0.5, splitter: :v, title: @name) marker_command = CONSOLE_MARKING_COMMAND command = "#{Yamatanooroti::WindowsConsoleSettings.wt_exe} " \ "-w #{@wt_id} " \ "move-focus first; " \ "sp #{splitter == :v ? "-V" : "-H"} "\ - "--title #{name || @wt_id} " \ + "--title #{title || @wt_id} " \ "-s #{div} " \ "#{marker_command}" pid = invoke_wt_process(command, marker_command.split(" ").first, "split_pane") @@ -192,6 +192,8 @@ def self.setup_console(height, width, wait, timeout, name) mother_height = @@max_size[0] mother_width = @@max_size[1] + @@mother_wt.name = name + if height > mother_height raise "console height #{height} grater than maximum(#{mother_height})" end @@ -201,7 +203,7 @@ def self.setup_console(height, width, wait, timeout, name) if height != mother_height result_h = @@hsplit_info.search_div(height) do |div| - @@mother_wt.split_pane(div, splitter: :h, name: name) + @@mother_wt.split_pane(div, splitter: :h) @@mother_wt.move_focus("first") h = @@mother_wt.get_size[0] @@mother_wt.close_pane if h != height @@ -212,7 +214,7 @@ def self.setup_console(height, width, wait, timeout, name) if width != mother_width result_w = @@vsplit_info.search_div(width) do |div| - @@mother_wt.split_pane(div, splitter: :v, name: name) + @@mother_wt.split_pane(div, splitter: :v) @@mother_wt.move_focus("first") w = @@mother_wt.get_size[1] @@mother_wt.close_pane if w != width @@ -224,6 +226,8 @@ def self.setup_console(height, width, wait, timeout, name) return @@mother_wt end + attr_accessor :name + def initialize(height, width, wait, timeout) @wait = wait @timeout = timeout @@ -259,6 +263,7 @@ def close_console(need_to_close = true) end @process_ids = [@console_process_id = nt] @result = nil + @name = nil end def close! diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index e274d1c..98cfebe 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -407,20 +407,22 @@ def result end def close - if !@result && @console_process_id - sleep @timeout if @wrote_and_not_yet_waited # wait a long. avoid write();close() sequence - end - if @target && !@target.closed? - @target.close - end - if !DL.interrupted? && @console_process_id - @result = retrieve_screen + close_request = @target && !@target.closed? + retrieve_request = !DL.interrupted? && @console_process_id + + if close_request && retrieve_request && !@result + if @wrote_and_not_yet_waited # wait a long. avoid write();close() sequence + sleep @timeout + puts "\r#{@name}: close() just after write() will ultimately slow test down. Put assert_screen() before close()." + end end + + @target.close if close_request + @result = retrieve_screen if retrieve_request @result ||= "" end - def sleep_wait - sleep @wait + def clear_need_wait_flag @wrote_and_not_yet_waited = false end From f97a642935570b39d57065ebc1a8f1ed53eb3f5e Mon Sep 17 00:00:00 2001 From: YO4 Date: Sun, 20 Oct 2024 16:51:40 +0900 Subject: [PATCH 61/76] check successful win32_last_error successful_or_if_not_messageout() returns true otherwise raise exception or message output to stderr --- .../windows/windows-definition.rb | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/lib/yamatanooroti/windows/windows-definition.rb b/lib/yamatanooroti/windows/windows-definition.rb index ef9fdcb..d3ea6df 100644 --- a/lib/yamatanooroti/windows/windows-definition.rb +++ b/lib/yamatanooroti/windows/windows-definition.rb @@ -126,6 +126,7 @@ module Yamatanooroti::WindowsDefinition ATTACH_PARENT_PROCESS = -1 KEY_EVENT = 0x0001 SW_HIDE = 0 + SW_SHOWNORMAL = 1 SW_SHOWNOACTIVE = 4 SW_SHOWMINNOACTIVE = 7 SW_SHOWNA = 8 @@ -204,9 +205,13 @@ module Yamatanooroti::WindowsDefinition # BOOL WINAPI GenerateConsoleCtrlEvent(DWORD dwCtrlEvent, DWORD dwProcessGroupId); extern 'BOOL GenerateConsoleCtrlEvent(DWORD, DWORD);', :stdcall - private def error_message(r, method_name, exception: true) - return if not r.zero? + private def successful_or_if_not_messageout(r, method_name, exception: true) + return true if not r.zero? err = Fiddle.win32_last_error + if err == 0 + $stderr.puts "Info: #{method_name} returns zero but win32_last_error has successful zero." + return true + end string = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP, FREE) n = FormatMessageW( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, @@ -229,13 +234,14 @@ module Yamatanooroti::WindowsDefinition raise msg else $stderr.puts msg + return false end end def get_console_screen_buffer_info(handle) csbi = CONSOLE_SCREEN_BUFFER_INFO.malloc(FREE) r = GetConsoleScreenBufferInfo(handle, csbi) - error_message(r, 'GetConsoleScreenBufferInfo') + successful_or_if_not_messageout(r, 'GetConsoleScreenBufferInfo') return csbi end @@ -243,7 +249,7 @@ def set_console_screen_buffer_info_ex(handle, h, w, buffer_height) csbi = CONSOLE_SCREEN_BUFFER_INFOEX.malloc(FREE) csbi.cbSize = CONSOLE_SCREEN_BUFFER_INFOEX.size r = GetConsoleScreenBufferInfoEx(handle, csbi) - error_message(r, 'GetConsoleScreenBufferSize') + successful_or_if_not_messageout(r, 'GetConsoleScreenBufferSize') csbi.dwSize_X = w csbi.dwSize_Y = buffer_height csbi.Left = 0 @@ -251,8 +257,7 @@ def set_console_screen_buffer_info_ex(handle, h, w, buffer_height) csbi.Top = [csbi.Top, buffer_height - h].min csbi.Bottom = csbi.Top + h - 1 r = SetConsoleScreenBufferInfoEx(handle, csbi) - error_message(r, 'SetConsoleScreenBufferInfoEx') - return r != 0 + return successful_or_if_not_messageout(r, 'SetConsoleScreenBufferInfoEx') end def set_console_window_info(handle, h, w) @@ -262,8 +267,7 @@ def set_console_window_info(handle, h, w) rect.Right = w - 1 rect.Bottom = h - 1 r = SetConsoleWindowInfo(handle, 1, rect) - error_message(r, 'SetConsoleWindowInfo') - return r != 0 + return successful_or_if_not_messageout(r, 'SetConsoleWindowInfo') end def set_console_window_size(handle, h, w) @@ -290,26 +294,24 @@ def create_console_file_handle(name) 0 ) fh = [fh].pack("J").unpack1("J") - error_message(0, name) if fh == INVALID_HANDLE_VALUE + successful_or_if_not_messageout(0, name) if fh == INVALID_HANDLE_VALUE fh end def close_handle(handle) r = CloseHandle(handle) - error_message(r, "CloseHandle") - return r != 0 + return successful_or_if_not_messageout(r, "CloseHandle") end def free_console r = FreeConsole() - error_message(r, "FreeConsole") - return r != 0 + return successful_or_if_not_messageout(r, "FreeConsole") end def attach_console(pid = ATTACH_PARENT_PROCESS, maybe_fail: false) r = AttachConsole(pid) - error_message(r, 'AttachConsole') unless maybe_fail - return r != 0 + return successful_or_if_not_messageout(r, 'AttachConsole') unless maybe_fail + return r != 0 # || Fiddle.win32_last_error == 0 # this case couses problem end SHOWWINDOW_MAP = { @@ -346,7 +348,7 @@ def create_console(command, show = SW_SHOWNORMAL) Fiddle::NULL, Fiddle::NULL, startup_info, console_process_info ) - error_message(r, 'CreateProcessW') + successful_or_if_not_messageout(r, 'CreateProcessW') end close_handle(console_process_info.hProcess) close_handle(console_process_info.hThread) @@ -355,7 +357,7 @@ def create_console(command, show = SW_SHOWNORMAL) def get_std_handle(stdhandle) fh = GetStdHandle(stdhandle) - error_message(0, name) if fh == INVALID_HANDLE_VALUE + successful_or_if_not_messageout(0, name) if fh == INVALID_HANDLE_VALUE fh end @@ -377,7 +379,7 @@ def read_console_output(handle, row, width) buffer = Fiddle::Pointer.malloc(Fiddle::SIZEOF_SHORT * width, FREE) n = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DWORD, FREE) r = ReadConsoleOutputCharacterW(handle, buffer, width, row << 16, n) - error_message(r, "ReadConsoleOutputCharacterW") + successful_or_if_not_messageout(r, "ReadConsoleOutputCharacterW") return wc2mb(buffer[0, n.to_str.unpack1("L") * 2]).gsub(/ *$/, "") end @@ -417,14 +419,14 @@ def build_key_input_record(str) def write_console_input(handle, records, n) written = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DWORD, FREE) r = WriteConsoleInputW(handle, records, n, written) - error_message(r, 'WriteConsoleInput') + successful_or_if_not_messageout(r, 'WriteConsoleInput') return written.to_str.unpack1('L') end def get_number_of_console_input_events(handle) n = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DWORD, FREE) r = GetNumberOfConsoleInputEvents(handle, n) - error_message(r, 'GetNumberOfConsoleInputEvents') + successful_or_if_not_messageout(r, 'GetNumberOfConsoleInputEvents') return n.to_str.unpack1('L') end @@ -432,8 +434,7 @@ def get_console_mode(handle) mode = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DWORD, FREE) mode[0, Fiddle::SIZEOF_DWORD] = "\0".b * Fiddle::SIZEOF_DWORD r = GetConsoleMode(handle, mode) - error_message(r, 'GetConsoleMode', exception: false) - mode.to_str.unpack1('L') + successful_or_if_not_messageout(r, 'GetConsoleMode', exception: false) ? mode.to_str.unpack1('L') : nil end def set_console_mode(handle, mode) @@ -442,12 +443,12 @@ def set_console_mode(handle, mode) def set_console_codepage(cp) r = SetConsoleCP(cp) - error_message(r, 'SetConsoleCP', exception: false) + return successful_or_if_not_messageout(r, 'SetConsoleCP', exception: false) end def set_console_output_codepage(cp) r = SetConsoleOutputCP(cp) - error_message(r, 'SetConsoleOutputCP', exception: false) + return successful_or_if_not_messageout(r, 'SetConsoleOutputCP', exception: false) end def get_console_codepage() @@ -460,8 +461,7 @@ def get_console_output_codepage() def generate_console_ctrl_event(event, pgrp) r = GenerateConsoleCtrlEvent(event, pgrp) - error_message(r, 'GenerateConsoleCtrlEvent') - return r != 0 + return successful_or_if_not_messageout(r, 'GenerateConsoleCtrlEvent') end # Ctrl+C trap support From ff0b20a2cce69329f29888943072d5922d983551 Mon Sep 17 00:00:00 2001 From: YO4 Date: Sun, 20 Oct 2024 18:01:18 +0900 Subject: [PATCH 62/76] make conhost.rb console startup waiting method the same way as terminal.rb --- lib/yamatanooroti/windows/conhost.rb | 7 ++++--- lib/yamatanooroti/windows/terminal.rb | 10 ---------- lib/yamatanooroti/windows/windows.rb | 10 ++++++++++ 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/lib/yamatanooroti/windows/conhost.rb b/lib/yamatanooroti/windows/conhost.rb index 8e0e6d5..83b482a 100644 --- a/lib/yamatanooroti/windows/conhost.rb +++ b/lib/yamatanooroti/windows/conhost.rb @@ -15,10 +15,11 @@ def initialize(height, width, wait, timeout, name) @console_process_id = DL.create_console(CONSOLE_KEEPING_COMMAND.sub("NAME", name), show_console_param()) + sleep 0.1 if Yamatanooroti.options.windows == :"legacy-conhost" # ad-hoc + # wait for console startup complete - 8.times do |n| - break if attach_terminal(open: false, exception: false) { true } - sleep 0.01 * 2**n + with_timeout("Console process startup timed out.") do + attach_terminal(open: false, exception: false) { true } end attach_terminal do |conin, conout| diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb index dca357b..b34c12a 100644 --- a/lib/yamatanooroti/windows/terminal.rb +++ b/lib/yamatanooroti/windows/terminal.rb @@ -51,16 +51,6 @@ def pid_from_windowtitle(name) do_tasklist("WINDOWTITLE eq #{name}") end - private def with_timeout(timeout_message, timeout = @timeout, &block) - wait_until = Time.now + timeout - loop do - result = block.call - break result if result - raise timeout_message if wait_until < Time.now - sleep @wait - end - end - private def invoke_wt_process(command, marker, keeper_name) DL.create_console(command, show_console_param()) # default timeout seems to be too short diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index 98cfebe..4b1cbf0 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -212,6 +212,16 @@ module Yamatanooroti::WindowsTermMixin map.fetch(Yamatanooroti.options.show_console ? :show : :hide) end + private def with_timeout(timeout_message, timeout = @timeout, &block) + wait_until = Time.now + timeout + loop do + result = block.call + break result if result + raise timeout_message if wait_until < Time.now + sleep @wait + end + end + private def attach_terminal(open: true, exception: true) stderr = $stderr $stderr = StringIO.new From f570abb3f607ae109d78d90f7e1d609314da05a1 Mon Sep 17 00:00:00 2001 From: YO4 Date: Tue, 22 Oct 2024 07:09:10 +0900 Subject: [PATCH 63/76] prevent startup message testcase timeout --- test/yamatanooroti/test_run_ruby.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/yamatanooroti/test_run_ruby.rb b/test/yamatanooroti/test_run_ruby.rb index 1554186..b9e5044 100644 --- a/test/yamatanooroti/test_run_ruby.rb +++ b/test/yamatanooroti/test_run_ruby.rb @@ -12,7 +12,7 @@ def test_winsize def test_wait_for_startup_message code = 'sleep 1; puts "aaa"; sleep 10; puts "bbb"' - start_terminal(5, 30, ['ruby', '-e', code], startup_message: 'aaa') + start_terminal(5, 30, ['ruby', '--disable=gems', '-e', code], startup_message: 'aaa') # The start_terminal method waits 1 sec for "aaa" as specified by # wait_for_startup_message option and close immediately by the close # method at the next line. The next "bbb" after waiting 1 sec more doesn't From 5d47b25ba899afde5f96d1691bcf91c4c4e5b1c7 Mon Sep 17 00:00:00 2001 From: YO4 Date: Fri, 25 Oct 2024 02:12:07 +0900 Subject: [PATCH 64/76] restructure windows source code new windows/wt.rb (split from windows/terminal.rb, new WindowsTerminal class) new windows/wmi.rb (WMI Win32_Process interface) new windows/windows-setup.rb (split from windows/windows.rb) change windows terminal pane split strategy. make 3 panes at once if sp --size parameter cached. --- lib/yamatanooroti/windows.rb | 4 +- lib/yamatanooroti/windows/conhost.rb | 8 +- lib/yamatanooroti/windows/terminal.rb | 282 ++++--------------- lib/yamatanooroti/windows/windows-setup.rb | 201 +++++++++++++ lib/yamatanooroti/windows/windows.rb | 221 +-------------- lib/yamatanooroti/windows/wmi.rb | 77 +++++ lib/yamatanooroti/windows/wt.rb | 310 +++++++++++++++++++++ 7 files changed, 667 insertions(+), 436 deletions(-) create mode 100644 lib/yamatanooroti/windows/windows-setup.rb create mode 100644 lib/yamatanooroti/windows/wmi.rb create mode 100644 lib/yamatanooroti/windows/wt.rb diff --git a/lib/yamatanooroti/windows.rb b/lib/yamatanooroti/windows.rb index c18170a..2b0d152 100644 --- a/lib/yamatanooroti/windows.rb +++ b/lib/yamatanooroti/windows.rb @@ -1,5 +1,6 @@ require 'test/unit' require_relative 'windows/windows-definition' +require_relative 'windows/windows-setup' require_relative 'windows/windows' require_relative 'windows/conhost' require_relative 'windows/terminal' @@ -27,6 +28,7 @@ def identify def start_terminal(height, width, command, wait: nil, timeout: nil, startup_message: nil, codepage: nil) @timeout = timeout || Yamatanooroti.options.default_timeout + @startup_timeout = @timeout + 2 @wait = wait || Yamatanooroti.options.default_wait @result = nil if @terminal @@ -51,7 +53,7 @@ def start_terminal(height, width, command, wait: nil, timeout: nil, startup_mess end private def wait_startup_message - wait_until = Time.now + @timeout + wait_until = Time.now + @startup_timeout chunks = +'' loop do wait = wait_until - Time.now diff --git a/lib/yamatanooroti/windows/conhost.rb b/lib/yamatanooroti/windows/conhost.rb index 83b482a..844f341 100644 --- a/lib/yamatanooroti/windows/conhost.rb +++ b/lib/yamatanooroti/windows/conhost.rb @@ -5,6 +5,8 @@ def self.setup_console(height, width, wait, timeout, name) new(height, width, wait, timeout, name) end + attr_reader :console_process_id + def initialize(height, width, wait, timeout, name) @wait = wait @timeout = timeout @@ -13,7 +15,7 @@ def initialize(height, width, wait, timeout, name) @codepage_success_p = nil @wrote_and_not_yet_waited = false - @console_process_id = DL.create_console(CONSOLE_KEEPING_COMMAND.sub("NAME", name), show_console_param()) + @console_process_id = DL.create_console(keeper_commandline(name), show_console_param()) sleep 0.1 if Yamatanooroti.options.windows == :"legacy-conhost" # ad-hoc @@ -40,4 +42,8 @@ def close_console(need_to_close = true) end end end + + def close! + close_console(!Yamatanooroti.options.show_console) + end end diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb index b34c12a..b7151cb 100644 --- a/lib/yamatanooroti/windows/terminal.rb +++ b/lib/yamatanooroti/windows/terminal.rb @@ -1,272 +1,98 @@ +require_relative "./wt" + class Yamatanooroti::WindowsTerminalTerm include Yamatanooroti::WindowsTermMixin + Self = self - @@count = 0 - @@cradle = {} - - def call_spawn(command) - pid = spawn(command) - if t = Process.detach(pid) - @@cradle[pid] = t - end - pid - end - - def kill_and_wait(pid) - return unless pid - t = @@cradle[pid] - begin - Process.kill(:KILL, pid) - rescue Errno::ESRCH # No such process - end - if t - if t.join(@timeout) == nil - puts "Caution: process #{pid} does not terminate in #{@timeout} seconds." - end - @@cradle.delete(pid) - end - end - - def get_size - attach_terminal do |conin, conout| - csbi = DL.get_console_screen_buffer_info(conout) - [csbi.Bottom + 1, csbi.Right + 1] - end + def self.window_title + @count = @count ? @count + 1 : 0 + "yamatanooroti##{@count}@#{Process.pid}" end - def do_tasklist(filter) - list = `tasklist /FI "#{filter}"`.lines - if list.length != 4 - return nil - end - pid_start = list[2].index(/ \K=/) - list[3][pid_start..-1].to_i + def self.testcase_title(title) + count = @iter_count&.fetch(title) + count ||= countup_testcase_title(title) + "#{title}##{count}@#{Process.pid}" end - def pid_from_imagename(name) - do_tasklist("IMAGENAME eq #{name}") + def self.countup_testcase_title(title) + counter = (@iter_count ||= {}) + count = counter[title] || 0 + counter[title] = count + 1 end - def pid_from_windowtitle(name) - do_tasklist("WINDOWTITLE eq #{name}") + class << self + attr_accessor :wt, :max_size, :min_size, :split_cache_h, :split_cache_v end - private def invoke_wt_process(command, marker, keeper_name) - DL.create_console(command, show_console_param()) - # default timeout seems to be too short - begin - marker_pid = with_timeout("Windows Terminal marker process detection failed.", @timeout + 5) do - pid_from_imagename(marker) - end - rescue => e - # puts `tasklist /FI "SESSION ge 0"` - raise e - end - @console_process_id = marker_pid - - # wait for console startup complete - with_timeout("Console process startup timed out.") do - attach_terminal(open: false, exception: false) { true } - end - - keeper_pid = attach_terminal(open: false) do - call_spawn(CONSOLE_KEEPING_COMMAND.sub("NAME", keeper_name)) - end - @console_process_id = keeper_pid - - # wait for console keeping process startup complete - with_timeout("Console process startup timed out.") do - attach_terminal(open: false, exception: false) { true } + def self.setup_console(height, width, wait, timeout, name) + if !Self.wt + Self.wt = self.new(*max_size, wait, timeout, Self.window_title) end - return keeper_pid - ensure - kill_and_wait(marker_pid) if marker_pid - end - - def new_wt(rows, cols) - marker_command = CONSOLE_MARKING_COMMAND - - @wt_id = "yamaoro#{Process.pid}##{@@count}" - @@count += 1 - command = "#{Yamatanooroti::WindowsConsoleSettings.wt_exe} -w #{@wt_id} --size #{cols},#{rows} nt --title #{@wt_id} #{marker_command}" - - return invoke_wt_process(command, marker_command.split(" ").first, "new_wt") + Self.wt.new_tab(height, width, name) + Self.wt end - def split_pane(div = 0.5, splitter: :v, title: @name) - marker_command = CONSOLE_MARKING_COMMAND - - command = "#{Yamatanooroti::WindowsConsoleSettings.wt_exe} " \ - "-w #{@wt_id} " \ - "move-focus first; " \ - "sp #{splitter == :v ? "-V" : "-H"} "\ - "--title #{title || @wt_id} " \ - "-s #{div} " \ - "#{marker_command}" - pid = invoke_wt_process(command, marker_command.split(" ").first, "split_pane") - @process_ids.push pid - @console_process_id = @process_ids[0] + def console_process_id + @wt.active_tab.pid end - def move_focus(direction) - command = "#{Yamatanooroti::WindowsConsoleSettings.wt_exe} " \ - "-w #{@wt_id} " \ - "move-pane #{direction}" - system(command) + def get_size + (@wt.active_tab || @wt.base_tab).get_size end - def new_tab - marker_command = CONSOLE_MARKING_COMMAND - - command = "#{Yamatanooroti::WindowsConsoleSettings.wt_exe} " \ - "-w #{@wt_id} " \ - "new-tab " \ - "#{marker_command}" - invoke_wt_process(command, marker_command.split(" ").first, "new_tab") - end + def initialize(height, width, wait, timeout, title = "yamatanooroti") + @wait = wait + @timeout = timeout + @result = nil + @codepage_success_p = nil + @wrote_and_not_yet_waited = false - def close_pane - kill_and_wait(@process_ids.pop) + @wt = Yamatanooroti::WindowsTerminal.new(height, width, title, title, wait, timeout, self) end - class List - def initialize(total) - @total = total - @div_to_x = {} - @x_to_div = {} - end + def new_tab(height, width, name) + @result = nil + @codepage_success_p = nil + @wrote_and_not_yet_waited = false + @name = name - def search_div(x, &block) - denominator = 100 - div, denominator = @x_to_div[x] if @x_to_div[x] - div ||= (@total - x) * (denominator * 95 / 100) / @total + denominator * 4 / 100 - loop do - # STDOUT.write "target: #{x} total: #{@total} div: #{div.to_f / denominator}" - result = block.call(div.to_f / denominator) - # puts " result: #{result}" - @div_to_x[div.to_f / denominator] = result - @x_to_div[x] = [div, denominator] - return result if result == x - if result < x - div -= 1 - return nil if div <= 0 - if @div_to_x[div.to_f / denominator] && @div_to_x[div.to_f / denominator] != x - div = div * 10 + 5 - denominator *= 10 - end - else - div += 1 - return nil if div >= denominator - if @div_to_x[div.to_f / denominator] && @div_to_x[div.to_f / denominator] != x - div = div * 10 - 5 - denominator *= 10 - end - end - end - end + Self.countup_testcase_title(name) + @wt.new_tab(height, width, Self.testcase_title(name)) end - @@mother_wt = nil - @@div_to_width = {} - @@width_to_div = {} - - # raise "Could not set Windows Terminal to size #{[height, width]}" - - def self.setup_console(height, width, wait, timeout, name) - if @@mother_wt == nil - @@mother_wt = self.new(*@@max_size, wait, timeout) - @@hsplit_info = List.new(@@max_size[0]) - @@vsplit_info = List.new(@@max_size[1]) - end - mother_height = @@max_size[0] - mother_width = @@max_size[1] - - @@mother_wt.name = name - - if height > mother_height - raise "console height #{height} grater than maximum(#{mother_height})" - end - if width > mother_width - raise "console width #{width} grater than maximum(#{mother_width})" - end - - if height != mother_height - result_h = @@hsplit_info.search_div(height) do |div| - @@mother_wt.split_pane(div, splitter: :h) - @@mother_wt.move_focus("first") - h = @@mother_wt.get_size[0] - @@mother_wt.close_pane if h != height - h - end - raise "console height deviding to #{height} failed" if !result_h - end - - if width != mother_width - result_w = @@vsplit_info.search_div(width) do |div| - @@mother_wt.split_pane(div, splitter: :v) - @@mother_wt.move_focus("first") - w = @@mother_wt.get_size[1] - @@mother_wt.close_pane if w != width - w + def close_console(need_to_close = true) + if need_to_close + if @target && !@target.closed? + @target.close end - raise "console widtht deviding to #{width} failed" if !result_w + @wt.close_tab + else + @wt.detach_tab end - - return @@mother_wt end - attr_accessor :name - - def initialize(height, width, wait, timeout) - @wait = wait - @timeout = timeout - @result = nil - @codepage_success_p = nil - @wrote_and_not_yet_waited = false - - @process_ids = [new_wt(height, width)] + def close! + @wt&.close + @wt = nil end def self.diagnose_size_capability wt = self.new(999, 999, 0.01, 5.0) max_size = wt.get_size - @@max_size = [[max_size[0], 60].min, [max_size[1], 200].min] + Self.max_size = [[max_size[0], 60].min, [max_size[1], 200].min] puts max_size.then { |r, c| "Windows Terminal maximum size: rows: #{r}, columns: #{c}" } wt.close! wt = self.new(2, 2, 0.01, 5.0) - min_size = @@min_size = wt.get_size + min_size = wt.get_size + Self.min_size = min_size puts min_size.then { |r, c| "Windows Terminal smallest size: rows: #{r}, columns: #{c}" } wt.close! - puts @@max_size.then {|r, c| "Use test window size: rows: #{r}, columns: #{c}" } - end - - def close_console(need_to_close = true) - nt = new_tab() - if need_to_close && @process_ids - if @target && !@target.closed? - @target.close - end - while @process_ids.size > 0 - kill_and_wait(@process_ids.pop) - end - end - @process_ids = [@console_process_id = nt] - @result = nil - @name = nil - end - - def close! - if @process_ids - while @process_ids.size > 0 - kill_and_wait(@process_ids.pop) - end - end - @@mother_wt = nil - @process_ids = @console_process_id = nil + puts Self.max_size.then {|r, c| "Use test window size: rows: #{r}, columns: #{c}" } end Test::Unit.at_exit do - @@mother_wt&.close! + Self.wt&.close! end end diff --git a/lib/yamatanooroti/windows/windows-setup.rb b/lib/yamatanooroti/windows/windows-setup.rb new file mode 100644 index 0000000..d4febcf --- /dev/null +++ b/lib/yamatanooroti/windows/windows-setup.rb @@ -0,0 +1,201 @@ +require 'win32/registry' +require 'tmpdir' +require 'fileutils' +require 'uri' +require 'digest/sha2' + +module Yamatanooroti::WindowsConsoleSetup + DelegationConsoleSetting = { + conhost: "{B23D10C0-E52E-411E-9D5B-C09FDF709C7D}", + terminal: "{2EACA947-7F5F-4CFA-BA87-8F7FBEEFBE69}", + preview: "{06EC847C-C0A5-46B8-92CB-7C92F6E35CD5}", + }.freeze + DelegationTerminalSetting = { + conhost: "{B23D10C0-E52E-411E-9D5B-C09FDF709C7D}", + terminal: "{E12CFF52-A866-4C77-9A90-F570A7AA2C6B}", + preview: "{86633F1F-6454-40EC-89CE-DA4EBA977EE2}", + }.freeze + + def self.wt_exe + @wt_exe + end + + def self.wt_wait + 0 + end + + begin + Win32::Registry::HKEY_CURRENT_USER.open('Console') do |reg| + @orig_conhost = reg['ForceV2'] + end + rescue Win32::Registry::Error + end + begin + Win32::Registry::HKEY_CURRENT_USER.open('Console\%%Startup') do |reg| + @orig_console = reg['DelegationConsole'] + @orig_terminal = reg['DelegationTerminal'] + end + rescue Win32::Registry::Error + end + + Test::Unit.at_start do + case Yamatanooroti.options.windows + when :conhost + puts "use conhost(classic, conhostV2) for windows console" + Win32::Registry::HKEY_CURRENT_USER.open('Console', Win32::Registry::KEY_WRITE) do |reg| + reg['ForceV2', Win32::Registry::REG_DWORD] = 1 + end + Win32::Registry::HKEY_CURRENT_USER.open('Console\%%Startup', Win32::Registry::KEY_WRITE) do |reg| + reg['DelegationConsole', Win32::Registry::REG_SZ] = DelegationConsoleSetting[:conhost] + reg['DelegationTerminal', Win32::Registry::REG_SZ] = DelegationTerminalSetting[:conhost] + end if @orig_console && @orig_terminal + when :"legacy-conhost" + puts "use conhost(legacy, conhostV1) for windows console" + Win32::Registry::HKEY_CURRENT_USER.open('Console', Win32::Registry::KEY_WRITE) do |reg| + reg['ForceV2', Win32::Registry::REG_DWORD] = 0 + end + Win32::Registry::HKEY_CURRENT_USER.open('Console\%%Startup', Win32::Registry::KEY_WRITE) do |reg| + reg['DelegationConsole', Win32::Registry::REG_SZ] = DelegationConsoleSetting[:conhost] + reg['DelegationTerminal', Win32::Registry::REG_SZ] = DelegationTerminalSetting[:conhost] + end if @orig_console && @orig_terminal + when :canary + @wt_exe = extract_terminal(prepare_terminal_canary) + else + @wt_exe = extract_terminal(prepare_terminal_portable) + end + if @wt_exe + Yamatanooroti::WindowsTerminalTerm.diagnose_size_capability + end + end + + Test::Unit.at_exit do + Win32::Registry::HKEY_CURRENT_USER.open('Console', Win32::Registry::KEY_WRITE) do |reg| + reg['ForceV2', Win32::Registry::REG_DWORD] = @orig_conhost + end if @orig_conhost + Win32::Registry::HKEY_CURRENT_USER.open('Console\%%Startup', Win32::Registry::KEY_WRITE) do |reg| + reg['DelegationConsole', Win32::Registry::REG_SZ] = @orig_console + reg['DelegationTerminal', Win32::Registry::REG_SZ] = @orig_terminal + end if @orig_console && @orig_terminal + end + + def self.tmpdir + return @tmpdir if @tmpdir + dir = nil + if Yamatanooroti.options.terminal_workdir + dir = Yamatanooroti.options.terminal_workdir + FileUtils.mkdir_p(dir) + else + @tmpdir_t = Thread.new do + Thread.current.abort_on_exception = true + Dir.mktmpdir do |tmpdir| + dir = tmpdir + sleep + ensure + sleep 0.5 # wait for terminate windows terminal + end + end + Thread.pass while dir == nil + end + return @tmpdir = dir + end + + def self.extract_terminal(path) + tar = File.join(ENV['SystemRoot'], "system32", "tar.exe") + extract_dir = File.join(tmpdir, "wt") + FileUtils.remove_entry(extract_dir) if File.exist?(extract_dir) + FileUtils.mkdir_p(extract_dir) + puts "extracting #{File.basename(path)}" + system tar, "xf", path, "-C", extract_dir + wt = Dir["**/wt.exe", base: extract_dir] + raise "not found wt.exe. aborted." if wt.size < 1 + raise "found wt.exe #{wt.size} times unexpectedly. aborted." if wt.size > 1 + wt = File.join(extract_dir, wt[0]) + wt_dir = File.dirname(wt) + portable_mark = File.join(wt_dir, ".portable") + open(portable_mark, "w") { |f| f.puts } unless File.exist?(portable_mark) + settings = File.join(wt_dir, "settings", "settings.json") + FileUtils.mkdir_p(File.dirname(settings)) + open(settings, "wb") do |settings| + settings.write <<~'JSON' + { + "defaultProfile": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", + "disableAnimations": true, + "minimizeToNotificationArea": true, + "profiles": + { + "defaults": + { + "bellStyle": "none", + "closeOnExit": "always", + "font": + { + "size": 9 + }, + "padding": "0", + "scrollbarState": "always" + }, + "list": + [ + { + "commandline": "%SystemRoot%\\System32\\cmd.exe", + "guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", + "name": "cmd.exe" + } + ] + }, + "showTabsInTitlebar": false, + "warning.confirmCloseAllTabs": false, + "warning.largePaste": false, + "warning.multiLinePaste": false + } + JSON + end + puts "use #{wt} for windows console" + wt + end + + def self.prepare_terminal_canary + dir = tmpdir + header = `curl --head -sS -o #{tmpdir}/header -L -w "%{url_effective}\n%header{ETag}\n%header{Content-Length}\n%header{Last-Modified}" https://aka.ms/terminal-canary-zip-x64` + url, etag, length, timestamp = *header.lines.map(&:chomp) + name = File.basename(URI.parse(url).path) + path = File.join(dir, "wt_dists", "canary", etag.delete('"'), name) + if File.exist?(path) + if File.size(path) == length.to_i + puts "use existing #{path}" + return path + else + FileUtils.remove_entry(path) + end + else + if Dir.empty?(dir) + puts "removing old canary zip" + Dir.entries.each { |olddir| FileUtils.remove_entry(olddir) } + end + end + FileUtils.mkdir_p(File.dirname(path)) + system "curl #{$stdin.isatty ? "" : "-sS "}-L -o #{path} https://aka.ms/terminal-canary-zip-x64" + path + end + + def self.prepare_terminal_portable + releases = Yamatanooroti::Options::WindowsTerminal::RELEASES + url = releases[Yamatanooroti.options.windows.to_sym][:url] + sha256 = releases[Yamatanooroti.options.windows.to_sym][:sha256] + dir = tmpdir + name = File.basename(URI.parse(url).path) + path = File.join(dir, "wt_dists", Yamatanooroti.options.windows, name) + if File.exist?(path) + if Digest::SHA256.new.file(path).hexdigest.upcase == sha256 + puts "use existing #{path}" + return path + else + FileUtils.remove_entry(path) + end + end + FileUtils.mkdir_p(File.dirname(path)) + system "curl #{$stdin.isatty ? "" : "-sS "}-L -o #{path} #{url}" + raise "not match windows terminal distribution zip sha256" unless Digest::SHA256.new.file(path).hexdigest.upcase == sha256 + path + end +end diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index 4b1cbf0..2844efe 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -1,213 +1,20 @@ require 'stringio' -require 'win32/registry' -require 'tmpdir' -require 'fileutils' -require 'uri' -require 'digest/sha2' - -module Yamatanooroti::WindowsConsoleSettings - DelegationConsoleSetting = { - conhost: "{B23D10C0-E52E-411E-9D5B-C09FDF709C7D}", - terminal: "{2EACA947-7F5F-4CFA-BA87-8F7FBEEFBE69}", - preview: "{06EC847C-C0A5-46B8-92CB-7C92F6E35CD5}", - }.freeze - DelegationTerminalSetting = { - conhost: "{B23D10C0-E52E-411E-9D5B-C09FDF709C7D}", - terminal: "{E12CFF52-A866-4C77-9A90-F570A7AA2C6B}", - preview: "{86633F1F-6454-40EC-89CE-DA4EBA977EE2}", - }.freeze - - def self.wt_exe - @wt_exe - end - - def self.wt_wait - 0 - end - - begin - Win32::Registry::HKEY_CURRENT_USER.open('Console') do |reg| - @orig_conhost = reg['ForceV2'] - end - rescue Win32::Registry::Error - end - begin - Win32::Registry::HKEY_CURRENT_USER.open('Console\%%Startup') do |reg| - @orig_console = reg['DelegationConsole'] - @orig_terminal = reg['DelegationTerminal'] - end - rescue Win32::Registry::Error - end - - Test::Unit.at_start do - case Yamatanooroti.options.windows - when :conhost - puts "use conhost(classic, conhostV2) for windows console" - Win32::Registry::HKEY_CURRENT_USER.open('Console', Win32::Registry::KEY_WRITE) do |reg| - reg['ForceV2', Win32::Registry::REG_DWORD] = 1 - end - Win32::Registry::HKEY_CURRENT_USER.open('Console\%%Startup', Win32::Registry::KEY_WRITE) do |reg| - reg['DelegationConsole', Win32::Registry::REG_SZ] = DelegationConsoleSetting[:conhost] - reg['DelegationTerminal', Win32::Registry::REG_SZ] = DelegationTerminalSetting[:conhost] - end if @orig_console && @orig_terminal - when :"legacy-conhost" - puts "use conhost(legacy, conhostV1) for windows console" - Win32::Registry::HKEY_CURRENT_USER.open('Console', Win32::Registry::KEY_WRITE) do |reg| - reg['ForceV2', Win32::Registry::REG_DWORD] = 0 - end - Win32::Registry::HKEY_CURRENT_USER.open('Console\%%Startup', Win32::Registry::KEY_WRITE) do |reg| - reg['DelegationConsole', Win32::Registry::REG_SZ] = DelegationConsoleSetting[:conhost] - reg['DelegationTerminal', Win32::Registry::REG_SZ] = DelegationTerminalSetting[:conhost] - end if @orig_console && @orig_terminal - when :canary - @wt_exe = extract_terminal(prepare_terminal_canary) - else - @wt_exe = extract_terminal(prepare_terminal_portable) - end - if @wt_exe - Yamatanooroti::WindowsTerminalTerm.diagnose_size_capability - end - end - Test::Unit.at_exit do - Win32::Registry::HKEY_CURRENT_USER.open('Console', Win32::Registry::KEY_WRITE) do |reg| - reg['ForceV2', Win32::Registry::REG_DWORD] = @orig_conhost - end if @orig_conhost - Win32::Registry::HKEY_CURRENT_USER.open('Console\%%Startup', Win32::Registry::KEY_WRITE) do |reg| - reg['DelegationConsole', Win32::Registry::REG_SZ] = @orig_console - reg['DelegationTerminal', Win32::Registry::REG_SZ] = @orig_terminal - end if @orig_console && @orig_terminal - end - - def self.tmpdir - return @tmpdir if @tmpdir - dir = nil - if Yamatanooroti.options.terminal_workdir - dir = Yamatanooroti.options.terminal_workdir - FileUtils.mkdir_p(dir) - else - @tmpdir_t = Thread.new do - Thread.current.abort_on_exception = true - Dir.mktmpdir do |tmpdir| - dir = tmpdir - sleep - ensure - sleep 0.5 # wait for terminate windows terminal - end - end - Thread.pass while dir == nil - end - return @tmpdir = dir - end +module Yamatanooroti::WindowsTermMixin + DL = Yamatanooroti::WindowsDefinition - def self.extract_terminal(path) - tar = File.join(ENV['SystemRoot'], "system32", "tar.exe") - extract_dir = File.join(tmpdir, "wt") - FileUtils.remove_entry(extract_dir) if File.exist?(extract_dir) - FileUtils.mkdir_p(extract_dir) - puts "extracting #{File.basename(path)}" - system tar, "xf", path, "-C", extract_dir - wt = Dir["**/wt.exe", base: extract_dir] - raise "not found wt.exe. aborted." if wt.size < 1 - raise "found wt.exe #{wt.size} times unexpectedly. aborted." if wt.size > 1 - wt = File.join(extract_dir, wt[0]) - wt_dir = File.dirname(wt) - portable_mark = File.join(wt_dir, ".portable") - open(portable_mark, "w") { |f| f.puts } unless File.exist?(portable_mark) - settings = File.join(wt_dir, "settings", "settings.json") - FileUtils.mkdir_p(File.dirname(settings)) - open(settings, "wb") do |settings| - settings.write <<~'JSON' - { - "defaultProfile": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", - "disableAnimations": true, - "minimizeToNotificationArea": true, - "profiles": - { - "defaults": - { - "bellStyle": "none", - "closeOnExit": "always", - "font": - { - "size": 9 - }, - "padding": "0", - "scrollbarState": "always" - }, - "list": - [ - { - "commandline": "%SystemRoot%\\System32\\cmd.exe", - "guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", - "name": "cmd.exe" - } - ] - }, - "showTabsInTitlebar": false, - "warning.confirmCloseAllTabs": false, - "warning.largePaste": false, - "warning.multiLinePaste": false - } - JSON - end - puts "use #{wt} for windows console" - wt - end + CONSOLE_KEEPING_COMMANDNAME = "ruby.exe" + CONSOLE_KEEPING_COMMANDLINE = %q[ruby.exe --disable=gems -e "Signal.trap(:INT, nil) and sleep or sleep # SIG"] - def self.prepare_terminal_canary - dir = tmpdir - header = `curl --head -sS -o #{tmpdir}/header -L -w "%{url_effective}\n%header{ETag}\n%header{Content-Length}\n%header{Last-Modified}" https://aka.ms/terminal-canary-zip-x64` - url, etag, length, timestamp = *header.lines.map(&:chomp) - name = File.basename(URI.parse(url).path) - path = File.join(dir, "wt_dists", "canary", etag.delete('"'), name) - if File.exist?(path) - if File.size(path) == length.to_i - puts "use existing #{path}" - return path - else - FileUtils.remove_entry(path) - end - else - if Dir.empty?(dir) - puts "removing old canary zip" - Dir.entries.each { |olddir| FileUtils.remove_entry(olddir) } - end - end - FileUtils.mkdir_p(File.dirname(path)) - system "curl #{$stdin.isatty ? "" : "-sS "}-L -o #{path} https://aka.ms/terminal-canary-zip-x64" - path + module_function def keeper_commandname + CONSOLE_KEEPING_COMMANDNAME end - def self.prepare_terminal_portable - releases = Yamatanooroti::Options::WindowsTerminal::RELEASES - url = releases[Yamatanooroti.options.windows.to_sym][:url] - sha256 = releases[Yamatanooroti.options.windows.to_sym][:sha256] - dir = tmpdir - name = File.basename(URI.parse(url).path) - path = File.join(dir, "wt_dists", Yamatanooroti.options.windows, name) - if File.exist?(path) - if Digest::SHA256.new.file(path).hexdigest.upcase == sha256 - puts "use existing #{path}" - return path - else - FileUtils.remove_entry(path) - end - end - FileUtils.mkdir_p(File.dirname(path)) - system "curl #{$stdin.isatty ? "" : "-sS "}-L -o #{path} #{url}" - raise "not match windows terminal distribution zip sha256" unless Digest::SHA256.new.file(path).hexdigest.upcase == sha256 - path + module_function def keeper_commandline(signature) + CONSOLE_KEEPING_COMMANDLINE.sub("SIG", signature) end -end - -module Yamatanooroti::WindowsTermMixin - DL = Yamatanooroti::WindowsDefinition - - CONSOLE_KEEPING_COMMAND = %q[ruby.exe --disable=gems -e "Signal.trap(:INT, nil); sleep; #NAME"] - CONSOLE_MARKING_COMMAND = %q[findstr.exe yamatanooroti] - private def show_console_param + module_function def show_console_param map = DL::SHOWWINDOW_MAP[Yamatanooroti.options.windows] || DL::SHOWWINDOW_MAP[:terminal] map.fetch(Yamatanooroti.options.show_console ? :show : :hide) end @@ -228,9 +35,10 @@ module Yamatanooroti::WindowsTermMixin conin = conout = nil check_interrupt + return nil if !console_process_id DL.free_console # this can be fail while new process is starting - r = DL.attach_console(@console_process_id, maybe_fail: !exception) + r = DL.attach_console(console_process_id, maybe_fail: !exception) return nil unless r if open @@ -359,7 +167,7 @@ def codepage_success? @codepage_success_p end - def do_write(str) + private def do_write(str) check_interrupt records, count = DL.build_key_input_record(str) attach_terminal do |conin, conout| @@ -418,12 +226,12 @@ def result def close close_request = @target && !@target.closed? - retrieve_request = !DL.interrupted? && @console_process_id + retrieve_request = !DL.interrupted? && console_process_id if close_request && retrieve_request && !@result if @wrote_and_not_yet_waited # wait a long. avoid write();close() sequence sleep @timeout - puts "\r#{@name}: close() just after write() will ultimately slow test down. Put assert_screen() before close()." + puts "\r#{@name}: close() just after write() will ultimately slow test down. use close() after assert_screen()." end end @@ -485,6 +293,7 @@ def check_interrupt end def raise_interrupt + @target.close close! DL.at_exit raise Interrupt diff --git a/lib/yamatanooroti/windows/wmi.rb b/lib/yamatanooroti/windows/wmi.rb new file mode 100644 index 0000000..3126e05 --- /dev/null +++ b/lib/yamatanooroti/windows/wmi.rb @@ -0,0 +1,77 @@ +require "win32ole" + +class Yamatanooroti + module WMI + WIN32OLE.codepage = WIN32OLE::CP_UTF8 + @locator = WIN32OLE.new('WbemScripting.SWbemLocator') + @server = @locator.ConnectServer('.', 'root\cimv2') + @server.Security_.ImpersonationLevel = 3 + + def self.server + @server + end + + module Win32_Process + class << self + def eq(k, v) + "#{k} = '#{v}'" + end + + def like(k, v) + "#{k} LIKE \"#{v}\"" + end + + def &(k, v) + "#{k} AND #{v}" + end + + def query(filters, *properties) + properties << "*" if properties.empty? + filters = Array(filters) + where = filters.empty? ? "" : " WHERE #{filters.join(" OR ")}" + query = "SELECT #{properties&.join(",") || "*"} FROM Win32_Process#{where}" + list = WMI.server.ExecQuery(query) + list.each.map do |process| + process.GetObjectText_.lines.reduce({}) do |hash, line| + if kv = line.match(/(\w*) = (.*);/) + key = kv[1] + value = kv[2].match(/\A".*"\z/) + if value == nil + value = kv[2]&.to_i + else + value = value[0].undump + end + hash[key] = value + end + hash + end + end || [] + end + + def query_name(name, *properties) + query(eq("Name", name), *properties) + end + + def query_name_and_commandline(name, commandline, *properties) + query(self.&(eq("Name", name), like("CommandLine", "%#{commandline}%")), *properties) + end + + def query_pid(pid, *properties) + query(eq("ProcessId"), pid, *properties) + end + + def query_ppid(ppid, *properties) + query(eq("ParentProcessId"), ppid, *properties) + end + end + end + end +end + +# ary = WMI::Win32_Process.query_ppid(Process.pid, "Name", "CommandLine", "ProcessId", "ParentProcessId", "CreationDate", "UserModeTime", "KernelModeTime") +# ary.each { |e| puts [e["Name"], e["ProcessId"]] } + +if $0 == __FILE__ + WMI = Yamatanooroti::WMI + binding.irb +end diff --git a/lib/yamatanooroti/windows/wt.rb b/lib/yamatanooroti/windows/wt.rb new file mode 100644 index 0000000..e33db33 --- /dev/null +++ b/lib/yamatanooroti/windows/wt.rb @@ -0,0 +1,310 @@ +require_relative 'wmi' + +class Yamatanooroti + + ### Yamatanooroti::WindowsTerminal + # + # represents windows terminal window + # + + class WindowsTerminal + Self = self + class << self + attr_reader :split_cache_h, :split_cache_v + end + + @split_cache_h = {} + @split_cache_v = {} + + attr_reader :name, :title, :testcase, :wt_command, :wait, :timeout, :active_tab, :base_tab + + def initialize(rows, cols, name, title, wait, timeout, testcase) + @name = name + @title = title + @wait = wait + @timeout = timeout + @testcase = testcase + @wt_command = "#{Yamatanooroti::WindowsConsoleSetup.wt_exe} " \ + "-w #{title} " \ + "--size #{cols},#{rows} " + @tabs = [] + @active_tab = nil + @base_tab = Tab.new_tab(self, title) + end + + def new_tab(height, width, title) + base_height, base_width = @base_tab.get_size + if height > base_height + raise "console height #{height} grater than maximum(#{base_height})" + end + if width > base_width + raise "console width #{width} grater than maximum(#{base_width})" + end + + hsplitter = Self.split_cache_h[base_height] || SplitSizeManager.new(base_height) + vsplitter = Self.split_cache_v[base_width] || SplitSizeManager.new(base_width) + + hsplit = hsplitter.query(height) + vsplit = vsplitter.query(width) + begin + if hsplit && vsplit + tab = Tab.new_tab_hv(self, title, hsplit[0].to_f / hsplit[1], vsplit[0].to_f / vsplit[1]) + if tab.get_size == [height, width] + return tab + else + tab.close_pane + tab.close_pane + end + else + tab = Tab.new_tab(self, title) + end + + if height != base_height + hsplit = hsplitter.search_div(height) do |div| + tab.split_pane(div, splitter: :h) + h = tab.get_size[0] + tab.close_pane if h != height + h + end + raise "console height deviding to #{height} failed" if !hsplit + end + + if width != base_width + vsplit = vsplitter.search_div(width) do |div| + tab.split_pane(div, splitter: :v) + w = tab.get_size[1] + tab.close_pane if w != width + w + end + raise "console widtht deviding to #{width} failed" if !vsplit + end + + return tab + ensure + @tabs << tab + @active_tab = tab + Self.split_cache_h[base_height] = hsplitter + Self.split_cache_v[base_width] = vsplitter + end + end + + def close_tab + if @active_tab + @active_tab.close + @active_tab = nil + @tabs.pop + end + end + + def detach_tab + if @active_tab + @active_tab = nil + @tabs.pop + end + end + + def close + @active_tab&.close + @base_tab&.close + @base_tab = @active_tab = nil + end + + ### Yamatanooroti::WindowsTerminal::Tab + # + # represents and manipulates windows terminal tab + # + + class Tab + include Yamatanooroti::WindowsTermMixin + M = WindowsTermMixin + attr_reader :wt, :name, :title + + def raise_interrupt + wt.testcase.raise_interrupt + end + + private_class_method :new + + def initialize(wt, title, *keys) + @wt = wt + @wait = wt.wait + @timeout = wt.timeout + @name = wt.name + @title = title + @closed = false + if keys[0].is_a?(Array) + @keys = keys + else + @keys = [keys] # [[image_name, search_signature], ...] + end + @pid = {} + end + + def self.new_tab(wt, title) + signature = "#{title}:main" + keeper_command = M.keeper_commandline(signature) + command = "#{wt.wt_command} " \ + "nt --title #{title} " \ + "#{keeper_command}" + + DL.create_console(command, M.show_console_param()) + self.new(wt, title, [M.keeper_commandname, signature]) + end + + def self.new_tab_hv(wt, title, hsplit, vsplit) + signature = "#{title}:main" + signature_h = "#{title}:h" + signature_v = "#{title}:v" + keeper_command = M.keeper_commandline(signature) + keeper_command_h = M.keeper_commandline(signature_h) + keeper_command_v = M.keeper_commandline(signature_v) + command = "#{wt.wt_command} " \ + "nt --title #{title} " \ + "#{keeper_command}" \ + "; " \ + "sp -H "\ + "--title #{title} " \ + "-s #{hsplit} " \ + "#{keeper_command_h}" \ + "; " \ + "move-focus first; " \ + "sp -V "\ + "--title #{title} " \ + "-s #{vsplit} " \ + "#{keeper_command_v}" + + DL.create_console(command, M.show_console_param()) + self.new(wt, title, + [M.keeper_commandname, signature], + [M.keeper_commandname, signature_h], + [M.keeper_commandname, signature_v] + ) + end + + def split_pane(div = 0.5, splitter: :v) + signature = "#{title}:#{splitter}" + keeper_command = M.keeper_commandline(signature) + command = "#{@wt.wt_command} " \ + "move-focus first; " \ + "sp #{splitter == :v ? "-V" : "-H"} "\ + "--title #{title} " \ + "-s #{div} " \ + "#{keeper_command}" + + orig_size = get_size() + DL.create_console(command, M.show_console_param()) + @keys.push [M.keeper_commandname, signature] + with_timeout("split console timed out.") { orig_size != get_size() } + end + + def close_pane + orig_size = get_size() + begin + Process.kill(:KILL, pid(@keys.last)) + rescue Errno::ESRCH # No such process + end + with_timeout("close pane timed out.") { orig_size != get_size() } + @pid[@keys.pop] = nil + end + + def close + begin + Process.kill(:KILL, *all_pid) if !@closed + rescue Errno::ESRCH # No such process + end + @closed = true + end + + def pid(key = @keys[0]) + pid = @pid[key] + if !pid + pid = keeper_pid = with_timeout("Windows Terminal keeper process detection failed.", @timeout) do + @pid[key] = search_pid(*key) + end + end + pid + end + + def search_pid(image_name, signature) + process = WMI::Win32_Process.query_name_and_commandline(image_name, signature, "ProcessId")[0] + process&.fetch("ProcessId") + end + + def all_pid + keys = @keys.map { |key| @pid[key] ? nil : key }.compact + return @pid.values if keys.empty? + filter = keys.map do |sig| + WMI::Win32_Process.&(WMI::Win32_Process.eq("Name", sig[0]), WMI::Win32_Process.like("Commandline", "%#{sig[1]}%")) + end + result = WMI::Win32_Process.query(filter, "ProcessId").map { |h| h["ProcessId"] } + @pid.values.concat(result).compact + end + + def console_process_id + pid = pid() + if !@console_ready + with_timeout("console startup check failed.") do + DL.free_console + DL.attach_console(pid, maybe_fail: true) + ensure + DL.free_console + DL.attach_console + end + @console_ready = true + end + pid + end + + def get_size + attach_terminal do |conin, conout| + csbi = DL.get_console_screen_buffer_info(conout) + [csbi.Bottom + 1, csbi.Right + 1] + end + end + end + + ### Yamatanooroti::WindowsTerminal::SplitSizeManager + # + # cache manager of windows terminal pane splitter divisor parameter + # + + class SplitSizeManager + def initialize(total) + @total = total + @div_to_x = {} + @x_to_div = {} + end + + def query(x) + @x_to_div[x] + end + + def search_div(x, &block) + denominator = 200 + div, denominator = @x_to_div[x] if @x_to_div[x] + div ||= 2 * ((@total - x) * (denominator * 97) / @total + denominator * 2) / 200 + loop do + result = block.call(div.to_f / denominator) + @div_to_x[div.to_f / denominator] = result + @x_to_div[x] = [div, denominator] + return result if result == x + if result < x + div -= 1 + return nil if div <= 0 + if @div_to_x[div.to_f / denominator] && @div_to_x[div.to_f / denominator] != x + div = div * 2 + 1 + denominator *= 2 + end + else + div += 1 + return nil if div >= denominator + if @div_to_x[div.to_f / denominator] && @div_to_x[div.to_f / denominator] != x + div = div * 2 - 1 + denominator *= 2 + end + end + end + end + end + end +end From 06875d427d6f9e1de2dab477a7b00fa4196fb53d Mon Sep 17 00:00:00 2001 From: YO4 Date: Fri, 25 Oct 2024 07:51:13 +0900 Subject: [PATCH 65/76] avoid error in ci --- lib/yamatanooroti/windows/terminal.rb | 12 ++++++------ lib/yamatanooroti/windows/wt.rb | 5 +++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb index b7151cb..6bb5c1c 100644 --- a/lib/yamatanooroti/windows/terminal.rb +++ b/lib/yamatanooroti/windows/terminal.rb @@ -35,7 +35,7 @@ def self.setup_console(height, width, wait, timeout, name) end def console_process_id - @wt.active_tab.pid + @wt.active_tab&.pid end def get_size @@ -84,11 +84,11 @@ def self.diagnose_size_capability Self.max_size = [[max_size[0], 60].min, [max_size[1], 200].min] puts max_size.then { |r, c| "Windows Terminal maximum size: rows: #{r}, columns: #{c}" } wt.close! - wt = self.new(2, 2, 0.01, 5.0) - min_size = wt.get_size - Self.min_size = min_size - puts min_size.then { |r, c| "Windows Terminal smallest size: rows: #{r}, columns: #{c}" } - wt.close! + #wt = self.new(2, 2, 0.01, 5.0) + #min_size = wt.get_size + #Self.min_size = min_size + #puts min_size.then { |r, c| "Windows Terminal smallest size: rows: #{r}, columns: #{c}" } + #wt.close! puts Self.max_size.then {|r, c| "Use test window size: rows: #{r}, columns: #{c}" } end diff --git a/lib/yamatanooroti/windows/wt.rb b/lib/yamatanooroti/windows/wt.rb index e33db33..00ae33c 100644 --- a/lib/yamatanooroti/windows/wt.rb +++ b/lib/yamatanooroti/windows/wt.rb @@ -138,6 +138,7 @@ def initialize(wt, title, *keys) @keys = [keys] # [[image_name, search_signature], ...] end @pid = {} + @console_ready = false end def self.new_tab(wt, title) @@ -148,7 +149,7 @@ def self.new_tab(wt, title) "#{keeper_command}" DL.create_console(command, M.show_console_param()) - self.new(wt, title, [M.keeper_commandname, signature]) + new(wt, title, [M.keeper_commandname, signature]) end def self.new_tab_hv(wt, title, hsplit, vsplit) @@ -174,7 +175,7 @@ def self.new_tab_hv(wt, title, hsplit, vsplit) "#{keeper_command_v}" DL.create_console(command, M.show_console_param()) - self.new(wt, title, + new(wt, title, [M.keeper_commandname, signature], [M.keeper_commandname, signature_h], [M.keeper_commandname, signature_v] From 724ff6e49981b3355a915c746be2adfd46f9ebb9 Mon Sep 17 00:00:00 2001 From: YO4 Date: Thu, 31 Oct 2024 21:28:21 +0900 Subject: [PATCH 66/76] change windows terminal settings --- lib/yamatanooroti/windows/windows-setup.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/yamatanooroti/windows/windows-setup.rb b/lib/yamatanooroti/windows/windows-setup.rb index d4febcf..e542676 100644 --- a/lib/yamatanooroti/windows/windows-setup.rb +++ b/lib/yamatanooroti/windows/windows-setup.rb @@ -120,7 +120,8 @@ def self.extract_terminal(path) { "defaultProfile": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", "disableAnimations": true, - "minimizeToNotificationArea": true, + "experimental.detectURLs": false, + "minimizeToNotificationArea": false, "profiles": { "defaults": @@ -144,6 +145,7 @@ def self.extract_terminal(path) ] }, "showTabsInTitlebar": false, + "tabWidthMode": "compact", "warning.confirmCloseAllTabs": false, "warning.largePaste": false, "warning.multiLinePaste": false From cc8beb0dc8d68726a8a11ed2fcff84759e9778cd Mon Sep 17 00:00:00 2001 From: YO4 Date: Sat, 2 Nov 2024 17:09:47 +0900 Subject: [PATCH 67/76] change child process detection strategy More lightweight pid searching process. More lightweight console keeping process. Bonus: child process terminate automatically when yamatanooroti abnormal stopped. before: search with commandline string via wmi Win32_Process after: Yamatanootori creates a named pipe and child process connects that. Then use GetNamedPipeClientProcessId() --- lib/yamatanooroti/windows/conhost.rb | 27 ++--- lib/yamatanooroti/windows/terminal.rb | 10 +- .../windows/windows-definition.rb | 46 +++++++- lib/yamatanooroti/windows/windows.rb | 46 ++++++-- lib/yamatanooroti/windows/wt.rb | 105 ++++++------------ 5 files changed, 137 insertions(+), 97 deletions(-) diff --git a/lib/yamatanooroti/windows/conhost.rb b/lib/yamatanooroti/windows/conhost.rb index 844f341..345637f 100644 --- a/lib/yamatanooroti/windows/conhost.rb +++ b/lib/yamatanooroti/windows/conhost.rb @@ -15,13 +15,14 @@ def initialize(height, width, wait, timeout, name) @codepage_success_p = nil @wrote_and_not_yet_waited = false - @console_process_id = DL.create_console(keeper_commandline(name), show_console_param()) - - sleep 0.1 if Yamatanooroti.options.windows == :"legacy-conhost" # ad-hoc + countup_testcase_title(name) + pipename = get_pipename(name) + @pipe_handle = DL.create_named_pipe(pipename) + DL.create_console(keeper_commandline(pipename), show_console_param()) # wait for console startup complete with_timeout("Console process startup timed out.") do - attach_terminal(open: false, exception: false) { true } + @console_process_id = DL.get_named_pipe_client_processid(@pipe_handle, maybe_fail: true) end attach_terminal do |conin, conout| @@ -30,16 +31,16 @@ def initialize(height, width, wait, timeout, name) end def close_console(need_to_close = true) - if (need_to_close) - if @target && !@target.closed? - @target.close - end - begin - Process.kill("KILL", @console_process_id) if @console_process_id - rescue Errno::ESRCH # No such process - ensure - @console_process_id = nil + if @console_process_id + if (need_to_close) + if @target && !@target.closed? + @target.close + end + DL.close_handle(@pipe_handle) + else + castling(@pipe_handle) end + @console_process_id = @pipe_handle = nil end end diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb index 6bb5c1c..e2807f7 100644 --- a/lib/yamatanooroti/windows/terminal.rb +++ b/lib/yamatanooroti/windows/terminal.rb @@ -30,12 +30,13 @@ def self.setup_console(height, width, wait, timeout, name) Self.wt = self.new(*max_size, wait, timeout, Self.window_title) end + countup_testcase_title(name) Self.wt.new_tab(height, width, name) Self.wt end def console_process_id - @wt.active_tab&.pid + @wt.active_tab&.console_process_id end def get_size @@ -58,8 +59,7 @@ def new_tab(height, width, name) @wrote_and_not_yet_waited = false @name = name - Self.countup_testcase_title(name) - @wt.new_tab(height, width, Self.testcase_title(name)) + @wt.new_tab(height, width, testcase_title(name)) end def close_console(need_to_close = true) @@ -67,9 +67,9 @@ def close_console(need_to_close = true) if @target && !@target.closed? @target.close end - @wt.close_tab + @wt&.close_tab else - @wt.detach_tab + @wt&.detach_tab end end diff --git a/lib/yamatanooroti/windows/windows-definition.rb b/lib/yamatanooroti/windows/windows-definition.rb index d3ea6df..6a9dba0 100644 --- a/lib/yamatanooroti/windows/windows-definition.rb +++ b/lib/yamatanooroti/windows/windows-definition.rb @@ -22,8 +22,7 @@ module Yamatanooroti::WindowsDefinition typealias 'LPSTR', 'void*' typealias 'LPCCH', 'void*' typealias 'LPBOOL', 'void*' - typealias 'LPWORD', 'void*' - typealias 'ULONG_PTR', 'ULONG*' + typealias 'PULONG', 'ULONG*' typealias 'LONG', 'int' typealias 'HLOCAL', 'HANDLE' @@ -193,6 +192,18 @@ module Yamatanooroti::WindowsDefinition OPEN_EXISTING = 3 INVALID_HANDLE_VALUE = 0xffffffff + # HANDLE CreateNamedPipeA(LPCSTR lpName, DWORD dwOpenMode, DWORD dwPipeMode, DWORD nMaxInstances, DWORD nOutBufferSize, DWORD nInBufferSize, DWORD nDefaultTimeOut, LPSECURITY_ATTRIBUTES lpSecurityAttributes); + extern 'HANDLE CreateNamedPipeA(LPCSTR, DWORD, DWORD, DWORD, DWORD, DWORD, DWORD, LPSECURITY_ATTRIBUTES);', :stdcall + # BOOL ConnectNamedPipe(HANDLE hNamedPipe, LPOVERLAPPED lpOverlapped); + extern 'BOOL ConnectNamedPipe(HANDLE, LPVOID);', :stdcall + # BOOL DisconnectNamedPipe(HANDLE hNamedPipe); + extern 'BOOL DisconnectNamedPipe(HANDLE);', :stdcall + # BOOL GetNamedPipeClientProcessId(HANDLE Pipe, PULONG ClientProcessId); + extern 'BOOL GetNamedPipeClientProcessId(HANDLE, PULONG);', :stdcall + PIPE_ACCESS_INBOUND = 0x00000001 + PIPE_ACCESS_OUTBOUND = 0x00000002 + PIPE_ACCESS_DUPLEX = 0x00000003 + # DWORD FormatMessageW(DWORD dwFlags, LPCVOID lpSource, DWORD dwMessageId, DWORD dwLanguageId, LPWSTR lpBuffer, DWORD nSize, va_list *Arguments); extern 'DWORD FormatMessageW(DWORD dwFlags, LPCVOID lpSource, DWORD dwMessageId, DWORD dwLanguageId, LPWSTR lpBuffer, DWORD nSize, va_list *Arguments);', :stdcall # HLOCAL LocalFree(HLOCAL hMem); @@ -217,7 +228,7 @@ module Yamatanooroti::WindowsDefinition FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, Fiddle::NULL, err, - 0x0, + 0x409, # en-US string, 0, Fiddle::NULL @@ -464,6 +475,35 @@ def generate_console_ctrl_event(event, pgrp) return successful_or_if_not_messageout(r, 'GenerateConsoleCtrlEvent') end + def create_named_pipe(name) + ph = CreateNamedPipeA(%Q[\\\\.\\\pipe\\#{name}], PIPE_ACCESS_OUTBOUND, 0, 1, 0, 0, 0, nil) + ph = [ph].pack("J").unpack1("L") + successful_or_if_not_messageout(0, name) if ph == INVALID_HANDLE_VALUE + ph + end + + def connect_named_pipe(pipe_handle) + r = ConnectNamedPipe(pipe_handle, nil) + return successful_or_if_not_messageout(r, 'ConnectNamedPipe') + end + + def disconnect_named_pipe(pipe_handle) + r = DisconnectNamedPipe(pipe_handle) + return successful_or_if_not_messageout(r, 'DisconnectNamedPipe') + end + + def get_named_pipe_client_processid(pipe_handle, maybe_fail: false) + pid = Fiddle::Pointer.malloc(Fiddle::SIZEOF_ULONG, FREE) + r = GetNamedPipeClientProcessId(pipe_handle, pid) + if !maybe_fail + successful_or_if_not_messageout(r, "GetNamedPipeClientProcessId") + return pid.to_str.unpack1("L") + else + return pid.to_str.unpack1("L") if r != 0 + return nil + end + end + # Ctrl+C trap support # FreeConsole(), AttachConsole() clears console control handlers. # Make matter worse, C runtime does not provide the function to restore that handlers. diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index 2844efe..4737cf0 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -3,15 +3,15 @@ module Yamatanooroti::WindowsTermMixin DL = Yamatanooroti::WindowsDefinition - CONSOLE_KEEPING_COMMANDNAME = "ruby.exe" - CONSOLE_KEEPING_COMMANDLINE = %q[ruby.exe --disable=gems -e "Signal.trap(:INT, nil) and sleep or sleep # SIG"] + CONSOLE_KEEPING_COMMANDLINE = %Q[cmd.exe /c start "" /B #{ENV["SystemRoot"]||"C:\\Windows"}\\System32\\more \\\\.\\pipe\\PIPENAME] + CONSOLE_LEAVING_COMMANDLINE = %q[ruby.exe --disable=gems -e "Signal.trap(:INT, nil) or true and sleep"] module_function def keeper_commandname CONSOLE_KEEPING_COMMANDNAME end - module_function def keeper_commandline(signature) - CONSOLE_KEEPING_COMMANDLINE.sub("SIG", signature) + module_function def keeper_commandline(pipename) + CONSOLE_KEEPING_COMMANDLINE.sub("PIPENAME", pipename) end module_function def show_console_param @@ -19,6 +19,41 @@ module Yamatanooroti::WindowsTermMixin map.fetch(Yamatanooroti.options.show_console ? :show : :hide) end + module_function def testcase_title(title) + count = (@@iter_count ||= {})[title] + count ||= countup_testcase_title(title) + "#{title}##{count}@#{Process.pid}".strip + end + + module_function def countup_testcase_title(title) + counter = (@@iter_count ||= {}) + count = counter[title] || 0 + counter[title] = count + 1 + end + + module_function def get_pipename(title, suffix = nil) + "yamatanooroti_#{self.testcase_title(title)}#{suffix ? ":#{suffix}" : ""}" + end + + private def get_pid_from_pipe(pipehandle) + with_timeout("Waiting pipe connection timed out.") do + DL.get_named_pipe_client_processid(pipehandle, maybe_fail: true) + end + end + + private def castling(pipehandle) + pid = get_pid_from_pipe(pipehandle) + begin + DL.free_console + DL.attach_console(pid) + spawn CONSOLE_LEAVING_COMMANDLINE + ensure + DL.free_console + DL.attach_console + end + DL.close_handle(pipehandle) + end + private def with_timeout(timeout_message, timeout = @timeout, &block) wait_until = Time.now + timeout loop do @@ -293,9 +328,6 @@ def check_interrupt end def raise_interrupt - @target.close close! - DL.at_exit - raise Interrupt end end diff --git a/lib/yamatanooroti/windows/wt.rb b/lib/yamatanooroti/windows/wt.rb index 00ae33c..8a08443 100644 --- a/lib/yamatanooroti/windows/wt.rb +++ b/lib/yamatanooroti/windows/wt.rb @@ -1,5 +1,3 @@ -require_relative 'wmi' - class Yamatanooroti ### Yamatanooroti::WindowsTerminal @@ -98,6 +96,7 @@ def close_tab def detach_tab if @active_tab + @active_tab.detach @active_tab = nil @tabs.pop end @@ -125,40 +124,39 @@ def raise_interrupt private_class_method :new - def initialize(wt, title, *keys) + def initialize(wt, title, *handles) @wt = wt @wait = wt.wait @timeout = wt.timeout @name = wt.name @title = title @closed = false - if keys[0].is_a?(Array) - @keys = keys - else - @keys = [keys] # [[image_name, search_signature], ...] - end - @pid = {} - @console_ready = false + @handles = handles + @pid = nil end def self.new_tab(wt, title) - signature = "#{title}:main" - keeper_command = M.keeper_commandline(signature) + pipename = M.get_pipename(title, "main") + pipe_handle = DL.create_named_pipe(pipename) + keeper_command = M.keeper_commandline(pipename) command = "#{wt.wt_command} " \ "nt --title #{title} " \ "#{keeper_command}" DL.create_console(command, M.show_console_param()) - new(wt, title, [M.keeper_commandname, signature]) + new(wt, title, pipe_handle) end def self.new_tab_hv(wt, title, hsplit, vsplit) - signature = "#{title}:main" - signature_h = "#{title}:h" - signature_v = "#{title}:v" - keeper_command = M.keeper_commandline(signature) - keeper_command_h = M.keeper_commandline(signature_h) - keeper_command_v = M.keeper_commandline(signature_v) + main_pipename = M.get_pipename(title, "main") + h_pipename = M.get_pipename(title, "h") + v_pipename = M.get_pipename(title, "v") + main_pipe_handle = DL.create_named_pipe(main_pipename) + h_pipe_handle = DL.create_named_pipe(h_pipename) + v_pipe_handle = DL.create_named_pipe(v_pipename) + keeper_command = M.keeper_commandline(main_pipename) + keeper_command_h = M.keeper_commandline(h_pipename) + keeper_command_v = M.keeper_commandline(v_pipename) command = "#{wt.wt_command} " \ "nt --title #{title} " \ "#{keeper_command}" \ @@ -175,16 +173,13 @@ def self.new_tab_hv(wt, title, hsplit, vsplit) "#{keeper_command_v}" DL.create_console(command, M.show_console_param()) - new(wt, title, - [M.keeper_commandname, signature], - [M.keeper_commandname, signature_h], - [M.keeper_commandname, signature_v] - ) + new(wt, title, main_pipe_handle, h_pipe_handle, v_pipe_handle) end def split_pane(div = 0.5, splitter: :v) - signature = "#{title}:#{splitter}" - keeper_command = M.keeper_commandline(signature) + pipename = get_pipename(title, ":#{splitter}") + pipe_handle = DL.create_named_pipe(pipename) + keeper_command = M.keeper_commandline(pipename) command = "#{@wt.wt_command} " \ "move-focus first; " \ "sp #{splitter == :v ? "-V" : "-H"} "\ @@ -193,67 +188,39 @@ def split_pane(div = 0.5, splitter: :v) "#{keeper_command}" orig_size = get_size() - DL.create_console(command, M.show_console_param()) - @keys.push [M.keeper_commandname, signature] + DL.create_console(command, show_console_param()) + @handles.push pipe_handle with_timeout("split console timed out.") { orig_size != get_size() } end def close_pane orig_size = get_size() - begin - Process.kill(:KILL, pid(@keys.last)) - rescue Errno::ESRCH # No such process - end + DL.close_handle(@handles.pop) with_timeout("close pane timed out.") { orig_size != get_size() } - @pid[@keys.pop] = nil end def close - begin - Process.kill(:KILL, *all_pid) if !@closed - rescue Errno::ESRCH # No such process + unless @closed + @handles.each do |pipe_handle| + DL.close_handle(pipe_handle) + end + @handles.clear end @closed = true end - def pid(key = @keys[0]) - pid = @pid[key] - if !pid - pid = keeper_pid = with_timeout("Windows Terminal keeper process detection failed.", @timeout) do - @pid[key] = search_pid(*key) + def detach + unless @closed + @handles.each do |pipe_handle| + castling(pipe_handle) end + @handles.clear end - pid - end - - def search_pid(image_name, signature) - process = WMI::Win32_Process.query_name_and_commandline(image_name, signature, "ProcessId")[0] - process&.fetch("ProcessId") - end - - def all_pid - keys = @keys.map { |key| @pid[key] ? nil : key }.compact - return @pid.values if keys.empty? - filter = keys.map do |sig| - WMI::Win32_Process.&(WMI::Win32_Process.eq("Name", sig[0]), WMI::Win32_Process.like("Commandline", "%#{sig[1]}%")) - end - result = WMI::Win32_Process.query(filter, "ProcessId").map { |h| h["ProcessId"] } - @pid.values.concat(result).compact + @closed = true end def console_process_id - pid = pid() - if !@console_ready - with_timeout("console startup check failed.") do - DL.free_console - DL.attach_console(pid, maybe_fail: true) - ensure - DL.free_console - DL.attach_console - end - @console_ready = true - end - pid + @pid ||= get_pid_from_pipe(@handles[0]) end def get_size From dc6c49aee42bab6695c460c14a4cade6daaac3a8 Mon Sep 17 00:00:00 2001 From: YO4 Date: Sat, 2 Nov 2024 18:42:47 +0900 Subject: [PATCH 68/76] more robust interrupt detection and terminate process --- lib/yamatanooroti/windows/conhost.rb | 1 + lib/yamatanooroti/windows/terminal.rb | 1 + lib/yamatanooroti/windows/windows-definition.rb | 16 ++++++++++------ lib/yamatanooroti/windows/windows.rb | 3 +++ 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/lib/yamatanooroti/windows/conhost.rb b/lib/yamatanooroti/windows/conhost.rb index 345637f..3265c55 100644 --- a/lib/yamatanooroti/windows/conhost.rb +++ b/lib/yamatanooroti/windows/conhost.rb @@ -8,6 +8,7 @@ def self.setup_console(height, width, wait, timeout, name) attr_reader :console_process_id def initialize(height, width, wait, timeout, name) + check_interrupt @wait = wait @timeout = timeout @name = name diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb index e2807f7..52c4474 100644 --- a/lib/yamatanooroti/windows/terminal.rb +++ b/lib/yamatanooroti/windows/terminal.rb @@ -44,6 +44,7 @@ def get_size end def initialize(height, width, wait, timeout, title = "yamatanooroti") + check_interrupt @wait = wait @timeout = timeout @result = nil diff --git a/lib/yamatanooroti/windows/windows-definition.rb b/lib/yamatanooroti/windows/windows-definition.rb index 6a9dba0..2002735 100644 --- a/lib/yamatanooroti/windows/windows-definition.rb +++ b/lib/yamatanooroti/windows/windows-definition.rb @@ -521,22 +521,26 @@ def self.restore_console_control_handler(&block) end end - @interrupt_monitor_pid = spawn("ruby --disable=gems -e sleep #InterruptMonitor", [:out, :err] => "NUL") - @interrupt_monitor = Process.detach(@interrupt_monitor_pid) + @pipe = IO.pipe + @interrupt_catcher_pid = spawn("choice /m #InterruptCatcher", {:in => @pipe[0], [:out, :err] => "NUL"}) + @interrupt_catcher = Process.detach(@interrupt_catcher_pid) ignore_console_control_handler @interrupted_p = nil + @pipe[0].close def self.interrupted? @interrupted_p || - unless @interrupt_monitor.alive? - @interrupted_p = (@interrupt_monitor.value.exitstatus == 3) + unless @interrupt_catcher.alive? + @interrupted_p = (@interrupt_catcher.value.exitstatus == 0) end end def self.at_exit - if @interrupt_monitor.alive? + @pipe[1].close unless @pipe[1].closed? + if @interrupt_catcher.alive? + sleep 0.01 begin - Process.kill("KILL", @interrupt_monitor_pid) + Process.kill("KILL", @interrupt_catcher_pid) rescue Errno::ESRCH # No such process end end diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index 4737cf0..4503188 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -273,6 +273,7 @@ def close @target.close if close_request @result = retrieve_screen if retrieve_request @result ||= "" + check_interrupt end def clear_need_wait_flag @@ -328,6 +329,8 @@ def check_interrupt end def raise_interrupt + @target&.close close! + raise Interrupt, "Interrupt: Interrupt catcher process died." end end From 08044130d58a58e67f9bd9d3114ac40a876754dc Mon Sep 17 00:00:00 2001 From: YO4 Date: Sat, 2 Nov 2024 21:29:55 +0900 Subject: [PATCH 69/76] move codepage setup timing Sometimes conhost v1(legacy mode conhost) changes console size when changing codepage. Probably the behavior is only at the transition between SBCS and DBCS. --- lib/yamatanooroti/windows.rb | 5 ++--- lib/yamatanooroti/windows/conhost.rb | 8 +++++--- lib/yamatanooroti/windows/terminal.rb | 9 +++++++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/lib/yamatanooroti/windows.rb b/lib/yamatanooroti/windows.rb index 2b0d152..196fd08 100644 --- a/lib/yamatanooroti/windows.rb +++ b/lib/yamatanooroti/windows.rb @@ -37,11 +37,10 @@ def start_terminal(height, width, command, wait: nil, timeout: nil, startup_mess end end if Yamatanooroti.options.conhost - @terminal = Yamatanooroti::ConhostTerm.setup_console(height, width, @wait, @timeout, local_name) + @terminal = Yamatanooroti::ConhostTerm.setup_console(height, width, codepage, @wait, @timeout, local_name) else - @terminal = Yamatanooroti::WindowsTerminalTerm.setup_console(height, width, @wait, @timeout, local_name) + @terminal = Yamatanooroti::WindowsTerminalTerm.setup_console(height, width, codepage, @wait, @timeout, local_name) end - @terminal.setup_cp(codepage) if codepage @terminal.launch(command) case startup_message diff --git a/lib/yamatanooroti/windows/conhost.rb b/lib/yamatanooroti/windows/conhost.rb index 3265c55..a9fbd2f 100644 --- a/lib/yamatanooroti/windows/conhost.rb +++ b/lib/yamatanooroti/windows/conhost.rb @@ -1,13 +1,13 @@ class Yamatanooroti::ConhostTerm include Yamatanooroti::WindowsTermMixin - def self.setup_console(height, width, wait, timeout, name) - new(height, width, wait, timeout, name) + def self.setup_console(height, width, codepage, wait, timeout, name) + new(height, width, codepage, wait, timeout, name) end attr_reader :console_process_id - def initialize(height, width, wait, timeout, name) + def initialize(height, width, codepage, wait, timeout, name) check_interrupt @wait = wait @timeout = timeout @@ -26,6 +26,8 @@ def initialize(height, width, wait, timeout, name) @console_process_id = DL.get_named_pipe_client_processid(@pipe_handle, maybe_fail: true) end + setup_cp(codepage) if codepage + attach_terminal do |conin, conout| DL.set_console_window_size(conout, height, width) end diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb index 52c4474..3978548 100644 --- a/lib/yamatanooroti/windows/terminal.rb +++ b/lib/yamatanooroti/windows/terminal.rb @@ -25,16 +25,21 @@ class << self attr_accessor :wt, :max_size, :min_size, :split_cache_h, :split_cache_v end - def self.setup_console(height, width, wait, timeout, name) + def self.setup_console(height, width, codepage, wait, timeout, name) if !Self.wt Self.wt = self.new(*max_size, wait, timeout, Self.window_title) end countup_testcase_title(name) - Self.wt.new_tab(height, width, name) + new_tab = Self.wt.new_tab(height, width, name) + new_tab.setup_cp(codepage) if codepage Self.wt end + def codepage_success? + @wt.active_tab.codepage_success? + end + def console_process_id @wt.active_tab&.console_process_id end From da95f38ec3ac1ac0d292bab974ed704744151632 Mon Sep 17 00:00:00 2001 From: YO4 Date: Sat, 2 Nov 2024 21:54:17 +0900 Subject: [PATCH 70/76] reuse existing wt if executable running can't delete running .exe --- lib/yamatanooroti/windows/windows-setup.rb | 123 ++++++++++++--------- 1 file changed, 73 insertions(+), 50 deletions(-) diff --git a/lib/yamatanooroti/windows/windows-setup.rb b/lib/yamatanooroti/windows/windows-setup.rb index e542676..98e4149 100644 --- a/lib/yamatanooroti/windows/windows-setup.rb +++ b/lib/yamatanooroti/windows/windows-setup.rb @@ -102,57 +102,79 @@ def self.tmpdir def self.extract_terminal(path) tar = File.join(ENV['SystemRoot'], "system32", "tar.exe") extract_dir = File.join(tmpdir, "wt") - FileUtils.remove_entry(extract_dir) if File.exist?(extract_dir) - FileUtils.mkdir_p(extract_dir) - puts "extracting #{File.basename(path)}" - system tar, "xf", path, "-C", extract_dir - wt = Dir["**/wt.exe", base: extract_dir] - raise "not found wt.exe. aborted." if wt.size < 1 - raise "found wt.exe #{wt.size} times unexpectedly. aborted." if wt.size > 1 - wt = File.join(extract_dir, wt[0]) - wt_dir = File.dirname(wt) - portable_mark = File.join(wt_dir, ".portable") - open(portable_mark, "w") { |f| f.puts } unless File.exist?(portable_mark) - settings = File.join(wt_dir, "settings", "settings.json") - FileUtils.mkdir_p(File.dirname(settings)) - open(settings, "wb") do |settings| - settings.write <<~'JSON' - { - "defaultProfile": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", - "disableAnimations": true, - "experimental.detectURLs": false, - "minimizeToNotificationArea": false, - "profiles": - { - "defaults": - { - "bellStyle": "none", - "closeOnExit": "always", - "font": - { - "size": 9 - }, - "padding": "0", - "scrollbarState": "always" - }, - "list": - [ - { - "commandline": "%SystemRoot%\\System32\\cmd.exe", - "guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", - "name": "cmd.exe" - } - ] - }, - "showTabsInTitlebar": false, - "tabWidthMode": "compact", - "warning.confirmCloseAllTabs": false, - "warning.largePaste": false, - "warning.multiLinePaste": false - } - JSON + running_wt_exist = false + if File.exist?(extract_dir) + wt = Dir["**/OpenConsole.exe", base: extract_dir] + running_wt_exist = wt.reduce(false) do |result, path| + result ||= begin + File.delete(File.join(extract_dir, path)) + false + rescue SystemCallError + true + end + end + FileUtils.remove_entry(extract_dir) if !running_wt_exist + end + if !running_wt_exist + FileUtils.mkdir_p(extract_dir) + puts "extracting #{File.basename(path)}" + system tar, "xf", path, "-C", extract_dir + wt = Dir["**/wt.exe", base: extract_dir] + raise "not found wt.exe. aborted." if wt.size < 1 + raise "found wt.exe #{wt.size} times unexpectedly. aborted." if wt.size > 1 + wt = File.join(extract_dir, wt[0]) + wt_dir = File.dirname(wt) + portable_mark = File.join(wt_dir, ".portable") + open(portable_mark, "w") { |f| f.puts } unless File.exist?(portable_mark) + settings = File.join(wt_dir, "settings", "settings.json") + FileUtils.mkdir_p(File.dirname(settings)) + open(settings, "wb") do |settings| + settings.write <<~'JSON' + { + "defaultProfile": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", + "disableAnimations": true, + "experimental.detectURLs": false, + "minimizeToNotificationArea": false, + "profiles": + { + "defaults": + { + "bellStyle": "none", + "closeOnExit": "always", + "font": + { + "size": 9 + }, + "padding": "0", + "scrollbarState": "always" + }, + "list": + [ + { + "commandline": "%SystemRoot%\\System32\\cmd.exe", + "guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", + "name": "cmd.exe" + } + ] + }, + "showTabsInTitlebar": false, + "tabWidthMode": "compact", + "warning.confirmCloseAllTabs": false, + "warning.largePaste": false, + "warning.multiLinePaste": false + } + JSON + end + puts "use #{wt} for windows console" + else + puts "running Windows Terminal found." + wt = Dir["**/wt.exe", base: extract_dir] + raise "not found wt.exe. aborted." if wt.size < 1 + raise "found wt.exe #{wt.size} times unexpectedly. aborted." if wt.size > 1 + wt = File.join(extract_dir, wt[0]) + wt_dir = File.dirname(wt) + puts "use existing #{wt} for windows console" end - puts "use #{wt} for windows console" wt end @@ -160,6 +182,7 @@ def self.prepare_terminal_canary dir = tmpdir header = `curl --head -sS -o #{tmpdir}/header -L -w "%{url_effective}\n%header{ETag}\n%header{Content-Length}\n%header{Last-Modified}" https://aka.ms/terminal-canary-zip-x64` url, etag, length, timestamp = *header.lines.map(&:chomp) + puts "Windows Terminal canary #{timestamp}" name = File.basename(URI.parse(url).path) path = File.join(dir, "wt_dists", "canary", etag.delete('"'), name) if File.exist?(path) From 951c89b2d36f095c184c14ee45daf8f65bc8e2b6 Mon Sep 17 00:00:00 2001 From: YO4 Date: Sat, 2 Nov 2024 22:37:55 +0900 Subject: [PATCH 71/76] misc. compatibility fix --- lib/yamatanooroti/windows/windows-definition.rb | 1 + lib/yamatanooroti/windows/windows.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/yamatanooroti/windows/windows-definition.rb b/lib/yamatanooroti/windows/windows-definition.rb index 2002735..7a448f9 100644 --- a/lib/yamatanooroti/windows/windows-definition.rb +++ b/lib/yamatanooroti/windows/windows-definition.rb @@ -28,6 +28,7 @@ module Yamatanooroti::WindowsDefinition Fiddle::SIZEOF_DWORD = Fiddle::SIZEOF_LONG Fiddle::SIZEOF_WORD = Fiddle::SIZEOF_SHORT + Fiddle::SIZEOF_ULONG = Fiddle::SIZEOF_LONG if !Fiddle.const_defined?(:SIZEOF_ULONG) COORD = struct [ 'SHORT X', diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index 4503188..cc905bb 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -32,7 +32,7 @@ module Yamatanooroti::WindowsTermMixin end module_function def get_pipename(title, suffix = nil) - "yamatanooroti_#{self.testcase_title(title)}#{suffix ? ":#{suffix}" : ""}" + "yamatanooroti_#{testcase_title(title)}#{suffix ? ":#{suffix}" : ""}" end private def get_pid_from_pipe(pipehandle) From acef53be209b3696772faca7dd9d51e3e465e809 Mon Sep 17 00:00:00 2001 From: YO4 Date: Mon, 4 Nov 2024 12:12:22 +0900 Subject: [PATCH 72/76] kill target process properly when yamatanooroti interrupted. --- lib/yamatanooroti/windows/conhost.rb | 2 +- lib/yamatanooroti/windows/terminal.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/yamatanooroti/windows/conhost.rb b/lib/yamatanooroti/windows/conhost.rb index a9fbd2f..86da194 100644 --- a/lib/yamatanooroti/windows/conhost.rb +++ b/lib/yamatanooroti/windows/conhost.rb @@ -35,7 +35,7 @@ def initialize(height, width, codepage, wait, timeout, name) def close_console(need_to_close = true) if @console_process_id - if (need_to_close) + if need_to_close || DL.interrupted? if @target && !@target.closed? @target.close end diff --git a/lib/yamatanooroti/windows/terminal.rb b/lib/yamatanooroti/windows/terminal.rb index 3978548..3ec9073 100644 --- a/lib/yamatanooroti/windows/terminal.rb +++ b/lib/yamatanooroti/windows/terminal.rb @@ -69,7 +69,7 @@ def new_tab(height, width, name) end def close_console(need_to_close = true) - if need_to_close + if need_to_close || DL.interrupted? if @target && !@target.closed? @target.close end From 20eb1737bc3e69991102127338ee1adc6474b493 Mon Sep 17 00:00:00 2001 From: YO4 Date: Mon, 4 Nov 2024 12:15:56 +0900 Subject: [PATCH 73/76] avoid start /b for conhost target It seems to ```start /b``` enables ENABLE_VIRTUAL_TERMINAL_PROCESSING SetConsoleMode() flag. This affects to console behavior. --- lib/yamatanooroti/windows/windows.rb | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/yamatanooroti/windows/windows.rb b/lib/yamatanooroti/windows/windows.rb index cc905bb..2913b9e 100644 --- a/lib/yamatanooroti/windows/windows.rb +++ b/lib/yamatanooroti/windows/windows.rb @@ -3,15 +3,16 @@ module Yamatanooroti::WindowsTermMixin DL = Yamatanooroti::WindowsDefinition - CONSOLE_KEEPING_COMMANDLINE = %Q[cmd.exe /c start "" /B #{ENV["SystemRoot"]||"C:\\Windows"}\\System32\\more \\\\.\\pipe\\PIPENAME] + CONSOLE_KEEPING_COMMANDLINE_CONHOST = %Q[#{ENV["SystemRoot"]||"C:\\Windows"}\\System32\\more.com \\\\.\\pipe\\PIPENAME] + CONSOLE_KEEPING_COMMANDLINE_WT = %Q[cmd.exe /c start "" /B #{ENV["SystemRoot"]||"C:\\Windows"}\\System32\\more \\\\.\\pipe\\PIPENAME] CONSOLE_LEAVING_COMMANDLINE = %q[ruby.exe --disable=gems -e "Signal.trap(:INT, nil) or true and sleep"] - module_function def keeper_commandname - CONSOLE_KEEPING_COMMANDNAME - end - module_function def keeper_commandline(pipename) - CONSOLE_KEEPING_COMMANDLINE.sub("PIPENAME", pipename) + if Yamatanooroti.options.conhost + CONSOLE_KEEPING_COMMANDLINE_CONHOST.sub("PIPENAME", pipename) + else + CONSOLE_KEEPING_COMMANDLINE_WT.sub("PIPENAME", pipename) + end end module_function def show_console_param From 23212e688f4abf2f1d2c9b9ce7ac1f70d7769714 Mon Sep 17 00:00:00 2001 From: YO4 Date: Mon, 4 Nov 2024 17:20:30 +0900 Subject: [PATCH 74/76] Ctrl+C resistant for conhost --- lib/yamatanooroti/windows/conhost.rb | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/yamatanooroti/windows/conhost.rb b/lib/yamatanooroti/windows/conhost.rb index 86da194..1e8fd89 100644 --- a/lib/yamatanooroti/windows/conhost.rb +++ b/lib/yamatanooroti/windows/conhost.rb @@ -17,15 +17,28 @@ def initialize(height, width, codepage, wait, timeout, name) @wrote_and_not_yet_waited = false countup_testcase_title(name) - pipename = get_pipename(name) - @pipe_handle = DL.create_named_pipe(pipename) + pipename = get_pipename(name, "open") + pipe_handle = DL.create_named_pipe(pipename) DL.create_console(keeper_commandline(pipename), show_console_param()) # wait for console startup complete - with_timeout("Console process startup timed out.") do + with_timeout("Console opening process startup timed out.") do + @console_process_id = DL.get_named_pipe_client_processid(pipe_handle, maybe_fail: true) + end + + pipename = get_pipename(name, "main") + @pipe_handle = DL.create_named_pipe(pipename) + attach_terminal(open: false) do + spawn(keeper_commandline(pipename)) + end + + # wait for console startup complete + with_timeout("Console keeping process startup timed out.") do @console_process_id = DL.get_named_pipe_client_processid(@pipe_handle, maybe_fail: true) end + DL.close_handle(pipe_handle) + sleep 0.1 setup_cp(codepage) if codepage attach_terminal do |conin, conout| From cbeb333d0ecd9be37fd264255bf2511f5d9c99aa Mon Sep 17 00:00:00 2001 From: YO4 Date: Mon, 4 Nov 2024 17:44:53 +0900 Subject: [PATCH 75/76] ruby 2.7 windows also needs newer reline for test_meta_key --- test/yamatanooroti/test_run_ruby.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/yamatanooroti/test_run_ruby.rb b/test/yamatanooroti/test_run_ruby.rb index da1b20d..f6710d4 100644 --- a/test/yamatanooroti/test_run_ruby.rb +++ b/test/yamatanooroti/test_run_ruby.rb @@ -33,7 +33,7 @@ def test_move_cursor_and_render def test_meta_key get_into_tmpdir - if !Yamatanooroti.win? || (RUBY_VERSION > '2.6.99' && RUBY_VERSION < '3.4.0') + if !Yamatanooroti.win? || (RUBY_VERSION >= '3' && RUBY_VERSION < '3.4.0') command = ['ruby', '-rreline', '-e', 'Reline.readline(%{>>>})'] else command = ['bundle', 'exec', 'ruby', '-e', 'require "reline"; Reline.readline(%{>>>})'] From 47bb74ab4a669167a838a857d8c55a705166e102 Mon Sep 17 00:00:00 2001 From: YO4 Date: Thu, 14 Nov 2024 21:36:01 +0900 Subject: [PATCH 76/76] property exit windows-definition REPL --- lib/yamatanooroti/windows/windows-definition.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/yamatanooroti/windows/windows-definition.rb b/lib/yamatanooroti/windows/windows-definition.rb index 7a448f9..8b56e59 100644 --- a/lib/yamatanooroti/windows/windows-definition.rb +++ b/lib/yamatanooroti/windows/windows-definition.rb @@ -557,6 +557,7 @@ def self.at_exit end if __FILE__ == $0 + at_exit { Yamatanooroti::WindowsDefinition.at_exit } DL = Yamatanooroti::WindowsDefinition def invoke_key(conin, str) DL.write_console_input(conin, *DL.build_key_input_record(str))