zenityはGUIを扱う最も簡単な方法の一つ
Linux上で、Rubyなどのスクリプト言語からGUIを扱う最も簡単な方法として、zenityというものがある。
以下はたったの一行でウインドウをひらいて、ユーザーの入力を行うサンプルである。
#coding: utf-8
s = open("|zenity --entry --entry-text あいうえお").read
puts s
Windowsのイケてない部分
機能が削られているがWindows版もあり、同じようなことができる。Windows版においても文字列の受け渡しがUTF-8なので、そこを考慮する必要があるが。
#coding: cp932
s = open("|zenity --entry --entry-text あいうえお".encode('UTF-8')).read
s.force_encoding('UTF-8')
puts s
ただ、Windows上でrubyw(コンソールなしのRuby)からzenityを実行すると背後霊のようなウインドウがこっそりと現れる。
タスクバーからみると何かウイルスが感染しているような不吉な様子である。
実はこれはWindowsの仕様で、パイプを使用するプログラムは必ずコンソールを持っていることが必須であるという制約に由来する。
問題に対処する
実はこの問題は無理矢理解決することは可能で、プロセスの起動のパラメーターをいじって、ウインドウなしのコンソールをプログラムに与えて、この幽霊を不可視にすることはできる。
こういう強引な方法は、後述する罠があるので注意してやる必要がある。
概要としては、Fiddleの機能を使ってOSのAPIとUniversal CRTをRubyから直接アクセスして、プロセスのパラメーターを変え、透明なコンソールを持つプロセスを起動するものである(そのため、Ruby本体はこちらのものを使わなければいけない)。
具体的には「透明なパイプ=アクリルのパイプ」である「acrylic_pipe」という関数を経由して使う。また、Windowsのインターフェースは後始末をしないとリークが発生する問題があるのでブロックパラメーター経由で使ってもらうことになっている。コマンドは好きなエンコーディングで渡して、自動的にUTF-16LEに変換して実行される。
この処理の本体は以下のソース中でrequireで参照されている「win32.rb」である。
#coding: cp932
require 'win32.rb'
Win32::acrylic_pipe("zenity -entry -entry-text あいうえお") {
|stdin, stdout, stderr|
s = stdout.read
s.force_encoding('UTF-8')
puts s
}
使用上の注意
このようにメッセージポンプを駆動せずに、強引な方法でGUIを駆使するプログラムは、デスクトップに居座ってはいけない。
Windowsにおけるウインドウとは表向きはただのGUI部品なのだが、実はOSとアプリケーションの重要な通信路であり、ここを通して長時間OSとの対話をしないプログラムはタスクマネージャーからアプリケーションとして正しく認識されず、UPSクライアントのようなシャットダウンに関わる機能や省電力機能の不具合を引き起こす可能性がある。
ちなみに、Windowsにおけるデーモンプロセスを作る正しい方法は、普通にコンソール上のアプリとして作っておいてタスクスケジューラーへの登録時に「デスクトップとの対話」のスイッチをOFFにすることである。
つまりウインドウを作ったうえでユーザーの視界から消えるのが正しい作法である。
ダイアログを出してパラメーターを取得後に、即座に処理をして終了するようなパターンでは気にしなくていい。
プログラムの本体
プログラム本体の「win32.rb」の中身を以下にに掲載する。
#coding: cp932
require 'fiddle/import'
require 'fiddle/types'
require 'nkf'
# メモ:
# ・メッセージボックス
# Win32::msgbox("はろー", "テスト", Win32::MB_OK)
# ・エラー文字列
# puts Win32::errstr
# puts Win32::errstr(130)
# ・透明パイプ
# Win32::acrylic_pipe("zenity --entry --entry-text ABC") { |in, out, err, pi|
# # stdin, stdout, stderror, PROCESS_INFORMATION
# s = out.read
# s.force_encoding('UTF-8')
# puts s
# }
# ・透明パイプ(その2)
# pipe = Win32::AcrylicPipe.new("zenity --entry --entry-text ABC")
# s = pipe.opipe.read
# s.force_encoding('UTF-8')
# puts s
# pipe.finish
# # pipe.ipipe, pipe.opipe, pipe.epipe, pipe.pi
# # (STDIN, STDOUT, STDERROR, PROCESS_INFORMATION)
module Win32
extend Fiddle::Importer
# Win32関係のdlloadはまとめて指定する
dlload('USER32.DLL', 'KERNEL32.DLL', 'UCRTBASE.DLL')
# includeはdlloadの後方でなければいけない
include Fiddle::BasicTypes
include Fiddle::Win32Types
typealias 'LPWSTR', 'wchar_t *'
typealias 'LPCWSTR', 'const wchar_t *'
typealias 'LPBYTE', 'const uinsgined char_t *'
typealias 'LPVOID', 'void *'
typealias 'LPCVOID', 'const void *'
typealias 'LPHANDLE', 'HANDLE *'
typealias 'LPDWORD', 'DWORD *'
SECURITY_ATTRIBUTES = struct([
"DWORD nLength",
"LPVOID lpSecurityDescriptor",
"BOOL bInheritHandle"
])
PROCESS_INFORMATION = struct([
"HANDLE hProcess",
"HANDLE hThread",
"DWORD dwProcessId",
"DWORD dwThreadId"
])
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",
])
STARTUPINFOA = struct([
"DWORD cb",
"LPSTR lpReserved",
"LPSTR lpDesktop",
"LPSTR 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 'LPSECURITY_ATTRIBUTES', 'SECURITY_ATTRIBUTES *'
typealias 'LPPROCESS_INFORMATION', 'PROCESS_INFORMATION *'
typealias 'LPSTARTUPINFOA', 'STARTUPINFOA *'
typealias 'LPSTARTUPINFOW', 'STARTUPINFOW *'
TRUE = 1
FALSE = 1
INVALID_HANDLE_VALUE = 0xffffffff
extern "BOOL CreateProcessA(" \
"LPCSTR lpApplicationName," \
"LPSTR lpCommandLine," \
"LPSECURITY_ATTRIBUTES lpProcessAttributes," \
"LPSECURITY_ATTRIBUTES lpThreadAttributes," \
"BOOL bInheritHandles," \
"DWORD dwCreationFlags," \
"LPVOID lpEnvironment," \
"LPCSTR lpCurrentDirectory," \
"LPSTARTUPINFOA 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)"
CREATE_NO_WINDOW = 0x08000000
STARTF_USESTDHANDLES = 0x100
extern "DWORD GetLastError()"
extern "void SetLastError(DWORD)"
extern "HANDLE GlobalAlloc(UINT, DWORD)"
extern "HANDLE GlobalFree(HANDLE)"
extern "LPVOID GlobalLock(HANDLE)"
extern "BOOL GlobalUnlock(HANDLE)"
extern "HANDLE LocalAlloc(UINT, DWORD)"
extern "HANDLE LocalFree(HANDLE)"
extern "LPVOID LocalLock(HANDLE)"
extern "BOOL LocalUnlock(HANDLE)"
extern "BOOL CreatePipe(PHANDLE, PHANDLE, LPSECURITY_ATTRIBUTES, DWORD)"
extern "BOOL SetHandleInformation(HANDLE, DWORD, DWORD)"
HANDLE_FLAG_INHERIT = 1
HANDLE_FLAG_PROTECT_FROM_CLOSE = 2
extern "BOOL SetStdHandle(DWORD, HANDLE)"
extern "BOOL GetStdHandle(DWORD)"
STD_INPUT_HANDLE = -10
STD_OUTPUT_HANDLE = -11
STD_ERROR_HANDLE = -12
extern "BOOL CloseHandle(HANDLE)"
extern "HANDLE CreateFileA(LPCSTR, DWORD, DWORD, " \
"LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE)"
extern "HANDLE CreateFileW(LPCWSTR, DWORD, DWORD, " \
"LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE)"
FILE_SHARE_DELETE = 4
FILE_SHARE_READ = 1
FILE_SHARE_WRITE = 2
CREATE_ALWAYS = 2
CREATE_NEW = 1
OPEN_ALWAYS = 4
OPEN_EXISTING = 3
TRUNCATE_EXISTING = 5
GENERIC_READ = 0x80000000
GENERIC_WRITE = 0x40000000
extern "BOOL ReadFile(HANDLE, LPVOID, DWORD, LPDWORD, LPVOID)"
extern "BOOL WriteFile(HANDLE, LPCVOID, DWORD, LPDWORD, LPVOID)"
extern "int lstrlenA(LPCSTR)"
extern "int lstrlenW(LPCWSTR)"
extern "DWORD FormatMessageA(" \
"DWORD, LPCVOID, DWORD, DWORD, LPCSTR, DWORD, LPVOID)"
extern "DWORD FormatMessageW(" \
"DWORD, LPCVOID, DWORD, DWORD, LPCWSTR, DWORD, LPVOID)"
FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000
FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200
FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100
extern "int MessageBoxA(HWND, LPCSTR, LPCSTR, DWORD)"
extern "int MessageBoxW(HWND, LPCWSTR, LPCWSTR, DWORD)"
MB_OK = 0
MB_OKCANCEL = 1
MB_ABORTRETRYIGNORE = 2
MB_YESNOCANCEL = 3
MB_YESNO = 4
MB_RETRYCANCEL = 5
MB_SETFOREGROUND = 0x10000
MB_SYSMODAL = 0x1000
MB_DEFBUTTON1 = 0
MB_DEFBUTTON2 = 0x100
MB_DEFBUTTON3 = 0x200
MB_DEFBUTTON4 = 0x300
MB_ICONASTERISK = 0x40
MB_ICONEXCLAMATION = 0x30
MB_ICONHAND = 0x10
MB_ICONINFORMATION = 0x40 # same:ICONASTERISK
MB_ICONQUESTION = 0x20
MB_ICONSTOP = 0x10 # same:ICONHAND
MB_ICONERROR = 0x10 # same:ICONHAND
# "%SYSTEMROOT%"\SYSTEM32\ucrtbase.dll
extern "int _open_osfhandle(HANDLE, int)"
O_RDONLY = 0x0000 # open for reading only
O_WRONLY = 0x0001 # open for writing only
O_RDWR = 0x0002 # open for reading and writing
O_APPEND = 0x0008 # writes done at eof
O_CREAT = 0x0100 # create and open file
O_TRUNC = 0x0200 # open and truncate
O_EXCL = 0x0400 # open only if file doesn't already exist
O_TEXT = 0x4000 # file mode is text (translated)
O_BINARY = 0x8000 # file mode is binary (untranslated)
O_WTEXT = 0x10000 # file mode is UTF16 (translated)
O_U16TEXT = 0x20000 # file mode is UTF16 no BOM (translated)
O_U8TEXT = 0x40000 # file mode is UTF8 no BOM (translated)
O_NOINHERIT = 0x0080 # child process doesn't inherit file
O_TEMPORARY = 0x0040 # temporary file bit
O_SHORT_LIVED = 0x1000 # temporary storage file, try not to flush
O_OBTAIN_DIR = 0x2000 # get information about a directory
O_SEQUENTIAL = 0x0020 # file access is primarily sequential
O_RANDOM = 0x0010 # file access is primarily random
# module_functions
def errstr(code = Win32::GetLastError())
# FORMAT_MESSAGE_ALLOCATE_BUFFER 0x00000100
# FORMAT_MESSAGE_FROM_SYSTEM 0x00001000
# FORMAT_MESSAGE_IGNORE_INSERTS 0x00000200
# MAKELANGID(1, 2) ::= ((((WORD)(2)) << 10) | (WORD)(1))
# MAKELANGID(JAPAN) = 0x0411
# MAKELANGID(ENGLISTH) = 0x0409
buff = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
e = Win32::FormatMessageW(0x1300, Fiddle::NULL,
code, 0x0411, buff, 0, Fiddle::NULL)
p = Win32::LocalLock(buff)
n = Win32::lstrlenW(buff.ptr)
s = buff.ptr.to_s(n * 2) # バイト数
s.force_encoding('UTF-16LE')
# encode('CP932')は0x0dでバグる(Ruby3.4)のでNKF
s = NKF.nkf('--ic=UTF-16LE --oc=CP932', s)
Win32::LocalUnlock(buff)
Win32::LocalFree(buff)
return s
end
module_function(:errstr)
def msgbox(text, title = Fiddle::NULL, style = Win32::MB_OK)
text = NKF.nkf('--oc=UTF-16LE', text) \
if (text != Fiddle::NULL) && (text.encoding != Encoding::UTF_16LE)
title = NKF.nkf('--oc=UTF-16LE', title) \
if (title != Fiddle::NULL) && (title.encoding != Encoding::UTF_16LE)
return Win32::MessageBoxW(Fiddle::NULL, text, title, style)
end
module_function(:msgbox)
class AcrylicPipe < Struct.new(:pi, :ipipe, :opipe, :epipe)
def initialize(cmd)
# Fiddleのmallocは事前にゼロフィルする
# RUBY_FREEを指定するとGC時にfreeが自動実行される。
# 64bit環境ならばptr.to_iは64bitになる
# パイプハンドルの所有権は->fd(CRT)->IO(ruby)と移る
@sa = Win32::SECURITY_ATTRIBUTES.malloc(Fiddle::RUBY_FREE)
@sa.nLength = Win32.sizeof(Win32::SECURITY_ATTRIBUTES)
@sa.bInheritHandle = Win32::TRUE;
@crecv = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP, Fiddle::RUBY_FREE)
@psend = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP, Fiddle::RUBY_FREE)
@precv = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP, Fiddle::RUBY_FREE)
@csend = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP, Fiddle::RUBY_FREE)
@erecv = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP, Fiddle::RUBY_FREE)
@esend = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP, Fiddle::RUBY_FREE)
Win32::CreatePipe(@crecv, @psend, @sa, 0);
Win32::CreatePipe(@precv, @csend, @sa, 0);
Win32::CreatePipe(@erecv, @esend, @sa, 0);
Win32::SetHandleInformation(@psend, Win32::HANDLE_FLAG_INHERIT, 0);
Win32::SetHandleInformation(@precv, Win32::HANDLE_FLAG_INHERIT, 0);
Win32::SetHandleInformation(@erecv, Win32::HANDLE_FLAG_INHERIT, 0);
self.pi = Win32::PROCESS_INFORMATION.malloc(Fiddle::RUBY_FREE)
@si = Win32::STARTUPINFOW.malloc(Fiddle::RUBY_FREE)
@si.cb = Win32.sizeof(Win32::STARTUPINFOW)
@si.dwFlags = Win32::STARTF_USESTDHANDLES;
@si.hStdInput = @crecv.ptr.to_i;
@si.hStdOutput = @csend.ptr.to_i;
@si.hStdError = @esend.ptr.to_i;
cmd = NKF.nkf('--oc=UTF-16LE', cmd) \
if cmd.encoding != Encoding::UTF_16LE
if Win32::CreateProcessW(
Fiddle::NULL, cmd, Fiddle::NULL, Fiddle::NULL,
Win32::TRUE,
Win32::CREATE_NO_WINDOW,
Fiddle::NULL, Fiddle::NULL, @si, self.pi) == 0 then
ec = Win32::GetLastError()
cmd = NKF.nkf('--ic=UTF-16LE --oc=CP932', cmd)
if ec == 0 then
raise "CrateProcess:cannot exec:#{cmd}"
else
raise "CreateProcess:#{Win32::errstr(ec)}:#{cmd}"
end
end
Win32::CloseHandle(@crecv.ptr.to_i);
Win32::CloseHandle(@csend.ptr.to_i);
Win32::CloseHandle(@esend.ptr.to_i);
self.ipipe = IO.for_fd(Win32::_open_osfhandle(
@psend.ptr.to_i, Win32::O_WRONLY|Win32::O_BINARY), 'w')
self.opipe = IO.for_fd(Win32::_open_osfhandle(
@precv.ptr.to_i, Win32::O_RDONLY|Win32::O_BINARY), 'r')
self.epipe = IO.for_fd(Win32::_open_osfhandle(
@erecv.ptr.to_i, Win32::O_RDONLY|Win32::O_BINARY), 'r')
end
def finish
Win32::CloseHandle(self.pi.hThread)
Win32::CloseHandle(self.pi.hProcess)
self.ipipe.close if !self.ipipe.closed?
self.opipe.close if !self.opipe.closed?
self.epipe.close if !self.epipe.closed?
end
end
def acrylic_pipe(cmd)
if block_given?
pipe = AcrylicPipe.new(cmd)
yield(pipe.ipipe, pipe.opipe, pipe.epipe, pipe.pi)
pipe.finish
pipe = nil
else
raise "usage: acrylic_pipe(commandline) " \
"{ |stdin, stdout, stderr, PROCESS_INFORMATION| ... }"
end
end
module_function(:acrylic_pipe)
end

