2022/08/17(水) [n年前の日記]
#1 [ruby][windows] Rubyスクリプトの多重起動を抑止する処理
Windows10 x64 21H2 + Ruby 1.8 / 1.9 / 2.6 で、Rubyスクリプトの多重起動を抑止できないか試してみた。
Windowsの場合、kernel32.dll の CreateMutex() を使えば、多重起動してるかどうかを判別できるらしい。
動作確認に使った Ruby のバージョンは以下。
とりあえず、Ruby 1.8 / 1.9 の場合は win32api を使っても警告が出なかったので、Ruby 1.8 / 1.9 なら win32api を使ってこの手の処理を書けばよさそうだなと…。
また、ffi を使えば Ruby 1.8 - 2.x でも同じ記述ができるのではと期待していたけれど、そんなことはなかった。最近の ffi じゃないと記述が通らない・正常動作しない場合もあるのだなと。そうなると、Ruby 1.8 / 1.9 で、あえて ffi を使う理由は無さそうな気がする。win32api を使ったほうが悩まなくて済む。
Windowsの場合、kernel32.dll の CreateMutex() を使えば、多重起動してるかどうかを判別できるらしい。
動作確認に使った Ruby のバージョンは以下。
- Windows10 x64 21H2
- Ruby 1.8.7 p330 i386-mswin32 (ActiveScriptRuby)
- Ruby 1.9.3 p551 i386-mingw32 (RubyInstaller)
- Ruby 2.6.10 p210 i386-mingw32 (RubyInstaller)
◎ win32apiを利用して実現。 :
Ruby 1.8 時代に標準で添付されていた win32api を使って試してみた。
環境は以下。
処理内容は、1秒毎に数値を 0 〜 9 まで出力する。また、起動時に、既にどこかで同じスクリプトが動いてたら即座に終了する。
_03_mutex_win32api.rb
環境は以下。
- Windows10 x64 21H2
- Ruby 1.8.7 p330 i386-mswin32
- Ruby 1.9.3 p551 i386-mingw32
処理内容は、1秒毎に数値を 0 〜 9 まで出力する。また、起動時に、既にどこかで同じスクリプトが動いてたら即座に終了する。
_03_mutex_win32api.rb
require "Win32API" create_mutex = Win32API.new("kernel32", "CreateMutex", "llp", "l") release_mutex = Win32API.new("kernel32", "ReleaseMutex", "l", "l") close_handle = Win32API.new("kernel32", "CloseHandle", "l", "l") get_last_error = Win32API.new("kernel32", "GetLastError", "", "l") MUTEX_NAME = "ruby_win32api_mutex_sample" ERROR_ALREADY_EXISTS = 183 mutex = create_mutex.call(0, 1, MUTEX_NAME) err = get_last_error.call() puts "Mutex : #{mutex}" puts "GetLastError : #{err}" if mutex == 0 or err == ERROR_ALREADY_EXISTS puts "already exists" exit else 10.times do |i| puts i sleep 1 end end if mutex != 0 release_mutex.call(mutex) close_handle.call(mutex) end
- CreateMutex() を呼んだ直後に、GetLastError() でエラーコードを調べて、ERROR_ALREADY_EXISTS = 183 が出ていたら既にスクリプトがどこかで起動してる。
- Mutex が確保できていたら、処理終了時に ReleaseMutex() と CloseHandle() を呼んでおく。
◎ ffiを利用して実現。 :
外部ライブラリ ffi を利用して同じことが実現できないか試してみた。ffi のインストール方法は昨日のメモを参照のこと。
_Ruby + ffi の動作確認をした
動作した環境は、Windows10 x64 21H2 + Ruby 2.6.10 p210 i386-mingw32 + ffi 1.15.5 x86-mingw32。
残念ながら、Ruby 1.8 / 1.9 (+ ffi 1.9.14)では動作しなかった。
_04_mutex_ffi.rb
動作する状態にするまで、かなりハマった…。
問題その1。ffi を使った場合、何故か CreateMutex() が呼べなかった。指定しても「そんな関数は無い」と言われてしまう。CreateMutexA() か CreateMutexW() にしたら呼び出すことができた。ちなみに、CreateMutexW() の呼び出し方は、win32-mutex のソースを参考にさせてもらった。
_win32-mutex/mutex.rb at main - chef/win32-mutex
問題その2.kernel32.dll の GetLastError() を呼んでも、それらしい値が全く返ってこなくて悩んでしまった…。何度試しても、どんな状況でも、必ず 0 になってしまう。
そのあたり、以下のページにヒントがあった。
_Add API.last_error #55 - Github Lab
Ruby から Win32 API を呼ぶと、GetLastError() の値がリセットされてしまう場合があるそうで。そんな時のために、ffi は FFI::LastError.winapi_error というものを用意してあって、そこに GetLastError() の本来の値が格納されているらしい。
_Method: FFI::LastError.winapi_error - Documentation for ffi/ffi (master)
そんなわけで、FFI::LastError.winapi_error を使ってエラーコードを取得したら期待通りの動作になった。
ただ、Ruby 2.6 にインストールした ffi 1.15.5 には、FFI::LastError.winapi_error が用意されていたのだけど。Ruby 1.8 / 1.9 にインストールした ffi 1.9.14 にはメソッドが用意されてなくてエラーになってしまう。
昔は winapi_error ではなく win_error という名前だった、という情報にも辿り着いたのだけど。win_error に書き換えても、やはりエラーが出る。
_Rename to winapi_error - ffi/ffi@4ba55c5
どうやらどこかのバージョンの時点で win_error が追加されたけど、それは Ruby 1.8 / 1.9 でも動作してくれる ffi 1.9.14 には実装されていないようだなと…。そんなわけで、Ruby 1.8 / 1.9 + ffi 1.9.14 では、CreateMutex() を使う方法は分からなかった。
_Ruby + ffi の動作確認をした
動作した環境は、Windows10 x64 21H2 + Ruby 2.6.10 p210 i386-mingw32 + ffi 1.15.5 x86-mingw32。
残念ながら、Ruby 1.8 / 1.9 (+ ffi 1.9.14)では動作しなかった。
_04_mutex_ffi.rb
require "rubygems" require "ffi" module WinKernel extend FFI::Library ffi_lib :kernel32 ffi_convention :stdcall attach_function :CreateMutexW, [:long, :bool, :pointer], :ulong attach_function :ReleaseMutex, [:ulong], :bool attach_function :CloseHandle, [:ulong], :long # attach_function :GetLastError, [], :ulong end mutex_name = "rubyffimutexsample" ERROR_ALREADY_EXISTS = 183 mutex = WinKernel.CreateMutexW(0, false, mutex_name.encode("UTF-16LE")) # Add API.last_error #55 - Github Lab # https://githublab.com/repository/issues/cosmo0920/win32-api/55 # # GetLastError() is reset when calling Win32 API from Ruby. # FFI::LastError.winapi_error" is a good choice. begin # err = WinKernel.GetLastError # err = FFI::LastError.error err = FFI::LastError.winapi_error rescue => e puts "Error : This version of ffi does not support FFI::LastError.winapi_error." if mutex != 0 WinKernel.ReleaseMutex(mutex) WininKernel.CloseHandle(mutex) end exit end puts "Mutex : #{mutex}" puts "GetLastError : #{err}" if mutex == 0 or err == ERROR_ALREADY_EXISTS puts "already exists" exit else 10.times do |i| puts i sleep 1 end end if mutex != 0 WinKernel.ReleaseMutex(mutex) WinKernel.CloseHandle(mutex) end
動作する状態にするまで、かなりハマった…。
問題その1。ffi を使った場合、何故か CreateMutex() が呼べなかった。指定しても「そんな関数は無い」と言われてしまう。CreateMutexA() か CreateMutexW() にしたら呼び出すことができた。ちなみに、CreateMutexW() の呼び出し方は、win32-mutex のソースを参考にさせてもらった。
_win32-mutex/mutex.rb at main - chef/win32-mutex
問題その2.kernel32.dll の GetLastError() を呼んでも、それらしい値が全く返ってこなくて悩んでしまった…。何度試しても、どんな状況でも、必ず 0 になってしまう。
そのあたり、以下のページにヒントがあった。
_Add API.last_error #55 - Github Lab
Ruby から Win32 API を呼ぶと、GetLastError() の値がリセットされてしまう場合があるそうで。そんな時のために、ffi は FFI::LastError.winapi_error というものを用意してあって、そこに GetLastError() の本来の値が格納されているらしい。
_Method: FFI::LastError.winapi_error - Documentation for ffi/ffi (master)
そんなわけで、FFI::LastError.winapi_error を使ってエラーコードを取得したら期待通りの動作になった。
ただ、Ruby 2.6 にインストールした ffi 1.15.5 には、FFI::LastError.winapi_error が用意されていたのだけど。Ruby 1.8 / 1.9 にインストールした ffi 1.9.14 にはメソッドが用意されてなくてエラーになってしまう。
昔は winapi_error ではなく win_error という名前だった、という情報にも辿り着いたのだけど。win_error に書き換えても、やはりエラーが出る。
_Rename to winapi_error - ffi/ffi@4ba55c5
どうやらどこかのバージョンの時点で win_error が追加されたけど、それは Ruby 1.8 / 1.9 でも動作してくれる ffi 1.9.14 には実装されていないようだなと…。そんなわけで、Ruby 1.8 / 1.9 + ffi 1.9.14 では、CreateMutex() を使う方法は分からなかった。
◎ DL と fiddle を利用して実現。 :
Ruby 1.9.3 限定の記述になるけれど、当時標準で添付されていた DL と fiddle を使って実現してみた。
動作確認環境は、Windows10 x64 21H2 + Ruby 1.9.3 p551 i386-mingw32。Ruby 1.8 や Ruby 2.6 では、「DL なんて無い」と言われて動かなかった。
_05_mutex_dl.rb
Ruby 1.9.3 の fiddle にも、ffi と同様に、Fiddle::win32_last_error というそれらしいメソッドがあるのだけれど。何故か DL+ fiddle の組み合わせの場合は、kernel32.dll の GetLastError() を呼ぶほうが正しい値が返るようだった。
また、この場合は、CreateMutexA() や CreateMutexW() ではなく、CreateMutex() を呼んでも通る模様。
動作確認環境は、Windows10 x64 21H2 + Ruby 1.9.3 p551 i386-mingw32。Ruby 1.8 や Ruby 2.6 では、「DL なんて無い」と言われて動かなかった。
_05_mutex_dl.rb
require "dl" require "fiddle" include Fiddle libc = DL.dlopen("kernel32.dll") create_mutex = Fiddle::Function.new(libc["CreateMutex"], [TYPE_LONG, TYPE_LONG, TYPE_VOIDP], TYPE_LONG) release_mutex = Fiddle::Function.new(libc["ReleaseMutex"], [TYPE_LONG], TYPE_LONG) close_handle = Fiddle::Function.new(libc["CloseHandle"], [TYPE_LONG], TYPE_LONG) get_last_error = Fiddle::Function.new(libc["GetLastError"], [], TYPE_LONG) mutex_name = "rubydlfiddlemutexsample" ERROR_ALREADY_EXISTS = 183 mutex = create_mutex.call(0, 1, mutex_name) # err = Fiddle::win32_last_error err = get_last_error.call() puts "Mutex : #{mutex}" puts "GetLastError : #{err}" if mutex == 0 or err == ERROR_ALREADY_EXISTS puts "already exists" exit else 10.times do |i| puts i sleep 1 end end if mutex != 0 release_mutex.call(mutex) close_handle.call(mutex) end
Ruby 1.9.3 の fiddle にも、ffi と同様に、Fiddle::win32_last_error というそれらしいメソッドがあるのだけれど。何故か DL+ fiddle の組み合わせの場合は、kernel32.dll の GetLastError() を呼ぶほうが正しい値が返るようだった。
また、この場合は、CreateMutexA() や CreateMutexW() ではなく、CreateMutex() を呼んでも通る模様。
◎ ハマりそうなポイントのまとめ。 :
- CreateMutex の名前で呼べる場合と、CreateMutexA か CreateMutexW を呼ばないといけない場合がある。
- GetLastError() を呼んで正常動作する場合と、ffi や fiddle で別途用意されたエラーコード取得メソッドを呼ばないと正常動作しない場合がある。
とりあえず、Ruby 1.8 / 1.9 の場合は win32api を使っても警告が出なかったので、Ruby 1.8 / 1.9 なら win32api を使ってこの手の処理を書けばよさそうだなと…。
また、ffi を使えば Ruby 1.8 - 2.x でも同じ記述ができるのではと期待していたけれど、そんなことはなかった。最近の ffi じゃないと記述が通らない・正常動作しない場合もあるのだなと。そうなると、Ruby 1.8 / 1.9 で、あえて ffi を使う理由は無さそうな気がする。win32api を使ったほうが悩まなくて済む。
◎ 参考ページ。 :
[ ツッコむ ]
以上です。