0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Windowsの怪談 zenityを使うと幽霊が現れるはなし

0
Posted at

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を実行すると背後霊のようなウインドウがこっそりと現れる。

DG.jpg

タスクバーからみると何かウイルスが感染しているような不吉な様子である。

TB.jpg

実はこれは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


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?