eye-0.7/0000755000004100000410000000000012600364132012167 5ustar www-datawww-dataeye-0.7/Rakefile0000644000004100000410000000202512600364132013633 0ustar www-datawww-data#!/usr/bin/env rake require "bundler/gem_tasks" require 'rspec/core/rake_task' require 'coveralls/rake/task' Coveralls::RakeTask.new task :default => :pspec desc "run parallel tests" task :pspec do dirname = File.expand_path(File.dirname(__FILE__)) cmd = "bundle exec parallel_rspec -n #{ENV['N'] || 10} --runtime-log '#{dirname}/spec/weights.txt' #{dirname}/spec" abort unless system(cmd) end desc "run parallel split tests" task :split_test do dirname = File.expand_path(File.dirname(__FILE__)) ENV['PARALLEL_SPLIT_TEST_PROCESSES'] = (ENV['N'] || 10).to_s cmd = "bundle exec parallel_split_test #{dirname}/spec" abort unless system(cmd) end RSpec::Core::RakeTask.new(:spec) do |t| t.verbose = false end task :remove_coverage do require 'fileutils' FileUtils.rm_rf(File.expand_path(File.join(File.dirname(__FILE__), %w{ coverage }))) end task :env do require 'bundler/setup' require 'eye' Eye::Controller Eye::Process end desc "graph" task :graph => :env do StateMachine::Machine.draw("Eye::Process") end eye-0.7/bin/0000755000004100000410000000000012600364132012737 5ustar www-datawww-dataeye-0.7/bin/leye0000755000004100000410000000125112600364132013622 0ustar www-datawww-data#!/usr/bin/env ruby $:.unshift File.expand_path(File.join(File.dirname(__FILE__), %w[.. lib])) require 'eye' # Local version of eye # which looking for Eyefile # like foreman while true if ARGV[0] == '--eyefile' ARGV.shift ENV['EYE_FILE'] = File.expand_path(ARGV.shift.to_s) elsif ARGV[0] == '--eyehome' ARGV.shift ENV['EYE_HOME'] = File.expand_path(ARGV.shift.to_s) else break end end unless Eye::Local.eyefile puts "\033[31mNot found Eyefile (in #{File.expand_path('.')})\033[0m" exit 1 end unless ENV.key?('EYE_HOME') Eye::Local.dir = File.join(File.dirname(Eye::Local.eyefile), '.eye') end Eye::Local.local_runner = true Eye::Cli.start eye-0.7/bin/loader_eye0000755000004100000410000000343412600364132015001 0ustar www-datawww-data#!/usr/bin/env ruby $:.unshift File.expand_path(File.join(File.dirname(__FILE__), %w[.. lib])) require 'eye/loader' require 'optparse' require 'eye' options = {:debug => false} OptionParser.new do |opts| opts.on( '-h', '--help', 'Display this screen' ) do puts opts exit end opts.on( '-c', '--config CONFIG', 'load with config' ) do |config_path| options[:config] = config_path end opts.on( '-s', '--socket SOCKET', 'start listen on socket' ) do |socket_path| options[:socket_path] = socket_path end opts.on( '-l', '--logger LOGGER', 'custom logger' ) do |logger| options[:logger] = logger end opts.on( '-dr', '--dir DIR', 'Dir for local runner' ) do |dir| Eye::Local.dir = dir Eye::Local.local_runner = true end opts.on( '-st', '--stop_all', 'Stop all on exit' ) do |stop_all| options[:stop_all] = true end opts.on( '-d', '--debug', 'debug info to logger' ) do options[:debug] = true end end.parse! Eye::Local.ensure_eye_dir socket_path = options[:socket_path] || Eye::Local.socket_path server = Eye::Server.new(socket_path) Eye::Logger.log_level = options[:debug] ? Logger::DEBUG : Logger::INFO Eye::Logger.link_logger(options[:logger]) if options[:logger] config = options[:config] config = File.expand_path(config) if config && !config.empty? Eye::Control # preload if config res = server.command('load', config) exit(1) if res.values.any? { |r| r[:error] } end Eye::Control.set_proc_line server.async.run trap("USR1") { Eye::Logger.reopen } trap("USR2") { GC.start } trap("INT") { Eye::Logger.info("INT signal <#{$$}>"); exit } trap("QUIT") { Eye::Logger.info("QUIT signal <#{$$}>"); exit } trap("TERM") { Eye::Logger.info("TERM signal <#{$$}>"); exit } at_exit { Eye::Control.command(:stop_all) } if options[:stop_all] sleep eye-0.7/bin/eye0000755000004100000410000000017512600364132013452 0ustar www-datawww-data#!/usr/bin/env ruby $:.unshift File.expand_path(File.join(File.dirname(__FILE__), %w[.. lib])) require 'eye' Eye::Cli.start eye-0.7/Gemfile0000644000004100000410000000004612600364132013462 0ustar www-datawww-datasource 'https://rubygems.org' gemspec eye-0.7/examples/0000755000004100000410000000000012600364132014005 5ustar www-datawww-dataeye-0.7/examples/plugin/0000755000004100000410000000000012600364132015303 5ustar www-datawww-dataeye-0.7/examples/plugin/plugin.rb0000644000004100000410000000241612600364132017131 0ustar www-datawww-dataclass Reactor include Celluloid def initialize(interval, filename) @interval = interval @filename = filename every(@interval) do info "check file #{@filename}" if cmd = read_file execute_command cmd end end end def read_file if File.exist?(@filename) cmd = File.read(@filename).chop File.delete(@filename) rescue nil cmd end end def execute_command(cmd) Eye::Control.command(cmd, 'all') if %w{restart start stop}.include?(cmd) end end class Saver < Eye::Trigger::Custom param :log_name, String, true def check(trans) tlogger.info "#{process.full_name} transition from #{trans.from_name} to #{trans.to_name}" end def tlogger @tlogger ||= Logger.new(log_name) end end def reactor Celluloid::Actor[:reactor] end # Extend config options, add enable_reactor class Eye::Dsl::ConfigOpts def enable_reactor(*args) @config[:reactor] = args end def enable_saver(save_log) Eye.application '__default__' do trigger :saver, :log_name => save_log end end end # extend controller to execute method, and config loads class Eye::Controller def set_opt_reactor(args) reactor.terminate if reactor Celluloid::Actor[:reactor] = Reactor.supervise(*args) end end eye-0.7/examples/plugin/main.eye0000644000004100000410000000041212600364132016730 0ustar www-datawww-dataEye.load('./plugin.rb') Eye.config do logger "/tmp/eye.log" enable_reactor(1.second, "/tmp/cmd.txt") enable_saver("/tmp/saver.log") end Eye.app :app do process :process do pid_file "/tmp/p.pid" start_command "sleep 10" daemonize true end end eye-0.7/examples/plugin/README.md0000644000004100000410000000073012600364132016562 0ustar www-datawww-dataEye Plugin Example ------------------ This plugin adds reactor which try to reads command from file "/tmp/cmd.txt" every 1.second (then execute it and delete file). Also plugin add trigger to save every process state transition into "/tmp/saver.log". To test it: bundle exec eye l examples/plugin/main.eye tail -f /tmp/eye.log tail -f /tmp/saver.log echo 'restart' > /tmp/cmd.txt Also, here http example of gem: https://github.com/kostya/eye-http eye-0.7/examples/delayed_job.eye0000644000004100000410000000122212600364132016747 0ustar www-datawww-datacwd = File.expand_path(File.join(File.dirname(__FILE__), %w[ ../ ../ ])) config_path = File.join(cwd, %w{ config dj.yml } ) workers_count = if File.exist?(config_path) YAML.load_file(config_path).try(:[], :workers) || 5 else 5 end Eye.application 'delayed_job' do working_dir cwd stop_on_delete true group 'dj' do chain grace: 5.seconds (1 .. workers_count).each do |i| process "dj-#{i}" do pid_file "tmp/pids/delayed_job.#{i}.pid" start_command "rake jobs:work" daemonize true stop_signals [:INT, 30.seconds, :TERM, 10.seconds, :KILL] stdall "log/dj-#{i}.log" end end end end eye-0.7/examples/rbenv.eye0000644000004100000410000000056012600364132015626 0ustar www-datawww-dataEye.application "rbenv_example" do env 'RBENV_ROOT' => '/usr/local/rbenv', 'PATH' => "/usr/local/rbenv/shims:/usr/local/rbenv/bin:#{ENV['PATH']}" working_dir File.expand_path(File.join(File.dirname(__FILE__), %w[ processes ])) process "some_process" do pid_file "some.pid" start_command "ruby some.rb" daemonize true stdall "some.log" end end eye-0.7/examples/processes/0000755000004100000410000000000012600364132016013 5ustar www-datawww-dataeye-0.7/examples/processes/em.rb0000644000004100000410000000220312600364132016736 0ustar www-datawww-datarequire 'bundler/setup' require 'eventmachine' def answer(data) case data when 'ping' then "pong\n" when 'bad' then "what\n" when 'timeout' then sleep 5; "ok\n" when 'exception' then raise 'haha' when 'quit' then EM.stop when 'big' then 'a' * 10_000_000 end end class Echo < EM::Connection def post_init puts "-- someone connected to the echo server!" end def receive_data data puts "receive #{data.inspect} " send_data(answer(data)) end def unbind puts "-- someone disconnected from the echo server!" end end class EchoObj < EM::Connection include EM::P::ObjectProtocol def post_init puts "-- someone connected to the echo server!" end def receive_object obj # {:command => 'ping'} puts "receive #{obj.inspect}" send_object(answer(obj[:command]).chop) end def unbind puts "-- someone disconnected from the echo server!" end end trap "QUIT" do puts "quit signal, stopping" EM.stop end EM.run do EM.start_server '127.0.0.1', 33221, Echo EM.start_server '127.0.0.1', 33222, EchoObj EM.start_server "/tmp/em_test_sock", nil, Echo puts 'started' end eye-0.7/examples/processes/thin.ru0000644000004100000410000000022212600364132017321 0ustar www-datawww-datarequire 'bundler/setup' require 'sinatra' class Test < Sinatra::Base get '/hello' do sleep 0.5 "Hello World!" end end run Test.new eye-0.7/examples/processes/forking.rb0000644000004100000410000000105512600364132020000 0ustar www-datawww-datarequire 'bundler/setup' require 'forking' root = File.expand_path(File.dirname(__FILE__)) cnt = (ENV['FORKING_COUNT'] || 3).to_i f = Forking.new(:name => 'forking', :working_dir => root, :log_file => "#{root}/forking.log", :pid_file => "#{root}/forking.pid", :sync_log => true) cnt.times do |i| f.spawn(:log_file => "#{root}/child#{i}.log", :sync_log => true) do $0 = "forking child" t = 0 loop do p "#{Time.now} - #{Time.now.to_f} - #{i} - tick" sleep 0.1 t += 0.1 exit if t > 300 end end end f.run! eye-0.7/examples/processes/sample.rb0000644000004100000410000000624712600364132017632 0ustar www-datawww-data#!/usr/bin/env ruby require 'optparse' # This hash will hold all of the options # parsed from the command-line by # OptionParser. options = {} optparse = OptionParser.new do|opts| # This displays the help screen, all programs are # assumed to have this option. opts.on( '-h', '--help', 'Display this screen' ) do puts opts exit end opts.on( '-p', '--pid FILE', 'pid_file' ) do |a| options[:pid_file] = a end opts.on( '-l', '--log FILE', 'log_file' ) do |a| options[:log_file] = a end opts.on( '-L', '--lock FILE', 'lock_file' ) do |a| options[:lock_file] = a end opts.on( '-d', '--daemonize', 'Daemonize' ) do options[:daemonize] = true end opts.on( '-s', '--daemonize_delay DELAY', 'Daemonized time' ) do |d| options[:daemonize_delay] = d end opts.on( '-r', '--raise', 'Raised execution' ) do options[:raise] = true end opts.on( '-w', '--watch_file FILE', 'Exit on touched file' ) do |w| options[:watch_file] = w end opts.on( '-W', '--watch_file_delay DELAY', 'Exit on touched file, after delay' ) do |w| options[:watch_file_delay] = w end end optparse.parse! module Sample def puts(mes = "") tm = Time.now STDOUT.puts "#{tm} (#{tm.to_f}) - #{mes}" STDOUT.flush end def daemonize(pid_file, log_file, daemonize_delay = 0) puts "daemonize start #{pid_file}, #{log_file}, #{daemonize_delay}" if daemonize_delay && daemonize_delay.to_f > 0 puts "daemonize delay start #{daemonize_delay}" sleep daemonize_delay.to_f puts "daemonize delay end" end daemon STDOUT.reopen(log_file, "a") STDERR.reopen(log_file, "a") File.open(pid_file, 'w'){|f| f.write $$.to_s} puts "daemonized" end def daemon exit if fork # Parent exits, child continues. Process.setsid # Become session leader. exit if fork # Zap session leader. See [1]. STDIN.reopen "/dev/null" # Free file descriptors and STDOUT.reopen "/dev/null", "a" # point them somewhere sensible. STDERR.reopen '/dev/null', 'a' return 0 end end extend Sample if options[:daemonize] daemonize(options[:pid_file], options[:log_file], options[:daemonize_delay]) end puts "Started #{ARGV.inspect}, #{options.inspect}, #{ENV['ENV1']}" if options[:lock_file] if File.exist?(options[:lock_file]) puts "Lock file exists, exiting" exit 1 else File.open(options[:lock_file], 'w'){|f| f.write $$ } end end if options[:raise] puts "Raised" File.unlink(options[:lock_file]) if options[:lock_file] exit 1 end trap("USR1") do puts "USR1 signal!" end trap("USR2") do puts "USR2 start memory leak" $ar = [] 300_000.times{|i| $ar << "memory leak #{i}" * 10} end loop do sleep 0.1 puts "tick" if options[:watch_file] if File.exist?(options[:watch_file]) puts "watch file finded" File.unlink(options[:watch_file]) if options[:watch_file_delay] puts "watch_file delay start" sleep options[:watch_file_delay].to_f puts "watch_file delay end" end break end end end puts "exit" File.unlink(options[:lock_file]) if options[:lock_file] exit 0 eye-0.7/examples/stress_test.eye0000644000004100000410000000070212600364132017072 0ustar www-datawww-data# this is not example, just config for eye stress test PREFIX = ENV['PRE'] || ENV['EYE_V'] || 1 Eye.app :stress_test do working_dir "/tmp" 100.times do |i| process "sleep-#{i}" do pid_file "sleep-#{PREFIX}-#{i}.pid" start_command "sleep 120" daemonize true checks :cpu, :every => 5.seconds, :below => 10, :times => 5 checks :memory, :every => 6.seconds, :below => 50.megabytes, :times => 5 end end end eye-0.7/examples/thin-farm.eye0000644000004100000410000000131112600364132016372 0ustar www-datawww-dataRUBY = 'ruby' BUNDLE = 'bundle' Eye.load("process_thin.rb") Eye.config do logger "/tmp/eye.log" end Eye.app 'thin-farm' do working_dir File.expand_path(File.join(File.dirname(__FILE__), %w[ processes ])) env "RAILS_ENV" => "production" # more about stop_on_delete: https://github.com/kostya/eye/wiki/About-stop_on_delete-=-true stop_on_delete true trigger :flapping, :times => 10, :within => 1.minute check :memory, :below => 60.megabytes, :every => 30.seconds, :times => 5 start_timeout 30.seconds group :web do chain :action => :restart, :grace => 5.seconds chain :action => :start, :grace => 0.2.seconds (5555..5560).each do |port| thin self, port end end end eye-0.7/examples/unicorn.eye0000644000004100000410000000221112600364132016162 0ustar www-datawww-data# Example: how to run unicorn, and monitor its child processes RUBY = '/usr/local/ruby/1.9.3/bin/ruby' # ruby on the server RAILS_ENV = 'production' Eye.application "rails_unicorn" do env "RAILS_ENV" => RAILS_ENV # unicorn requires to be `ruby` in path (for soft restart) env "PATH" => "#{File.dirname(RUBY)}:#{ENV['PATH']}" working_dir File.expand_path(File.join(File.dirname(__FILE__), %w[ processes ])) process("unicorn") do pid_file "tmp/pids/unicorn.pid" start_command "#{RUBY} ./bin/unicorn -Dc ./config/unicorn.rb -E #{RAILS_ENV}" stdall "log/unicorn.log" # stop signals: # http://unicorn.bogomips.org/SIGNALS.html stop_signals [:TERM, 10.seconds] # soft restart restart_command "kill -USR2 {PID}" check :cpu, :every => 30, :below => 80, :times => 3 check :memory, :every => 30, :below => 150.megabytes, :times => [3,5] start_timeout 100.seconds restart_grace 30.seconds monitor_children do stop_command "kill -QUIT {PID}" check :cpu, :every => 30, :below => 80, :times => 3 check :memory, :every => 30, :below => 150.megabytes, :times => [3,5] end end end eye-0.7/examples/syslog.eye0000644000004100000410000000052112600364132016027 0ustar www-datawww-data# output eye logger to syslog, and process stdout to syslog too # experimental feature, in some cases may be unstable Eye.config do logger syslog end Eye.app :syslog_test do process :some do pid_file "/tmp/syslog_test.pid" start_command "ruby -e 'loop { p Time.now; sleep 1 }'" daemonize! stdall syslog end end eye-0.7/examples/notify.eye0000644000004100000410000000050312600364132016017 0ustar www-datawww-data# Notify example Eye.config do mail :host => "mx.some.host", :port => 25, :domain => "some.host" contact :errors, :mail, 'error@some.host' contact :dev, :mail, 'dev@some.host' end Eye.application :some do notify :errors process :some_process do notify :dev, :info pid_file "1.pid" #... end end eye-0.7/examples/dependency.eye0000644000004100000410000000126512600364132016633 0ustar www-datawww-data# process dependencies example Eye.app :dependency do process(:a) do start_command "sleep 100" daemonize true pid_file "/tmp/test_process_a.pid" end process(:b) do start_command "sleep 100" daemonize true pid_file "/tmp/test_process_b.pid" depend_on :a end process(:c) do start_command "sleep 100" daemonize true pid_file "/tmp/test_process_c.pid" depend_on :a end process(:d) do start_command "sleep 100" daemonize true pid_file "/tmp/test_process_d.pid" depend_on :b end process(:e) do start_command "sleep 100" daemonize true pid_file "/tmp/test_process_e.pid" depend_on [:d, :c] end end eye-0.7/examples/process_thin.rb0000644000004100000410000000127312600364132017035 0ustar www-datawww-data# part of thin-farm.eye config def thin(proxy, port) name = "thin-#{port}" opts = [ "-l thins.log", "-p #{port}", "-P #{name}.pid", "-d", "-R thin.ru", "--tag #{proxy.app.name}.#{proxy.name}", "-t 60", "-e #{proxy.env['RAILS_ENV']}", "-c #{proxy.working_dir}", "-a 127.0.0.1" ] proxy.process(name) do pid_file "#{name}.pid" start_command "#{BUNDLE} exec thin start #{opts * ' '}" stop_signals [:QUIT, 2.seconds, :TERM, 1.seconds, :KILL] stdall "thin.stdall.log" check :http, :url => "http://127.0.0.1:#{port}/hello", :pattern => /World/, :every => 5.seconds, :times => [2, 3], :timeout => 1.second end end eye-0.7/examples/sidekiq.eye0000644000004100000410000000125412600364132016144 0ustar www-datawww-data# Example: how to run sidekiq daemon def sidekiq_process(proxy, name) rails_env = proxy.env['RAILS_ENV'] proxy.process(name) do start_command "bin/sidekiq -e #{rails_env} -C ./config/sidekiq.#{rails_env}.yml" pid_file "tmp/pids/#{name}.pid" stdall "log/#{name}.log" daemonize true stop_signals [:USR1, 0, :TERM, 10.seconds, :KILL] check :cpu, :every => 30, :below => 100, :times => 5 check :memory, :every => 30, :below => 300.megabytes, :times => 5 end end Eye.application :sidekiq_test do working_dir File.expand_path(File.join(File.dirname(__FILE__), %w[ processes ])) env "RAILS_ENV" => 'production' sidekiq_process self, :sidekiq end eye-0.7/examples/test.eye0000644000004100000410000000552312600364132015475 0ustar www-datawww-data# load submodules, here just for example Eye.load('./eye/*.rb') # Eye self-configuration section Eye.config do logger '/tmp/eye.log' end # Adding application Eye.application 'test' do # All options inherits down to the config leafs. # except `env`, which merging down # uid "user_name" # run app as a user_name (optional) - available only on ruby >= 2.0 # gid "group_name" # run app as a group_name (optional) - available only on ruby >= 2.0 working_dir File.expand_path(File.join(File.dirname(__FILE__), %w[ processes ])) stdall 'trash.log' # stdout,err logs for processes by default env 'APP_ENV' => 'production' # global env for each processes trigger :flapping, times: 10, within: 1.minute, retry_in: 10.minutes check :cpu, every: 10.seconds, below: 100, times: 3 # global check for all processes group 'samples' do chain grace: 5.seconds # chained start-restart with 5s interval, one by one. # eye daemonized process process :sample1 do pid_file '1.pid' # pid_path will be expanded with the working_dir start_command 'ruby ./sample.rb' # when no stop_command or stop_signals, default stop is [:TERM, 0.5, :KILL] # default `restart` command is `stop; start` daemonize true stdall 'sample1.log' # ensure the CPU is below 30% at least 3 out of the last 5 times checked check :cpu, below: 30, times: [3, 5] end # self daemonized process process :sample2 do pid_file '2.pid' start_command 'ruby ./sample.rb -d --pid 2.pid --log sample2.log' stop_command 'kill -9 {PID}' # ensure the memory is below 300Mb the last 3 times checked check :memory, every: 20.seconds, below: 300.megabytes, times: 3 end end # daemon with 3 children process :forking do pid_file 'forking.pid' start_command 'ruby ./forking.rb start' stop_command 'ruby forking.rb stop' stdall 'forking.log' start_timeout 10.seconds stop_timeout 5.seconds monitor_children do restart_command 'kill -2 {PID}' # for this child process check :memory, below: 300.megabytes, times: 3 end end # eventmachine process, daemonized with eye process :event_machine do |p| pid_file 'em.pid' start_command 'ruby em.rb' stdout 'em.log' daemonize true stop_signals [:QUIT, 2.seconds, :KILL] check :socket, addr: 'tcp://127.0.0.1:33221', every: 10.seconds, times: 2, timeout: 1.second, send_data: 'ping', expect_data: /pong/ end # thin process, self daemonized process :thin do pid_file 'thin.pid' start_command 'bundle exec thin start -R thin.ru -p 33233 -d -l thin.log -P thin.pid' stop_signals [:QUIT, 2.seconds, :TERM, 1.seconds, :KILL] check :http, url: 'http://127.0.0.1:33233/hello', pattern: /World/, every: 5.seconds, times: [2, 3], timeout: 1.second end end eye-0.7/examples/triggers.eye0000644000004100000410000000313512600364132016341 0ustar www-datawww-data# Triggers example: # # to execute commands inside trigger need to use 2 methods: # process.execute_async(cmd, opts), and # process.execute_sync(cmd, opts) Eye.config do logger "/tmp/eye.log" end Eye.app :triggers do # Execute shell command before process start process :a do pid_file "/tmp/a.pid" start_command "sleep 100" daemonize true # send message async which sendxmpp, before process start trigger :transition, to: :starting, do: -> { process.execute_async "sendxmpp -s 'hhahahaa' someone@jabber.org" } end # Touch some file before process start, remove file after process die process :b do pid_file "/tmp/b.pid" start_command "sleep 100" daemonize true # before process starting, touch some file trigger :transition1, to: :starting, do: -> { process.execute_sync "touch /tmp/bla.file" } # after process, crashed, or stopped, remove that file trigger :transition2, to: :down, do: -> { process.execute_sync "rm /tmp/bla.file" } end # With restart :c process, send restart to process :a process :c do pid_file "/tmp/c.pid" start_command "sleep 100" daemonize true app_name = app.name trigger :transition, :event => :restarting, :do => ->{ info "send restarting to :a" Eye::Control.command('restart', "#{app_name}:a") } end # process d cant start, until file /tmp/bla contains string 'bla' process :d do pid_file "/tmp/d.pid" start_command "sleep 100" daemonize true trigger :starting_guard, every: 5.seconds, should: -> { `cat /tmp/bla` =~ /bla/ } end end eye-0.7/examples/puma.eye0000644000004100000410000000150412600364132015453 0ustar www-datawww-dataBUNDLE = 'bundle' RAILS_ENV = 'production' ROOT = File.expand_path(File.join(File.dirname(__FILE__), %w[ processes ])) Eye.config do logger "#{ROOT}/eye.log" end Eye.application :puma do env 'RAILS_ENV' => RAILS_ENV working_dir ROOT trigger :flapping, :times => 10, :within => 1.minute process :puma do daemonize true pid_file "puma.pid" stdall "puma.log" start_command "#{BUNDLE} exec puma --port 33280 --environment #{RAILS_ENV} thin.ru" stop_signals [:TERM, 5.seconds, :KILL] restart_command "kill -USR2 {PID}" restart_grace 10.seconds # just sleep this until process get up status # (maybe enought to puma soft restart) check :cpu, :every => 30, :below => 80, :times => 3 check :memory, :every => 30, :below => 70.megabytes, :times => [3,5] end end eye-0.7/.rspec0000644000004100000410000000003112600364132013276 0ustar www-datawww-data--color --format progresseye-0.7/.travis.yml0000644000004100000410000000015312600364132014277 0ustar www-datawww-datalanguage: ruby rvm: - "1.9.3" - "2.0.0" - "2.1" - "2.2.2" script: bundle exec rake split_test N=15 eye-0.7/lib/0000755000004100000410000000000012600364132012735 5ustar www-datawww-dataeye-0.7/lib/eye.rb0000644000004100000410000000176712600364132014057 0ustar www-datawww-datamodule Eye VERSION = "0.7" ABOUT = "Eye v#{VERSION} (c) 2012-2015 @kostya" PROCLINE = "eye monitoring v#{VERSION}" autoload :Process, 'eye/process' autoload :ChildProcess, 'eye/child_process' autoload :Server, 'eye/server' autoload :Logger, 'eye/logger' autoload :System, 'eye/system' autoload :SystemResources,'eye/system_resources' autoload :Checker, 'eye/checker' autoload :Trigger, 'eye/trigger' autoload :Group, 'eye/group' autoload :Dsl, 'eye/dsl' autoload :Application, 'eye/application' autoload :Local, 'eye/local' autoload :Client, 'eye/client' autoload :Utils, 'eye/utils' autoload :Notify, 'eye/notify' autoload :Config, 'eye/config' autoload :Reason, 'eye/reason' autoload :Sigar, 'eye/sigar' autoload :Controller, 'eye/controller' autoload :Control, 'eye/control' autoload :Cli, 'eye/cli' end eye-0.7/lib/eye/0000755000004100000410000000000012600364132013517 5ustar www-datawww-dataeye-0.7/lib/eye/application.rb0000644000004100000410000000236112600364132016351 0ustar www-datawww-dataclass Eye::Application attr_reader :groups, :name, :config def initialize(name, config = {}) @groups = Eye::Utils::AliveArray.new @name = name @config = config debug { 'created' } end def logger_tag full_name end def full_name @name end def add_group(group) @groups << group end # sort processes in name order def resort_groups @groups = @groups.sort { |a, b| a.hidden ? 1 : (b.hidden ? -1 : (a.name <=> b.name)) } end def status_data(debug = false) h = { name: @name, type: :application, subtree: @groups.map{|gr| gr.status_data(debug) }} h[:debug] = debug_data if debug h end def status_data_short { name: @name, type: :application, subtree: @groups.map(&:status_data_short) } end def debug_data end def send_command(command, *args) info "send_command #{command}" @groups.each do |group| group.send_command(command, *args) end end def alive? true # emulate celluloid actor method end def sub_object?(obj) res = @groups.include?(obj) res = @groups.any?{|gr| gr.sub_object?(obj)} if !res res end def processes out = [] @groups.each{|gr| out += gr.processes.to_a } Eye::Utils::AliveArray.new(out) end end eye-0.7/lib/eye/utils.rb0000644000004100000410000000242212600364132015204 0ustar www-datawww-datarequire 'date' module Eye::Utils autoload :Tail, 'eye/utils/tail' autoload :AliveArray, 'eye/utils/alive_array' autoload :CelluloidChain, 'eye/utils/celluloid_chain' def self.deep_clone(value) case when value.is_a?(Array) then value.map{|v| deep_clone(v) } when value.is_a?(Hash) then value.inject({}){|r, (k, v)| r[ deep_clone(k) ] = deep_clone(v); r } else value end end # deep merging b into a (a deeply changed) def self.deep_merge!(a, b, allowed_keys = nil) b.each do |k, v| next if allowed_keys && !allowed_keys.include?(k) if a[k].is_a?(Hash) && v.is_a?(Hash) deep_merge!(a[k], v) else a[k] = v end end a end D1 = '%H:%M' D2 = '%b%d' def self.human_time(unix_time) time = Time.at(unix_time.to_i) d1 = time.to_date d2 = Time.now.to_date time.strftime((d1 == d2) ? D1 : D2) end DF = '%d %b %H:%M' def self.human_time2(unix_time) Time.at(unix_time.to_i).strftime(DF) end def self.load_env(filename) content = File.read(filename) env_vars = content.split("\n") h = {} env_vars.each do |e| e = e.gsub(/#.+$/, '').strip next unless e.include?('=') k, v = e.split('=', 2) h[k] = v end h end end eye-0.7/lib/eye/system.rb0000644000004100000410000000637312600364132015401 0ustar www-datawww-datarequire 'shellwords' require 'etc' require 'timeout' module Eye::System class << self # Check that pid really exits # very fast # return result hash def check_pid_alive(pid) res = if pid ::Process.kill(0, pid) else false end {:result => res} rescue => ex {:error => ex} end # Check that pid really exits # very fast # return true/false def pid_alive?(pid) res = check_pid_alive(pid) !!res[:result] end # Send signal to process (uses for kill) # code: TERM(15), KILL(9), QUIT(3), ... def send_signal(pid, code = :TERM) code = 0 if code == '0' if code.to_s.to_i != 0 code = code.to_i code = -code if code < 0 end code = code.to_s.upcase if code.is_a?(String) || code.is_a?(Symbol) if pid ::Process.kill(code, pid) {:result => :ok} else {:error => Exception.new('no_pid')} end rescue => ex {:error => ex} end # Daemonize cmd, and detach # options: # :pid_file # :working_dir # :environment # :stdin, :stdout, :stderr def daemonize(cmd, cfg = {}) pid = ::Process::spawn(prepare_env(cfg), *Shellwords.shellwords(cmd), spawn_options(cfg)) {:pid => pid, :exitstatus => 0} rescue Errno::ENOENT, Errno::EACCES => ex {:error => ex} ensure Process.detach(pid) if pid end # Execute cmd with blocking, return status (be careful: inside actor blocks it mailbox, use with defer) # options # :working_dir # :environment # :stdin, :stdout, :stderr def execute(cmd, cfg = {}) pid = ::Process::spawn(prepare_env(cfg), *Shellwords.shellwords(cmd), spawn_options(cfg)) timeout = cfg[:timeout] || 1.second status = 0 Timeout.timeout(timeout) do _, st = Process.waitpid2(pid) status = st.exitstatus || st.termsig end {:pid => pid, :exitstatus => status} rescue Timeout::Error => ex if pid warn "[#{cfg[:name]}] sending :KILL signal to <#{pid}> due to timeout (#{timeout}s)" send_signal(pid, 9) end {:error => ex} rescue Errno::ENOENT, Errno::EACCES => ex {:error => ex} ensure Process.detach(pid) if pid end # normalize file def normalized_file(file, working_dir = nil) File.expand_path(file, working_dir) end def spawn_options(config = {}) options = { pgroup: true, chdir: config[:working_dir] || '/' } options[:out] = [config[:stdout], 'a'] if config[:stdout] options[:err] = [config[:stderr], 'a'] if config[:stderr] options[:in] = config[:stdin] if config[:stdin] options[:umask] = config[:umask] if config[:umask] options[:close_others] = false if config[:preserve_fds] options[:unsetenv_others] = true if config[:clear_env] if Eye::Local.root? options[:uid] = Etc.getpwnam(config[:uid]).uid if config[:uid] options[:gid] = Etc.getgrnam(config[:gid]).gid if config[:gid] end options end def prepare_env(config = {}) env = {} (config[:environment] || {}).each do |k,v| env[k.to_s] = v && v.to_s end env end end end eye-0.7/lib/eye/loader.rb0000644000004100000410000000034612600364132015315 0ustar www-datawww-data# add gems to $: by `gem` method # this is only way when install eye as system wide gem 'celluloid', '~> 0.16.0' gem 'celluloid-io', '~> 0.16.0' gem 'nio4r' gem 'timers' gem 'state_machine' gem 'sigar', '~> 0.7.2' eye-0.7/lib/eye/group/0000755000004100000410000000000012600364132014653 5ustar www-datawww-dataeye-0.7/lib/eye/group/chain.rb0000644000004100000410000000450412600364132016265 0ustar www-datawww-datamodule Eye::Group::Chain private def chain_schedule(type, grace, command, *args) info "starting #{type} with #{grace}s chain #{command} #{args}" @chain_processes_count = @processes.size @chain_processes_current = 0 @chain_breaker = false started_at = Time.now @processes.each do | process | if process.skip_group_action?(command) @chain_processes_current = @chain_processes_current.to_i + 1 next end chain_schedule_process(process, type, command, *args) @chain_processes_current = @chain_processes_current.to_i + 1 # to skip last sleep break if @chain_processes_current.to_i == @chain_processes_count.to_i break if @chain_breaker # wait next process sleep grace.to_f break if @chain_breaker end debug { "chain finished #{Time.now - started_at}s" } @chain_processes_count = nil @chain_processes_current = nil end def chain_schedule_process(process, type, command, *args) debug { "chain_schedule_process #{process.name} #{type} #{command}" } if type == :sync # sync command, with waiting # this is very hackety, because call method of the process without its scheduler # need to provide some scheduler future process.last_scheduled_reason = self.last_scheduled_reason process.send(command, *args) else # async command process.send_command(command, *args) end end def chain_status if @config[:chain] [:start, :restart].map{|c| @config[:chain][c].try(:[], :grace) } end end def chain_command(command, *args) chain_opts = chain_options(command) chain_schedule(chain_opts[:type], chain_opts[:grace], command, *args) end # with such delay will chained processes by default DEFAULT_CHAIN = 0.2 def chain_options(command) command = :start if command == :monitor # hack for monitor command, work as start if @config[:chain] && @config[:chain][command] type = @config[:chain][command].try :[], :type type = [:async, :sync].include?(type) ? type : :async grace = @config[:chain][command].try :[], :grace grace = grace ? (grace.to_f rescue DEFAULT_CHAIN) : DEFAULT_CHAIN {:type => type, :grace => grace} else # default chain case {:type => :async, :grace => DEFAULT_CHAIN} end end end eye-0.7/lib/eye/reason.rb0000644000004100000410000000046712600364132015342 0ustar www-datawww-dataclass Eye::Reason def initialize(mes = nil) @message = mes end def to_s @message.to_s end def user? self.class == User end class User < Eye::Reason def to_s "#{super} by user" end end class Flapping < Eye::Reason; end class StartingGuard < Eye::Reason; end endeye-0.7/lib/eye/checker.rb0000644000004100000410000001343312600364132015454 0ustar www-datawww-dataclass Eye::Checker include Eye::Dsl::Validation autoload :Memory, 'eye/checker/memory' autoload :Cpu, 'eye/checker/cpu' autoload :Http, 'eye/checker/http' autoload :FileCTime, 'eye/checker/file_ctime' autoload :FileSize, 'eye/checker/file_size' autoload :FileTouched,'eye/checker/file_touched' autoload :Socket, 'eye/checker/socket' autoload :SslSocket, 'eye/checker/ssl_socket' autoload :Nop, 'eye/checker/nop' autoload :Runtime, 'eye/checker/runtime' autoload :Cputime, 'eye/checker/cputime' autoload :ChildrenCount, 'eye/checker/children_count' autoload :ChildrenMemory,'eye/checker/children_memory' TYPES = {:memory => 'Memory', :cpu => 'Cpu', :http => 'Http', :ctime => 'FileCTime', :fsize => 'FileSize', :file_touched => 'FileTouched', :socket => 'Socket', :nop => 'Nop', :runtime => 'Runtime', :cputime => 'Cputime', :children_count => "ChildrenCount", :children_memory => "ChildrenMemory", :ssl_socket => 'SslSocket' } attr_accessor :value, :values, :options, :pid, :type, :check_count, :process param :every, [Fixnum, Float], false, 5 param :times, [Fixnum, Array], nil, 1 param :fires, [Symbol, Array], nil, nil, [:stop, :restart, :unmonitor, :start, :delete, :nothing, :notify] param :initial_grace, [Fixnum, Float] param :skip_initial_fails, [TrueClass, FalseClass] def self.name_and_class(type) type = type.to_sym return {:name => type, :type => type} if TYPES[type] if type =~ /\A(.*?)_?[0-9]+\z/ ctype = $1.to_sym return {:name => type, :type => ctype} if TYPES[ctype] end end def self.get_class(type) klass = eval("Eye::Checker::#{TYPES[type]}") rescue nil raise "Unknown checker #{type}" unless klass if deps = klass.requires Array(deps).each { |d| require d } end klass end def self.create(pid, options = {}, process = nil) get_class(options[:type]).new(pid, options, process) rescue Exception, Timeout::Error => ex log_ex(ex) nil end def self.validate!(options) get_class(options[:type]).validate(options) end def initialize(pid, options = {}, process = nil) @process = process @pid = pid @options = options.dup @type = options[:type] @full_name = @process.full_name if @process @initialized_at = Time.now debug { "create checker, with #{options}" } @value = nil @values = Eye::Utils::Tail.new(max_tries) @check_count = 0 end def inspect "<#{self.class} @process='#{@full_name}' @options=#{@options} @pid=#{@pid}>" end def logger_tag @process.logger.prefix if @process end def logger_sub_tag "check:#{check_name}" end def last_human_values h_values = @values.map do |v| sign = v[:good] ? '' : '*' sign + human_value(v[:value]).to_s end '[' + h_values * ', ' + ']' end def check if initial_grace && (Time.now - @initialized_at < initial_grace) debug { 'skipped initial grace' } return true else @options[:initial_grace] = nil end @value = get_value_safe @good_value = good?(value) @values << {:value => @value, :good => @good_value} result = true @check_count += 1 if @values.size == max_tries bad_count = @values.count{|v| !v[:good] } result = false if bad_count >= min_tries end if skip_initial_fails if @good_value @options[:skip_initial_fails] = nil else result = true end end info "#{last_human_values} => #{result ? 'OK' : 'Fail'}" result rescue Exception, Timeout::Error => ex log_ex(ex) end def get_value_safe get_value end def get_value raise NotImplementedError end def human_value(value) value.to_s end # true if check ok # false if check bad def good?(value) value end def check_name @check_name ||= @type.to_s end def max_tries @max_tries ||= if times if times.is_a?(Array) times[-1].to_i else times.to_i end else 1 end end def min_tries @min_tries ||= if times if times.is_a?(Array) times[0].to_i else max_tries end else max_tries end end def previous_value @values[-1][:value] if @values.present? end def run_in_process_context(p) process.instance_exec(&p) if process.alive? end def fire actions = fires ? Array(fires) : [:restart] process.notify :warn, "Bounded #{check_name}: #{last_human_values} send to #{actions}" actions.each do |action| process.schedule action, Eye::Reason.new("bounded #{check_name}") end end def defer(&block) Celluloid::Future.new(&block).value end class Defer < Eye::Checker def get_value_safe Celluloid::Future.new{ get_value }.value end end def self.register(base) name = base.to_s.gsub('Eye::Checker::', '') type = name.underscore.to_sym Eye::Checker::TYPES[type] = name Eye::Checker.const_set(name, base) end def self.requires end class CustomCell < Eye::Checker def self.inherited(base) super register(base) end end class Custom < Defer def self.inherited(base) super register(base) end end class CustomDefer < Defer def self.inherited(base) super register(base) end end class Measure < Eye::Checker param :below, [Fixnum, Float] param :above, [Fixnum, Float] def good?(value) return false if below && (value > below) return false if above && (value < above) true end def measure_str if below && above ">#{human_value(above)}<#{human_value(below)}" elsif below "<#{human_value(below)}" elsif above ">#{human_value(above)}" else '-' end end end end eye-0.7/lib/eye/process.rb0000644000004100000410000000414112600364132015522 0ustar www-datawww-datarequire 'celluloid' class Eye::Process include Celluloid autoload :Config, 'eye/process/config' autoload :Commands, 'eye/process/commands' autoload :Data, 'eye/process/data' autoload :Watchers, 'eye/process/watchers' autoload :Monitor, 'eye/process/monitor' autoload :System, 'eye/process/system' autoload :Controller, 'eye/process/controller' autoload :StatesHistory, 'eye/process/states_history' autoload :Children, 'eye/process/children' autoload :Trigger, 'eye/process/trigger' autoload :Notify, 'eye/process/notify' autoload :Scheduler, 'eye/process/scheduler' autoload :Validate, 'eye/process/validate' attr_accessor :pid, :parent_pid, :watchers, :config, :states_history, :children, :triggers, :name, :state_reason def initialize(config) raise 'you must supply a pid_file location' unless config[:pid_file] @config = prepare_config(config) @watchers = {} @children = {} @triggers = [] @name = @config[:name] @states_history = Eye::Process::StatesHistory.new(100) @states_history << :unmonitored debug { "creating with config: #{@config.inspect}" } add_triggers super() # for statemachine end # c(), self[] include Eye::Process::Config # full_name, status_data include Eye::Process::Data # commands: # start_process, stop_process, restart_process include Eye::Process::Commands # start, stop, restart, monitor, unmonit, delete include Eye::Process::Controller # add_watchers, remove_watchers: include Eye::Process::Watchers # check alive, crash methods: include Eye::Process::Monitor # system methods: include Eye::Process::System # manage child methods include Eye::Process::Children # manage triggers methods include Eye::Process::Trigger # manage notify methods include Eye::Process::Notify # scheduler include Eye::Process::Scheduler # validate extend Eye::Process::Validate end # include state_machine states require_relative 'process/states' eye-0.7/lib/eye/controller.rb0000644000004100000410000000222212600364132016225 0ustar www-datawww-datarequire 'celluloid' require 'yaml' require_relative 'utils/pmap' require_relative 'utils/leak_19' require_relative 'utils/mini_active_support' # Extend all objects with logger Object.send(:include, Eye::Logger::ObjectExt) # needs to preload Eye::Sigar Eye::SystemResources class Eye::Controller include Celluloid autoload :Load, 'eye/controller/load' autoload :Helpers, 'eye/controller/helpers' autoload :Commands, 'eye/controller/commands' autoload :Status, 'eye/controller/status' autoload :SendCommand, 'eye/controller/send_command' autoload :Options, 'eye/controller/options' include Eye::Controller::Load include Eye::Controller::Helpers include Eye::Controller::Commands include Eye::Controller::Status include Eye::Controller::SendCommand include Eye::Controller::Options attr_reader :applications, :current_config def initialize @applications = [] @current_config = Eye::Config.new Celluloid::logger = Eye::Logger.new('celluloid') info "starting #{Eye::ABOUT} <#{$$}>" end def settings current_config.settings end def logger_tag 'Eye' end end eye-0.7/lib/eye/logger.rb0000644000004100000410000000455012600364132015327 0ustar www-datawww-datarequire 'logger' class Eye::Logger attr_accessor :prefix, :subprefix class InnerLogger < Logger FORMAT = '%d.%m.%Y %H:%M:%S' def initialize(*args) super self.formatter = Proc.new do |s, d, p, m| "#{d.strftime(FORMAT)} #{s.ljust(5)} -- #{m}\n" end end end module ObjectExt def logger_tag [Class, Module].include?(self.class) ? to_s : "<#{self.class.to_s}>" end def logger_sub_tag end def logger @logger ||= Eye::Logger.new(logger_tag, logger_sub_tag) end Logger::Severity.constants.each do |level| method_name = level.to_s.downcase define_method method_name do |msg = nil, &block| logger.send(method_name, msg, &block) end end def log_ex(ex) error "#{ex.message} #{ex.backtrace}" # notify here? end end Logger::Severity.constants.each do |level| method_name = level.to_s.downcase define_method method_name do |msg = nil, &block| if block self.class.inner_logger.send(method_name) { "#{prefix_str}#{block.call}" } else self.class.inner_logger.send(method_name, "#{prefix_str}#{msg}") end end end def initialize(prefix = nil, subprefix = nil) @prefix = prefix @subprefix = subprefix end class << self attr_reader :dev, :log_level, :args def link_logger(dev, *args) old_dev = @dev @dev = @dev_fd = dev @args = args if dev.nil? @inner_logger = InnerLogger.new(nil) elsif dev.is_a?(String) @dev_fd = STDOUT if @dev.to_s.downcase == 'stdout' @dev_fd = STDERR if @dev.to_s.downcase == 'stderr' @inner_logger = InnerLogger.new(@dev_fd, *args) else @inner_logger = dev end @inner_logger.level = self.log_level || Logger::INFO rescue Exception @inner_logger = nil @dev = old_dev raise end def reopen link_logger(dev, *args) end def log_level=(level) @log_level = level @inner_logger.level = self.log_level if @inner_logger end def inner_logger @inner_logger ||= InnerLogger.new(nil) end end private def prefix_str @pref_string ||= begin pref_string = '' if @prefix pref_string = "[#{@prefix}] " pref_string += "#{@subprefix} " if @subprefix end pref_string end end end eye-0.7/lib/eye/notify.rb0000644000004100000410000000472612600364132015365 0ustar www-datawww-datarequire 'celluloid' class Eye::Notify include Celluloid include Eye::Dsl::Validation autoload :Mail, 'eye/notify/mail' autoload :Jabber, 'eye/notify/jabber' autoload :Slack, 'eye/notify/slack' TYPES = {:mail => 'Mail', :jabber => 'Jabber', :slack => 'Slack'} def self.get_class(type) klass = eval("Eye::Notify::#{TYPES[type]}") rescue nil raise "unknown notifier :#{type}" unless klass if deps = klass.requires Array(deps).each { |d| require d } end klass end def self.validate!(options) get_class(options[:type]).validate(options) end def self.notify(contact, message_h) contact = contact.to_s settings = Eye::Control.settings needed_hash = (settings[:contacts] || {})[contact] if needed_hash.blank? error "contact #{contact} not found; check your configuration" return end create_proc = lambda do |nh| type = nh[:type] config = (settings[type] || {}).merge(nh[:opts] || {}).merge(:contact => nh[:contact]) klass = get_class(type) notify = klass.new(config, message_h) notify.async_notify if notify end if needed_hash.is_a?(Array) needed_hash.each{|nh| create_proc[nh] } else create_proc[needed_hash] end rescue Exception, Timeout::Error => ex log_ex(ex) end TIMEOUT = 1 * 60 def initialize(options = {}, message_h = {}) @message_h = message_h @options = options debug { "created notifier #{options}" } end def logger_sub_tag @options[:contact] end def async_notify async.notify after(TIMEOUT){ terminate } end def notify debug { "start notify #{@message_h}" } execute debug { "end notify #{@message_h}" } terminate end def execute raise NotImplementedError end param :contact, [String] def message_subject "[#{msg_host}] [#{msg_full_name}] #{msg_message}" end def message_body "#{message_subject} at #{Eye::Utils.human_time2(msg_at)}" end def self.register(base) name = base.to_s.gsub('Eye::Notify::', '') type = name.underscore.to_sym Eye::Notify::TYPES[type] = name Eye::Notify.const_set(name, base) Eye::Dsl::ConfigOpts.add_notify(type) end def self.requires end class Custom < Eye::Notify def self.inherited(base) super register(base) end end %w{at host message name full_name pid level}.each do |name| define_method("msg_#{name}") do @message_h[name.to_sym] end end end eye-0.7/lib/eye/client.rb0000644000004100000410000000107512600364132015325 0ustar www-datawww-datarequire 'socket' require 'timeout' class Eye::Client attr_reader :socket_path def initialize(socket_path) @socket_path = socket_path end def command(cmd, *args) attempt_command(Marshal.dump([cmd, *args])) end def attempt_command(pack) Timeout.timeout(Eye::Local.client_timeout) { send_request(pack) } rescue Timeout::Error, EOFError :timeouted end def send_request(pack) UNIXSocket.open(@socket_path) do |socket| socket.write(pack) data = socket.read Marshal.load(data) rescue :corrupted_data end end end eye-0.7/lib/eye/cli.rb0000644000004100000410000001274512600364132014624 0ustar www-datawww-data# encoding: utf-8 gem 'thor' require 'thor' class Eye::Cli < Thor autoload :Server, 'eye/cli/server' autoload :Commands, 'eye/cli/commands' autoload :Render, 'eye/cli/render' include Eye::Cli::Server include Eye::Cli::Commands include Eye::Cli::Render desc "info [MASK]", "processes info" method_option :json, :type => :boolean, :aliases => "-j" def info(mask = nil) res = cmd(:info_data, *Array(mask)) if mask && res[:subtree] && res[:subtree].empty? error!("command :info, objects not found!") end if options[:json] require 'json' say JSON.dump(res) else say render_info(res) say end end desc "status NAME", "return exit status for process name 0-up, 3-unmonitored" def status(name) res = cmd(:info_data, *Array(name)) es, msg = render_status(res) say(msg, :red) if msg && !msg.empty? exit(es) end desc "xinfo", "eye-deamon info (-c show current config)" method_option :config, :type => :boolean, :aliases => "-c" def xinfo res = cmd(:debug_data, :config => options[:config]) say render_debug_info(res) say end desc "oinfo", "onelined info" def oinfo(mask = nil) res = cmd(:short_data, *Array(mask)) say render_info(res) say end desc "history [MASK,...]", "processes history" def history(*masks) res = cmd(:history_data, *masks) if !masks.empty? && res && res.empty? error!("command :history, objects not found!") end say render_history(res) say end desc "load [CONF, ...]", "load config (run eye-daemon if not) (-f foreground load)" method_option :foreground, :type => :boolean, :aliases => "-f" def load(*configs) configs.map!{ |c| File.expand_path(c) } if !configs.empty? if options[:foreground] # in foreground we stop another server, and run just 1 current config version error!("foreground expected only one config") if configs.size > 1 server_start_foreground(configs.first) elsif server_started? configs << Eye::Local.eyefile if Eye::Local.local_runner say_load_result cmd(:load, *configs) else server_start(configs) end end desc "quit", "eye-daemon quit" method_option :stop_all, :type => :boolean, :aliases => "-s" method_option :timeout, :type => :string, :aliases => "-t", :default => "600" def quit if options[:stop_all] Eye::Local.client_timeout = options[:timeout].to_i cmd(:stop_all, options[:timeout].to_i) end Eye::Local.client_timeout = Eye::Local.default_client_timeout res = _cmd(:quit) # if eye server got crazy, stop by force ensure_stop_previous_server if res != :corrupted_data # remove pid_file File.delete(Eye::Local.pid_path) if File.exist?(Eye::Local.pid_path) say "Quit ಠ╭╮ಠ", :yellow end [:start, :stop, :restart, :unmonitor, :monitor, :delete, :match].each do |command| desc "#{command} MASK[,...]", "#{command} app,group or process" define_method(command) do |*masks| send_command(command, *masks) end end desc "force_restart MASK[,...]", "restart by stop;start (not by restart_command)" def force_restart(*masks) send_command(:stop, *masks) send_command(:start, *masks) end desc "signal SIG MASK[,...]", "send signal to app,group or process" def signal(sig, *masks) send_command(:signal, sig, *masks) end desc "break MASK[,...]", "break chain executing" def break(*masks) send_command(:break_chain, *masks) end desc "trace [MASK]", "tracing log(tail + grep) for app,group or process" def trace(mask = "") log_trace(mask) end map ["-v", "--version"] => :version desc "version", "version" def version say Eye::ABOUT end desc "check CONF", "check config file syntax" method_option :host, :type => :string, :aliases => "-h" method_option :verbose, :type => :boolean, :aliases => "-v" def check(conf) conf = File.expand_path(conf) if conf && !conf.empty? Eye::Local.host = options[:host] if options[:host] Eye::Dsl.verbose = options[:verbose] say_load_result Eye::Controller.new.check(conf), :syntax => true end desc "explain CONF", "explain config tree" method_option :host, :type => :string, :aliases => "-h" method_option :verbose, :type => :boolean, :aliases => "-v" def explain(conf) conf = File.expand_path(conf) if conf && !conf.empty? Eye::Local.host = options[:host] if options[:host] Eye::Dsl.verbose = options[:verbose] say_load_result Eye::Controller.new.explain(conf), :print_config => true, :syntax => true end desc "watch [MASK]", "interactive processes info" def watch(*args) error!("You should install watch utility") if `which watch`.empty? cmd = if `watch --version 2>&1`.chop > '0.2.0' "watch -n 1 --color #{$0} i #{args * ' '}" else "watch -n 1 #{$0} i #{args * ' '}" end pid = Process.spawn(cmd) Process.waitpid(pid) rescue Interrupt end desc "user_command CMD [MASK]", "execute user_command (dsl command)" def user_command(cmd, *args) send_command(:user_command, cmd, *args) end private def error!(msg) say msg, :red exit 1 end def print(msg, new_line = true) say msg if msg && !msg.empty? say if new_line end def log_trace(tag = '') log_file = cmd(:logger_dev) if log_file && File.exist?(log_file) Process.exec "tail -n 100 -f #{log_file} | grep '#{tag}'" else error! "log file not found #{log_file.inspect}" end end def self.exit_on_failure? true end end eye-0.7/lib/eye/local.rb0000644000004100000410000000351012600364132015135 0ustar www-datawww-datarequire 'fileutils' module Eye::Local class << self def dir @dir ||= begin if root? '/var/run/eye' else File.expand_path(File.join(home, '.eye')) end end end attr_writer :dir, :client_timeout, :host def global_eyeconfig '/etc/eye.conf' end def eyeconfig File.expand_path(File.join(home, '.eyeconfig')) end def root? Process::UID.eid == 0 end def home h = ENV['EYE_HOME'] || ENV['HOME'] raise "HOME undefined, should be HOME or EYE_HOME environment" unless h h end def path(path) File.expand_path(path, dir) end def ensure_eye_dir FileUtils.mkdir_p(dir) end def socket_path path(ENV['EYE_SOCK'] || "sock#{ENV['EYE_V']}") end def pid_path path(ENV['EYE_PID'] || "pid#{ENV['EYE_V']}") end def cache_path path("processes#{ENV['EYE_V']}.cache") end def default_client_timeout (ENV['EYE_CLIENT_TIMEOUT'] || 5).to_i end def client_timeout @client_timeout ||= default_client_timeout end def supported_setsid? RUBY_VERSION >= '2.0' end def host @host ||= begin require 'socket' Socket.gethostname end end def eyefile @eyefile ||= find_eyefile('.') end def find_eyefile(start_from_dir) fromenv = ENV['EYE_FILE'] return fromenv if fromenv && !fromenv.empty? && File.file?(fromenv) previous = nil current = File.expand_path(start_from_dir) until !File.directory?(current) || current == previous filename = File.join(current, 'Eyefile') return filename if File.file?(filename) current, previous = File.expand_path('..', current), current end end attr_accessor :local_runner end end eye-0.7/lib/eye/trigger/0000755000004100000410000000000012600364132015162 5ustar www-datawww-dataeye-0.7/lib/eye/trigger/check_dependency.rb0000644000004100000410000000136512600364132020767 0ustar www-datawww-dataclass Eye::Trigger::CheckDependency < Eye::Trigger param :names, [Array], true, 5 def check(transition) check_dependency(transition.to_name) if transition.from_name == :up end private def check_dependency(to) processes = names.map do |name| Eye::Control.find_nearest_process(name, process.group_name_pure, process.app_name) end.compact.select { |p| p.state_name != :unmonitored } return if processes.empty? processes = Eye::Utils::AliveArray.new(processes) act = case to when :down, :restarting; :restart when :stopping; :stop when :unmonitored; :unmonitor end if act processes.each do |p| p.schedule act, Eye::Reason.new(:"#{act} dependecies") end end end end eye-0.7/lib/eye/trigger/flapping.rb0000644000004100000410000000216712600364132017315 0ustar www-datawww-dataclass Eye::Trigger::Flapping < Eye::Trigger # trigger :flapping, :times => 10, :within => 1.minute, # :retry_in => 10.minutes, :retry_times => 15 param :times, [Fixnum], true, 5 param :within, [Float, Fixnum], true param :retry_in, [Float, Fixnum] param :retry_times, [Fixnum] def initialize(*args) super @flapping_times = 0 end def check(transition) on_flapping if transition.event == :crashed && !good? end private def good? states = process.states_history.states_for_period( within, @last_at ) down_count = states.count{|st| st == :down } if down_count >= times @last_at = process.states_history.last_state_changed_at false else true end end def on_flapping debug { 'flapping recognized!!!' } process.notify :error, 'flapping!' process.schedule :unmonitor, Eye::Reason::Flapping.new(:flapping) return unless retry_in return if retry_times && @flapping_times >= retry_times @flapping_times += 1 process.schedule_in(retry_in.to_f, :conditional_start, Eye::Reason::Flapping.new('retry start after flapping')) end end eye-0.7/lib/eye/trigger/starting_guard.rb0000644000004100000410000000366212600364132020533 0ustar www-datawww-dataclass Eye::Trigger::StartingGuard < Eye::Trigger # check that process ready to start or not # by custom user condition # if not, process switched to :unmonitored, and then retry to start after :every interval # # trigger :starting_guard, every: 10.seconds, should: ->{ `cat /tmp/bla` == "bla" } param :every, [Float, Fixnum], false, 10 param :times, [Fixnum] param :retry_in, [Float, Fixnum] param :retry_times, [Fixnum] param :should, [Proc, Symbol] def initialize(*args) super @retry_count = 0 @reretry_count = 0 end def check(transition) check_start if transition.to_name == :starting end def check_start @retry_count += 1 condition = defer { exec_proc(:should) } if condition info "ok, process ready to start #{condition.inspect}" @retry_count = 0 @reretry_count = 0 return else info "false executed condition" end new_time = nil if every if times if (@retry_count < times) new_time = Time.now + every process.schedule_in every, :conditional_start, Eye::Reason::StartingGuard.new("starting_guard, retry start") else @retry_count = 0 @reretry_count += 1 if retry_in && (!retry_times || (@reretry_count < retry_times)) new_time = Time.now + retry_in process.schedule_in retry_in, :conditional_start, Eye::Reason::StartingGuard.new("starting_guard, reretry start") end end else new_time = Time.now + every process.schedule_in every, :conditional_start, Eye::Reason::StartingGuard.new("starting_guard, retry start") end end retry_msg = new_time ? ", retry at '#{Eye::Utils.human_time2(new_time.to_i)}'" : '' process.switch :unmonitoring, Eye::Reason::StartingGuard.new("starting_guard, failed condition#{retry_msg}") raise Eye::Process::StateError.new("starting_guard, refused to start") end end eye-0.7/lib/eye/trigger/stop_children.rb0000644000004100000410000000057412600364132020352 0ustar www-datawww-dataclass Eye::Trigger::StopChildren < Eye::Trigger # Kill process children when parent process crashed, or stopped: # # trigger :stop_children param :timeout, [Fixnum, Float], nil, 60 # default on stopped, crashed param_default :event, [:stopped, :crashed] def check(trans) debug { 'stopping children' } process.children.pmap { |pid, c| c.stop } end end eye-0.7/lib/eye/trigger/wait_dependency.rb0000644000004100000410000000240312600364132020650 0ustar www-datawww-dataclass Eye::Trigger::WaitDependency < Eye::Trigger param :names, [Array], true param :wait_timeout, [Numeric], nil, 15.seconds param :retry_after, [Numeric], nil, 1.minute param :should_start, [TrueClass, FalseClass] def check(transition) wait_dependency if transition.to_name == :starting end private def wait_dependency processes = names.map do |name| Eye::Control.find_nearest_process(name, process.group_name_pure, process.app_name) end.compact return if processes.empty? processes = Eye::Utils::AliveArray.new(processes) processes.each do |p| if p.state_name != :up && (should_start == nil || should_start) p.schedule :start, Eye::Reason.new(:start_dependency) end end res = true processes.pmap do |p| name = p.name res &= process.wait_for_condition(wait_timeout, 0.5) do info "wait for #{name} until it :up" p.state_name == :up end end unless res warn "not waited for #{names} to be up" process.switch :unmonitoring if retry_after process.schedule_in retry_after, :start, Eye::Reason.new(:wait_dependency) end raise Eye::Process::StateError.new('stop transition because dependency is not up') end end end eye-0.7/lib/eye/trigger/transition.rb0000644000004100000410000000030712600364132017701 0ustar www-datawww-dataclass Eye::Trigger::Transition < Eye::Trigger # trigger :transition, :to => :up, :from => :starting, :do => ->{ ... } param :do, [Proc, Symbol] def check(trans) exec_proc :do end end eye-0.7/lib/eye/checker/0000755000004100000410000000000012600364132015123 5ustar www-datawww-dataeye-0.7/lib/eye/checker/cpu.rb0000644000004100000410000000046512600364132016244 0ustar www-datawww-dataclass Eye::Checker::Cpu < Eye::Checker::Measure # check :cpu, :every => 3.seconds, :below => 80, :times => [3,5] def check_name @check_name ||= "cpu(#{measure_str})" end def get_value Eye::SystemResources.cpu(@pid).to_i # nil => 0 end def human_value(value) "#{value}%" end end eye-0.7/lib/eye/checker/ssl_socket.rb0000644000004100000410000000145012600364132017621 0ustar www-datawww-datarequire 'openssl' class Eye::Checker::SslSocket < Eye::Checker::Socket param :ctx, Hash, nil, {ssl_version: :SSLv23, verify_mode: OpenSSL::SSL::VERIFY_NONE} # other params inherits from socket check # # examples: # # check :ssl_socket, :addr => "tcp://127.0.0.1:443", :every => 5.seconds, :times => 1, :timeout => 1.second, # :ctx => {ssl_version: :SSLv23, verify_mode: OpenSSL::SSL::VERIFY_NONE} # # # ctx_params from http://ruby-doc.org/stdlib-1.9.3/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html private def open_socket OpenSSL::SSL::SSLSocket.new(super, ctx_params).tap do |socket| socket.sync_close = true socket.connect end end def ctx_params @ctx_params ||= OpenSSL::SSL::SSLContext.new().tap { |c| c.set_params(ctx) } end end eye-0.7/lib/eye/checker/file_ctime.rb0000644000004100000410000000103112600364132017543 0ustar www-datawww-dataclass Eye::Checker::FileCTime < Eye::Checker # Check that file changes (log for example) # check :ctime, :every => 5.seconds, :file => "/tmp/1.log", :times => [3,5] param :file, [String], true def initialize(*args) super self.file = process.expand_path(file) if process && file end def get_value File.ctime(file) rescue nil end def human_value(value) if value == nil 'Err' else value.strftime('%H:%M') end end def good?(value) value.to_i > previous_value.to_i end end eye-0.7/lib/eye/checker/children_memory.rb0000644000004100000410000000047312600364132020634 0ustar www-datawww-dataclass Eye::Checker::ChildrenMemory < Eye::Checker::Measure # check :children_memory, :every => 30.seconds, :below => 400.megabytes # monitor_children should be enabled def get_value process.children.values.inject(0) do |sum, ch| sum + Eye::SystemResources.memory(ch.pid).to_i end end end eye-0.7/lib/eye/checker/runtime.rb0000644000004100000410000000046412600364132017137 0ustar www-datawww-dataclass Eye::Checker::Runtime < Eye::Checker::Measure # check :runtime, :every => 1.minute, :below => 120.minutes def get_value st = Eye::SystemResources.start_time(@pid) if st Time.now.to_i - st.to_i else 0 end end def human_value(value) "#{value / 60}m" end end eye-0.7/lib/eye/checker/memory.rb0000644000004100000410000000052412600364132016761 0ustar www-datawww-dataclass Eye::Checker::Memory < Eye::Checker::Measure # check :memory, :every => 3.seconds, :below => 80.megabytes, :times => [3,5] def check_name @check_name ||= "memory(#{measure_str})" end def get_value Eye::SystemResources.memory(@pid).to_i end def human_value(value) "#{value.to_i / 1024 / 1024}Mb" end end eye-0.7/lib/eye/checker/cputime.rb0000644000004100000410000000036012600364132017115 0ustar www-datawww-dataclass Eye::Checker::Cputime < Eye::Checker::Measure # check :cputime, :every => 1.minute, :below => 120.minutes def get_value Eye::SystemResources.cputime(@pid).to_f end def human_value(value) "#{value / 60}m" end end eye-0.7/lib/eye/checker/file_size.rb0000644000004100000410000000144212600364132017422 0ustar www-datawww-dataclass Eye::Checker::FileSize < Eye::Checker::Measure # Check that file size changed (log for example) # check :fsize, :every => 5.seconds, :file => "/tmp/1.log", :times => [3,5], # :below => 30.kilobytes, :above => 10.kilobytes param :file, [String], true def initialize(*args) super self.file = process.expand_path(file) if process && file end def check_name @check_name ||= "fsize(#{measure_str})" end def get_value File.size(file) rescue nil end def human_value(value) "#{value.to_i / 1024}Kb" end def good?(value) return true unless previous_value diff = value.to_i - previous_value.to_i return true if diff < 0 # case when logger nulled return false unless super(diff) return false if diff == 0 true end end eye-0.7/lib/eye/checker/nop.rb0000644000004100000410000000020412600364132016240 0ustar www-datawww-dataclass Eye::Checker::Nop < Eye::Checker # check :nop, :every => 10.hours # means restart every 10 hours def get_value; end end eye-0.7/lib/eye/checker/children_count.rb0000644000004100000410000000167112600364132020455 0ustar www-datawww-dataclass Eye::Checker::ChildrenCount < Eye::Checker::Measure # check :children_count, :every => 30.seconds, :below => 10, :strategy => :kill_old # monitor_children should be enabled param :strategy, Symbol, nil, :restart, [:restart, :kill_old, :kill_new] def get_value process.children.size end def fire if strategy == :restart super else pids = ordered_by_date_children_pids pids = (strategy == :kill_old) ? pids[0...-below] : pids[below..-1] kill_pids(pids) end end private def kill_pids(pids) info "killing pids: #{pids.inspect} for strategy: #{strategy}" pids.each do |pid| if child = process.children[pid] child.schedule :stop, Eye::Reason.new("bounded #{check_name}") end end end def ordered_by_date_children_pids children = process.children.values children.sort_by { |ch| Eye::SystemResources.start_time(ch.pid).to_i }.map &:pid end end eye-0.7/lib/eye/checker/http.rb0000644000004100000410000000517212600364132016434 0ustar www-datawww-datarequire 'net/http' class Eye::Checker::Http < Eye::Checker::Defer # check :http, :every => 5.seconds, :times => 1, # :url => "http://127.0.0.1:3000/", :kind => :success, :pattern => /OK/, :timeout => 3.seconds param :url, String, true param :proxy_url, String param :pattern, [String, Regexp] param :kind, [String, Fixnum, Symbol] param :timeout, [Fixnum, Float] param :open_timeout, [Fixnum, Float] param :read_timeout, [Fixnum, Float] attr_reader :uri def initialize(*args) super @uri = URI.parse(url) @proxy_uri = URI.parse(proxy_url) if proxy_url @kind = case kind when Fixnum then Net::HTTPResponse::CODE_TO_OBJ[kind.to_s] when String, Symbol then Net.const_get("HTTP#{kind.to_s.camelize}") rescue Net::HTTPSuccess else Net::HTTPSuccess end @open_timeout = (open_timeout || 3).to_f @read_timeout = (read_timeout || timeout || 15).to_f end def get_value res = session.start{ |http| http.get(@uri.request_uri) } {:result => res} rescue Timeout::Error => ex debug { ex.inspect } if defined?(Net::OpenTimeout) # for ruby 2.0 mes = ex.is_a?(Net::OpenTimeout) ? "OpenTimeout<#{@open_timeout}>" : "ReadTimeout<#{@read_timeout}>" {:exception => mes} else {:exception => "Timeout<#{@open_timeout},#{@read_timeout}>"} end rescue => ex {:exception => "Error<#{ex.message}>"} end def good?(value) return false unless value[:result] unless value[:result].kind_of?(@kind) return false end if pattern matched = if pattern.is_a?(Regexp) pattern === value[:result].body else value[:result].body.include?(pattern.to_s) end value[:notice] = "missing '#{pattern.to_s}'" unless matched matched else true end end def human_value(value) if !value.is_a?(Hash) '-' elsif value[:exception] value[:exception] else body_size = value[:result].body.size / 1024 msg = "#{value[:result].code}=#{body_size}Kb" msg += "<#{value[:notice]}>" if value[:notice] msg end end private def session net_http.tap do |session| if @uri.scheme == 'https' require 'net/https' session.use_ssl = true session.verify_mode = OpenSSL::SSL::VERIFY_NONE end session.open_timeout = @open_timeout session.read_timeout = @read_timeout end end def net_http if @proxy_uri Net::HTTP.new(@uri.host, @uri.port, @proxy_uri.host, @proxy_uri.port) else Net::HTTP.new(@uri.host, @uri.port) end end end eye-0.7/lib/eye/checker/socket.rb0000644000004100000410000001023712600364132016743 0ustar www-datawww-dataclass Eye::Checker::Socket < Eye::Checker::Defer # check :socket, :every => 5.seconds, :times => 1, # :addr => "unix:/var/run/daemon.sock", :timeout => 3.seconds, # # Available parameters: # :addr the socket addr to open. The format is tcp://: or unix: # :timeout generic timeout for reading data from socket # :open_timeout override generic timeout for the connection # :read_timeout override generic timeout for data read/write # :send_data after connection send this data # :expect_data after sending :send_data expect this response. Can be a string, Regexp or a Proc # :protocol way of pack,unpack messages (default = socket default), example: :protocol => :em_object param :addr, String, true param :timeout, [Fixnum, Float] param :open_timeout, [Fixnum, Float] param :read_timeout, [Fixnum, Float] param :send_data param :expect_data, [String, Regexp, Proc] param :protocol, [Symbol], nil, nil, [:default, :em_object, :raw] def initialize(*args) super @open_timeout = (open_timeout || 1).to_f @read_timeout = (read_timeout || timeout || 5).to_f if addr =~ %r[\Atcp://(.*?):(.*?)\z] @socket_family = :tcp @socket_addr = $1 @socket_port = $2.to_i elsif addr =~ %r[\Aunix:(.*)\z] @socket_family = :unix @socket_path = $1 end end def get_value sock = begin Timeout::timeout(@open_timeout){ open_socket } rescue Timeout::Error return { :exception => "OpenTimeout<#{@open_timeout}>" } end if send_data begin Timeout::timeout(@read_timeout) do _write_data(sock, send_data) result = _read_data(sock) { :result => result } end rescue Timeout::Error if protocol == :raw return { :result => @buffer } else return { :exception => "ReadTimeout<#{@read_timeout}>" } end end else { :result => :listen } end rescue Exception => e { :exception => "Error<#{e.message}>" } ensure sock.close if sock end def good?(value) return false if !value[:result] if expect_data if expect_data.is_a?(Proc) match = begin !!expect_data[value[:result]] rescue Timeout::Error, Exception => ex mes = "proc match failed with '#{ex.message}'" error(mes) value[:notice] = mes return false end unless match warn "proc #{expect_data} not matched (#{value[:result].truncate(30)}) answer" value[:notice] = 'missing proc validation' end return match end return true if expect_data.is_a?(Regexp) && expect_data.match(value[:result]) return true if value[:result].to_s == expect_data.to_s warn "#{expect_data} not matched (#{value[:result].truncate(30)}) answer" value[:notice] = "missing '#{expect_data.to_s}'" return false end return true end def human_value(value) if !value.is_a?(Hash) '-' elsif value[:exception] value[:exception] else if value[:result] == :listen 'listen' else res = "#{value[:result].to_s.size}b" res += "<#{value[:notice]}>" if value[:notice] res end end end private def open_socket if @socket_family == :tcp TCPSocket.open(@socket_addr, @socket_port) elsif @socket_family == :unix UNIXSocket.open(@socket_path) else raise "Unknown socket addr #{addr}" end end def _write_data(socket, data) case protocol when :em_object data = Marshal.dump(data) socket.write([data.bytesize, data].pack('Na*')) else socket.write(data.to_s) end end def _read_data(socket) case protocol when :em_object content = '' msg_size = socket.recv(4).unpack('N')[0] rescue 0 content << socket.recv(msg_size - content.length) while content.length < msg_size if content.present? Marshal.load(content) rescue 'corrupted_marshal' end when :raw @buffer = '' loop { @buffer << socket.recv(1) } else socket.readline.chop end end end eye-0.7/lib/eye/checker/file_touched.rb0000644000004100000410000000053512600364132020105 0ustar www-datawww-dataclass Eye::Checker::FileTouched < Eye::Checker param :file, [String], true param :delete, [TrueClass, FalseClass] def initialize(*args) super self.file = process.expand_path(file) if process && file end def get_value File.exist?(file) end def good?(value) File.delete(file) if value && delete !value end end eye-0.7/lib/eye/notify/0000755000004100000410000000000012600364132015027 5ustar www-datawww-dataeye-0.7/lib/eye/notify/mail.rb0000644000004100000410000000230312600364132016274 0ustar www-datawww-datarequire 'net/smtp' class Eye::Notify::Mail < Eye::Notify # Eye.config do # mail :host => "some.host", :port => 12345, :user => "eye@some.host", :password => "123456", :domain => "some.host" # contact :vasya, :mail, "vasya@some.host" # end param :host, String, true param :port, [String, Fixnum], true param :domain, String param :user, String param :password, String param :auth, Symbol, nil, nil, [:plain, :login, :cram_md5] param :starttls, [TrueClass, FalseClass] param :from_mail, String param :from_name, String, nil, 'eye' def execute smtp end def smtp args = [host, port, domain, user, password, auth] debug { "called smtp with #{args}" } smtp = Net::SMTP.new host, port smtp.enable_starttls if starttls smtp.start(domain, user, password, auth) do |s| s.send_message(message, from_mail || user, contact) end end def message h = [] h << "From: #{from_name} <#{from_mail || user}>" if from_mail || user h << "To: <#{contact}>" h << "Subject: #{message_subject}" h << "Date: #{msg_at.httpdate}" h << "Message-Id: <#{rand(1000000000).to_s(36)}.#{$$}.#{contact}>" "#{h * "\n"}\n#{message_body}" end endeye-0.7/lib/eye/notify/slack.rb0000644000004100000410000000167512600364132016462 0ustar www-datawww-datarequire 'slack-notifier' class Eye::Notify::Slack < Eye::Notify # Eye.config do # slack :webhook_url => "http://...", :channel => "#default", :username => "eye" # contact :channel, :slack, "@channel" # end param :webhook_url, String, true param :channel, String, nil, "#default" param :username, String, nil, "eye" param :icon, String def execute debug { "send slack #{[channel, username]} - #{[contact, message_body]}" } options = { channel: channel, username: username } options[:icon_emoji] = icon if icon && icon.start_with?(':') options[:icon_url] = icon if icon && icon.start_with?('http') notifier = ::Slack::Notifier.new webhook_url, options notifier.ping message_body end def message_body payload = '' payload << "#{contact}: *#{msg_host}* _#{msg_full_name}_ at #{Eye::Utils.human_time2(msg_at)}\n" payload << "> #{msg_message}" payload end endeye-0.7/lib/eye/notify/jabber.rb0000644000004100000410000000140312600364132016577 0ustar www-datawww-datarequire 'xmpp4r' class Eye::Notify::Jabber < Eye::Notify # Eye.config do # jabber :host => "some.host", :port => 12345, :user => "eye@some.host", :password => "123456" # contact :vasya, :jabber, "vasya@some.host" # end param :host, String, true param :port, [String, Fixnum], true param :user, String, true param :password, String def execute debug { "send jabber #{[host, port, user, password]} - #{[contact, message_body]}" } mes = ::Jabber::Message.new(contact, message_body) mes.set_type(:normal) mes.set_id('1') mes.set_subject(message_subject) client = ::Jabber::Client.new(::Jabber::JID.new("#{user}/Eye")) client.connect(host, port) client.auth(password) client.send(mes) client.close end endeye-0.7/lib/eye/config.rb0000644000004100000410000000700312600364132015311 0ustar www-datawww-dataclass Eye::Config attr_reader :settings, :applications def initialize(settings = {}, applications = {}) @settings = settings @applications = applications end def merge(other_config) new_settings = {} Eye::Utils.deep_merge!(new_settings, @settings) Eye::Utils.deep_merge!(new_settings, other_config.settings) Eye::Config.new(new_settings, @applications.merge(other_config.applications)) end def merge!(other_config) Eye::Utils.deep_merge!(@settings, other_config.settings) @applications.merge!(other_config.applications) end def to_h h = {} h[:settings] = @settings if Eye.respond_to?(:parsed_default_app) d = Eye.parsed_default_app h[:defaults] = d ? d.config : {} end h[:applications] = @applications h end # raise an error if config wrong def validate!(validate_apps = []) all_processes = processes # Check blank pid_files no_pid_file = all_processes.select{|c| c[:pid_file].blank? } if no_pid_file.present? raise Eye::Dsl::Error, "blank pid_file for: #{no_pid_file.map{|c| c[:name]} * ', '}" end # Check duplicates of the full pid_file dupl_pids = all_processes.each_with_object(Hash.new(0)) do |o, h| ex_pid_file = Eye::System.normalized_file(o[:pid_file], o[:working_dir]) h[ex_pid_file] += 1 end dupl_pids = dupl_pids.select{|k,v| v>1} if dupl_pids.present? raise Eye::Dsl::Error, "duplicate pid_files: #{dupl_pids.inspect}" end # Check duplicates of the full_name dupl_names = all_processes.each_with_object(Hash.new(0)) do |o, h| full_name = "#{o[:application]}:#{o[:group]}:#{o[:name]}" h[full_name] += 1 end dupl_names = dupl_names.select{|k,v| v>1} if dupl_names.present? raise Eye::Dsl::Error, "duplicate names: #{dupl_names.inspect}" end # validate processes with their own validate all_processes.each do |process_cfg| Eye::Process.validate process_cfg, validate_apps.include?(process_cfg[:application]) end # just to be sure ENV was not removed ENV[''] rescue raise Eye::Dsl::Error.new("ENV is not a hash '#{ENV.inspect}'") end def transform! all_processes = processes # transform syslog option all_processes.each do |process| out = process[:stdout] && process[:stdout].start_with?(':syslog') err = process[:stderr] && process[:stderr].start_with?(':syslog') if err || out redir = err ? '2>&1' : '' process[:stdout] = nil if out process[:stderr] = nil if err escaped_start_command = process[:start_command].to_s.gsub(%{"}, %{\\"}) names = [process[:application], process[:group] == '__default__' ? nil : process[:group], process[:name]].compact logger = "logger -t \"#{names.join(':')}\"" process[:start_command] = %{sh -c "#{escaped_start_command} #{redir} | #{logger}"} process[:use_leaf_child] = true if process[:daemonize] end end end def processes applications.values.map{|e| (e[:groups] || {}).values.map{|c| (c[:processes] || {}).values} }.flatten end def application_names applications.keys end def delete_app(name) applications.delete(name) end def delete_group(name) applications.each do |app_name, app_cfg| (app_cfg[:groups] || {}).delete(name) end end def delete_process(name) applications.each do |app_name, app_cfg| (app_cfg[:groups] || {}).each do |gr_name, gr_cfg| (gr_cfg[:processes] || {}).delete(name) end end end end eye-0.7/lib/eye/dsl/0000755000004100000410000000000012600364132014301 5ustar www-datawww-dataeye-0.7/lib/eye/dsl/main.rb0000644000004100000410000000275012600364132015556 0ustar www-datawww-datamodule Eye::Dsl::Main attr_accessor :parsed_config, :parsed_filename, :parsed_default_app def application(name, &block) Eye::Dsl.check_name(name) name = name.to_s Eye::Dsl.debug { "=> app: #{name}" } if name == '__default__' @parsed_default_app ||= Eye::Dsl::ApplicationOpts.new(name) @parsed_default_app.instance_eval(&block) else opts = Eye::Dsl::ApplicationOpts.new(name, @parsed_default_app) opts.instance_eval(&block) @parsed_config.applications[name] = opts.config if opts.config end Eye::Dsl.debug { "<= app: #{name}" } end alias project application alias app application def load(glob = '') return if glob.blank? loaded = false Eye::Dsl::Opts.with_parsed_file(glob) do |mask| Dir[mask].each do |path| loaded = true Eye::Dsl.debug { "=> load #{path}" } Eye.parsed_filename = path res = Kernel.load(path) Eye.info "load: subload #{path} (#{res})" Eye::Dsl.debug { "<= load #{path}" } end end unless loaded puts "Warning! Eye.load not found: '#{glob}'" warn "not found: '#{glob}'" end end def config(&block) Eye::Dsl.debug { '=> config' } opts = Eye::Dsl::ConfigOpts.new opts.instance_eval(&block) Eye::Utils.deep_merge!(@parsed_config.settings, opts.config) Eye::Dsl.debug { '<= config' } end alias settings config def shared require 'ostruct' @shared_object ||= OpenStruct.new end end eye-0.7/lib/eye/dsl/child_process_opts.rb0000644000004100000410000000052012600364132020511 0ustar www-datawww-dataclass Eye::Dsl::ChildProcessOpts < Eye::Dsl::Opts def allow_options [:stop_command, :restart_command, :children_update_period, :stop_signals, :stop_grace, :stop_timeout, :restart_timeout] end def triggers(*args) raise Eye::Dsl::Error, 'triggers not allowed in monitor_children' end alias trigger triggers end eye-0.7/lib/eye/dsl/opts.rb0000644000004100000410000001442112600364132015615 0ustar www-datawww-dataclass Eye::Dsl::Opts < Eye::Dsl::PureOpts STR_OPTIONS = [ :pid_file, :working_dir, :stdout, :stderr, :stdall, :stdin, :start_command, :stop_command, :restart_command, :uid, :gid ] create_options_methods(STR_OPTIONS, String) BOOL_OPTIONS = [ :daemonize, :keep_alive, :auto_start, :stop_on_delete, :clear_pid, :preserve_fds, :use_leaf_child, :clear_env ] create_options_methods(BOOL_OPTIONS, [TrueClass, FalseClass]) INTERVAL_OPTIONS = [ :check_alive_period, :start_timeout, :restart_timeout, :stop_timeout, :start_grace, :restart_grace, :stop_grace, :children_update_period, :restore_in, :auto_update_pidfile_grace, :revert_fuckup_pidfile_grace ] create_options_methods(INTERVAL_OPTIONS, [Fixnum, Float]) create_options_methods([:environment], Hash) create_options_methods([:umask], Fixnum) def initialize(name = nil, parent = nil) super(name, parent) @config[:application] = parent.name if parent.is_a?(Eye::Dsl::ApplicationOpts) && parent.name != '__default__' @config[:group] = parent.name if parent.is_a?(Eye::Dsl::GroupOpts) # hack for full name @full_name = parent.full_name if @name == '__default__' && parent.respond_to?(:full_name) end def checks(type, opts = {}) nac = Eye::Checker.name_and_class(type.to_sym) raise Eye::Dsl::Error, "unknown checker type #{type}" unless nac opts[:type] = nac[:type] Eye::Checker.validate!(opts) @config[:checks] ||= {} @config[:checks][nac[:name]] = opts end def triggers(type, opts = {}) nac = Eye::Trigger.name_and_class(type.to_sym) raise Eye::Dsl::Error, "unknown trigger type #{type}" unless nac opts[:type] = nac[:type] Eye::Trigger.validate!(opts) @config[:triggers] ||= {} @config[:triggers][nac[:name]] = opts end # clear checks from parent def nochecks(type) nac = Eye::Checker.name_and_class(type.to_sym) raise Eye::Dsl::Error, "unknown checker type #{type}" unless nac @config[:checks].try :delete, nac[:name] end # clear triggers from parent def notriggers(type) nac = Eye::Trigger.name_and_class(type.to_sym) raise Eye::Dsl::Error, "unknown trigger type #{type}" unless nac @config[:triggers].try :delete, nac[:name] end alias check checks alias nocheck nochecks alias trigger triggers alias notrigger notriggers def command(cmd, arg) @config[:user_commands] ||= {} if arg.is_a?(Array) validate_signals(arg) elsif arg.is_a?(String) else raise Eye::Dsl::Error, "unknown command #{cmd.inspect} type should be String or Array" end @config[:user_commands][cmd.to_sym] = arg end def notify(contact, level = :warn) unless Eye::Process::Notify::LEVELS[level] raise Eye::Dsl::Error, "level should be in #{Eye::Process::Notify::LEVELS.keys}" end @config[:notify] ||= {} @config[:notify][contact.to_s] = level end def nonotify(contact) @config[:notify] ||= {} @config[:notify].delete(contact.to_s) end def set_stop_command(cmd) raise Eye::Dsl::Error, "cannot use both stop_signals and stop_command" if @config[:stop_signals] super end def stop_signals(*args) raise Eye::Dsl::Error, "cannot use both stop_signals and stop_command" if @config[:stop_command] if args.count == 0 return @config[:stop_signals] end signals = Array(args).flatten validate_signals(signals) @config[:stop_signals] = signals end def stop_signals=(s) stop_signals(s) end def set_environment(value) raise Eye::Dsl::Error, "environment should be a hash, but not #{value.inspect}" unless value.is_a?(Hash) @config[:environment] ||= {} @config[:environment].merge!(value) end alias dir working_dir alias env environment def set_stdall(value) super set_stdout value set_stderr value end def set_uid(value) raise Eye::Dsl::Error, ':uid not supported (use ruby >= 2.0)' unless Eye::Local.supported_setsid? super end def set_gid(value) raise Eye::Dsl::Error, ':gid not supported (use ruby >= 2.0)' unless Eye::Local.supported_setsid? super end def daemonize! set_daemonize true end def clear_bundler_env env('GEM_PATH' => nil, 'GEM_HOME' => nil, 'RUBYOPT' => nil, 'BUNDLE_BIN_PATH' => nil, 'BUNDLE_GEMFILE' => nil) end def scoped(&block) h = self.class.new(self.name, self) h.instance_eval(&block) Eye::Utils.deep_merge!(config, h.config, [:groups, :processes]) end # execute part of config on particular server # array of strings # regexp # string def with_server(glob = nil, &block) on_server = true if glob.present? host = Eye::Local.host if glob.is_a?(Array) on_server = !!glob.any?{|elem| elem == host} elsif glob.is_a?(Regexp) on_server = !!host.match(glob) elsif glob.is_a?(String) || glob.is_a?(Symbol) on_server = (host == glob.to_s) end end scoped do with_condition(on_server, &block) end on_server end def load_env(filename = '~/.env', raise_when_no_file = true) fnames = [File.expand_path(filename, @config[:working_dir]), File.expand_path(filename)].uniq filenames = fnames.select { |f| File.exist?(f) } if filenames.size < 1 unless raise_when_no_file warn "load_env not found file: '#{filenames.first}'" return else raise Eye::Dsl::Error, "load_env not found in #{fnames}" end end raise Eye::Dsl::Error, "load_env conflict filenames: #{filenames}" if filenames.size > 1 info "load_env from '#{filenames.first}'" Eye::Utils.load_env(filenames.first).each { |k, v| env k => v } end def skip_group_action(act, val = true) @config[:skip_group_actions] ||= {} @config[:skip_group_actions][act] = val end def syslog ':syslog' end private def validate_signals(signals = nil) return unless signals raise Eye::Dsl::Error, "signals should be Array" unless signals.is_a?(Array) s = signals.clone while s.present? sig = s.shift timeout = s.shift raise Eye::Dsl::Error, "signal should be String, Symbol, Fixnum, not #{sig.inspect}" if sig && ![String, Symbol, Fixnum].include?(sig.class) raise Eye::Dsl::Error, "signal sleep should be Numeric, not #{timeout.inspect}" if timeout && ![Fixnum, Float].include?(timeout.class) end end end eye-0.7/lib/eye/dsl/process_opts.rb0000644000004100000410000000155712600364132017361 0ustar www-datawww-dataclass Eye::Dsl::ProcessOpts < Eye::Dsl::Opts def monitor_children(&block) opts = Eye::Dsl::ChildProcessOpts.new opts.instance_eval(&block) if block @config[:monitor_children] ||= {} Eye::Utils.deep_merge!(@config[:monitor_children], opts.config) end alias xmonitor_children nop def application parent.try(:parent) end alias app application alias group parent def depend_on(names, opts = {}) names = Array(names).map(&:to_s) trigger("wait_dependency_#{unique_num}", {:names => names}.merge(opts)) nm = @config[:name] names.each do |name| parent.process(name) do trigger("check_dependency_#{unique_num}", :names => [ nm ] ) end end skip_group_action(:restart, [:up, :down, :starting, :stopping, :restarting]) end private def unique_num $unique_num ||= 0 $unique_num += 1 end end eye-0.7/lib/eye/dsl/pure_opts.rb0000644000004100000410000000532712600364132016655 0ustar www-datawww-dataclass Eye::Dsl::PureOpts def self.create_options_methods(arr, types = nil) m = Module.new do arr.each do |opt| define_method("set_#{opt}") do |arg| key = opt.to_sym if (disallow_options && disallow_options.include?(key)) || (allow_options && !allow_options.include?(key)) raise Eye::Dsl::Error, "disallow option #{key} for #{self.class.inspect}" end if types good_type = Array(types).any?{|type| arg.is_a?(type) } || arg.nil? raise Eye::Dsl::Error, "bad :#{opt} value #{arg.inspect}, type should be #{types.inspect}" unless good_type end @config[key] = arg end define_method("get_#{opt}") do @config[ opt.to_sym ] end define_method(opt) do |*args| if args.blank? # getter send "get_#{opt}" else send "set_#{opt}", *args end end define_method("#{opt}=") do |arg| send opt, arg end end end self.send :include, m end attr_reader :name, :full_name attr_reader :config, :parent def initialize(name = nil, parent = nil, merge_parent_config = true) @name = name.to_s @full_name = @name if parent @parent = parent if merge_parent_config @config = Eye::Utils::deep_clone(parent.config) parent.not_seed_options.each { |opt| @config.delete(opt) } else @config = {} end @full_name = "#{parent.full_name}:#{@full_name}" else @config = {} end @config[:name] = @name if @name.present? end def allow_options nil end def disallow_options [] end def not_seed_options [] end def with_condition(cond = true, &block) self.instance_eval(&block) if cond && block end def use(proc, *args) if proc.is_a?(String) self.class.with_parsed_file(proc) do |path| if File.exist?(path) Eye::Dsl.debug { "=> load #{path}" } self.instance_eval(File.read(path)) Eye::Dsl.debug { "<= load #{path}" } end end else ie = if args.present? lambda{|i| proc[i, *args] } else proc end self.instance_eval(&ie) end end def nop(*args, &block); end def self.with_parsed_file(file_name) saved_parsed_filename = Eye.parsed_filename real_filename = Eye.parsed_filename && File.symlink?(Eye.parsed_filename) ? File.readlink(Eye.parsed_filename) : Eye.parsed_filename dirname = File.dirname(real_filename) rescue nil path = File.expand_path(file_name, dirname) Eye.parsed_filename = path yield path ensure Eye.parsed_filename = saved_parsed_filename end end eye-0.7/lib/eye/dsl/validation.rb0000644000004100000410000000511012600364132016755 0ustar www-datawww-datamodule Eye::Dsl::Validation def self.included(base) base.extend(ClassMethods) end class Error < Exception; end module ClassMethods def inherited(subclass) subclass.validates = self.validates.clone subclass.should_bes = self.should_bes.clone subclass.defaults = self.defaults.clone subclass.variants = self.variants.clone end attr_accessor :validates, :should_bes, :defaults, :variants def validates; @validates ||= {}; end def should_bes; @should_bes ||= []; end def defaults; @defaults ||= {}; end def variants; @variants ||= {}; end def param(param, types = [], should_be = false, default = nil, variants = nil) param = param.to_sym self.validates[param] = types self.should_bes << param if should_be param_default(param, default) self.variants[param] = variants return if param == :do define_method "#{param}" do value = @options[param] value.nil? ? self.class.defaults[param] : value end define_method "#{param}=" do |value| @options[param] = value end end def param_default(param, default) param = param.to_sym defaults[param] = default end def del_param(param) param = param.to_sym validates.delete(param) should_bes.delete(param) defaults.delete(param) variants.delete(param) remove_method(param) end def validate(options = {}) options.each do |param, value| param = param.to_sym types = validates[param] unless types if param != :type raise Error, "#{self.name} unknown param :#{param} value #{value.inspect}" end end if self.variants[param] if value && !value.is_a?(Proc) if value.is_a?(Array) if (value - self.variants[param]).present? raise Error, "#{value.inspect} should be within #{self.variants[param].inspect}" end elsif !self.variants[param].include?(value) raise Error, "#{value.inspect} should be within #{self.variants[param].inspect}" end end end next if types.blank? types = Array(types) good = types.any?{|type| value.is_a?(type) } raise Error, "#{self.name} bad param :#{param} value #{value.inspect}, type #{types.inspect}" unless good end should_bes.each do |param| raise Error, "#{self.name} for param :#{param} value should be" unless options[param.to_sym] || defaults[param.to_sym] end end end end eye-0.7/lib/eye/dsl/application_opts.rb0000644000004100000410000000136612600364132020204 0ustar www-datawww-dataclass Eye::Dsl::ApplicationOpts < Eye::Dsl::Opts include Eye::Dsl::Chain def disallow_options [:pid_file, :start_command, :daemonize] end def not_seed_options [:groups] end def group(name, &block) Eye::Dsl.check_name(name) Eye::Dsl.debug { "=> group #{name}" } opts = Eye::Dsl::GroupOpts.new(name, self) opts.instance_eval(&block) @config[:groups] ||= {} @config[:groups][name.to_s] ||= {} if cfg = opts.config Eye::Utils.deep_merge!(@config[:groups][name.to_s], cfg) end Eye::Dsl.debug { "<= group #{name}" } opts end def process(name, &block) res = nil group('__default__'){ res = process(name.to_s, &block) } res end alias xgroup nop alias xprocess nop end eye-0.7/lib/eye/dsl/config_opts.rb0000644000004100000410000000346612600364132017151 0ustar www-datawww-dataclass Eye::Dsl::ConfigOpts < Eye::Dsl::PureOpts create_options_methods([:logger_level], Fixnum) create_options_methods([:http], Hash) def logger(*args) if args.empty? @config[:logger] else @config[:logger] = args end end alias logger= logger def syslog(name = 'eye', *args) require 'syslog/logger' Syslog::Logger.new(name, *args) rescue LoadError raise Eye::Dsl::Error, "logger syslog requires Ruby >= 2.0" end # ==== contact options ============================== def self.add_notify(type) create_options_methods([type], Hash) define_method("set_#{type}") do |value| value = value.merge(:type => type) super(value) Eye::Notify.validate!(value) end end Eye::Notify::TYPES.each_key { |name| add_notify(name) } def contact(contact_name, contact_type, contact, contact_opts = {}) raise Eye::Dsl::Error, "unknown contact_type #{contact_type}" unless Eye::Notify::TYPES[contact_type] raise Eye::Dsl::Error, 'contact should be a String' unless contact.is_a?(String) notify_hash = @config[contact_type] || (@parent && @parent.config[contact_type]) || Eye::parsed_config.settings[contact_type] || {} validate_hash = notify_hash.merge(contact_opts).merge(:type => contact_type) Eye::Notify.validate!(validate_hash) @config[:contacts] ||= {} @config[:contacts][contact_name.to_s] = {name: contact_name.to_s, type: contact_type, contact: contact, opts: contact_opts} end def contact_group(contact_group_name, &block) c = Eye::Dsl::ConfigOpts.new nil, self, false c.instance_eval(&block) cfg = c.config @config[:contacts] ||= {} if cfg[:contacts].present? @config[:contacts][contact_group_name.to_s] = cfg[:contacts].values @config[:contacts].merge!(cfg[:contacts]) end end end eye-0.7/lib/eye/dsl/chain.rb0000644000004100000410000000036612600364132015715 0ustar www-datawww-datamodule Eye::Dsl::Chain def chain(opts = {}) acts = Array(opts[:action] || opts[:actions] || [:start, :restart]) acts.each do |act| @config[:chain] ||= {} @config[:chain][act] = opts.merge(:action => act) end end endeye-0.7/lib/eye/dsl/helpers.rb0000644000004100000410000000061212600364132016267 0ustar www-datawww-data # Dsl Helpers # current eye parsed config path def current_config_path Eye.parsed_filename && File.symlink?(Eye.parsed_filename) ? File.readlink(Eye.parsed_filename) : Eye.parsed_filename end # host name def hostname Eye::Local.host end def example_process(proxy, name) proxy.process(name) do pid_file "/tmp/#{name}.pid" start_command "sleep 100" daemonize true end end eye-0.7/lib/eye/dsl/group_opts.rb0000644000004100000410000000125212600364132017027 0ustar www-datawww-dataclass Eye::Dsl::GroupOpts < Eye::Dsl::Opts include Eye::Dsl::Chain def disallow_options [:pid_file, :start_command, :daemonize] end def not_seed_options [:processes, :chain] end def process(name, &block) Eye::Dsl.check_name(name) Eye::Dsl.debug { "=> process #{name}" } opts = Eye::Dsl::ProcessOpts.new(name, self) opts.instance_eval(&block) @config[:processes] ||= {} @config[:processes][name.to_s] ||= {} Eye::Utils.deep_merge!(@config[:processes][name.to_s], opts.config) if opts.config Eye::Dsl.debug { "<= process #{name}" } opts end alias xprocess nop alias application parent alias app application end eye-0.7/lib/eye/group.rb0000644000004100000410000000555612600364132015213 0ustar www-datawww-datarequire 'celluloid' class Eye::Group include Celluloid autoload :Chain, 'eye/group/chain' include Eye::Process::Scheduler include Eye::Group::Chain attr_reader :processes, :name, :hidden, :config def initialize(name, config) @name = name @config = config @processes = Eye::Utils::AliveArray.new @hidden = (name == '__default__') debug { 'created' } end def logger_tag full_name end def app_name @config[:application] end def full_name @full_name ||= "#{app_name}:#{@name}" end def update_config(cfg) @config = cfg @full_name = nil end def add_process(process) @processes << process end # sort processes in name order def resort_processes @processes = @processes.sort_by(&:name) end def status_data(debug = false) plist = @processes.map{|p| p.status_data(debug) } h = { name: name, type: :group, subtree: plist } h[:debug] = debug_data if debug # show current chain if current_scheduled_command h.update(current_command: current_scheduled_command) if (chain_commands = scheduler_actions_list) && chain_commands.present? h.update(chain_commands: chain_commands) end if @chain_processes_current && @chain_processes_count h.update(chain_progress: [@chain_processes_current, @chain_processes_count]) end end h end def status_data_short h = Hash.new @processes.each do |p| h[p.state] ||= 0 h[p.state] += 1 end { name: (@name == '__default__' ? 'default' : @name), type: :group, states: h } end def debug_data {:queue => scheduler_actions_list, :chain => chain_status} end def send_command(command, *args) info "send_command: #{command}" case command when :delete delete(*args) when :break_chain break_chain(*args) else schedule command, *args, Eye::Reason::User.new(command) end end def start chain_command :start end def stop async_schedule :stop end def restart chain_command :restart end def delete async_schedule :delete terminate end def monitor chain_command :monitor end def unmonitor async_schedule :unmonitor end def signal(sig) async_schedule :signal, sig end def user_command(cmd) async_schedule :user_command, cmd end def break_chain info 'break chain' scheduler_clear_pending_list @chain_breaker = true end def freeze async_schedule :freeze end def clear @processes = Eye::Utils::AliveArray.new end def sub_object?(obj) @processes.include?(obj) end private def async_schedule(command, *args) info "send to all processes #{command} #{args.present? ? args*',' : nil}" @processes.each do |process| process.send_command(command, *args) unless process.skip_group_action?(command) end end end eye-0.7/lib/eye/control.rb0000644000004100000410000000010012600364132015513 0ustar www-datawww-data# controller global singlton Eye::Control = Eye::Controller.new eye-0.7/lib/eye/controller/0000755000004100000410000000000012600364132015702 5ustar www-datawww-dataeye-0.7/lib/eye/controller/status.rb0000644000004100000410000000355012600364132017555 0ustar www-datawww-datamodule Eye::Controller::Status def debug_data(*args) h = args.extract_options! actors = Celluloid::Actor.all.map{|actor| actor.wrapped_object.class.to_s }.group_by{|a| a}.map{|k,v| [k, v.size]}.sort_by{ |a| a[1] }.reverse res = { :about => Eye::ABOUT, :resources => Eye::SystemResources.resources($$), :ruby => RUBY_DESCRIPTION, :gems => %w|Celluloid Celluloid::IO StateMachine NIO Timers Sigar|.map{|c| gem_version(c) }, :logger => Eye::Logger.args.present? ? [Eye::Logger.dev.to_s, *Eye::Logger.args] : Eye::Logger.dev.to_s, :dir => Eye::Local.dir, :pid_path => Eye::Local::pid_path, :sock_path => Eye::Local::socket_path, :actors => actors } res[:config_yaml] = YAML.dump(current_config.to_h) if h[:config].present? res end def info_data(*args) {:subtree => info_objects(*args).map{|a| a.status_data } } end def short_data(*args) {:subtree => info_objects(*args).select{ |o| o.class == Eye::Application }.map{|a| a.status_data_short } } end def history_data(*args) res = {} history_objects(*args).each do |process| res[process.full_name] = process.schedule_history.reject{|c| c[:state] == :check_crash } end res end private def info_objects(*args) res = [] return @applications if args.empty? matched_objects(*args){|obj| res << obj } res end def gem_version(klass) v = nil begin v = eval("#{klass}::VERSION::STRING") rescue v = eval("#{klass}::VERSION") rescue '' end "#{klass}=#{v}" end def history_objects(*args) args = ['*'] if args.empty? res = [] matched_objects(*args) do |obj| if obj.is_a?(Eye::Process) res << obj elsif obj.is_a?(Eye::ChildProcess) else res += obj.processes.to_a end end Eye::Utils::AliveArray.new(res) end end eye-0.7/lib/eye/controller/options.rb0000644000004100000410000000063612600364132017727 0ustar www-datawww-datamodule Eye::Controller::Options def set_opt_logger(logger_args) # do not apply logger, if in stdout state if !%w{stdout stderr}.include?(Eye::Logger.dev) Eye::Logger.link_logger(*logger_args) end end def set_opt_logger_level(level) Eye::Logger.log_level = level end def set_opt_http(opts = {}) warn "Warning, set http options not in reel-eye gem" if opts.present? end end eye-0.7/lib/eye/controller/send_command.rb0000644000004100000410000001023512600364132020657 0ustar www-datawww-datamodule Eye::Controller::SendCommand def send_command(command, *args) matched_objects(*args) do |obj| if command.to_sym == :delete remove_object_from_tree(obj) set_proc_line end obj.send_command(command) end end def match(*args) matched_objects(*args) end def signal(signal, *args) matched_objects(*args) do |obj| obj.send_command :signal, signal || 0 end end def user_command(cmd, *args) matched_objects(*args) do |obj| obj.send_command :user_command, cmd end end private class Error < Exception; end def matched_objects(*args, &block) objs = find_objects(*args) res = objs.map(&:full_name) objs.each{|obj| block[obj] } if block {:result => res} rescue Error => ex log_ex(ex) {:error => ex.message} rescue Celluloid::DeadActorError => ex log_ex(ex) {:error => "'#{ex.message}', try again!"} end def remove_object_from_tree(obj) klass = obj.class if klass == Eye::Application @applications.delete(obj) @current_config.delete_app(obj.name) end if klass == Eye::Group @applications.each{|app| app.groups.delete(obj) } @current_config.delete_group(obj.name) end if klass == Eye::Process @applications.each{|app| app.groups.each{|gr| gr.processes.delete(obj) }} @current_config.delete_process(obj.name) end end # find object to action, restart ... (app, group or process) # nil if not found def find_objects(*args) h = args.extract_options! obj_strs = args return [] if obj_strs.blank? if obj_strs.size == 1 && (obj_strs[0].to_s.strip == 'all' || obj_strs[0].to_s.strip == '*') if h[:application] return @applications.select { |app| app.name == h[:application]} else return @applications.dup end end res = Eye::Utils::AliveArray.new obj_strs.map{|c| c.to_s.split(',')}.flatten.each do |mask| objs = find_objects_by_mask(mask.to_s.strip) objs.select! { |obj| obj.app_name == h[:application] } if h[:application] res += objs end res end def find_objects_by_mask(mask) res = find_all_objects_by_mask(mask) if res.size > 1 final = Eye::Utils::AliveArray.new # try to find exactly matched if mask[-1] != '*' r = exact_regexp(mask) res.each do |obj| final << obj if obj.name =~ r || obj.full_name =~ r end end res = final if final.present? final = Eye::Utils::AliveArray.new # remove inherited targets res.each do |obj| sub_object = res.any?{|a| a.sub_object?(obj) } final << obj unless sub_object end res = final # try to remove objects with different applications apps, objs = Eye::Utils::AliveArray.new, Eye::Utils::AliveArray.new res.each do |obj| if obj.is_a?(Eye::Application) apps << obj else objs << obj end end return apps if apps.size > 0 if objs.map(&:app_name).uniq.size > 1 raise Error, "cannot match targets from different applications: #{res.map(&:full_name)}" end end res end def find_all_objects_by_mask(mask) res = Eye::Utils::AliveArray r = left_regexp(mask) # find app res = @applications.select{|a| a.name =~ r || a.full_name =~ r } # find group @applications.each do |a| res += a.groups.select{|gr| gr.name =~ r || gr.full_name =~ r } end # find process @applications.each do |a| a.groups.each do |gr| gr.processes.each do |p| res << p if p.name =~ r || p.full_name =~ r # child matching if p.children.present? children = p.children.values res += children.select do |ch| name = ch.name rescue '' full_name = ch.full_name rescue '' name =~ r || full_name =~ r end end end end end res end def left_regexp(mask) str = Regexp.escape(mask).gsub('\*', '.*?') %r|\A#{str}| end def exact_regexp(mask) str = Regexp.escape(mask).gsub('\*', '.*?') %r|\A#{str}\z| end end eye-0.7/lib/eye/controller/helpers.rb0000644000004100000410000000405212600364132017672 0ustar www-datawww-datamodule Eye::Controller::Helpers def set_proc_line str = Eye::PROCLINE str = 'l' + str if Eye::Local.local_runner str += " [#{@applications.map(&:name) * ', '}]" if @applications.present? str += " (v #{ENV['EYE_V']})" if ENV['EYE_V'] str += " (in #{Eye::Local.dir})" $0 = str end def save_cache File.open(Eye::Local.cache_path, 'w') { |f| f.write(cache_str) } rescue => ex log_ex(ex) end def cache_str all_processes.map{ |p| "#{p.full_name}=#{p.state}" } * "\n" end def process_by_name(name) name = name.to_s all_processes.detect { |c| c.name == name } end def process_by_full_name(name) name = name.to_s all_processes.detect { |c| c.full_name == name } end def find_nearest_process(name, group_name = nil, app_name = nil) return process_by_full_name(name) if name.include?(':') if app_name app = application_by_name(app_name) app.groups.each do |gr| p = gr.processes.detect { |c| c.name == name } return p if p end end if group_name gr = group_by_name(group_name) p = gr.processes.detect { |c| c.name == name } return p if p end process_by_name(name) end def group_by_name(name) name = name.to_s all_groups.detect { |c| c.name == name } end def application_by_name(name) name = name.to_s @applications.detect { |c| c.name == name } end def all_processes processes = [] all_groups.each do |gr| processes += gr.processes.to_a end processes end def all_groups groups = [] @applications.each do |app| groups += app.groups.to_a end groups end # {'app_name' => {'group_name' => {'process_name' => 'pid_file'}}} def short_tree res = {} @applications.each do |app| res2 = {} app.groups.each do |group| res3 = {} group.processes.each do |process| res3[process.name] = process[:pid_file_ex] end res2[group.name] = res3 end res[app.name] = res2 end res end end eye-0.7/lib/eye/controller/load.rb0000644000004100000410000001354712600364132017160 0ustar www-datawww-datamodule Eye::Controller::Load def check(filename) { filename => catch_load_error(filename) { parse_config(filename).to_h } } end def explain(filename) { filename => catch_load_error(filename) { parse_config(filename).to_h } } end def load(*args) args.extract_options! obj_strs = args.flatten info "=> loading: #{obj_strs}" res = Hash.new globbing(*obj_strs).each do |filename| res[filename] = catch_load_error(filename) do cfg = parse_config(filename) load_config(filename, cfg) nil end end set_proc_line info "<= loading: #{obj_strs}" res end private # regexp for clean backtrace to show for user BT_REGX = %r[/lib/eye/|lib/celluloid|internal:prelude|logger.rb:|active_support/core_ext|shellwords.rb|kernel/bootstrap].freeze def catch_load_error(filename = nil, &block) { :error => false, :config => yield } rescue Eye::Dsl::Error, Exception, NoMethodError => ex raise if ex.class.to_s.include?('RR') # skip RR exceptions error "loading: config error <#{filename}>: #{ex.message}" # filter backtrace for user output bt = (ex.backtrace || []) bt = bt.reject{|line| line.to_s =~ BT_REGX } unless ENV['EYE_FULL_BACKTRACE'] error bt.join("\n") res = { :error => true, :message => ex.message } res[:backtrace] = bt if bt.present? res end def globbing(*obj_strs) res = [] return res if obj_strs.empty? obj_strs.each do |filename| mask = File.directory?(filename) ? File.join(filename, '{*.eye}') : filename debug { "loading: globbing mask #{mask}" } sub = [] Dir[mask].each do |config_path| sub << config_path end sub = [mask] if sub.empty? res += sub end res end # return: result, config def parse_config(filename) debug { "parsing: #{filename}" } cfg = Eye::Dsl.parse(nil, filename) @current_config.merge(cfg).validate! # just validate summary config here Eye.parsed_config = nil # remove link on config, for better gc cfg end # !!! exclusive operation def load_config(filename, config) info "loading: #{filename}" new_cfg = @current_config.merge(config) new_cfg.validate!(config.application_names) load_options(new_cfg.settings) create_objects(new_cfg.applications, config.application_names) @current_config = new_cfg end # load global config options def load_options(opts) return if opts.blank? opts.each do |key, value| method = "set_opt_#{key}" send(method, value) if value && respond_to?(method) end end # create objects as diff, from configs def create_objects(apps_config, changed_apps = []) debug { 'creating objects' } apps_config.each do |app_name, app_cfg| update_or_create_application(app_name, app_cfg.clone) if changed_apps.include?(app_name) end # sorting applications @applications.sort_by!(&:name) end def update_or_create_application(app_name, app_config) @old_groups = {} @old_processes = {} app = @applications.detect { |c| c.name == app_name } if app app.groups.each do |group| @old_groups[group.name] = group group.processes.each do |proc| @old_processes[group.name + ":" + proc.name] = proc end end @applications.delete(app) debug { "updating app: #{app_name}" } else debug { "creating app: #{app_name}" } end app = Eye::Application.new(app_name, app_config) @applications << app @added_groups, @added_processes = [], [] new_groups = app_config.delete(:groups) || {} new_groups.each do |group_name, group_cfg| group = update_or_create_group(group_name, group_cfg.clone) app.add_group(group) group.resort_processes end # now, need to clear @old_groups, and @old_processes @old_groups.each{|_, group| group.clear; group.send_command(:delete) } @old_processes.each{|_, process| process.send_command(:delete) if process.alive? } # schedule monitoring for new groups, processes added_fully_groups = [] @added_groups.each do |group| if group.processes.size > 0 && (group.processes.pure - @added_processes).size == 0 added_fully_groups << group @added_processes -= group.processes.pure end end added_fully_groups.each{|group| group.send_command :monitor } @added_processes.each{|process| process.send_command :monitor } # remove links to prevent memory leaks @old_groups = nil @old_processes = nil @added_groups = nil @added_processes = nil app.resort_groups app end def update_or_create_group(group_name, group_config) group = if @old_groups[group_name] debug { "updating group: #{group_name}" } group = @old_groups.delete(group_name) group.schedule :update_config, group_config, Eye::Reason::User.new(:'load config') group.clear group else debug { "creating group: #{group_name}" } gr = Eye::Group.new(group_name, group_config) @added_groups << gr gr end processes = group_config.delete(:processes) || {} processes.each do |process_name, process_cfg| process = update_or_create_process(process_name, process_cfg.clone) group.add_process(process) end group end def update_or_create_process(process_name, process_cfg) postfix = ":" + process_name name = process_cfg[:group] + postfix key = @old_processes[name] ? name : @old_processes.keys.detect { |n| n.end_with?(postfix) } if @old_processes[key] debug { "updating process: #{name}" } process = @old_processes.delete(key) process.schedule :update_config, process_cfg, Eye::Reason::User.new(:'load config') process else debug { "creating process: #{name}" } process = Eye::Process.new(process_cfg) @added_processes << process process end end end eye-0.7/lib/eye/controller/commands.rb0000644000004100000410000000374012600364132020034 0ustar www-datawww-datamodule Eye::Controller::Commands NOT_IMPORTANT_COMMANDS = [:info_data, :short_data, :debug_data, :history_data, :ping, :logger_dev, :match, :explain, :check] # Main method, answer for the client command def command(cmd, *args) msg = "command: #{cmd} #{args * ', '}" log_str = "=> #{msg}" NOT_IMPORTANT_COMMANDS.include?(cmd) ? debug(log_str) : info(log_str) start_at = Time.now cmd = cmd.to_sym res = case cmd when :start, :stop, :restart, :unmonitor, :monitor, :break_chain send_command(cmd, *args) when :delete exclusive { send_command(cmd, *args) } when :signal signal(*args) when :user_command user_command(*args) when :load exclusive { load(*args) } when :quit quit when :stop_all stop_all(*args) when :check check(*args) when :explain explain(*args) when :match match(*args) when :ping :pong when :logger_dev Eye::Logger.dev.to_s # object commands, for api when :info_data info_data(*args) when :short_data short_data(*args) when :debug_data debug_data(*args) when :history_data history_data(*args) else :unknown_command end GC.start log_str = "<= #{msg} (#{Time.now - start_at}s)" NOT_IMPORTANT_COMMANDS.include?(cmd) ? debug(log_str) : info(log_str) res end private def quit info 'Quit!' Eye::System.send_signal($$, :TERM) sleep 1 Eye::System.send_signal($$, :KILL) end # stop all processes and wait def stop_all(timeout = nil) exclusive do send_command :break_chain, 'all' send_command :stop, 'all' send_command :freeze, 'all' end # wait until all processes goes to unmonitored timeout ||= 100 all_processes.pmap do |p| p.wait_for_condition(timeout, 0.3) do p.state_name == :unmonitored end end end end eye-0.7/lib/eye/cli/0000755000004100000410000000000012600364132014266 5ustar www-datawww-dataeye-0.7/lib/eye/cli/render.rb0000644000004100000410000001002612600364132016071 0ustar www-datawww-datamodule Eye::Cli::Render private def render_status(data) return [1, "unexpected server response #{data.inspect}"] unless data.is_a?(Hash) data = data[:subtree] return [1, "match #{data.size} objects (#{data.map{|d| d[:name]}}), but expected only 1 process"] if data.size != 1 process = data[0] return [1, "unknown status for :#{process[:type]}=#{process[:name]}"] unless process[:type] == :process state = process[:state].to_sym return [0, ''] if state == :up return [3, ''] if state == :unmonitored [4, "process #{process[:name]} state :#{state}"] end def render_info(data) error!("unexpected server response #{data.inspect}") unless data.is_a?(Hash) make_str data end def make_str(data, level = -1) return nil if !data || data.empty? if data.is_a?(Array) data.map{|el| make_str(el, level) }.compact * "\n" else str = nil if data[:name] return make_str(data[:subtree], level) if data[:name] == '__default__' off = level * 2 off_str = ' ' * off short_state = ((data[:type] == :application || data[:type] == :group) && data[:states]) is_text = data[:state] || data[:states] name = (data[:type] == :application && !is_text) ? "\033[1m#{data[:name]}\033[0m" : data[:name].to_s off_len = 35 str = off_str + (name + ' ').ljust(off_len - off, is_text ? '.' : ' ') if short_state str += ' ' + data[:states].map { |k, v| "#{k}:#{v}" }.join(', ') elsif data[:state] str += ' ' + data[:state].to_s str += ' (' + resources_str(data[:resources]) + ')' if data[:resources] && data[:state].to_sym == :up str += " (#{data[:state_reason]} at #{Eye::Utils.human_time2(data[:state_changed_at])})" if data[:state_reason] && data[:state] == 'unmonitored' elsif data[:current_command] chain_progress = if data[:chain_progress] " #{data[:chain_progress][0]} of #{data[:chain_progress][1]}" rescue '' end str += " \e[1;33m[#{data[:current_command]}#{chain_progress}]\033[0m" str += " (#{data[:chain_commands] * ', '})" if data[:chain_commands] end end if data[:subtree].nil? str elsif !data[:subtree] && data[:type] != :application nil else [str, make_str(data[:subtree], level + 1)].compact * "\n" end end end def resources_str(r) return '' if !r || r.empty? memory, cpu, start_time, pid = r[:memory], r[:cpu], r[:start_time], r[:pid] return '' unless memory && cpu && start_time "#{Eye::Utils.human_time(start_time)}, #{cpu.to_i}%, #{memory / 1024 / 1024}Mb, <#{pid}>" end def render_debug_info(data) error!("unexpected server response #{data.inspect}") unless data.is_a?(Hash) s = "" if config_yaml = data.delete(:config_yaml) s << config_yaml else data.each do |k, v| s << "#{"#{k}:".ljust(10)} " case k when :resources s << resources_str(v) else s << "#{v}" end s << "\n" end s << "\n" end s end def render_history(data) error!("unexpected server response #{data.inspect}") unless data.is_a?(Hash) data.map { |name, history| detail_process_info(name, history) }.join("\n") end def detail_process_info(name, history) return if history.empty? res = "\033[1m#{name}\033[0m\n" history = history.reverse history.chunk{|h| [h[:state], h[:reason].to_s] }.each do |_, hist| if hist.size >= 3 res << detail_process_info_string(hist[0]) res << detail_process_info_string(:state => "... #{hist.size - 2} times", :reason => '...') res << detail_process_info_string(hist[-1]) else hist.each do |h| res << detail_process_info_string(h) end end end res end def detail_process_info_string(h) state = h[:state].to_s.ljust(14) at = h[:at] ? Eye::Utils.human_time2(h[:at]) : '.' * 12 "#{at} - #{state} (#{h[:reason]})\n" end end eye-0.7/lib/eye/cli/commands.rb0000644000004100000410000000345012600364132016416 0ustar www-datawww-datamodule Eye::Cli::Commands private def client @client ||= Eye::Client.new(Eye::Local.socket_path) end def _cmd(cmd, *args) client.command(cmd, *args) rescue Errno::ECONNREFUSED, Errno::ENOENT :not_started end def cmd(cmd, *args) res = _cmd(cmd, *args) if res == :not_started error! "socket(#{Eye::Local.socket_path}) not found, did you run `eye load`?" elsif res == :timeouted error! 'eye timed out without responding...' end res end def say_load_result(res = {}, opts = {}) error!(res) unless res.is_a?(Hash) say_filename = (res.size > 1) error = false res.each do |filename, res2| say "#{filename}: ", nil, true if say_filename show_load_message(res2, opts) error = true if res2[:error] end exit(1) if error end def show_load_message(res, opts = {}) if res[:error] say res[:message], :red res[:backtrace].to_a.each{|line| say line, :red } else if opts[:syntax] say 'Config ok!', :green if !res[:empty] else say 'Config loaded!', :green if !res[:empty] end if opts[:print_config] require 'pp' PP.pp res[:config], STDOUT, 150 end end end def send_command(command, *args) res = cmd(command, *args) if res == :unknown_command error! "unknown command :#{command}" elsif res == :corrupted_data error! 'something crazy wrong, check eye logs!' elsif res.is_a?(Hash) if res[:error] error! "Error: #{res[:error]}" elsif res = res[:result] if res == [] error! "command :#{command}, objects not found!" else say "command :#{command} sent to [#{res * ', '}]" end end else error! "unknown result #{res.inspect}" end end end eye-0.7/lib/eye/cli/server.rb0000644000004100000410000000434112600364132016123 0ustar www-datawww-data# encoding: utf-8 module Eye::Cli::Server private def server_started? _cmd(:ping) == :pong end def loader_path filename = File.expand_path(File.join(File.dirname(__FILE__), %w[.. .. .. bin loader_eye])) File.exist?(filename) ? filename : nil end def ruby_path RbConfig.ruby end def ensure_loader_path unless loader_path error! "start monitoring needs to run under ruby with installed gem 'eye'" end end def server_start_foreground(conf = nil) ensure_loader_path Eye::Local.ensure_eye_dir if server_started? _cmd(:quit) && sleep(1) # stop previous server end args = [] args += ['--config', conf] if conf args += ['--logger', 'stdout'] if Eye::Local.local_runner args += ['--stop_all'] args += ['--dir', Eye::Local.dir] args += ['--config', Eye::Local.eyefile] unless conf end Process.exec(ruby_path, loader_path, *args) end def server_start(configs) ensure_loader_path Eye::Local.ensure_eye_dir ensure_stop_previous_server args = [] args += ['--dir', Eye::Local.dir] if Eye::Local.local_runner opts = {:out => '/dev/null', :err => '/dev/null', :in => '/dev/null', :chdir => '/', :pgroup => true} pid = Process.spawn(ruby_path, loader_path, *args, opts) Process.detach(pid) File.open(Eye::Local.pid_path, 'w'){|f| f.write(pid) } unless wait_server error! 'server has not started in 15 seconds, something is very wrong' end configs.unshift(Eye::Local.global_eyeconfig) if File.exist?(Eye::Local.global_eyeconfig) configs.unshift(Eye::Local.eyeconfig) if File.exist?(Eye::Local.eyeconfig) configs << Eye::Local.eyefile if Eye::Local.local_runner say 'Eye started! ㋡', :green if !configs.empty? say_load_result cmd(:load, *configs) end end def ensure_stop_previous_server Eye::Local.ensure_eye_dir pid = File.read(Eye::Local.pid_path).to_i rescue nil if pid Process.kill(9, pid) rescue nil end File.delete(Eye::Local.pid_path) rescue nil true end def wait_server(timeout = 15) Timeout.timeout(timeout) do sleep 0.3 while !server_started? end true rescue Timeout::Error false end end eye-0.7/lib/eye/trigger.rb0000644000004100000410000000647512600364132015523 0ustar www-datawww-dataclass Eye::Trigger include Eye::Dsl::Validation autoload :Flapping, 'eye/trigger/flapping' autoload :Transition, 'eye/trigger/transition' autoload :StopChildren, 'eye/trigger/stop_children' autoload :WaitDependency, 'eye/trigger/wait_dependency' autoload :CheckDependency, 'eye/trigger/check_dependency' autoload :StartingGuard, 'eye/trigger/starting_guard' TYPES = {:flapping => 'Flapping', :transition => 'Transition', :stop_children => 'StopChildren', :wait_dependency => 'WaitDependency', :check_dependency => 'CheckDependency', :starting_guard => 'StartingGuard' } attr_reader :message, :options, :process def self.name_and_class(type) type = type.to_sym return {:name => type, :type => type} if TYPES[type] if type =~ /\A(.*?)_?[0-9]+\z/ ctype = $1.to_sym return {:name => type, :type => ctype} if TYPES[ctype] end end def self.get_class(type) klass = eval("Eye::Trigger::#{TYPES[type]}") rescue nil raise "unknown trigger #{type}" unless klass if deps = klass.requires Array(deps).each { |d| require d } end klass end def self.create(process, options = {}) get_class(options[:type]).new(process, options) rescue Exception, Timeout::Error => ex log_ex(ex) nil end def self.validate!(options = {}) get_class(options[:type]).validate(options) end def initialize(process, options = {}) @options = options @process = process @full_name = @process.full_name if @process debug { "add #{options}" } end def inspect "<#{self.class} @process='#{@full_name}' @options=#{@options}>" end def logger_tag @process.logger.prefix end def logger_sub_tag "trigger(#{@options[:type]})" end def notify(transition, reason) debug { "check (:#{transition.event}) :#{transition.from} => :#{transition.to}" } @reason = reason @transition = transition check(transition) if filter_transition(transition) rescue Exception, Timeout::Error => ex if ex.class == Eye::Process::StateError raise ex else log_ex(ex) end end param :to, [Symbol, Array] param :from, [Symbol, Array] param :event, [Symbol, Array] def filter_transition(trans) return true unless to || from || event compare_state(trans.to_name, to) && compare_state(trans.from_name, from) && compare_state(trans.event, event) end def check(transition) raise NotImplementedError end def run_in_process_context(p) process.instance_exec(&p) if process.alive? end def exec_proc(name = :do) act = @options[name] if act res = instance_exec(&@options[name]) if act.is_a?(Proc) res = send(act, process) if act.is_a?(Symbol) res else true end end def defer(&block) Celluloid::Future.new(&block).value end def self.register(base) name = base.to_s.gsub('Eye::Trigger::', '') type = name.underscore.to_sym Eye::Trigger::TYPES[type] = name Eye::Trigger.const_set(name, base) end def self.requires end class Custom < Eye::Trigger def self.inherited(base) super register(base) end end private def compare_state(state_name, condition) case condition when Symbol state_name == condition when Array condition.include?(state_name) else true end end end eye-0.7/lib/eye/utils/0000755000004100000410000000000012600364132014657 5ustar www-datawww-dataeye-0.7/lib/eye/utils/alive_array.rb0000644000004100000410000000165712600364132017513 0ustar www-datawww-dataclass Eye::Utils::AliveArray extend Forwardable include Enumerable def_delegators :@arr, :[], :<<, :clear, :delete, :size, :empty?, :push, :flatten, :present?, :uniq!, :select! def initialize(arr = []) @arr = arr end def each(&block) @arr.each{|elem| elem && elem.alive? && block[elem] } end def to_a map{|x| x } end def full_size @arr.size end def pure @arr end def sort_by(&block) self.class.new super end def sort(&block) self.class.new super end def +(other) if other.is_a?(Eye::Utils::AliveArray) @arr += other.pure elsif other.is_a?(Array) @arr += other else raise "Unexpected + #{other}" end self end def ==(other) if other.is_a?(Eye::Utils::AliveArray) @arr == other.pure elsif other.is_a?(Array) @arr == other else raise "Unexpected == #{other}" end end endeye-0.7/lib/eye/utils/tail.rb0000644000004100000410000000036212600364132016136 0ustar www-datawww-dataclass Eye::Utils::Tail < Array # limited array def initialize(max_size = 100) @max_size = max_size super() end def push(el) super(el) shift if length > @max_size self end def << (el) push(el) end endeye-0.7/lib/eye/utils/celluloid_chain.rb0000644000004100000410000000256012600364132020325 0ustar www-datawww-datarequire 'celluloid' class Eye::Utils::CelluloidChain include Celluloid def initialize(target) @target = target @calls = [] @running = false @target_class = @target.class end def add(method_name, *args, &block) @calls << {:method_name => method_name, :args => args, :block => block} ensure_process end def add_wo_dups(method_name, *args, &block) h = {:method_name => method_name, :args => args, :block => block} if @calls[-1] != h @calls << h ensure_process end end def add_wo_dups_current(method_name, *args, &block) h = {:method_name => method_name, :args => args, :block => block} if !@calls.include?(h) && @call != h @calls << h ensure_process end end def list @calls end def names_list list.map{|el| el[:method_name].to_sym } end def clear @calls = [] end alias :clear_pending_list :clear # need, because of https://github.com/celluloid/celluloid/issues/22 def inspect "Celluloid::Chain(#{@target_class}: #{@calls.size})" end attr_reader :running private def ensure_process unless @running @running = true async.process end end def process while @call = @calls.shift @running = true @target.send(@call[:method_name], *@call[:args], &@call[:block]) if @target.alive? end @running = false end endeye-0.7/lib/eye/utils/leak_19.rb0000644000004100000410000000036412600364132016434 0ustar www-datawww-data# http://stackoverflow.com/questions/7263268/ruby-symbolto-proc-leaks-references-in-1-9-2-p180 unless defined?(SimpleCov) # simplecov somehow crashed with this class Symbol def to_proc lambda { |x| x.send(self) } end end endeye-0.7/lib/eye/utils/pmap.rb0000644000004100000410000000024412600364132016141 0ustar www-datawww-datamodule Enumerable # Simple parallel map using Celluloid::Futures def pmap(&block) map { |elem| Celluloid::Future.new(elem, &block) }.map(&:value) end end eye-0.7/lib/eye/utils/mini_active_support.rb0000644000004100000410000000302612600364132021270 0ustar www-datawww-datarequire 'time' def silence_warnings old_verbose, $VERBOSE = $VERBOSE, nil yield ensure $VERBOSE = old_verbose end class Object def blank? respond_to?(:empty?) ? empty? : !self end def present? !blank? end def try(m, *args) send(m, *args) if respond_to?(m) end end class NilClass def try(*args) end end class String def underscore word = self.dup word.gsub!('::', '/') word.gsub!(/(?:([A-Za-z\d])|^)((?=a)b)(?=\b|[^a-z])/) { "#{$1}#{$1 && '_'}#{$2.downcase}" } word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2') word.gsub!(/([a-z\d])([A-Z])/,'\1_\2') word.tr!("-", "_") word.downcase! word end def truncate(l) self[0..l] end end class Array def extract_options! self[-1].is_a?(Hash) ? self.pop : {} end end class Numeric def percents self end alias :percent :percents def seconds self end alias :second :seconds def minutes self * 60 end alias :minute :minutes def hours self * 3600 end alias :hour :hours def days self * 86400 end alias :day :days def weeks self * 86400 * 7 end alias :week :weeks def ago ::Time.now - self end def bytes self end alias :byte :bytes def kilobytes self * 1024 end alias :kilobyte :kilobytes def megabytes self * 1024 * 1024 end alias :megabyte :megabytes def gigabytes self * 1024 * 1024 * 1024 end alias :gigabyte :gigabytes def terabytes self * 1024 * 1024 * 1024 * 1024 end alias :terabyte :terabytes end eye-0.7/lib/eye/system_resources.rb0000644000004100000410000000425412600364132017467 0ustar www-datawww-datarequire 'celluloid' class Eye::SystemResources # cached system resources class << self def memory(pid) if mem = cache.proc_mem(pid) mem.resident end end def cpu(pid) if cpu = cache.proc_cpu(pid) cpu.percent * 100 end end def children(parent_pid) cache.children(parent_pid) end def start_time(pid) # unixtime if cpu = cache.proc_cpu(pid) cpu.start_time.to_i / 1000 end end # total cpu usage in seconds def cputime(pid) if cpu = cache.proc_cpu(pid) cpu.total.to_f / 1000 end end # last child in a children tree def leaf_child(pid) if dc = deep_children(pid) dc.detect do |child| args = Eye::Sigar.proc_args(child)[0] rescue '' !args.start_with?('logger') && child != pid end end end def deep_children(pid) Array(pid_or_children(pid)).flatten.sort_by { |pid| -pid } end def pid_or_children(pid) c = children(pid) if !c || c.empty? pid else c.map { |ppid| pid_or_children(ppid) } end end def resources(pid) { :memory => memory(pid), :cpu => cpu(pid), :start_time => start_time(pid), :pid => pid } end def cache Celluloid::Actor[:system_resources_cache] end end class Cache include Celluloid attr_reader :expire def initialize clear setup_expire end def setup_expire(expire = 5) @expire = expire @timer.cancel if @timer @timer = every(@expire) { clear } end def clear @memory = {} @cpu = {} @ppids = {} end def proc_mem(pid) @memory[pid] ||= Eye::Sigar.proc_mem(pid) if pid rescue ArgumentError # when incorrect PID end def proc_cpu(pid) @cpu[pid] ||= Eye::Sigar.proc_cpu(pid) if pid rescue ArgumentError # when incorrect PID end def children(pid) if pid @ppids[pid] ||= Eye::Sigar.proc_list("State.Ppid.eq=#{pid}") else [] end end end # Setup global sigar singleton here Cache.supervise_as(:system_resources_cache) end eye-0.7/lib/eye/process/0000755000004100000410000000000012600364132015175 5ustar www-datawww-dataeye-0.7/lib/eye/process/system.rb0000644000004100000410000000463712600364132017060 0ustar www-datawww-datarequire 'timeout' module Eye::Process::System def load_pid_from_file res = if File.exist?(self[:pid_file_ex]) _pid = File.read(self[:pid_file_ex]).to_i _pid > 0 ? _pid : nil end res end def set_pid_from_file self.pid = load_pid_from_file end def save_pid_to_file if self.pid File.open(self[:pid_file_ex], 'w') do |f| f.write self.pid end true else false end end def clear_pid_file info "delete pid_file: #{self[:pid_file_ex]}" File.unlink(self[:pid_file_ex]) true rescue nil end def pid_file_ctime File.ctime(self[:pid_file_ex]) rescue Time.now end def process_really_running? process_pid_running?(self.pid) end def process_pid_running?(pid) res = Eye::System.check_pid_alive(pid) debug { "process_really_running?: <#{pid}> #{res.inspect}" } !!res[:result] end def send_signal(code) res = Eye::System.send_signal(self.pid, code) msg = "send_signal #{code} to <#{self.pid}>" msg += ", error<#{res[:error]}>" if res[:error] info msg res[:result] == :ok end def wait_for_condition(timeout, step = 0.1, &block) res = nil sumtime = 0 loop do tm = Time.now res = yield # note that yield can block actor here and timeout can be overhead return res if res sleep step.to_f sumtime += (Time.now - tm) return false if sumtime > timeout end end def execute(cmd, cfg = {}) defer { Eye::System::execute cmd, cfg }.tap do |res| notify(:debug, "Bad exit status of command #{cmd.inspect}(#{res[:exitstatus].inspect})") if res[:exitstatus] != 0 end end def daemonize(cmd, cfg = {}) Eye::System.daemonize(cmd, cfg) end def execute_sync(cmd, opts = {:timeout => 1.second}) execute(cmd, self.config.merge(opts)).tap do |res| info "execute_sync `#{cmd}` with res: #{res}" end end def execute_async(cmd, opts = {}) daemonize(cmd, self.config.merge(opts)).tap do |res| info "execute_async `#{cmd}` with res: #{res}" end end def failsafe_load_pid pid = load_pid_from_file if !pid # this is can be symlink changed case sleep 0.1 pid = load_pid_from_file end pid end def failsafe_save_pid save_pid_to_file true rescue => ex log_ex(ex) false end def expand_path(path) File.expand_path(path, self[:working_dir]) end end eye-0.7/lib/eye/process/controller.rb0000644000004100000410000000304312600364132017705 0ustar www-datawww-datamodule Eye::Process::Controller def send_command(command, *args) schedule command, *args, Eye::Reason::User.new(command) end def start res = if set_pid_from_file if process_really_running? info "process <#{self.pid}> from pid_file is already running" switch :already_running :ok else info "pid_file found, but process <#{self.pid}> is down, starting..." start_process end else info 'pid_file not found, starting...' start_process end res end def stop stop_process switch :unmonitoring end def restart unless pid # unmonitored case try_update_pid_from_file end restart_process end def monitor if self[:auto_start] start else if try_update_pid_from_file info "process <#{self.pid}> from pid_file is already running" switch :already_running else warn 'process not found, unmonitoring' schedule :unmonitor, Eye::Reason.new(:'not found') end end end def unmonitor switch :unmonitoring end def delete if self[:stop_on_delete] info 'process has stop_on_delete option, so sync-stop it first' stop end remove_watchers remove_children remove_triggers terminate end def signal(sig = 0) send_signal(sig) if self.pid end def user_command(name) if self[:user_commands] && c = self[:user_commands][name.to_sym] execute_user_command(name, c) end end def freeze scheduler_freeze end end eye-0.7/lib/eye/process/notify.rb0000644000004100000410000000146612600364132017041 0ustar www-datawww-datamodule Eye::Process::Notify # notify to user: # 1) process crashed by itself, and we restart it [:info] # 2) checker bounded to restart process [:warn] # 3) flapping + switch to unmonitored [:error] LEVELS = {:debug => 0, :info => 1, :warn => 2, :error => 3, :fatal => 4} def notify(level, msg) # logging it error "NOTIFY: #{msg}" if ilevel(level) > ilevel(:info) # send notifies if self[:notify].present? message = {:message => msg, :name => name, :full_name => full_name, :pid => pid, :host => Eye::Local.host, :level => level, :at => Time.now } self[:notify].each do |contact, not_level| Eye::Notify.notify(contact, message) if ilevel(level) >= ilevel(not_level) end end end private def ilevel(level) LEVELS[level].to_i end endeye-0.7/lib/eye/process/states_history.rb0000644000004100000410000000106212600364132020605 0ustar www-datawww-dataclass Eye::Process::StatesHistory < Eye::Utils::Tail def push(state, reason = nil, tm = Time.now) super(state: state, at: tm.to_i, reason: reason) end def states self.map{|c| c[:state] } end def states_for_period(period, from_time = nil) tm = Time.now - period tm = [tm, from_time].max if from_time tm = tm.to_f self.select{|s| s[:at] >= tm }.map{|c| c[:state] } end def last_state last[:state] end def last_reason last[:reason] rescue nil end def last_state_changed_at Time.at(last[:at]) end end eye-0.7/lib/eye/process/scheduler.rb0000644000004100000410000000531312600364132017502 0ustar www-datawww-datamodule Eye::Process::Scheduler # ex: schedule :update_config, config, "reason: update_config" def schedule(command, *args, &block) if scheduler.alive? if scheduler_freeze? warn ":#{command} ignoring to schedule, because scheduler is freeze" return end unless self.respond_to?(command, true) warn ":#{command} scheduling is unsupported" return end reason = if args.present? && args[-1].kind_of?(Eye::Reason) args.pop end info "schedule :#{command} #{reason ? "(reason: #{reason})" : nil}" if reason.class == Eye::Reason # for auto reasons # skip already running commands and all in chain scheduler.add_wo_dups_current(:scheduled_action, command, {:args => args, :reason => reason}, &block) else # for manual, or without reason # skip only for last in chain scheduler.add_wo_dups(:scheduled_action, command, {:args => args, :reason => reason}, &block) end end end def schedule_in(interval, command, *args, &block) debug { "schedule_in #{interval} :#{command} #{args}" } after(interval.to_f) do debug { "scheduled_in #{interval} :#{command} #{args}" } schedule(command, *args, &block) end end def scheduled_action(command, h = {}, &block) reason = h.delete(:reason) info "=> #{command} #{h[:args].present? ? "#{h[:args]*',' }" : nil} #{reason ? "(reason: #{reason})" : nil}" @current_scheduled_command = command @last_scheduled_command = command @last_scheduled_reason = reason @last_scheduled_at = Time.now send(command, *h[:args], &block) @current_scheduled_command = nil info "<= #{command}" schedule_history.push(command, reason, @last_scheduled_at.to_i) if parent = self.try(:parent) parent.schedule_history.push("#{command}_child", reason, @last_scheduled_at.to_i) end end def scheduler_actions_list scheduler.list.map{|c| c[:args].first rescue nil }.compact end def scheduler_clear_pending_list scheduler.clear_pending_list end def self.included(base) base.finalizer :remove_scheduler end attr_accessor :current_scheduled_command attr_accessor :last_scheduled_command, :last_scheduled_reason, :last_scheduled_at def schedule_history @schedule_history ||= Eye::Process::StatesHistory.new(50) end def scheduler_freeze @scheduler_freeze = true end def scheduler_unfreeze @scheduler_freeze = nil end def scheduler_freeze? @scheduler_freeze end private def remove_scheduler @scheduler.terminate if @scheduler && @scheduler.alive? end def scheduler @scheduler ||= Eye::Utils::CelluloidChain.new(current_actor) end end eye-0.7/lib/eye/process/monitor.rb0000644000004100000410000000564712600364132017225 0ustar www-datawww-datamodule Eye::Process::Monitor private def check_alive_with_refresh_pid_if_needed if process_really_running? return true else warn 'process not really running' try_update_pid_from_file end end def try_update_pid_from_file # if pid file was rewritten newpid = load_pid_from_file if newpid != self.pid info "process <#{self.pid}> changed pid to <#{newpid}>, updating..." if self.pid self.pid = newpid if process_really_running? return true else warn "process <#{newpid}> was not found" return false end else debug { 'process was not found' } return false end end def check_alive if up? # check that process runned unless process_really_running? warn "check_alive: process <#{self.pid}> not found" notify :info, 'crashed!' clear_pid_file if control_pid? && self.pid && load_pid_from_file == self.pid switch :crashed, Eye::Reason.new(:crashed) else # check that pid_file still here ppid = failsafe_load_pid if ppid != self.pid msg = "check_alive: pid_file (#{self[:pid_file]}) changed by itself (<#{self.pid}> => <#{ppid}>)" if control_pid? msg += ", reverting to <#{self.pid}> (the pid_file is controlled by eye)" unless failsafe_save_pid msg += ", pid_file write failed! O_o" end else changed_ago_s = Time.now - pid_file_ctime if ppid == nil msg += ", reverting to <#{self.pid}> (the pid_file is empty)" unless failsafe_save_pid msg += ", pid_file write failed! O_o" end elsif (changed_ago_s > self[:auto_update_pidfile_grace]) && process_pid_running?(ppid) msg += ", trusting this change, and now monitor <#{ppid}>" self.pid = ppid elsif (changed_ago_s > self[:revert_fuckup_pidfile_grace]) msg += " over #{self[:revert_fuckup_pidfile_grace]}s ago, reverting to <#{self.pid}>, because <#{ppid}> not alive" unless failsafe_save_pid msg += ", pid_file write failed! O_o" end else msg += ', ignoring self-managed pid change' end end warn msg end end end end def check_crash if down? if self[:keep_alive] warn 'check crashed: process is down' if self[:restore_in] schedule_in self[:restore_in].to_f, :restore, Eye::Reason.new(:crashed) else schedule :restore, Eye::Reason.new(:crashed) end else warn 'check crashed: process without keep_alive' schedule :unmonitor, Eye::Reason.new(:crashed) end else debug { 'check crashed: skipped, process is not in down' } end end def restore start if down? end end eye-0.7/lib/eye/process/config.rb0000644000004100000410000000446612600364132017001 0ustar www-datawww-datamodule Eye::Process::Config DEFAULTS = { :keep_alive => true, # restart when crashed :check_alive_period => 5.seconds, :start_timeout => 15.seconds, :stop_timeout => 10.seconds, :restart_timeout => 10.seconds, :start_grace => 2.5.seconds, :stop_grace => 0.5.seconds, :restart_grace => 1.second, :daemonize => false, :auto_start => true, # auto start on monitor action :children_update_period => 30.seconds, :clear_pid => true, # by default clear pid on stop :auto_update_pidfile_grace => 30.seconds, :revert_fuckup_pidfile_grace => 120.seconds, } def prepare_config(new_config) h = DEFAULTS.merge(new_config) h[:pid_file_ex] = Eye::System.normalized_file(h[:pid_file], h[:working_dir]) if h[:pid_file] h[:checks] = {} if h[:checks].blank? h[:triggers] = {} if h[:triggers].blank? h[:children_update_period] = h[:monitor_children][:children_update_period] if h[:monitor_children] && h[:monitor_children][:children_update_period] # check speedy flapping by default if h[:triggers].blank? || !h[:triggers][:flapping] h[:triggers] ||= {} h[:triggers][:flapping] = {:type => :flapping, :times => 10, :within => 10.seconds} end h[:stdout] = Eye::System.normalized_file(h[:stdout], h[:working_dir]) if h[:stdout] h[:stderr] = Eye::System.normalized_file(h[:stderr], h[:working_dir]) if h[:stderr] h[:stdall] = Eye::System.normalized_file(h[:stdall], h[:working_dir]) if h[:stdall] h[:environment] = Eye::System.prepare_env(h) h end def c(name) @config[name] end def [](name) @config[name] end def update_config(new_config = {}) new_config = prepare_config(new_config) @config = new_config @full_name = nil @logger = nil debug { "updating config to: #{@config.inspect}" } remove_triggers add_triggers if up? # rebuild checks for this process remove_watchers remove_children add_watchers add_children end end # is pid_file under Eye::Process control, or not def control_pid? !!self[:daemonize] end def skip_group_action?(action) if sga = self[:skip_group_actions] res = sga[action] if res == true res elsif res.is_a?(Array) res.include?(self.state_name) end end end end eye-0.7/lib/eye/process/states.rb0000644000004100000410000000412312600364132017025 0ustar www-datawww-datarequire 'state_machine' require 'state_machine/version' class Eye::Process class StateError < Exception; end # do transition def switch(name, reason = nil) @state_reason = reason || last_scheduled_reason self.send("#{name}!") end state_machine :state, :initial => :unmonitored do state :unmonitored, :up, :down state :starting, :stopping, :restarting event :starting do transition [:unmonitored, :down] => :starting end event :already_running do transition [:unmonitored, :down, :up] => :up end event :started do transition :starting => :up end event :crashed do transition [:starting, :restarting, :up] => :down end event :stopping do transition [:up, :restarting] => :stopping end event :stopped do transition :stopping => :down end event :cant_kill do transition :stopping => :up end event :restarting do transition [:unmonitored, :up, :down] => :restarting end event :restarted do transition :restarting => :up end event :unmonitoring do transition any => :unmonitored end after_transition any => any, :do => :log_transition after_transition any => any, :do => :check_triggers after_transition any => :unmonitored, :do => :on_unmonitored after_transition any-:up => :up, :do => :add_watchers after_transition :up => any-:up, :do => :remove_watchers after_transition any-:up => :up, :do => :add_children after_transition any => [:unmonitored, :down], :do => :remove_children after_transition :on => :crashed, :do => :on_crashed end def on_crashed schedule :check_crash, Eye::Reason.new(:crashed) end def on_unmonitored self.pid = nil end def log_transition(transition) if transition.to_name != transition.from_name || @state_reason.is_a?(Eye::Reason::User) @states_history.push transition.to_name, @state_reason info "switch :#{transition.event} [:#{transition.from_name} => :#{transition.to_name}] #{@state_reason ? "(reason: #{@state_reason})" : nil}" end end end eye-0.7/lib/eye/process/commands.rb0000644000004100000410000002116412600364132017327 0ustar www-datawww-datamodule Eye::Process::Commands def start_process debug { 'start_process command' } switch :starting unless self[:start_command] warn 'no :start_command found, unmonitoring' switch :unmonitoring, Eye::Reason.new(:no_start_command) return :no_start_command end result = self[:daemonize] ? daemonize_process : execute_process if !result[:error] debug { "process <#{self.pid}> started successfully" } switch :started else error "process <#{self.pid}> failed to start (#{result[:error].inspect})" if process_really_running? warn "killing <#{self.pid}> due to error" send_signal(:KILL) sleep 0.2 # little grace end self.pid = nil switch :crashed end result rescue StateMachine::InvalidTransition, Eye::Process::StateError => e warn "wrong switch '#{e.message}'" :state_error end def stop_process debug { 'stop_process command' } switch :stopping kill_process if process_really_running? warn "process <#{self.pid}> was not stopped; try checking your command/signals or tuning the stop_timeout/stop_grace values" switch :unmonitoring, Eye::Reason.new(:'not stopped (soft command)') nil else switch :stopped clear_pid_file if self[:clear_pid] # by default for all true end rescue StateMachine::InvalidTransition, Eye::Process::StateError => e warn "wrong switch '#{e.message}'" nil end def restart_process debug { 'restart_process command' } switch :restarting if self[:restart_command] execute_restart_command sleep_grace(:restart_grace) result = check_alive_with_refresh_pid_if_needed switch(result ? :restarted : :crashed) else stop_process start_process end true rescue StateMachine::InvalidTransition, Eye::Process::StateError => e warn "wrong switch '#{e.message}'" nil end private def kill_process unless self.pid error 'cannot stop a process without a pid' return end if self[:stop_command] cmd = prepare_command(self[:stop_command]) info "executing: `#{cmd}` with stop_timeout: #{self[:stop_timeout].to_f}s and stop_grace: #{self[:stop_grace].to_f}s" res = execute(cmd, config.merge(:timeout => self[:stop_timeout])) if res[:error] if res[:error].class == Timeout::Error error "stop_command failed with #{res[:error].inspect}; try tuning the stop_timeout value" else error "stop_command failed with #{res[:error].inspect}" end end sleep_grace(:stop_grace) elsif self[:stop_signals] info "executing stop_signals #{self[:stop_signals].inspect}" stop_signals = self[:stop_signals].clone signal = stop_signals.shift send_signal(signal) while stop_signals.present? delay = stop_signals.shift signal = stop_signals.shift if wait_for_condition(delay.to_f, 0.3){ !process_really_running? } info 'has terminated' break end send_signal(signal) if signal end sleep_grace(:stop_grace) else # default command debug { "executing: `kill -TERM #{self.pid}` with stop_grace: #{self[:stop_grace].to_f}s" } send_signal(:TERM) sleep_grace(:stop_grace) # if process not die here, by default we force kill it if process_really_running? warn "process <#{self.pid}> did not die after TERM, sending KILL" send_signal(:KILL) sleep 0.1 # little grace end end end def execute_restart_command unless self.pid error 'cannot restart a process without a pid' return end cmd = prepare_command(self[:restart_command]) info "executing: `#{cmd}` with restart_timeout: #{self[:restart_timeout].to_f}s and restart_grace: #{self[:restart_grace].to_f}s" res = execute(cmd, config.merge(:timeout => self[:restart_timeout])) if res[:error] if res[:error].class == Timeout::Error error "restart_command failed with #{res[:error].inspect}; try tuning the restart_timeout value" else error "restart_command failed with #{res[:error].inspect}" end end res end def daemonize_process time_before = Time.now res = daemonize(self[:start_command], config) start_time = Time.now - time_before info "daemonizing: `#{self[:start_command]}` with start_grace: #{self[:start_grace].to_f}s, env: '#{environment_string}', <#{res[:pid]}> (in #{self[:working_dir]})" if res[:error] if res[:error].message == 'Permission denied - open' error "daemonize failed with #{res[:error].inspect}; make sure #{[self[:stdout], self[:stderr]]} are writable" else error "daemonize failed with #{res[:error].inspect}" end return {:error => res[:error].inspect} end self.pid = res[:pid] unless self.pid error 'no pid was returned' return {:error => :empty_pid} end sleep_grace(:start_grace) unless process_really_running? error "process <#{self.pid}> not found, it may have crashed (#{check_logs_str})" return {:error => :not_really_running} end # if we using leaf child stratedy, pid should be used as last child process if self[:use_leaf_child] if lpid = Eye::SystemResources.leaf_child(self.pid) info "leaf child for <#{self.pid}> found: <#{lpid}>, accepting it!" self.parent_pid = self.pid self.pid = lpid else warn "leaf child not found for <#{self.pid}>, skipping it" end end if control_pid? && !failsafe_save_pid return {:error => :cant_write_pid} end res end def execute_process info "executing: `#{self[:start_command]}` with start_timeout: #{config[:start_timeout].to_f}s, start_grace: #{self[:start_grace].to_f}s, env: '#{environment_string}' (in #{self[:working_dir]})" time_before = Time.now res = execute(self[:start_command], config.merge(:timeout => config[:start_timeout])) start_time = Time.now - time_before if res[:error] if res[:error].message == 'Permission denied - open' error "execution failed with #{res[:error].inspect}; ensure that #{[self[:stdout], self[:stderr]]} are writable" elsif res[:error].class == Timeout::Error error "execution failed with #{res[:error].inspect}; try increasing the start_timeout value (the current value of #{self[:start_timeout]}s seems too short)" else error "execution failed with #{res[:error].inspect}" end return {:error => res[:error].inspect} end sleep_grace(:start_grace) unless set_pid_from_file error "exit status #{res[:exitstatus]}, pid_file (#{self[:pid_file_ex]}) did not appear within the start_grace period (#{self[:start_grace].to_f}s); check your start_command, or tune the start_grace value (eye expect process to create pid_file in self-daemonization mode)" return {:error => :pid_not_found} end unless process_really_running? error "exit status #{res[:exitstatus]}, process <#{self.pid}> (from #{self[:pid_file_ex]}) was not found; ensure that the pid_file is being updated correctly (#{check_logs_str})" return {:error => :not_really_running} end res[:pid] = self.pid info "exit status #{res[:exitstatus]}, process <#{res[:pid]}> (from #{self[:pid_file_ex]}) was found" res end def check_logs_str if !self[:stdout] && !self[:stderr] 'you may want to configure stdout/err/all logs for this process' else "you should check the process logs #{[self[:stdout], self[:stderr]]}" end end def prepare_command(command) if self.pid command.to_s.gsub('{{PID}}', self.pid.to_s).gsub('{PID}', self.pid.to_s) else command end end def sleep_grace(grace_name) grace = self[grace_name].to_f info "sleeping for :#{grace_name} #{grace}" sleep grace end def execute_user_command(name, cmd) info "executing user command #{name} #{cmd.inspect}" # cmd is string, or array of signals if cmd.is_a?(String) cmd = prepare_command(cmd) res = execute(cmd, config.merge(:timeout => 120)) error "cmd #{cmd} error #{res.inspect}" if res[:error] elsif cmd.is_a?(Array) signals = cmd.clone signal = signals.shift send_signal(signal) while signals.present? delay = signals.shift signal = signals.shift if wait_for_condition(delay.to_f, 0.3){ !process_really_running? } info 'has terminated' break end send_signal(signal) if signal end else warn "unknown user command #{c}" end end end eye-0.7/lib/eye/process/watchers.rb0000644000004100000410000000277112600364132017351 0ustar www-datawww-datamodule Eye::Process::Watchers def add_watchers(force = false) return unless self.up? remove_watchers if force if @watchers.blank? # default watcher :check_alive add_watcher(:check_alive, self[:check_alive_period]) do check_alive end # monitor children pids if self[:monitor_children] add_watcher(:check_children, self[:children_update_period]) do add_or_update_children end end # monitor conditional watchers start_checkers else warn 'add_watchers failed, watchers are already present' end end def remove_watchers @watchers.each{|_, h| h[:timer].cancel } @watchers = {} end private def add_watcher(type, period = 2, subject = nil, &block) return if @watchers[type] debug { "adding watcher: #{type}(#{period})" } timer = every(period.to_f) do debug { "check #{type}" } block.call(subject) end @watchers[type] ||= {:timer => timer, :subject => subject} end def start_checkers self[:checks].each{|name, cfg| start_checker(name, cfg) } end def start_checker(name, cfg) subject = Eye::Checker.create(pid, cfg, current_actor) # ex: {:type => :memory, :every => 5.seconds, :below => 100.megabytes, :times => [3,5]} add_watcher("check_#{name}".to_sym, subject.every, subject, &method(:watcher_tick).to_proc) if subject end def watcher_tick(subject) unless subject.check return unless up? subject.fire end end end eye-0.7/lib/eye/process/data.rb0000644000004100000410000000341012600364132016431 0ustar www-datawww-datamodule Eye::Process::Data def logger_tag full_name end def app_name self[:application] end def group_name (self[:group] == '__default__') ? nil : self[:group] end def group_name_pure self[:group] end def full_name @full_name ||= [app_name, group_name, self[:name]].compact.join(':') end def status_data(debug = false) p_st = self_status_data(debug) if children.present? p_st.merge(:subtree => Eye::Utils::AliveArray.new(children.values).map{|c| c.status_data(debug) } ) elsif self[:monitor_children] && self.up? p_st.merge(:subtree => [{name: '=loading children='}]) else # common state p_st end end def self_status_data(debug = false) h = { name: name, state: state, type: (self.class == Eye::ChildProcess ? :child_process : :process), resources: Eye::SystemResources.resources(pid) } if @states_history h.merge!( state_changed_at: @states_history.last_state_changed_at.to_i, state_reason: @states_history.last_reason.to_s ) end h[:debug] = debug_data if debug h[:current_command] = current_scheduled_command if current_scheduled_command h end def debug_data { queue: scheduler_actions_list, watchers: @watchers.keys } end def sub_object?(obj) return false if self.class == Eye::ChildProcess self.children.each { |_, child| return true if child == obj } false end def environment_string s = [] @config[:environment].each { |k, v| s << "#{k}=#{v}" } s * ' ' end def shell_string(dir = true) str = '' str += "cd #{self[:working_dir]} && " if dir str += environment_string str += ' ' str += self[:start_command] str += ' &' if self[:daemonize] str end end eye-0.7/lib/eye/process/trigger.rb0000644000004100000410000000174212600364132017171 0ustar www-datawww-datamodule Eye::Process::Trigger def add_triggers if self[:triggers] self[:triggers].each do |type, cfg| add_trigger(cfg) end end end def remove_triggers self.triggers = [] end def check_triggers(transition) self.triggers.each { |trigger| trigger.notify(transition, state_reason) } end # conditional start, used in triggers, to start only from unmonitored state, and only if special reason def conditional_start unless unmonitored? warn "skip, because in state #{state_name}" return end previous_reason = state_reason if last_scheduled_reason && previous_reason && last_scheduled_reason.class != previous_reason.class warn "skip, last_scheduled_reason(#{last_scheduled_reason.inspect}) != previous_reason(#{previous_reason})" return end start end private def add_trigger(cfg = {}) trigger = Eye::Trigger.create(current_actor, cfg) self.triggers << trigger if trigger end end eye-0.7/lib/eye/process/children.rb0000644000004100000410000000275612600364132017324 0ustar www-datawww-datamodule Eye::Process::Children def add_children add_or_update_children end def add_or_update_children return unless self[:monitor_children] return unless self.up? return if @updating_children @updating_children = true unless self.pid warn "can't add children; pid not set" return end now_children = Eye::SystemResources.children(self.pid) new_children = [] exist_children = [] now_children.each do |child_pid| if self.children[child_pid] exist_children << child_pid else new_children << child_pid end end removed_children = self.children.keys - now_children if new_children.present? new_children.each do |child_pid| cfg = self[:monitor_children].try :update, :notify => self[:notify] self.children[child_pid] = Eye::ChildProcess.new(child_pid, cfg, logger.prefix, current_actor) end end if removed_children.present? removed_children.each{|child_pid| remove_child(child_pid) } end h = {:new => new_children.size, :removed => removed_children.size, :exists => exist_children.size } debug { "children info: #{ h.inspect }" } @updating_children = false h end def remove_children children.each_key { |child_pid| clear_child(child_pid) } end def remove_child(child_pid) clear_child(child_pid) end def clear_child(child_pid) child = self.children.delete(child_pid) child.destroy if child && child.alive? end end eye-0.7/lib/eye/process/validate.rb0000644000004100000410000000167012600364132017317 0ustar www-datawww-datarequire 'shellwords' require 'etc' module Eye::Process::Validate class Error < Exception; end def validate(config, localize = true) if (str = config[:start_command]) # it should parse with Shellwords and not raise spl = Shellwords.shellwords(str) * '#' if config[:daemonize] && !config[:use_leaf_child] if spl =~ %r[sh#\-c|#&&#|;#] raise Error, "#{config[:name]}, daemonize does not support concats like '&&' in start_command" end end end Shellwords.shellwords(config[:stop_command]) if config[:stop_command] Shellwords.shellwords(config[:restart_command]) if config[:restart_command] if localize Etc.getpwnam(config[:uid]) if config[:uid] Etc.getgrnam(config[:gid]) if config[:gid] if config[:working_dir] raise Error, "working_dir '#{config[:working_dir]}' is invalid" unless File.directory?(config[:working_dir]) end end end end eye-0.7/lib/eye/server.rb0000644000004100000410000000204412600364132015352 0ustar www-datawww-datarequire 'celluloid/io' require 'celluloid/autostart' class Eye::Server include Celluloid::IO attr_reader :socket_path, :server def initialize(socket_path) @socket_path = socket_path @server = begin UNIXServer.open(socket_path) rescue Errno::EADDRINUSE unlink_socket_file UNIXServer.open(socket_path) end end def run loop { async.handle_connection @server.accept } end def handle_connection(socket) text = socket.read begin cmd, *args = Marshal.load(text) rescue => ex error "Failed to read from socket: #{ex.message}" return end response = command(cmd, *args) socket.write(Marshal.dump(response)) rescue Errno::EPIPE # client timeouted # do nothing ensure socket.close end def command(cmd, *args) Eye::Control.command(cmd, *args) end def unlink_socket_file File.delete(@socket_path) if @socket_path rescue end finalizer :close_socket def close_socket @server.close if @server unlink_socket_file end endeye-0.7/lib/eye/dsl.rb0000644000004100000410000000256012600364132014631 0ustar www-datawww-datarequire_relative 'dsl/helpers' Eye::BINDING = binding class Eye::Dsl autoload :Main, 'eye/dsl/main' autoload :ApplicationOpts, 'eye/dsl/application_opts' autoload :GroupOpts, 'eye/dsl/group_opts' autoload :ProcessOpts, 'eye/dsl/process_opts' autoload :ChildProcessOpts, 'eye/dsl/child_process_opts' autoload :Opts, 'eye/dsl/opts' autoload :PureOpts, 'eye/dsl/pure_opts' autoload :Chain, 'eye/dsl/chain' autoload :ConfigOpts, 'eye/dsl/config_opts' autoload :Validation, 'eye/dsl/validation' class Error < Exception; end class << self attr_accessor :verbose def debug(msg = '') puts msg if verbose end def parse(content = nil, filename = nil) Eye.parsed_config = Eye::Config.new Eye.parsed_filename = filename content = File.read(filename) if content.blank? silence_warnings do Kernel.eval(content, Eye::BINDING, filename.to_s) end Eye.parsed_config.transform! Eye.parsed_config.validate! Eye.parsed_config end def parse_apps(*args) parse(*args).applications end def check_name(name) raise Error, "':' is not allowed in name '#{name}'" if name.to_s.include?(':') end end end # extend here global module Eye.send(:extend, Eye::Dsl::Main) eye-0.7/lib/eye/sigar.rb0000644000004100000410000000014112600364132015145 0ustar www-datawww-datarequire 'sigar' require 'logger' Eye::Sigar = ::Sigar.new Eye::Sigar.logger = ::Logger.new(nil) eye-0.7/lib/eye/child_process.rb0000644000004100000410000000315512600364132016671 0ustar www-datawww-datarequire 'celluloid' class Eye::ChildProcess include Celluloid # needs: kill_process include Eye::Process::Commands # easy config + defaults: prepare_config, c, [] include Eye::Process::Config # conditional watchers: start_checkers include Eye::Process::Watchers # system methods: send_signal include Eye::Process::System # self_status_data include Eye::Process::Data # manage notify methods include Eye::Process::Notify # scheduler include Eye::Process::Scheduler attr_reader :pid, :name, :full_name, :config, :watchers, :parent def initialize(pid, config = {}, logger_prefix = nil, parent = nil) raise 'Empty pid' unless pid @pid = pid @parent = parent @config = prepare_config(config) @name = "child-#{pid}" @full_name = [logger_prefix, @name] * ':' @watchers = {} debug { "start monitoring CHILD config: #{@config.inspect}" } start_checkers end def logger_tag full_name end def state :up end def up? state == :up end def send_command(command, *args) schedule command, *args, Eye::Reason::User.new(command) end def start end def stop kill_process end def restart if self[:restart_command] execute_restart_command else stop end end def monitor end def unmonitor end def delete end def destroy remove_watchers terminate end def signal(sig) send_signal(sig) if self.pid end def status_data(debug = false) self_status_data(debug) end def prepare_command(command) # override super.gsub('{PARENT_PID}', @parent.pid.to_s) end end eye-0.7/metadata.yml0000644000004100000410000002615212600364132014500 0ustar www-datawww-data--- !ruby/object:Gem::Specification name: eye version: !ruby/object:Gem::Version version: '0.7' platform: ruby authors: - Konstantin Makarchev autorequire: bindir: bin cert_chain: [] date: 2015-09-14 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: celluloid requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: 0.16.0 type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: 0.16.0 - !ruby/object:Gem::Dependency name: celluloid-io requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: 0.16.0 type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: 0.16.0 - !ruby/object:Gem::Dependency name: state_machine requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: thor requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: sigar requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: 0.7.3 type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: 0.7.3 - !ruby/object:Gem::Dependency name: rake requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: rspec requirement: !ruby/object:Gem::Requirement requirements: - - "<" - !ruby/object:Gem::Version version: '2.14' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "<" - !ruby/object:Gem::Version version: '2.14' - !ruby/object:Gem::Dependency name: rr requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: ruby-graphviz requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: forking requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: fakeweb requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: eventmachine requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 1.0.3 type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 1.0.3 - !ruby/object:Gem::Dependency name: sinatra requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: thin requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: xmpp4r requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: slack-notifier requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: coveralls requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: simplecov requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 0.8.1 type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 0.8.1 - !ruby/object:Gem::Dependency name: parallel_tests requirement: !ruby/object:Gem::Requirement requirements: - - "<=" - !ruby/object:Gem::Version version: 1.3.1 type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "<=" - !ruby/object:Gem::Version version: 1.3.1 - !ruby/object:Gem::Dependency name: parallel_split_test requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' description: Process monitoring tool. Inspired from Bluepill and God. Requires Ruby(MRI) >= 1.9.3-p194. Uses Celluloid and Celluloid::IO. email: eye-rb@googlegroups.com executables: - eye - leye - loader_eye extensions: [] extra_rdoc_files: [] files: - ".gitignore" - ".rspec" - ".travis.yml" - CHANGES.md - Gemfile - LICENSE - README.md - Rakefile - bin/eye - bin/leye - bin/loader_eye - examples/delayed_job.eye - examples/dependency.eye - examples/notify.eye - examples/plugin/README.md - examples/plugin/main.eye - examples/plugin/plugin.rb - examples/process_thin.rb - examples/processes/em.rb - examples/processes/forking.rb - examples/processes/sample.rb - examples/processes/thin.ru - examples/puma.eye - examples/rbenv.eye - examples/sidekiq.eye - examples/stress_test.eye - examples/syslog.eye - examples/test.eye - examples/thin-farm.eye - examples/triggers.eye - examples/unicorn.eye - eye.gemspec - lib/eye.rb - lib/eye/application.rb - lib/eye/checker.rb - lib/eye/checker/children_count.rb - lib/eye/checker/children_memory.rb - lib/eye/checker/cpu.rb - lib/eye/checker/cputime.rb - lib/eye/checker/file_ctime.rb - lib/eye/checker/file_size.rb - lib/eye/checker/file_touched.rb - lib/eye/checker/http.rb - lib/eye/checker/memory.rb - lib/eye/checker/nop.rb - lib/eye/checker/runtime.rb - lib/eye/checker/socket.rb - lib/eye/checker/ssl_socket.rb - lib/eye/child_process.rb - lib/eye/cli.rb - lib/eye/cli/commands.rb - lib/eye/cli/render.rb - lib/eye/cli/server.rb - lib/eye/client.rb - lib/eye/config.rb - lib/eye/control.rb - lib/eye/controller.rb - lib/eye/controller/commands.rb - lib/eye/controller/helpers.rb - lib/eye/controller/load.rb - lib/eye/controller/options.rb - lib/eye/controller/send_command.rb - lib/eye/controller/status.rb - lib/eye/dsl.rb - lib/eye/dsl/application_opts.rb - lib/eye/dsl/chain.rb - lib/eye/dsl/child_process_opts.rb - lib/eye/dsl/config_opts.rb - lib/eye/dsl/group_opts.rb - lib/eye/dsl/helpers.rb - lib/eye/dsl/main.rb - lib/eye/dsl/opts.rb - lib/eye/dsl/process_opts.rb - lib/eye/dsl/pure_opts.rb - lib/eye/dsl/validation.rb - lib/eye/group.rb - lib/eye/group/chain.rb - lib/eye/loader.rb - lib/eye/local.rb - lib/eye/logger.rb - lib/eye/notify.rb - lib/eye/notify/jabber.rb - lib/eye/notify/mail.rb - lib/eye/notify/slack.rb - lib/eye/process.rb - lib/eye/process/children.rb - lib/eye/process/commands.rb - lib/eye/process/config.rb - lib/eye/process/controller.rb - lib/eye/process/data.rb - lib/eye/process/monitor.rb - lib/eye/process/notify.rb - lib/eye/process/scheduler.rb - lib/eye/process/states.rb - lib/eye/process/states_history.rb - lib/eye/process/system.rb - lib/eye/process/trigger.rb - lib/eye/process/validate.rb - lib/eye/process/watchers.rb - lib/eye/reason.rb - lib/eye/server.rb - lib/eye/sigar.rb - lib/eye/system.rb - lib/eye/system_resources.rb - lib/eye/trigger.rb - lib/eye/trigger/check_dependency.rb - lib/eye/trigger/flapping.rb - lib/eye/trigger/starting_guard.rb - lib/eye/trigger/stop_children.rb - lib/eye/trigger/transition.rb - lib/eye/trigger/wait_dependency.rb - lib/eye/utils.rb - lib/eye/utils/alive_array.rb - lib/eye/utils/celluloid_chain.rb - lib/eye/utils/leak_19.rb - lib/eye/utils/mini_active_support.rb - lib/eye/utils/pmap.rb - lib/eye/utils/tail.rb homepage: http://github.com/kostya/eye licenses: - MIT metadata: {} post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 1.9.2 required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 1.3.6 requirements: [] rubyforge_project: rubygems_version: 2.4.7 signing_key: specification_version: 4 summary: Process monitoring tool. Inspired from Bluepill and God. Requires Ruby(MRI) >= 1.9.3-p194. Uses Celluloid and Celluloid::IO. test_files: [] eye-0.7/eye.gemspec0000644000004100000410000000342712600364132014324 0ustar www-datawww-datarequire File.expand_path('../lib/eye', __FILE__) Gem::Specification.new do |gem| gem.authors = "Konstantin Makarchev" gem.email = "eye-rb@googlegroups.com" gem.description = gem.summary = \ %q{Process monitoring tool. Inspired from Bluepill and God. Requires Ruby(MRI) >= 1.9.3-p194. Uses Celluloid and Celluloid::IO.} gem.homepage = "http://github.com/kostya/eye" gem.files = `git ls-files`.split($\).reject{|n| n =~ %r[png|gif\z]}.reject{|n| n =~ %r[^(test|spec|features)/]} gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } #gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) gem.name = "eye" gem.require_paths = ["lib"] gem.version = Eye::VERSION gem.license = "MIT" gem.required_ruby_version = '>= 1.9.2' gem.required_rubygems_version = '>= 1.3.6' gem.add_dependency 'celluloid', '~> 0.16.0' gem.add_dependency 'celluloid-io', '~> 0.16.0' gem.add_dependency 'state_machine' gem.add_dependency 'thor' gem.add_dependency 'sigar', '~> 0.7.3' gem.add_development_dependency 'rake' gem.add_development_dependency 'rspec', '< 2.14' gem.add_development_dependency 'rr' gem.add_development_dependency 'ruby-graphviz' gem.add_development_dependency 'forking' gem.add_development_dependency 'fakeweb' gem.add_development_dependency 'eventmachine', ">= 1.0.3" gem.add_development_dependency 'sinatra' gem.add_development_dependency 'thin' gem.add_development_dependency 'xmpp4r' gem.add_development_dependency 'slack-notifier' gem.add_development_dependency 'coveralls' gem.add_development_dependency 'simplecov', '>= 0.8.1' gem.add_development_dependency 'parallel_tests', '<= 1.3.1' gem.add_development_dependency 'parallel_split_test' end eye-0.7/.gitignore0000644000004100000410000000051112600364132014154 0ustar www-datawww-data*.gem *.rbc .bundle .config .ruby-version .ruby-gemset .yardoc Gemfile.lock InstalledFiles _yardoc coverage doc/ lib/bundler/man pkg rdoc spec/reports test/tmp test/version_tmp tmp *.pid *.log *.swp *~ TODO .todo *.png *.lock experiments .git2 *.stop *sublime* examples/work*.eye script [0-9].rb *.cache *.tmp /vendor/ *.gz .eye eye-0.7/LICENSE0000644000004100000410000000207412600364132013177 0ustar www-datawww-dataCopyright (c) 2012-2015 'Konstantin Makarchev' MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. eye-0.7/CHANGES.md0000644000004100000410000000736212600364132013571 0ustar www-datawww-data0.7 ------- * add `stdall syslog`, example: https://github.com/kostya/eye/blob/master/examples/syslog.eye * added check `ssl_socket` #125 * some fixes with `eye q -s` * fixed `__default__` apps * default loaded configs with first eye start, is: `/etc/eye.conf`, and `~/.eyeconfig` * add trigger `starting_guard` * fix `load_env` function * fix multiple contacts #118 * add slack notifier #115 * some fixes in depend_on * some fixes in flapping * add proxy_url to http check * process with children, shows children history now * Update Celluloid to 0.16.0 0.6.4 ----- * leye: rename env variable EYEFILE to EYE_FILE * leye: add options --eyefile and --eyehome #102 * leye: now store pid and sock into "DIR(eyefile)/.eye" (requires to leye quit && leye load) * add dsl load_env method * add trigger executing helpers :execute_sync, :execute_async * add [triggers example](https://github.com/kostya/eye/blob/master/examples/triggers.eye) * fix user command expand {PID} #104 * add EYE_CLIENT_TIMEOUT variable to set client timeout #99 0.6.3 ----- * Add custom logger #81 * Revert check by procline, this was hack, fix for #62 should be in 0.7 * Fix ruby path, and expand_paths #69, #75 * Add json info `eye info -j` * Rename local runner to `leye` 0.6.2 ----- * Add user defined command #65 * eye status PROCESS_NAME, now return exit status for process name (0: up, 3: unmonitored) #68 * test pid from pid_file for eye-lwp (hackety), probably fix #62 * fix exclusive `eye load` 0.6.1 ------ * Add log rotation gem (https://github.com/kostya/eye-rotate) * Add option to clear environment variables #64 * Get group names from /etc/group via Etc#getgrnam #63 0.6 ------ * add processes dependencies (#43) * add eye-http gem (https://github.com/kostya/eye-http) * add eye plugin example (https://github.com/kostya/eye/tree/master/examples/plugin) * add quit option --stop_all (#39) * add local eye runner (like foreman, used Eyefile) * add use_leaf_child monitoring strategy (to daemonize sh -c '...') (788488a) * add children_count, children_memory checks * add dsl default application options (__default__) * trusting external pid_file changes (#52) 0.5.2 ----- * rename dsl :childs_update_period to :children_update_period * grammar fixes * add checker option `above` 0.5.1 ----- * fix ordering in info (#27) * add log rotation (#26) * minor load fixes 0.5 ------- * little fixes in dsl * remove activesupport dependency * rename `state` trigger to `transition` * add runtime, cputime, file_touched checks * real cpu check (#9) * use sigar gem instead of `ps ax` * refactor cli (requires `eye q && eye l` after update gem from 0.4.x) * update celluloid to 0.15 0.4.2 ----- * add checker options :initial_grace, :skip_initial_fails * allow deleting env variables (#15) 0.4.1 --------- * add nop checker for periodic restart * catch errors in custom checkers, triggers * add custom notify * checker can fires array of commands * fix targets matching * remove autoset PWD env 0.4 --------- * pass tests on 1.9.2 * relax activesupport dependency * change client-server protocol (requires `eye q && eye l` after update gem from 0.3.x) * not matching targets from different applications * improve triggers (custom, better flapping) * delete pid_file on crash for daemonize process * delete pid_file on stop for all process types (`clear_pid false` to disable) * parallel tests (from 30 mins to 3min) * update celluloid to 0.14 0.3.2 --------- * improve matching targers * possibility to add many checkers with the same type per process (ex: checks :http_2, ...) * add uid, gid options (only for ruby 2.0) 0.3.1 ----- * load multiple configs (folder,...) now not breaks on first error (each config loads separately) * load ~/.eyeconfig with first eye load * some concurrency fixes * custom checker 0.3 --- * stable version eye-0.7/README.md0000644000004100000410000001554312600364132013456 0ustar www-datawww-dataEye === [![Gem Version](https://badge.fury.io/rb/eye.png)](http://rubygems.org/gems/eye) [![Build Status](https://secure.travis-ci.org/kostya/eye.png?branch=master)](http://travis-ci.org/kostya/eye) [![Coverage Status](https://coveralls.io/repos/kostya/eye/badge.png?branch=master)](https://coveralls.io/r/kostya/eye?branch=master) Process monitoring tool. Inspired from Bluepill and God. Requires Ruby(MRI) >= 1.9.3-p194. Uses Celluloid and Celluloid::IO. Little demo, shows general commands and how chain works: [![Eye](https://raw.github.com/kostya/stuff/master/eye/eye.png)](https://raw.github.com/kostya/stuff/master/eye/eye.gif) Recommended installation on the server (system wide): $ sudo /usr/local/ruby/1.9.3/bin/gem install eye $ sudo ln -sf /usr/local/ruby/1.9.3/bin/eye /usr/local/bin/eye ### Why? We have used god and bluepill in production and always ran into bugs (segfaults, crashes, lost processes, kill not-related processes, load problems, deploy problems, ...) We wanted something more robust and production stable. We wanted the features of bluepill and god, with a few extras like chains, nested configuring, mask matching, easy debug configs I hope we've succeeded, we're using eye in production and are quite happy. ### Config example examples/test.eye ([more examples](https://github.com/kostya/eye/tree/master/examples)) ```ruby # load submodules, here just for example Eye.load('./eye/*.rb') # Eye self-configuration section Eye.config do logger '/tmp/eye.log' end # Adding application Eye.application 'test' do # All options inherits down to the config leafs. # except `env`, which merging down # uid "user_name" # run app as a user_name (optional) - available only on ruby >= 2.0 # gid "group_name" # run app as a group_name (optional) - available only on ruby >= 2.0 working_dir File.expand_path(File.join(File.dirname(__FILE__), %w[ processes ])) stdall 'trash.log' # stdout,err logs for processes by default env 'APP_ENV' => 'production' # global env for each processes trigger :flapping, times: 10, within: 1.minute, retry_in: 10.minutes check :cpu, every: 10.seconds, below: 100, times: 3 # global check for all processes group 'samples' do chain grace: 5.seconds # chained start-restart with 5s interval, one by one. # eye daemonized process process :sample1 do pid_file '1.pid' # pid_path will be expanded with the working_dir start_command 'ruby ./sample.rb' # when no stop_command or stop_signals, default stop is [:TERM, 0.5, :KILL] # default `restart` command is `stop; start` daemonize true stdall 'sample1.log' # ensure the CPU is below 30% at least 3 out of the last 5 times checked check :cpu, below: 30, times: [3, 5] end # self daemonized process process :sample2 do pid_file '2.pid' start_command 'ruby ./sample.rb -d --pid 2.pid --log sample2.log' stop_command 'kill -9 {PID}' # ensure the memory is below 300Mb the last 3 times checked check :memory, every: 20.seconds, below: 300.megabytes, times: 3 end end # daemon with 3 children process :forking do pid_file 'forking.pid' start_command 'ruby ./forking.rb start' stop_command 'ruby forking.rb stop' stdall 'forking.log' start_timeout 10.seconds stop_timeout 5.seconds monitor_children do restart_command 'kill -2 {PID}' # for this child process check :memory, below: 300.megabytes, times: 3 end end # eventmachine process, daemonized with eye process :event_machine do |p| pid_file 'em.pid' start_command 'ruby em.rb' stdout 'em.log' daemonize true stop_signals [:QUIT, 2.seconds, :KILL] check :socket, addr: 'tcp://127.0.0.1:33221', every: 10.seconds, times: 2, timeout: 1.second, send_data: 'ping', expect_data: /pong/ end # thin process, self daemonized process :thin do pid_file 'thin.pid' start_command 'bundle exec thin start -R thin.ru -p 33233 -d -l thin.log -P thin.pid' stop_signals [:QUIT, 2.seconds, :TERM, 1.seconds, :KILL] check :http, url: 'http://127.0.0.1:33233/hello', pattern: /World/, every: 5.seconds, times: [2, 3], timeout: 1.second end end ``` ### Start eye daemon and/or load config: $ eye l(oad) examples/test.eye load folder with configs: $ eye l examples/ $ eye l examples/*.rb foreground load: $ eye l CONF -f If the eye daemon has already started and you call the `load` command, the config will be updated (into eye daemon). New objects(applications, groups, processes) will be added and monitored. Processes removed from the config will be removed (and stopped if the process has `stop_on_delete true`). Other objects will update their configs. Process statuses: $ eye i(nfo) ``` test samples sample1 ....................... up (21:52, 0%, 13Mb, <4107>) sample2 ....................... up (21:52, 0%, 12Mb, <4142>) event_machine ................... up (21:52, 3%, 26Mb, <4112>) forking ......................... up (21:52, 0%, 41Mb, <4203>) child-4206 .................... up (21:52, 0%, 41Mb, <4206>) child-4211 .................... up (21:52, 0%, 41Mb, <4211>) child-4214 .................... up (21:52, 0%, 41Mb, <4214>) thin ............................ up (21:53, 2%, 54Mb, <4228>) ``` $ eye i -j # show info in JSON format ### Commands: start, stop, restart, delete, monitor, unmonitor Command params (with restart for example): $ eye r(estart) all $ eye r test $ eye r samples $ eye r sample1 $ eye r sample* $ eye r test:samples $ eye r test:samples:sample1 $ eye r test:samples:sample* $ eye r test:*sample* Check config syntax: $ eye c(heck) examples/test.eye Config explain (for debug): $ eye e(xplain) examples/test.eye Log tracing (tail and grep): $ eye t(race) $ eye t test $ eye t sample Quit monitoring: $ eye q(uit) $ eye q -s # stop all processes and quit Interactive info: $ eye w(atch) Process statuses history: $ eye hi(story) Eye daemon info: $ eye x(info) $ eye x -c # for show current config Local Eye version LEye (like foreman): [LEye](https://github.com/kostya/eye/wiki/What-is-loader_eye-and-leye) Process states and events: [![Eye](https://raw.github.com/kostya/stuff/master/eye/mprocess.png)](https://raw.github.com/kostya/stuff/master/eye/process.png) How to write Eye extensions, plugins, gems: [Eye-http](https://github.com/kostya/eye-http) [Eye-rotate](https://github.com/kostya/eye-rotate) [Eye-hipchat](https://github.com/tmeinlschmidt/eye-hipchat) [Plugin example](https://github.com/kostya/eye/tree/master/examples/plugin) [Eye related projects](https://github.com/kostya/eye/wiki/Related-projects) [Articles](https://github.com/kostya/eye/wiki/Articles) [Wiki](https://github.com/kostya/eye/wiki) Thanks `Bluepill` for the nice config ideas.