10.06.2009Ruby daemon, или как сделать демона на руби
Задача
Иногда в процессе работы с разными сторонними библиотеками, которые содержат тяжелые блокирующие методы, не обойтись обычным Thread. В таком случае на помощь приходят демоны :) Вот несколько приемов, которые я освоил в работе с нимим.
Базовый механизм
Запустить демона можно с помощью метода Kernel.fork, передав ему блок. Метод возвращает pid процесса, который можно записать в файл и просто использовать в дальнейшем.
pid = fork do
puts "from daemon"
exit!(1)
end
Определение статуса
Я не нашёл удобного способа определения методами руби, запущен ли процесс. Есть возможность фильтровать вывод ps ax, ища в нём pid процесса. Но есть метод Process.waitpid, который можно использовать хитрым образом. Так же для будущих задач, упакуем наш код в класс:
require 'timeout'
class Daemon
class << self
def start
@pid = fork do
puts "from daemon"
sleep 1
exit!(1)
end
end
def running?
if @pid
begin
Timeout::timeout(0.01) do
Process.waitpid(@pid)
if $?.exited?
return false
end
end
rescue Timeout::Error
end
return true
else
return false
end
end
end
end
Daemon.start
puts "running: #{Daemon.running?}"
sleep 1.5
puts "running: #{Daemon.running?}"
Сообщение об ошибках
Предположим, что у нашего демона есть некоторый процесс инициализации, и мы хотим знать, завершился ли он или произошла ошибка до того, как покинем метод Daemon.start.
Для передачи подобных сообщений хорошо подходит IO.pipe. Для двустороннего общения нужно создавать два канала, но нам хватит и одного:
require 'timeout'
class Daemon
def initialize
puts "from daemon: initializing"
end
class << self
def start
@rd, @wr = IO.pipe
@pid = fork do
@rd.close
begin
dmn = new
@wr.write "ok"
@wr.close
sleep 1
rescue Exception => e
@wr.write e.to_s
@wr.close
ensure
exit!(1)
end
end
@wr.close
str = @rd.read
if str == "ok"
puts "daemon started ok"
else
puts "error while initializing daemon: #{str}"
end
@rd.close
end
def running?
if @pid
begin
Timeout::timeout(0.01) do
Process.waitpid(@pid)
if $?.exited?
return false
end
end
rescue Timeout::Error
end
return true
else
return false
end
end
end
end
Daemon.start
puts "running: #{Daemon.running?}"
sleep 1.5
puts "running: #{Daemon.running?}"
Остановка
У каждого демона (для того они обычно и создаются) есть циклическая часть. Нам хотелось бы запускать и останавливать процесс тогда, когда нам нужно. Если с запуском всё понятно, то для остановки потребуется ещё одна вещь. Ведь в тот момент, когда мы создали демона, он создает копии всех переменных и дальнейшее их изменение внутри и снаружи демона становится независимым. Ещё один способ общения с демоном — сигнал:
require 'timeout'
class Daemon
def initialize
puts "from daemon: initializing"
@cnt = 0
end
def main_loop
@cnt += 1
puts "from daemon: running loop ##{@cnt}"
sleep 0.1
end
class << self
def start
@rd, @wr = IO.pipe
@pid = fork do
@rd.close
running = true
Signal.trap("TERM") do
running = false
end
begin
dmn = new
@wr.write "ok"
@wr.close
while running
dmn.main_loop
end
rescue Exception => e
@wr.write e.to_s
@wr.close
ensure
exit!(1)
end
end
@wr.close
str = @rd.read
if str == "ok"
puts "daemon started ok"
else
puts "error while initializing daemon: #{str}"
end
@rd.close
end
def stop
unless @pid.nil?
Process.kill("TERM", @pid)
@pid = nil
end
end
def running?
if @pid
begin
Timeout::timeout(0.01) do
Process.waitpid(@pid)
if $?.exited?
return false
end
end
rescue Timeout::Error
end
return true
else
return false
end
end
end
end
Daemon.start
puts "running: #{Daemon.running?}"
sleep 1
Daemon.stop
puts "running: #{Daemon.running?}"
Теперь, если наследовать от этого класса свой класс и переопределить методы initialize и main_loop, получится вполне себе демон :)
Для самостоятельного изучения
Есть, конечно, ещё недостатки. Например:
- Если ошибка возникает в main_loop, то канал вывода уже закрыт. А если не закрывать канал, то метод IO#read не позволит нам выйти из метода start. Что делать?
- Если нужно периодически общаться с демоном общирными объемами информации, что делать? (я в своей задаче использовал TCPSocket, но это, ведь, не панацея)
- Хорошо бы хранить pid в pid-файле на случай нашествия зомби. И, соответственно, обрабатывать возникающие зомбо-проблемы.
Но это я оставлю на самостоятельное решение пытливым читателям.