diff --git a/lib/fluent/command/fluentd.rb b/lib/fluent/command/fluentd.rb index b2b737c1f2..d14d8fece6 100644 --- a/lib/fluent/command/fluentd.rb +++ b/lib/fluent/command/fluentd.rb @@ -335,5 +335,18 @@ end worker = Fluent::Supervisor.new(opts) worker.configure - worker.run_worker + + if opts[:daemonize] + require 'fluent/daemonizer' + args = ARGV.dup + i = args.index('--daemon') + args.delete_at(i + 1) # value of --daemon + args.delete_at(i) # --daemon itself + + Fluent::Daemonizer.daemonize(opts[:daemonize], args) do + worker.run_worker + end + else + worker.run_worker + end end diff --git a/lib/fluent/daemonizer.rb b/lib/fluent/daemonizer.rb new file mode 100644 index 0000000000..3549d13fbf --- /dev/null +++ b/lib/fluent/daemonizer.rb @@ -0,0 +1,88 @@ +# +# Fluentd +# +# 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. +# + +require 'fluent/config/error' + +module Fluent + class Daemonizer + def self.daemonize(pid_path, args = [], &block) + new.daemonize(pid_path, args, &block) + end + + def daemonize(pid_path, args = []) + pid_fullpath = File.absolute_path(pid_path) + check_pidfile(pid_fullpath) + + begin + Process.daemon(false, false) + + File.write(pid_fullpath, Process.pid.to_s) + + # install signal and set process name are performed by supervisor + install_at_exit_handlers(pid_fullpath) + + yield + rescue NotImplementedError + daemonize_with_spawn(pid_fullpath, args) + end + end + + private + + def daemonize_with_spawn(pid_fullpath, args) + pid = Process.spawn(*['fluentd'].concat(args)) + + File.write(pid_fullpath, pid.to_s) + + pid + end + + def check_pidfile(pid_path) + if File.exist?(pid_path) + if !File.readable?(pid_path) || !File.writable?(pid_path) + raise Fluent::ConfigError, "Cannot access pid file: #{pid_path}" + end + + pid = + begin + Integer(File.read(pid_path), 10) + rescue TypeError, ArgumentError + return # ignore + end + + begin + Process.kill(0, pid) + raise Fluent::ConfigError, "pid(#{pid}) is running" + rescue Errno::EPERM + raise Fluent::ConfigError, "pid(#{pid}) is running" + rescue Errno::ESRCH + end + else + unless File.writable?(File.dirname(pid_path)) + raise Fluent::ConfigError, "Cannot access directory for pid file: #{File.dirname(pid_path)}" + end + end + end + + def install_at_exit_handlers(pidfile) + at_exit do + if File.exist?(pidfile) + File.delete(pidfile) + end + end + end + end +end diff --git a/test/test_daemonizer.rb b/test/test_daemonizer.rb new file mode 100644 index 0000000000..c4344f1ed5 --- /dev/null +++ b/test/test_daemonizer.rb @@ -0,0 +1,91 @@ +require_relative 'helper' +require 'fluent/daemonizer' + +class DaemonizerTest < ::Test::Unit::TestCase + TMP_DIR = File.join(File.dirname(__FILE__), 'tmp', 'daemonizer') + + setup do + FileUtils.mkdir_p(TMP_DIR) + end + + teardown do + FileUtils.rm_rf(TMP_DIR) rescue nil + end + + test 'makes pid file' do + pid_path = File.join(TMP_DIR, 'file.pid') + + mock(Process).daemon(anything, anything).once + r = Fluent::Daemonizer.daemonize(pid_path) { 'ret' } + assert_equal 'ret', r + assert File.exist?(pid_path) + assert Process.pid.to_s, File.read(pid_path).to_s + end + + test 'in platforms which do not support fork' do + pid_path = File.join(TMP_DIR, 'file.pid') + + mock(Process).daemon(anything, anything) { raise NotImplementedError } + args = ['-c', 'test.conf'] + mock(Process).spawn(anything, *args) { Process.pid } + + Fluent::Daemonizer.daemonize(pid_path, args) { 'ret' } + assert File.exist?(pid_path) + assert Process.pid.to_s, File.read(pid_path).to_s + end + + sub_test_case 'when pid file already exists' do + test 'raise an error when process is running' do + omit 'chmod of file does not affetct root user' if Process.uid.zero? + pid_path = File.join(TMP_DIR, 'file.pid') + File.write(pid_path, '1') + + mock(Process).daemon(anything, anything).never + mock(Process).kill(0, 1).once + + assert_raise(Fluent::ConfigError.new('pid(1) is running')) do + Fluent::Daemonizer.daemonize(pid_path) { 'ret' } + end + end + + test 'raise an error when file is not redable' do + omit 'chmod of file does not affetct root user' if Process.uid.zero? + not_readable_path = File.join(TMP_DIR, 'not_readable.pid') + + File.write(not_readable_path, '1') + FileUtils.chmod(0333, not_readable_path) + + mock(Process).daemon(anything, anything).never + assert_raise(Fluent::ConfigError.new("Cannot access pid file: #{File.absolute_path(not_readable_path)}")) do + Fluent::Daemonizer.daemonize(not_readable_path) { 'ret' } + end + end + + test 'raise an error when file is not writable' do + omit 'chmod of file does not affetct root user' if Process.uid.zero? + not_writable_path = File.join(TMP_DIR, 'not_writable.pid') + + File.write(not_writable_path, '1') + FileUtils.chmod(0555, not_writable_path) + + mock(Process).daemon(anything, anything).never + assert_raise(Fluent::ConfigError.new("Cannot access pid file: #{File.absolute_path(not_writable_path)}")) do + Fluent::Daemonizer.daemonize(not_writable_path) { 'ret' } + end + end + + test 'raise an error when directory is not writable' do + omit 'chmod of file does not affetct root user' if Process.uid.zero? + not_writable_dir = File.join(TMP_DIR, 'not_writable') + pid_path = File.join(not_writable_dir, 'file.pid') + + FileUtils.mkdir_p(not_writable_dir) + FileUtils.chmod(0555, not_writable_dir) + + mock(Process).daemon(anything, anything).never + assert_raise(Fluent::ConfigError.new("Cannot access directory for pid file: #{File.absolute_path(not_writable_dir)}")) do + Fluent::Daemonizer.daemonize(pid_path) { 'ret' } + end + end + end +end