# -*- coding: utf-8 -*- # # YaDaemon is a wrapper class with Process::daemon and it uses # a typical pid file to protect multiple invocations. # # If you are a root user, you can also change your effective # uid and gid. # # To use this class, simply give your code to the run method # with some options. Please refer the Usage section. # #=Title # # YaDaemon - a wrapper daemon class using a pid file to protect # multiple invocations. # #=Scenario of YaDaemon # # 1. User creates an instance and call the "run" method. # 2. Checking already running process. # 3a. If there is a pid file, checking the pid number written # in the pid file. # If there is a running process, then exit. # 3b. If there is no pid file, touch the pid file. # 4. Checking the pid file is writable or not. # 5. If specified, let the process fork by Process::daemon. # 6. Store the pid number into the defined pid file. # 7. If you are root user, then call Process::{euid,egid}. # 8. Yield from the "run" method. # #=Usage # # #!/usr/bin/env ruby1.9 # ## assumption: This script and mydaemon.rb are located in the # ## same directory. # $:.unshift File::dirname($0) # require 'mydaemon' # daemon = YaDaemon.new("myappname","myappname.pid","/tmp", # {:daemon=>false,:debug=>true}) # daemon.run do |pid| ## pid == /tmp/myappname/myappname.pid # ## please write you code like the following; # while daemon.running # puts Time.now # sleep 3 # end # end # #==:appname (essential argument) # * String: a name of this application. # It must be the unique name and be used for the subdirectory name. # #==:pidfile (essential argument) # * String: a pid file name without path. # #==:pidpdir (essential argument) # * String: a parent pid directory path which will be converted to the # absolute path by File::expand_path. # # The ":piddir/:appname" directory must be writable by Process::uid. # #=Options #==:debug # # * true/false (default: false) # If it's true, it outputs debug messages into :piddir/:appname/:pidfile. # #==:daemon # # * true/false (default: false) # # If it's true, then the script will be forked and working as # a background process. # # If it's false, then working the run method as foreground process. # If you want to use daemontools distributed by cr.yp.to, this options # must be "false." # #==:euid # # * Integer (default: Process::euid) # # Overwrite Process::euid. # #==:egid # # * Integer (default: Process::egid) # # Overwrite Process::egid. # #==:perms # # * String or Numeric(Octal) # # "0755" or "755" will be converted to a octal variable by the 'oct' method. # If you use a numeric variable, you must use octal expression, like 0755. # It means that 755 is wrong expression, but 493 is correct. # #=File Permissions # # The process pid will be stored into :piddir/:appname/:pidfile, such as # /tmp/app/app.pid. # In this case, file permissions of the pidfile and piddir/appname # directory will be checked. # # If you use :debug=true option, the piddir/appname/"debug.log" file # will be also created by uid:gid, and then chown-ed by euid:egid. # # | @pidpath | | | # | @piddir / | | | # | pidpdir / appname / pidfile | "debug.log" | "stop.txt" | # ---------+---------+---------+---------+-------------+------------+ # user id | x | @euid= | = | @euid= | @euid* | # group id | x | - | - | @egid= | @egid= | # ---------+---------+---------+---------+-------------+------------+ # (, is same as Process::uid, Process::gid) # ('=' means these ownerships will be overwritten) # ('*' means it should be writable by that id) # ('x' means it never be checked and changed) # #=Copyright # # Copyright (C) 2010,2011 Yasuhiro ABE # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # It's a utility class to process common tasks. # These methods should be tested by the unit test script. # module YaDaemonUtils ## ## Note: ## The following methods having same prefix 'check_' are support methods for the option parser. ## def check_boolean(opts, label) (opts.has_key?(label) and (opts[label].kind_of?(FalseClass) or opts[label].kind_of?(TrueClass))) ? opts[label] : false end def check_eugid(opts, label) label = "uid" if Process::methods.index(label) == nil (Process::uid == 0 and opts.has_key?(label) and opts[label].methods.index(:to_i) != nil) ? opts[label].to_i : Process.method(label).call end def check_default_perms(opts,label) perm = 0711 if opts.has_key?(label) if opts[label].kind_of?(String) perm = opts[label].oct elsif opts[label].kind_of?(Numeric) perm = opts[label] end end perm end ## It's a wrapper method to write something to the given filepath. ## args: perms must be fixed num ## return: true(if success) or false def write_file(filepath, perms, data) ret = false begin open(filepath, File::RDWR|File::CREAT, perms) do |f| f.flock(File::LOCK_EX) f.write(data.to_s) f.flush f.truncate(f.pos) end ret = true rescue ret = false end ret end end ## It is a simple unix-like daemon class. class YaDaemon include YaDaemonUtils ## logging message for debug def logit(msg) return if not @debug return if not msg.kind_of?(String) puts msg if not @daemon_mode open(@debugfile, "a") do |f| require 'csv' ## buf='';f.write(CSV.generate_row(["pid=#{$$}","date=#{Time.now}","msg=#{msg.to_s}"], 2, buf)) ## ruby 1.8.x f.write(["pid=#{$$}","date=#{Time.now}","msg=#{msg.to_s}"].to_csv) f.flush end end ## raise all exception during initialize def initialize(appname="app", pidfile="app.pid", pidpdir="/tmp", opts = { :debug=>false, :daemon=>false, }) ## opts might be null opts = {} if not opts.kind_of?(Hash) ## check options @debug = check_boolean(opts,:debug) @daemon_mode = check_boolean(opts,:daemon) @euid = check_eugid(opts,:euid) @egid = check_eugid(opts,:egid) @file_perms = check_default_perms(opts,:perms) ## take care the pidpdir which must exists. raise "pidpdir, #{pidpdir}, not exist." if not FileTest::exist?(pidpdir) ## prepare @piddir and @pidpath @piddir = File::join([File::expand_path(pidpdir),appname]) raise "cannot mkdir(#{piddir})" if not FileTest::exist?(@piddir) and Dir::mkdir(@piddir) != 0 ## change ownership of @piddir stat = File::Stat.new(@piddir) if stat.uid != @euid if File::chown(@euid, nil, @piddir) == 1 logit "initialize: succesfully changed #{@piddir}'s ownership." else raise "failed to chown #{@piddir}." end end @pidpath = File::join([@piddir, pidfile]) ## prepare the @stopfile ## used by run/stop methods and delete if existing. @stopfile = File::join([@piddir, "stop.txt"]) if FileTest.exist?(@stopfile) if File::unlink(@stopfile) != 1 logit "failed to delete the stop file, #{@stopfile}." raise "failed to delete the stop file, #{@stopfile}." end end ## prepare the @debugfile @debugfile = File::join([@piddir,"debug.log"]) if @debug ## change owner,group of @debugfile. if @debug and Process::uid == 0 if not FileTest::exist?(@debugfile) if write_file(@debugfile, @file_perms, "") logit "succesfully create the debug.log file, #{@debugfile}." else logit "failed to touch the debug.log file, #{@debugfile}." raise "failed to touch the debug.log file, #{@debugfile}." end end stat = File::Stat.new(@debugfile) if stat.uid != @euid or stat.gid != @egid if File::chown(@euid, @egid, @debugfile) == 1 logit "initialize: succesfully changed #{@debugfile}'s ownership." else logit "failed to chown #{@debugfile}." raise "failed to chown #{@debugfile}." end end end end def get_pid logit "get_pid: called" pid = 0 begin open(@pidpath).each_line do |l| pid = l.to_i end rescue logit "get_pid: failed to open the pid file, @pidpath." end logit "get_pid: return #{pid}" pid end ## If a running process was found and it was not the own process, then return true. ## There is no running process without myself, then return false. def check_proc logit "check_proc: called" ret = false pid = get_pid logit "check_proc: pid = #{pid}" ## case 0: if pid file is empty then it assumes there is no running process. return ret if pid < 3 ## case 1: check the $$ value at first. if pid == $$ logit("check_proc: the pid file exists, but it has own process number. It's harmless, but should be never called.") return ret end ## case 2a: check /proc/$pid. (available on limited systems only) ## skipped. ## case 2b: using kill -0 method (widely available on unix-like systems) begin k = Process::kill(0, pid) ret = true if k == 1 rescue logit "check_proc: exception occurred by kill(0, #{pid}): #{$!}" ret = false end logit "check_proc: return #{ret}" ret end def create_stop_file ret = write_file(@stopfile, @file_perms, "") logit "create_stop_file: return #{ret}" ret end ## If there is no running process, then the pid file will be overwritten. def overwrite_pid_file ret = write_file(@pidpath, @file_perms, Process::pid.to_s) logit "overwrite_pid_file: return #{ret}" ret end def run logit "run: called" if FileTest::exist?(@pidpath) ## process scenario: A3a if check_proc logit "run: a process is already running, exiting now." return end else ## process scenario: A2,A3b begin open(@pidpath,"w") do |f| f.write("") end rescue logit "run: failed to write the pid file, #{@pidpath}." return end end ## comfirm scenario: A4 if not File::writable?(@pidpath) logit "run: pidfile, #{@pidpath}, not writable." return end ## process scenario: A5 if @daemon_mode logit "run: move to daemon mode" Process::daemon end ## process scenario: A6 return if not overwrite_pid_file ## process scenario: A7 change_privilege(@egid, :egid) change_privilege(@euid, :euid) ## process scenario: A8 yield @pidpath logit "run: return" end ## safelly def stop logit "stop: called" create_stop_file() end def force_stop if not check_proc logit "force_stop: this process has already stopped." return end pid = get_pid ## case 1 if pid == $$ create_stop_file() exit(0) end ## case 2 if pid > 0 n = Process::kill(15, pid) if n > 0 logit "force_stop: the terminate signal succesfully sent." else logit "force_stop: failed to sent the terminate signal." end while check_proc logit "stop: waiting terminate process, pid=#{pid}." sleep 3 end else logit "force_stop: failed to get my pid number" end logit "force_stop: return" end ## change privileges using from run method, but place here for unit test. def change_privilege(eugid=0, label=:uid) logit "change_privilege: called (eugid=#{eugid}, label=#{label})" if Process::uid == 0 and eugid != Process.method(label).call ret = Process.method("#{label}=").call(eugid) logit "change_privilege: change #{label} to #{eugid}: #{ret}" end ret end def running not FileTest.exist?(@stopfile) end end