ziltoid 1.0.1 → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/ziltoid/command_parser.rb +53 -0
- data/lib/ziltoid/email_notifier.rb +24 -0
- data/lib/ziltoid/process.rb +182 -0
- data/lib/ziltoid/system.rb +77 -0
- data/lib/ziltoid/watcher.rb +108 -0
- metadata +22 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 20d41ee7c21c5543faea2a88d7cabc1ab9636abd
|
4
|
+
data.tar.gz: 85dc0a7c9005ef96c15d66874d096c23eade24f3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 148418049387dc672aa32aeec694f009c250ce0ec7ba2aa0232a353d32fe6c56c936a9b568a00bb1062db0866f76ef28644d2c64858581d24ee9c555bf8eb88e
|
7
|
+
data.tar.gz: b3028c71a803a81e6361d822c9a738b22966e0127641dad6b9c6295f2093fe1befe7b542d0c147f7031d459b0d6b29ee7bbcfa96a287181e79c10f62ca05184f
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
module Ziltoid
|
5
|
+
class CommandParser
|
6
|
+
|
7
|
+
ALLOWED_COMMANDS = ["watch", "start", "stop", "restart"]
|
8
|
+
|
9
|
+
# Returns a structure describing the options.
|
10
|
+
def self.parse(args)
|
11
|
+
runnable = OpenStruct.new
|
12
|
+
|
13
|
+
helptext = <<-HELP
|
14
|
+
Available commands are :
|
15
|
+
watch : watches all processes
|
16
|
+
start : starts all processes
|
17
|
+
stop : stops all processes
|
18
|
+
restart : restarts all processes
|
19
|
+
HELP
|
20
|
+
|
21
|
+
opt_parser = OptionParser.new do |opts|
|
22
|
+
# Printing generic help at the top of commands summary
|
23
|
+
opts.banner = "Usage: ziltoid.rb [options]"
|
24
|
+
opts.separator ""
|
25
|
+
opts.separator helptext
|
26
|
+
opts.separator ""
|
27
|
+
opts.separator "Common options :"
|
28
|
+
|
29
|
+
# No argument, shows at tail. This will print a commands summary.
|
30
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
31
|
+
puts opts
|
32
|
+
exit
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Retrieves all arguments except option-like ones (e.g. '-h' or '-v')
|
37
|
+
opt_parser.parse!(args)
|
38
|
+
# Fetches the first argument as the intended command
|
39
|
+
command = args.shift
|
40
|
+
|
41
|
+
# Making sure the command is valid, otherwise print commands summary
|
42
|
+
if command && ALLOWED_COMMANDS.include?(command)
|
43
|
+
runnable.command = command
|
44
|
+
else
|
45
|
+
puts opt_parser.help
|
46
|
+
exit
|
47
|
+
end
|
48
|
+
|
49
|
+
runnable
|
50
|
+
end # parse()
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'pony'
|
2
|
+
|
3
|
+
module Ziltoid
|
4
|
+
class EmailNotifier
|
5
|
+
attr_accessor :via_options, :from, :to, :subject
|
6
|
+
|
7
|
+
def initialize(options)
|
8
|
+
self.via_options = options[:via_options]
|
9
|
+
self.to = options[:to]
|
10
|
+
self.from = options[:from]
|
11
|
+
self.subject = options[:subject]
|
12
|
+
end
|
13
|
+
|
14
|
+
def send(message)
|
15
|
+
Pony.mail(
|
16
|
+
:to => self.to,
|
17
|
+
:via => :smtp,
|
18
|
+
:via_options => self.via_options,
|
19
|
+
:from => self.from,
|
20
|
+
:subject => self.subject, :body => message
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
module Ziltoid
|
2
|
+
class Process
|
3
|
+
attr_accessor :name, :ram_limit, :cpu_limit, :start_command, :stop_command, :restart_command, :pid_file, :start_grace_time, :stop_grace_time, :ram_grace_time, :cpu_grace_time, :restart_grace_time
|
4
|
+
|
5
|
+
WAIT_TIME_BEFORE_CHECK = 1.0
|
6
|
+
ALLOWED_STATES = ["started", "stopped", "restarted", "above_cpu_limit", "above_ram_limit"]
|
7
|
+
PREDOMINANT_STATES = ["started", "stopped", "restarted"]
|
8
|
+
|
9
|
+
def initialize(name, options = {})
|
10
|
+
self.name = name
|
11
|
+
self.ram_limit = options[:limit] ? options[:limit][:ram] : nil
|
12
|
+
self.cpu_limit = options[:limit] ? options[:limit][:cpu] : nil
|
13
|
+
self.pid_file = options[:pid_file] || "~/.ziltoid/#{name}.pid"
|
14
|
+
|
15
|
+
if options[:commands]
|
16
|
+
self.start_command = options[:commands][:start] || nil
|
17
|
+
self.stop_command = options[:commands][:stop] || nil
|
18
|
+
self.restart_command = options[:commands][:restart] || nil
|
19
|
+
end
|
20
|
+
|
21
|
+
if options[:grace_times]
|
22
|
+
self.start_grace_time = options[:grace_times][:start] || 0
|
23
|
+
self.stop_grace_time = options[:grace_times][:stop] || 0
|
24
|
+
self.restart_grace_time = options[:grace_times][:restart] || 0
|
25
|
+
self.ram_grace_time = options[:grace_times][:ram] || 0
|
26
|
+
self.cpu_grace_time = options[:grace_times][:cpu] || 0
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def pid
|
31
|
+
if self.pid_file && File.exist?(self.pid_file)
|
32
|
+
str = File.read(pid_file)
|
33
|
+
str.to_i if str.size > 0
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def alive?
|
38
|
+
Ziltoid::System.pid_alive?(self.pid)
|
39
|
+
end
|
40
|
+
|
41
|
+
def dead?
|
42
|
+
!alive?
|
43
|
+
end
|
44
|
+
|
45
|
+
def remove_pid_file
|
46
|
+
if self.pid_file && File.exist?(self.pid_file)
|
47
|
+
File.delete(self.pid_file)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def above_cpu_limit?(include_children = true)
|
52
|
+
Ziltoid::System.cpu_usage(self.pid, include_children) > self.cpu_limit.to_f
|
53
|
+
end
|
54
|
+
|
55
|
+
def above_ram_limit?(include_children = true)
|
56
|
+
Ziltoid::System.ram_usage(self.pid, include_children) > self.ram_limit.to_i * 1024
|
57
|
+
end
|
58
|
+
|
59
|
+
def state
|
60
|
+
state_hash = Ziltoid::Watcher.read_state[self.name]
|
61
|
+
state_hash["state"] if state_hash
|
62
|
+
end
|
63
|
+
|
64
|
+
def updated_at
|
65
|
+
state_hash = Ziltoid::Watcher.read_state[self.name]
|
66
|
+
state_hash["updated_at"] if state_hash
|
67
|
+
end
|
68
|
+
|
69
|
+
def processable?(target_state)
|
70
|
+
current_state = self.state
|
71
|
+
# started, stopped and restarted are 'predominant' current states,
|
72
|
+
# we never proceed unless the corresponding grace time is over
|
73
|
+
Watcher.log("Current state : #{current_state} - updated_at : #{self.updated_at.to_i} - target_state : #{target_state}")
|
74
|
+
return false if pending_grace_time?
|
75
|
+
return true if PREDOMINANT_STATES.include?(target_state)
|
76
|
+
|
77
|
+
# above_cpu_limit and above_ram_limit grace times are different,
|
78
|
+
# they represent a time a process has to be in that state to actually be processed (restarted most likely)
|
79
|
+
case target_state
|
80
|
+
when "above_cpu_limit"
|
81
|
+
current_state == target_state && self.updated_at.to_i < Time.now.to_i - self.cpu_grace_time.to_i
|
82
|
+
when "above_ram_limit"
|
83
|
+
current_state == target_state && self.updated_at.to_i < Time.now.to_i - self.ram_grace_time.to_i
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def pending_grace_time?
|
88
|
+
current_state = self.state
|
89
|
+
PREDOMINANT_STATES.include?(current_state) && self.updated_at.to_i > Time.now.to_i - self.send("#{current_state.gsub(/p?ed/, '')}_grace_time").to_i
|
90
|
+
end
|
91
|
+
|
92
|
+
def update_state(state)
|
93
|
+
process_states = Ziltoid::Watcher.read_state
|
94
|
+
return nil unless ALLOWED_STATES.include?(state)
|
95
|
+
memoized_process_state = process_states[self.name]
|
96
|
+
|
97
|
+
process_states[self.name] = {
|
98
|
+
"state" => state,
|
99
|
+
"updated_at" => memoized_process_state && memoized_process_state["state"] == state ? memoized_process_state["updated_at"].to_i : Time.now.to_i
|
100
|
+
}
|
101
|
+
Ziltoid::Watcher.write_state(process_states)
|
102
|
+
end
|
103
|
+
|
104
|
+
def watch!
|
105
|
+
Watcher.log("Ziltoid is watching process #{self.name}")
|
106
|
+
if !alive?
|
107
|
+
Watcher.log("Process #{self.name} is dead", Logger::WARN)
|
108
|
+
return start!
|
109
|
+
end
|
110
|
+
if above_cpu_limit?
|
111
|
+
update_state("above_cpu_limit") unless pending_grace_time?
|
112
|
+
if processable?("above_cpu_limit")
|
113
|
+
Watcher.log("Process #{self.name} is above CPU limit (#{self.cpu_limit.to_f})", Logger::WARN)
|
114
|
+
return restart!
|
115
|
+
end
|
116
|
+
end
|
117
|
+
if above_ram_limit?
|
118
|
+
update_state("above_ram_limit") unless pending_grace_time?
|
119
|
+
if processable?("above_ram_limit")
|
120
|
+
Watcher.log("Process #{self.name} is above RAM limit (#{self.ram_limit.to_f})", Logger::WARN)
|
121
|
+
return restart!
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def start!
|
127
|
+
return if Ziltoid::System.pid_alive?(self.pid)
|
128
|
+
return unless processable?("started")
|
129
|
+
|
130
|
+
Watcher.log("Ziltoid is starting process #{self.name}", Logger::WARN)
|
131
|
+
remove_pid_file
|
132
|
+
system(self.start_command)
|
133
|
+
update_state("started")
|
134
|
+
end
|
135
|
+
|
136
|
+
def stop!
|
137
|
+
return unless processable?("stopped")
|
138
|
+
|
139
|
+
Watcher.log("Ziltoid is stoping process #{self.name}", Logger::WARN)
|
140
|
+
memoized_pid = self.pid
|
141
|
+
|
142
|
+
if dead?
|
143
|
+
remove_pid_file
|
144
|
+
else
|
145
|
+
|
146
|
+
Thread.new do
|
147
|
+
system(self.stop_command)
|
148
|
+
sleep(WAIT_TIME_BEFORE_CHECK)
|
149
|
+
if alive?
|
150
|
+
system("kill #{memoized_pid}")
|
151
|
+
sleep(WAIT_TIME_BEFORE_CHECK)
|
152
|
+
if alive?
|
153
|
+
system("kill -9 #{memoized_pid}")
|
154
|
+
sleep(WAIT_TIME_BEFORE_CHECK)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
if dead?
|
158
|
+
remove_pid_file
|
159
|
+
update_state("stopped")
|
160
|
+
end
|
161
|
+
end.join
|
162
|
+
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def restart!
|
167
|
+
return unless processable?("restarted")
|
168
|
+
|
169
|
+
Watcher.log("Ziltoid is restarting process #{self.name}", Logger::WARN)
|
170
|
+
alive = self.alive?
|
171
|
+
|
172
|
+
if alive && self.restart_command
|
173
|
+
update_state("restarted")
|
174
|
+
return system("#{self.restart_command}")
|
175
|
+
end
|
176
|
+
|
177
|
+
stop! if alive
|
178
|
+
return start!
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Ziltoid
|
2
|
+
|
3
|
+
module System
|
4
|
+
|
5
|
+
# The position of each field in ps output
|
6
|
+
PS_FIELDS = [:pid, :ppid, :cpu, :ram]
|
7
|
+
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def pid_alive?(pid)
|
11
|
+
return false if pid.nil?
|
12
|
+
begin
|
13
|
+
::Process.kill(0, pid)
|
14
|
+
true
|
15
|
+
rescue Errno::EPERM # no permission, but it is definitely alive
|
16
|
+
true
|
17
|
+
rescue Errno::ESRCH
|
18
|
+
false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def ps_aux
|
23
|
+
# BSD style ps invocation
|
24
|
+
processes = `ps axo pid,ppid,pcpu,rss`.split("\n")
|
25
|
+
|
26
|
+
processes.inject({}) do |result, process|
|
27
|
+
info = process.split(/\s+/)
|
28
|
+
info.delete_if { |p_info| p_info.strip.empty? }
|
29
|
+
info.map! { |p_info| p_info.gsub(",", ".") }
|
30
|
+
|
31
|
+
info = PS_FIELDS.each_with_index.inject({}) do |info_hash, (field, field_index)|
|
32
|
+
info_hash[field] = info[field_index]
|
33
|
+
info_hash
|
34
|
+
end
|
35
|
+
|
36
|
+
pid = info[:pid].strip.to_i
|
37
|
+
result[pid] = info
|
38
|
+
result
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def cpu_usage(pid, include_children = true)
|
43
|
+
ps = ps_aux
|
44
|
+
return unless ps[pid]
|
45
|
+
cpu_used = ps[pid][:cpu].to_f
|
46
|
+
|
47
|
+
get_children(pid).each do |child_pid|
|
48
|
+
cpu_used += ps[child_pid][:cpu].to_f if ps[child_pid]
|
49
|
+
end if include_children
|
50
|
+
|
51
|
+
cpu_used
|
52
|
+
end
|
53
|
+
|
54
|
+
def ram_usage(pid, include_children = true)
|
55
|
+
ps = ps_aux
|
56
|
+
return unless ps[pid]
|
57
|
+
mem_used = ps[pid][:ram].to_i
|
58
|
+
|
59
|
+
get_children(pid).each do |child_pid|
|
60
|
+
mem_used += ps[child_pid][:ram].to_i if ps[child_pid]
|
61
|
+
end if include_children
|
62
|
+
|
63
|
+
mem_used
|
64
|
+
end
|
65
|
+
|
66
|
+
def get_children(parent_pid)
|
67
|
+
child_pids = []
|
68
|
+
ps_aux.each_pair do |_pid, info|
|
69
|
+
child_pids << info[:pid].to_i if info[:ppid].to_i == parent_pid.to_i
|
70
|
+
end
|
71
|
+
grand_children = child_pids.map { |pid| get_children(pid) }.flatten
|
72
|
+
child_pids.concat grand_children
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require "logger"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module Ziltoid
|
5
|
+
class Watcher
|
6
|
+
attr_accessor :watchlist
|
7
|
+
|
8
|
+
def initialize(options = {})
|
9
|
+
self.watchlist ||= {}
|
10
|
+
@@logger = options[:logger] || Logger.new($stdout)
|
11
|
+
@@logger.progname = options[:progname] || "Ziltoid"
|
12
|
+
@@logger.level = options[:log_level] || Logger::INFO
|
13
|
+
@@notifiers = options[:notifiers] if options[:notifiers]
|
14
|
+
@@state_file = options[:state_file] || File.join(File.dirname(__FILE__), "..", "state.ziltoid")
|
15
|
+
end
|
16
|
+
|
17
|
+
def add(watchable)
|
18
|
+
self.watchlist[watchable.name] = watchable
|
19
|
+
end
|
20
|
+
|
21
|
+
def logger
|
22
|
+
Ziltoid::Watcher.logger
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.logger
|
26
|
+
@@logger
|
27
|
+
end
|
28
|
+
|
29
|
+
def notifiers
|
30
|
+
Ziltoid::Watcher.notifiers
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.notifiers
|
34
|
+
@@notifiers ||= []
|
35
|
+
return @@notifiers
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.log(message, level = Logger::INFO)
|
39
|
+
@@logger ||= Logger.new($stdout)
|
40
|
+
@@logger.add(level, message)
|
41
|
+
if level > Logger::INFO
|
42
|
+
self.notifiers.each do |n|
|
43
|
+
n.send(message)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def state_file
|
49
|
+
Ziltoid::Watcher.state_file
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.state_file
|
53
|
+
@@state_file
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.read_state
|
57
|
+
json = File.read(state_file) if File.exist?(state_file)
|
58
|
+
json = "{}" if json.nil? || json.empty?
|
59
|
+
JSON.load(json)
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.write_state(state = {})
|
63
|
+
File.open(state_file, "w+") do |file|
|
64
|
+
file.puts JSON.generate(state)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def run!(command = :watch)
|
69
|
+
watchlist.values.each do |watchable|
|
70
|
+
watchable.send("#{command}!".to_sym)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def watch!
|
75
|
+
Watcher.log("Ziltoid is now on duty : watching all watchables !")
|
76
|
+
run!(:watch)
|
77
|
+
end
|
78
|
+
|
79
|
+
def start!
|
80
|
+
Watcher.log("Ziltoid is now on duty : all watchables starting !")
|
81
|
+
run!(:start)
|
82
|
+
end
|
83
|
+
|
84
|
+
def stop!
|
85
|
+
Watcher.log("Ziltoid is now on duty : all watchables stoping !")
|
86
|
+
run!(:stop)
|
87
|
+
end
|
88
|
+
|
89
|
+
def restart!
|
90
|
+
Watcher.log("Ziltoid is now on duty : all watchables restarting !")
|
91
|
+
run!(:restart)
|
92
|
+
end
|
93
|
+
|
94
|
+
def run(command = :watch)
|
95
|
+
case command
|
96
|
+
when :watch
|
97
|
+
watch!
|
98
|
+
when :start
|
99
|
+
start!
|
100
|
+
when :stop
|
101
|
+
stop!
|
102
|
+
when :restart
|
103
|
+
restart!
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ziltoid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stéphane Akkaoui
|
@@ -10,17 +10,25 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
date: 2018-08-06 00:00:00.000000000 Z
|
13
|
-
dependencies:
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: pony
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '1.11'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '1.11'
|
14
28
|
description: |2
|
15
29
|
There are many software applications that aim to watch processes, and keep them alive and clean. Some of them are well known: god, monit, bluepill.
|
16
30
|
All have good and bad sides. One of the bad sides is that each alternative is based on a deamon that computes data and then sleeps for a while. Who is monitoring this particular deamon ? What if this process suddenly stops ? Also, you often need root rights to run those tools. On some hosting environments (mainly in shared hosting), this is an issue.
|
17
|
-
|
18
|
-
Ziltoid is an attempt to solve those issues using the crontab system, which comes with many good sides :
|
19
|
-
- it's on every system
|
20
|
-
- it launches a task periodically then waits for an amount of time
|
21
|
-
- it doesn't need monitoring
|
22
|
-
- it can send emails to warn of an error
|
23
|
-
- and it can run any script.
|
31
|
+
Ziltoid is an attempt to solve those issues using the crontab system, which comes with many good sides : it's on every system, it launches a task periodically then waits for an amount of time, it doesn't need monitoring, it can send emails to warn of an error and it can run any script.
|
24
32
|
email:
|
25
33
|
- sakkaoui@gmail.com
|
26
34
|
- vincent.gabou@gmail.com>
|
@@ -29,6 +37,11 @@ extensions: []
|
|
29
37
|
extra_rdoc_files: []
|
30
38
|
files:
|
31
39
|
- lib/ziltoid.rb
|
40
|
+
- lib/ziltoid/command_parser.rb
|
41
|
+
- lib/ziltoid/email_notifier.rb
|
42
|
+
- lib/ziltoid/process.rb
|
43
|
+
- lib/ziltoid/system.rb
|
44
|
+
- lib/ziltoid/watcher.rb
|
32
45
|
homepage: https://github.com/meuble/ziltoid
|
33
46
|
licenses:
|
34
47
|
- WTFPL
|