mieki256's diary



2022/08/17(水) [n年前の日記]

#1 [ruby][windows] Rubyスクリプトの多重起動を抑止する処理

Windows10 x64 21H2 + Ruby 1.8 / 1.9 / 2.6 で、Rubyスクリプトの多重起動を抑止できないか試してみた。

Windowsの場合、kernel32.dll の CreateMutex() を使えば、多重起動してるかどうかを判別できるらしい。

動作確認に使った Ruby のバージョンは以下。

win32apiを利用して実現。 :

Ruby 1.8 時代に標準で添付されていた win32api を使って試してみた。

環境は以下。
  • Windows10 x64 21H2
  • Ruby 1.8.7 p330 i386-mswin32
  • Ruby 1.9.3 p551 i386-mingw32
Ruby 1.8 / 1.9 の両方で動作した。

処理内容は、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
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
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 を使ったほうが悩まなくて済む。

参考ページ。 :


以上です。

過去ログ表示

Prev - 2022/08 - Next
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31

カテゴリで表示

検索機能は Namazu for hns で提供されています。(詳細指定/ヘルプ


注意: 現在使用の日記自動生成システムは Version 2.19.6 です。
公開されている日記自動生成システムは Version 2.19.5 です。

Powered by hns-2.19.6, HyperNikkiSystem Project