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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dcb27a64e424602045c22c62ffa2d1f272d0113f
4
- data.tar.gz: c3cd9add836c7460ef1c91feded47f5300b47bff
3
+ metadata.gz: 20d41ee7c21c5543faea2a88d7cabc1ab9636abd
4
+ data.tar.gz: 85dc0a7c9005ef96c15d66874d096c23eade24f3
5
5
  SHA512:
6
- metadata.gz: 8acc9991ab54b8b22af355f32089e1097ce2e1101c0670b4f9979176560abd155ff56ff61bfd53f8974a01576677af83f668fd23e8def0513048bfb52f9b0958
7
- data.tar.gz: cd56cf1a67d3b8199df6baeed68cad43c86ddd4c0beb53037b4c9ba7a78f547239e830f15f16eeb968be9daddc039e6bdacdab103a9a43300e3b003c558160a4
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.1
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