ziltoid 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|