evented_bluepill 0.0.46 → 0.0.47

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,7 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  module Bluepill
2
- module ProcessConditions
4
+ module ProcessConditions
3
5
  def self.[](name)
4
6
  const_get(name.to_s.camelcase)
5
7
  end
@@ -1,14 +1,16 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  module Bluepill
2
4
  module ProcessConditions
3
5
  class AlwaysTrue < ProcessCondition
4
6
  def initialize(options = {})
5
7
  @below = options[:below]
6
8
  end
7
-
9
+
8
10
  def run(pid)
9
11
  1
10
12
  end
11
-
13
+
12
14
  def check(value)
13
15
  true
14
16
  end
@@ -1,15 +1,17 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  module Bluepill
2
4
  module ProcessConditions
3
5
  class CpuUsage < ProcessCondition
4
6
  def initialize(options = {})
5
7
  @below = options[:below]
6
8
  end
7
-
9
+
8
10
  def run(pid)
9
11
  # third col in the ps axu output
10
12
  System.cpu_usage(pid).to_f
11
13
  end
12
-
14
+
13
15
  def check(value)
14
16
  value < @below
15
17
  end
@@ -1,3 +1,5 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  require 'net/http'
2
4
  require 'uri'
3
5
 
@@ -1,3 +1,5 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  module Bluepill
2
4
  module ProcessConditions
3
5
  class MemUsage < ProcessCondition
@@ -5,20 +7,20 @@ module Bluepill
5
7
  FORMAT_STR = "%d%s"
6
8
  MB_LABEL = "MB"
7
9
  KB_LABEL = "KB"
8
-
10
+
9
11
  def initialize(options = {})
10
12
  @below = options[:below]
11
13
  end
12
-
14
+
13
15
  def run(pid)
14
16
  # rss is on the 5th col
15
17
  System.memory_usage(pid).to_f
16
18
  end
17
-
19
+
18
20
  def check(value)
19
21
  value.kilobytes < @below
20
22
  end
21
-
23
+
22
24
  def format_value(value)
23
25
  if value.kilobytes >= MB
24
26
  FORMAT_STR % [(value / 1024).round, MB_LABEL]
@@ -1,18 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  module Bluepill
2
4
  module ProcessConditions
3
- class ProcessCondition
5
+ class ProcessCondition
4
6
  def initialize(options = {})
5
7
  @options = options
6
8
  end
7
-
9
+
8
10
  def run(pid)
9
11
  raise "Implement in subclass!"
10
12
  end
11
-
13
+
12
14
  def check(value)
13
15
  raise "Implement in subclass!"
14
16
  end
15
-
17
+
16
18
  def format_value(value)
17
19
  value
18
20
  end
@@ -1,5 +1,7 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  module Bluepill
2
- class ProcessStatistics
4
+ class ProcessStatistics
3
5
  STRFTIME = "%m/%d/%Y %H:%I:%S"
4
6
  # possibly persist this data.
5
7
  def initialize
@@ -1,7 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  require 'socket'
2
4
 
5
+ require 'coolio'
6
+
3
7
  module Bluepill
4
8
  module Socket
9
+ class CmdListenerConnection < Coolio::UNIXSocket
10
+ def initialize(socket, application)
11
+ @application = application
12
+ super(socket)
13
+ end
14
+
15
+ def on_read(data)
16
+ command, *args = data.strip.split(":")
17
+ response = begin
18
+ @application.send(command, *args)
19
+ rescue Exception => e
20
+ e
21
+ end
22
+ write(Marshal.dump(response))
23
+ end
24
+ end
25
+
5
26
  TIMEOUT = 10
6
27
 
7
28
  extend self
@@ -9,7 +30,7 @@ module Bluepill
9
30
  def client(base_dir, name, &b)
10
31
  UNIXSocket.open(socket_path(base_dir, name), &b)
11
32
  end
12
-
33
+
13
34
  def client_command(base_dir, name, command)
14
35
  client(base_dir, name) do |socket|
15
36
  Timeout.timeout(TIMEOUT) do
@@ -20,28 +41,27 @@ module Bluepill
20
41
  rescue EOFError, Timeout::Error
21
42
  abort("Socket Timeout: Server may not be responding")
22
43
  end
23
-
24
- def server(base_dir, name)
44
+
45
+ def server(base_dir, name, application)
25
46
  socket_path = self.socket_path(base_dir, name)
26
47
  begin
27
- UNIXServer.open(socket_path)
48
+ Coolio::UNIXServer.new(socket_path, CmdListenerConnection, application)
28
49
  rescue Errno::EADDRINUSE
29
- # if sock file has been created. test to see if there is a server
50
+ # if sock file has been created. test to see if there is a server
30
51
  begin
31
52
  UNIXSocket.open(socket_path)
32
53
  rescue Errno::ECONNREFUSED
33
54
  File.delete(socket_path)
34
- return UNIXServer.open(socket_path)
55
+ Coolio::UNIXServer.new(socket_path, CmdListenerConnection, application)
35
56
  else
36
57
  logger.err("Server is already running!")
37
58
  exit(7)
38
59
  end
39
60
  end
40
61
  end
41
-
62
+
42
63
  def socket_path(base_dir, name)
43
- File.join(base_dir, 'socks', name + ".sock")
64
+ File.join(base_dir, 'socks', name + ".sock")
44
65
  end
45
66
  end
46
67
  end
47
-
@@ -1,12 +1,16 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  require 'etc'
4
+ require 'shellwords'
2
5
 
3
6
  module Bluepill
4
7
  # This class represents the system that bluepill is running on.. It's mainly used to memoize
5
8
  # results of running ps auxx etc so that every watch in the every process will not result in a fork
6
9
  module System
7
10
  APPEND_MODE = "a"
11
+ SECONDS_VALID = 2
8
12
  extend self
9
-
13
+
10
14
  # The position of each field in ps output
11
15
  IDX_MAP = {
12
16
  :pid => 0,
@@ -14,7 +18,7 @@ module Bluepill
14
18
  :pcpu => 2,
15
19
  :rss => 3
16
20
  }
17
-
21
+
18
22
  def pid_alive?(pid)
19
23
  begin
20
24
  ::Process.kill(0, pid)
@@ -23,23 +27,23 @@ module Bluepill
23
27
  false
24
28
  end
25
29
  end
26
-
30
+
27
31
  def cpu_usage(pid)
28
32
  ps_axu[pid] && ps_axu[pid][IDX_MAP[:pcpu]].to_f
29
33
  end
30
-
34
+
31
35
  def memory_usage(pid)
32
36
  ps_axu[pid] && ps_axu[pid][IDX_MAP[:rss]].to_f
33
37
  end
34
-
38
+
35
39
  def get_children(parent_pid)
36
40
  child_pids = Array.new
37
- ps_axu.each_pair do |pid, chunks|
41
+ ps_axu.each_pair do |pid, chunks|
38
42
  child_pids << chunks[IDX_MAP[:pid]].to_i if chunks[IDX_MAP[:ppid]].to_i == parent_pid.to_i
39
43
  end
40
44
  child_pids
41
45
  end
42
-
46
+
43
47
  # Returns the pid of the child that executes the cmd
44
48
  def daemonize(cmd, options = {})
45
49
  rd, wr = IO.pipe
@@ -47,76 +51,76 @@ module Bluepill
47
51
  if child = Daemonize.safefork
48
52
  # we do not wanna create zombies, so detach ourselves from the child exit status
49
53
  ::Process.detach(child)
50
-
54
+
51
55
  # parent
52
56
  wr.close
53
-
57
+
54
58
  daemon_id = rd.read.to_i
55
59
  rd.close
56
-
60
+
57
61
  return daemon_id if daemon_id > 0
58
-
62
+
59
63
  else
60
64
  # child
61
65
  rd.close
62
66
 
63
67
  drop_privileges(options[:uid], options[:gid])
64
-
68
+
65
69
  # if we cannot write the pid file as the provided user, err out
66
70
  exit unless can_write_pid_file(options[:pid_file], options[:logger])
67
-
71
+
68
72
  to_daemonize = lambda do
69
73
  # Setting end PWD env emulates bash behavior when dealing with symlinks
70
74
  Dir.chdir(ENV["PWD"] = options[:working_dir]) if options[:working_dir]
71
75
  options[:environment].each { |key, value| ENV[key.to_s] = value.to_s } if options[:environment]
72
-
76
+
73
77
  redirect_io(*options.values_at(:stdin, :stdout, :stderr))
74
-
75
- ::Kernel.exec(cmd)
78
+
79
+ ::Kernel.exec(*Shellwords.shellwords(cmd))
76
80
  exit
77
81
  end
78
82
 
79
83
  daemon_id = Daemonize.call_as_daemon(to_daemonize, nil, cmd)
80
-
84
+
81
85
  File.open(options[:pid_file], "w") {|f| f.write(daemon_id)}
82
-
83
- wr.write daemon_id
86
+
87
+ wr.write daemon_id
84
88
  wr.close
85
89
 
86
90
  exit
87
91
  end
88
92
  end
89
-
93
+
90
94
  # Returns the stdout, stderr and exit code of the cmd
91
95
  def execute_blocking(cmd, options = {})
92
96
  rd, wr = IO.pipe
93
-
97
+
94
98
  if child = Daemonize.safefork
95
99
  # parent
96
100
  wr.close
97
-
101
+
98
102
  cmd_status = rd.read
99
103
  rd.close
100
-
104
+
101
105
  ::Process.waitpid(child)
102
-
106
+
103
107
  return Marshal.load(cmd_status)
104
-
108
+
105
109
  else
106
110
  # child
107
111
  rd.close
108
-
112
+
109
113
  # create a child in which we can override the stdin, stdout and stderr
110
114
  cmd_out_read, cmd_out_write = IO.pipe
111
115
  cmd_err_read, cmd_err_write = IO.pipe
112
-
116
+
113
117
  pid = fork {
114
118
  # grandchild
115
119
  drop_privileges(options[:uid], options[:gid])
116
-
120
+
117
121
  Dir.chdir(ENV["PWD"] = options[:working_dir]) if options[:working_dir]
118
122
  options[:environment].each { |key, value| ENV[key.to_s] = value.to_s } if options[:environment]
119
-
123
+
120
124
  # close unused fds so ancestors wont hang. This line is the only reason we are not
121
125
  # using something like popen3. If this fd is not closed, the .read call on the parent
122
126
  # will never return because "wr" would still be open in the "exec"-ed cmd
@@ -136,16 +140,16 @@ module Bluepill
136
140
  cmd_err_write.close
137
141
 
138
142
  # finally, replace grandchild with cmd
139
- ::Kernel.exec(cmd)
143
+ ::Kernel.exec(*Shellwords.shellwords(cmd))
140
144
  }
141
145
 
142
146
  # we do not use these ends of the pipes in the child
143
147
  cmd_out_write.close
144
148
  cmd_err_write.close
145
-
149
+
146
150
  # wait for the cmd to finish executing and acknowledge it's death
147
151
  ::Process.waitpid(pid)
148
-
152
+
149
153
  # collect stdout, stderr and exitcode
150
154
  result = {
151
155
  :stdout => cmd_out_read.read,
@@ -156,30 +160,24 @@ module Bluepill
156
160
  # We're done with these ends of the pipes as well
157
161
  cmd_out_read.close
158
162
  cmd_err_read.close
159
-
163
+
160
164
  # Time to tell the parent about what went down
161
165
  wr.write Marshal.dump(result)
162
166
  wr.close
163
167
 
164
- exit
168
+ exit
165
169
  end
166
170
  end
167
-
168
- def store
169
- @store ||= Hash.new
170
- end
171
-
172
- def reset_data
173
- store.clear unless store.empty?
174
- end
175
-
171
+
176
172
  def ps_axu
177
- # TODO: need a mutex here
178
- store[:ps_axu] ||= begin
173
+ return @store if @valid_until and @valid_until >= Time.now
174
+ @valid_until = Time.now + SECONDS_VALID
175
+
176
+ @store = begin
179
177
  # BSD style ps invocation
180
178
  lines = `ps axo pid=,ppid=,pcpu=,rss=`.split("\n")
181
179
 
182
- lines.inject(Hash.new) do |mem, line|
180
+ lines.inject(Hash.new) do |mem, line|
183
181
  chunks = line.split(/\s+/)
184
182
  chunks.delete_if {|c| c.strip.empty? }
185
183
  pid = chunks[IDX_MAP[:pid]].strip.to_i
@@ -188,7 +186,7 @@ module Bluepill
188
186
  end
189
187
  end
190
188
  end
191
-
189
+
192
190
  # be sure to call this from a fork otherwise it will modify the attributes
193
191
  # of the bluepill daemon
194
192
  def drop_privileges(uid, gid)
@@ -201,25 +199,25 @@ module Bluepill
201
199
  ::Process::Sys.setuid(uid_num) if uid
202
200
  end
203
201
  end
204
-
202
+
205
203
  def can_write_pid_file(pid_file, logger)
206
204
  FileUtils.touch(pid_file)
207
205
  File.unlink(pid_file)
208
206
  return true
209
-
207
+
210
208
  rescue Exception => e
211
209
  logger.warning "%s - %s" % [e.class.name, e.message]
212
210
  e.backtrace.each {|l| logger.warning l}
213
211
  return false
214
212
  end
215
-
213
+
216
214
  def redirect_io(io_in, io_out, io_err)
217
215
  $stdin.reopen(io_in) if io_in
218
-
216
+
219
217
  if !io_out.nil? && !io_err.nil? && io_out == io_err
220
218
  $stdout.reopen(io_out, APPEND_MODE)
221
219
  $stderr.reopen($stdout)
222
-
220
+
223
221
  else
224
222
  $stdout.reopen(io_out, APPEND_MODE) if io_out
225
223
  $stderr.reopen(io_err, APPEND_MODE) if io_err
@@ -1,70 +1,72 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'active_support'
4
+
1
5
  module Bluepill
6
+ class TriggerTimer < Coolio::TimerWatcher
7
+ def initialize(trigger, event, delay)
8
+ @trigger = trigger
9
+ @event = event
10
+ super(delay, false)
11
+ end
12
+
13
+ def on_timer
14
+ begin
15
+ @trigger.logger.info("Retrying from flapping")
16
+ @trigger.dispatch!(@event)
17
+ @trigger.remove_timer(self)
18
+ rescue StandardError => e
19
+ @trigger.logger.err(e)
20
+ @trigger.logger.err(e.backtrace.join("\n"))
21
+ end
22
+ end
23
+ end
24
+
2
25
  class Trigger
3
26
  @implementations = {}
4
27
  def self.inherited(klass)
5
28
  @implementations[klass.name.split('::').last.underscore.to_sym] = klass
6
29
  end
7
-
30
+
8
31
  def self.[](name)
9
32
  @implementations[name]
10
33
  end
11
34
 
12
- attr_accessor :process, :logger, :mutex, :scheduled_events
13
-
35
+ attr_accessor :process, :logger, :scheduled_events
36
+
14
37
  def initialize(process, options = {})
15
38
  self.process = process
16
39
  self.logger = options[:logger]
17
- self.mutex = Mutex.new
18
40
  self.scheduled_events = []
19
41
  end
20
-
42
+
21
43
  def reset!
22
44
  self.cancel_all_events
23
45
  end
24
-
46
+
25
47
  def notify(transition)
26
48
  raise "Implement in subclass"
27
49
  end
28
-
50
+
29
51
  def dispatch!(event)
30
52
  self.process.dispatch!(event, self.class.name.split("::").last)
31
53
  end
32
-
33
- def deep_copy
34
- # TODO: This is a kludge. Ideally, process templates
35
- # would be facotries, and not a template object.
36
- mutex, @mutex = @mutex, nil
37
- clone = Marshal.load(Marshal.dump(self))
38
- clone.instance_variable_set("@mutex", Monitor.new)
39
- @mutex = mutex
40
- clone
54
+
55
+ def remove_timer(timer)
56
+ self.scheduled_events.delete(timer)
41
57
  end
42
-
58
+
43
59
  def schedule_event(event, delay)
44
- # TODO: maybe wrap this in a ScheduledEvent class with methods like cancel
45
- thread = Thread.new(self) do |trigger|
46
- begin
47
- sleep delay.to_f
48
- trigger.logger.info("Retrying from flapping")
49
- trigger.dispatch!(event)
50
- trigger.mutex.synchronize do
51
- trigger.scheduled_events.delete_if { |_, thread| thread == Thread.current }
52
- end
53
- rescue StandardError => e
54
- trigger.logger.err(e)
55
- trigger.logger.err(e.backtrace.join("\n"))
56
- end
57
- end
60
+ timer = Bluepill::TriggerTimer.new(self, event, delay)
61
+ self.scheduled_events << timer
58
62
 
59
- self.scheduled_events.push([event, thread])
63
+ Bluepill::Event.attach(timer)
60
64
  end
61
-
65
+
62
66
  def cancel_all_events
63
67
  self.logger.info "Canceling all scheduled events"
64
- self.mutex.synchronize do
65
- self.scheduled_events.each {|_, thread| thread.kill}
66
- end
68
+ self.scheduled_events.each {|e| e.detach }
69
+ self.scheduled_events.clear
67
70
  end
68
-
69
71
  end
70
72
  end