epi 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/bin/epi +1 -1
  3. data/lib/epi/cli/command.rb +7 -0
  4. data/lib/epi/cli/commands/concerns/daemon.rb +33 -0
  5. data/lib/epi/cli/commands/config.rb +11 -3
  6. data/lib/epi/cli/commands/daemon.rb +14 -0
  7. data/lib/epi/cli/commands/help.rb +0 -0
  8. data/lib/epi/cli/commands/job.rb +1 -1
  9. data/lib/epi/cli/commands/restart.rb +16 -0
  10. data/lib/epi/cli/commands/start.rb +15 -0
  11. data/lib/epi/cli/commands/status.rb +6 -2
  12. data/lib/epi/cli/commands/stop.rb +16 -0
  13. data/lib/epi/cli.rb +1 -1
  14. data/lib/epi/connection.rb +7 -0
  15. data/lib/epi/core_ext/inflector.rb +1 -1
  16. data/lib/epi/daemon/receiver.rb +37 -0
  17. data/lib/epi/{server → daemon}/responder.rb +14 -5
  18. data/lib/epi/{server → daemon}/responders/config.rb +14 -2
  19. data/lib/epi/{server → daemon}/responders/job.rb +5 -6
  20. data/lib/epi/{server → daemon}/responders/shutdown.rb +1 -1
  21. data/lib/epi/daemon/responders/start.rb +19 -0
  22. data/lib/epi/{server → daemon}/responders/status.rb +4 -2
  23. data/lib/epi/daemon/responders/stop_all.rb +20 -0
  24. data/lib/epi/daemon/sender.rb +74 -0
  25. data/lib/epi/{server.rb → daemon.rb} +26 -27
  26. data/lib/epi/data.rb +4 -5
  27. data/lib/epi/job.rb +60 -2
  28. data/lib/epi/job_description.rb +6 -8
  29. data/lib/epi/jobs.rb +30 -2
  30. data/lib/epi/launch.rb +1 -1
  31. data/lib/epi/logging.rb +33 -0
  32. data/lib/epi/process_status.rb +1 -1
  33. data/lib/epi/running_process.rb +21 -3
  34. data/lib/epi/trigger.rb +53 -0
  35. data/lib/epi/triggers/concerns/comparison.rb +43 -0
  36. data/lib/epi/triggers/memory.rb +16 -0
  37. data/lib/epi/triggers/touch.rb +37 -0
  38. data/lib/epi/triggers/uptime.rb +12 -0
  39. data/lib/epi/version.rb +1 -1
  40. data/lib/epi.rb +4 -22
  41. metadata +26 -26
  42. data/lib/epi/cli/commands/server.rb +0 -38
  43. data/lib/epi/server/receiver.rb +0 -46
  44. data/lib/epi/server/responders/command.rb +0 -15
  45. data/lib/epi/server/sender.rb +0 -64
data/lib/epi/job.rb CHANGED
@@ -15,6 +15,7 @@ module Epi
15
15
 
16
16
  def initialize(job_description, state)
17
17
  @job_description = job_description
18
+ @triggers = job_description.triggers.map { |t| Trigger.make self, *t }
18
19
  @expected_count = state['expected_count'] || job_description.initial_processes
19
20
  @pids = state['pids']
20
21
  @dying_pids = state['dying_pids']
@@ -76,25 +77,80 @@ module Epi
76
77
  stop_one while running_count > expected_count
77
78
  end
78
79
 
80
+ def run_triggers!
81
+ @triggers.each &:try
82
+ end
83
+
84
+ def shutdown!(&callback)
85
+ count = running_count
86
+ if count > 0
87
+ count.times do
88
+ stop_one do
89
+ count -= 1
90
+ callback.call if callback && count == 0
91
+ end
92
+ end
93
+ else
94
+ callback.call if callback
95
+ end
96
+ end
97
+
79
98
  def terminate!
80
99
  self.expected_count = 0
81
100
  sync!
82
101
  end
83
102
 
103
+ def restart!
104
+ count = expected_count
105
+ if count > 0
106
+ self.expected_count = 0
107
+ sync!
108
+ self.expected_count = count
109
+ sync!
110
+ end
111
+ self
112
+ end
113
+
114
+ def running_processes
115
+ pids.map do |proc_id, pid|
116
+ [proc_id, ProcessStatus[pid] || RunningProcess.new(pid)]
117
+ end.to_h
118
+ end
119
+
84
120
  def running_count
85
121
  pids.count
86
122
  end
87
123
 
124
+ def dying_count
125
+ dying_pids.count
126
+ end
127
+
128
+ # Replace a running process with a new one
129
+ # @param pid [Fixnum] PID of the process to replace
130
+ def replace(pid, &callback)
131
+ stop_one pid do
132
+ start_one while running_count < expected_count
133
+ callback.call if callback
134
+ end
135
+ end
136
+
88
137
  private
89
138
 
90
139
  def start_one
91
140
  proc_id, pid = job_description.launch
92
141
  pids[proc_id] = pid
93
142
  Data.write pid_key(proc_id), pid
143
+ Jobs.by_pid[pid] = self
94
144
  end
95
145
 
96
- def stop_one
97
- proc_id, pid = pids.shift
146
+ def stop_one(pid = nil, &callback)
147
+ if pid
148
+ proc_id = pids.key pid
149
+ raise Exceptions::Fatal, "Process #{pid} isn't managed by job #{id}" unless proc_id
150
+ pids.delete proc_id
151
+ else
152
+ proc_id, pid = pids.shift
153
+ end
98
154
  dying_pids[proc_id] = pid
99
155
  work = proc do
100
156
  ProcessStatus[pid].kill job_description.kill_timeout
@@ -102,6 +158,8 @@ module Epi
102
158
  done = proc do
103
159
  dying_pids.delete proc_id
104
160
  Data.write pid_key(proc_id), nil
161
+ Jobs.by_pid.delete pid
162
+ callback.call if callback
105
163
  end
106
164
  EventMachine.defer work, done
107
165
  end
@@ -56,11 +56,11 @@ module Epi
56
56
  'Must be a non-negative number' unless value.is_a?(Numeric) && value >= 0
57
57
  end
58
58
 
59
- attr_reader :id
59
+ attr_reader :id, :triggers
60
60
 
61
61
  def initialize(id)
62
62
  @id = id
63
- @handlers = {}
63
+ @triggers = []
64
64
  @props = {}
65
65
  end
66
66
 
@@ -78,15 +78,13 @@ module Epi
78
78
  end
79
79
 
80
80
  def reconfigure
81
- @handlers = {}
81
+ @triggers = []
82
82
  yield self
83
+ # TODO: trigger an update of existing/running jobs
83
84
  end
84
85
 
85
- def on(event, *args, &handler)
86
- (@handlers[event] ||= []) << {
87
- args: args,
88
- handler: handler
89
- }
86
+ def on(trigger_name, *args, &handler)
87
+ @triggers << [trigger_name, args, handler]
90
88
  end
91
89
 
92
90
  def pid_key(proc_id)
data/lib/epi/jobs.rb CHANGED
@@ -8,13 +8,14 @@ module Epi
8
8
  class << self
9
9
  extend Forwardable
10
10
 
11
- delegate [:[], :[]=, :delete, :each_value, :map] => :@jobs
11
+ delegate [:[], :[]=, :delete, :each_value, :map, :find, :count] => :@jobs
12
12
 
13
- attr_reader :configuration_files
13
+ attr_reader :configuration_files, :by_pid
14
14
 
15
15
  def reset!
16
16
  @configuration_files = {}
17
17
  @jobs = {}
18
+ @by_pid = {}
18
19
  end
19
20
 
20
21
  def beat!
@@ -37,6 +38,13 @@ module Epi
37
38
  # Sync each job with its expectations
38
39
  each_value &:sync!
39
40
 
41
+ # Snapshot processes again, so that triggers have access to
42
+ # newly-spawned processes
43
+ ProcessStatus.take!
44
+
45
+ # Run job triggers
46
+ each_value &:run_triggers!
47
+
40
48
  # Write state of each job to data file
41
49
  Data.jobs = map { |id, job| [id.to_s, job.state] }.to_h
42
50
  Data.save
@@ -45,6 +53,26 @@ module Epi
45
53
  @next_beat = EventMachine.add_timer(5) { beat! } # TODO: make interval configurable
46
54
  end
47
55
 
56
+ def shutdown!(&callback)
57
+ EventMachine.cancel_timer @next_beat if @next_beat
58
+ ProcessStatus.take!
59
+ remaining = count
60
+ if remaining > 0
61
+ each_value do |job|
62
+ job.shutdown! do
63
+ remaining -= 1
64
+ callback.call if callback && remaining == 0
65
+ end
66
+ end
67
+ else
68
+ callback.call if callback
69
+ end
70
+ end
71
+
72
+ def running_process_count
73
+ each_value.map(&:running_count).reduce(:+) || 0
74
+ end
75
+
48
76
  def job_descriptions
49
77
  configuration_files.values.inject({}) { |all, conf_file| all.merge! conf_file.job_descriptions }
50
78
  end
data/lib/epi/launch.rb CHANGED
@@ -53,7 +53,7 @@ module Epi
53
53
 
54
54
  # Run the command and read the resulting PID from its STDOUT
55
55
  IO.popen(env, cmd) { |p| p.read }.to_i.tap do |pid|
56
- logger.debug "Process #{pid} started: #{`ps -p #{pid} -o command=`.chomp}"
56
+ logger.info "Process #{pid} started: #{`ps -p #{pid} -o command=`.chomp}"
57
57
  end
58
58
  end
59
59
  end
@@ -0,0 +1,33 @@
1
+ module Epi
2
+
3
+ class << self
4
+
5
+ DEFAULT_LEVEL = 'info'
6
+
7
+ def logger
8
+ @logger ||= make_logger
9
+ end
10
+
11
+ def logger=(value)
12
+ @logger = Logger === value ? value : make_logger(value)
13
+ end
14
+
15
+ private
16
+
17
+ def make_logger(target = nil)
18
+ Logger.new(target || ENV['EPI_LOG'] || default_log_path).tap do |logger|
19
+ logger.level = log_level
20
+ end
21
+ end
22
+
23
+ def default_log_path
24
+ Data.home.join('epi.log').to_s
25
+ end
26
+
27
+ def log_level
28
+ Logger::Severity.const_get (ENV['EPI_LOG_LEVEL'] || DEFAULT_LEVEL).upcase
29
+ end
30
+
31
+ end
32
+
33
+ end
@@ -63,7 +63,7 @@ module Epi
63
63
 
64
64
  def find_by_pid(pid)
65
65
  line = @lines[pid]
66
- RunningProcess.new(pid, line) if line
66
+ RunningProcess.new(pid, ps_line: line) if line
67
67
  end
68
68
 
69
69
 
@@ -42,10 +42,11 @@ module Epi
42
42
  Epi.logger
43
43
  end
44
44
 
45
- def initialize(pid, ps_line = nil)
46
- @pid = pid
45
+ def initialize(pid, ps_line: nil, job: nil)
46
+ @pid = pid.to_i
47
47
  @ps_line = ps_line
48
48
  @props = {}
49
+ @job = job
49
50
  reload! unless ps_line
50
51
  end
51
52
 
@@ -96,6 +97,12 @@ module Epi
96
97
  @started_at ||= Time.parse parts[5..9].join ' '
97
98
  end
98
99
 
100
+ # Duration the process has been running (in seconds)
101
+ # @return [Float]
102
+ def uptime
103
+ Time.now - started_at
104
+ end
105
+
99
106
  # Name of the user that owns the process
100
107
  # @return [String]
101
108
  def user
@@ -136,17 +143,28 @@ module Epi
136
143
  else
137
144
  signal = timeout ? 'KILL' : 'TERM'
138
145
  logger.info "Sending #{signal} to process #{pid}"
139
- Process.kill signal, pid
146
+ Process.kill signal, pid rescue Errno::ESRCH false
140
147
  sleep 0.2 while `ps -p #{pid} > /dev/null 2>&1; echo $?`.chomp.to_i == 0
141
148
  logger.info "Process #{pid} terminated by signal #{signal}"
142
149
  end
143
150
  self
144
151
  end
145
152
 
153
+ # Kill a running process immediately and synchronously with kill -9
154
+ # @return [RunningProcess]
146
155
  def kill!
147
156
  kill true
148
157
  end
149
158
 
159
+ def job
160
+ @job ||= Jobs.by_pid[pid]
161
+ end
162
+
163
+ def restart!
164
+ raise Exceptions::Fatal, 'Cannot restart this process because it is not managed by a job' unless job
165
+ job.replace pid
166
+ end
167
+
150
168
  private
151
169
 
152
170
  def parts
@@ -0,0 +1,53 @@
1
+ module Epi
2
+ class Trigger
3
+
4
+ def self.make(job, name, args, handler)
5
+ klass_name = name.camelize
6
+ klass = Triggers.const_defined?(klass_name) && Triggers.const_get(klass_name)
7
+ raise Exceptions::Fatal, "No trigger exists named #{name}" unless Class === klass && klass < self
8
+ klass.new job, handler, *args
9
+ end
10
+
11
+ attr_reader :job, :args
12
+
13
+ def initialize(job, handler, *args)
14
+ @job = job
15
+ @handler = handler
16
+ @args = args
17
+ end
18
+
19
+ def logger
20
+ Epi.logger
21
+ end
22
+
23
+ def message
24
+ nil
25
+ end
26
+
27
+ def try
28
+ case self
29
+ when ProcessTrigger then job.running_processes.each_value { |process| try_with process }
30
+ when JobTrigger then try_with nil
31
+ else nil
32
+ end
33
+ end
34
+
35
+ def try_with(process)
36
+ args = [process].reject(&:nil?)
37
+ if test *args
38
+ text = "Trigger on job #{job.id}"
39
+ text << " (PID #{process.pid})" if process
40
+ text << ": " << message if message
41
+ logger.info text
42
+ @handler.call process || job
43
+ end
44
+ end
45
+
46
+ class JobTrigger < self; end
47
+ class ProcessTrigger < self; end
48
+
49
+ end
50
+ end
51
+
52
+ Dir[File.expand_path '../triggers/concerns/*.rb', __FILE__].each { |f| require f }
53
+ Dir[File.expand_path '../triggers/*.rb', __FILE__].each { |f| require f }
@@ -0,0 +1,43 @@
1
+ module Epi
2
+ module Triggers
3
+ module Concerns
4
+ module Comparison
5
+
6
+ def compare(subject)
7
+ tester.call subject, object
8
+ end
9
+
10
+ private
11
+
12
+ def tester
13
+ @tester ||= choose_tester
14
+ end
15
+
16
+ def op
17
+ @op ||= args[0]
18
+ end
19
+
20
+ def object
21
+ @object ||= args[1]
22
+ end
23
+
24
+ def choose_tester
25
+ case op
26
+ when :gt then -> a, b { a > b }
27
+ when :lt then -> a, b { a < b }
28
+ when :gte then -> a, b { a >= b }
29
+ when :lte then -> a, b { a <= b }
30
+ when :eq then -> a, b { a == b }
31
+ when :not_eq then -> a, b { a != b}
32
+ when :match then -> a, b { a =~ b }
33
+ when :not_match then -> a, b { a !~ b }
34
+ when :like then -> a, b { a === b }
35
+ when :not_like then -> a, b { !(a === b) }
36
+ else raise Exceptions::Fatal, "Unknown operation #{op}"
37
+ end
38
+ end
39
+
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,16 @@
1
+ module Epi
2
+ module Triggers
3
+ class Memory < Trigger::ProcessTrigger
4
+ include Concerns::Comparison
5
+
6
+ def test(process)
7
+ compare process.physical_memory
8
+ end
9
+
10
+ def message
11
+ "Physical memory exceeded #{object} bytes"
12
+ end
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,37 @@
1
+ module Epi
2
+ module Triggers
3
+ class Touch < Trigger::JobTrigger
4
+
5
+ def initialize(*args)
6
+ super *args
7
+ update
8
+ end
9
+
10
+ def test
11
+ ino = @ino; mtime = @mtime
12
+ update
13
+ ino != @ino || mtime != @mtime
14
+ end
15
+
16
+ def message
17
+ "Path '#{path}' was touched"
18
+ end
19
+
20
+ private
21
+
22
+ def path
23
+ @path ||= Pathname args.first
24
+ end
25
+
26
+ def update
27
+ @ino, @mtime = begin
28
+ stat = path.stat
29
+ [stat.ino, stat.mtime]
30
+ rescue Errno::ENOENT
31
+ [nil, nil]
32
+ end
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,12 @@
1
+ module Epi
2
+ module Triggers
3
+ class Uptime < Trigger::ProcessTrigger
4
+ include Concerns::Comparison
5
+
6
+ def test(process)
7
+ compare process.uptime
8
+ end
9
+
10
+ end
11
+ end
12
+ end
data/lib/epi/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Epi
2
- VERSION = '0.0.1'
2
+ VERSION = '0.1.0'
3
3
  end
data/lib/epi.rb CHANGED
@@ -3,10 +3,12 @@ require 'logger'
3
3
  require 'shellwords'
4
4
 
5
5
  require 'epi/core_ext'
6
+ require 'epi/logging'
6
7
  require 'epi/exceptions'
7
8
  require 'epi/version'
8
9
  require 'epi/cli'
9
- require 'epi/server'
10
+ require 'epi/connection'
11
+ require 'epi/daemon'
10
12
  require 'epi/data'
11
13
  require 'epi/process_status'
12
14
  require 'epi/running_process'
@@ -15,38 +17,18 @@ require 'epi/jobs'
15
17
  require 'epi/configuration_file'
16
18
  require 'epi/job_description'
17
19
  require 'epi/launch'
20
+ require 'epi/trigger'
18
21
 
19
22
  module Epi
20
23
  ROOT = Pathname File.expand_path('../..', __FILE__)
21
24
 
22
25
  class << self
23
26
 
24
- def logger
25
- @logger ||= make_logger
26
- end
27
-
28
- def logger=(value)
29
- @logger = Logger === value ? value : make_logger(value)
30
- end
31
-
32
27
  def root?
33
28
  @is_root = `whoami`.chomp == 'root' if @is_root.nil?
34
29
  @is_root
35
30
  end
36
31
 
37
- private
38
-
39
- def make_logger(target = nil)
40
- Logger.new(target || ENV['EPI_LOG'] || STDOUT).tap do |logger|
41
- logger.level = Logger::Severity::WARN
42
- level = ENV['EPI_LOG_LEVEL']
43
- if level
44
- level = level.upcase
45
- logger.level = Logger::Severity.const_get(level) if Logger::Severity.const_defined? level
46
- end
47
- end
48
- end
49
-
50
32
  end
51
33
 
52
34
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: epi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Neil E. Pearson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-10-22 00:00:00.000000000 Z
11
+ date: 2014-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: eventmachine
@@ -24,20 +24,6 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.0'
27
- - !ruby/object:Gem::Dependency
28
- name: bson
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '2.3'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '2.3'
41
27
  description: Manage your background processes
42
28
  email:
43
29
  - neil@helium.net.au
@@ -50,13 +36,29 @@ files:
50
36
  - lib/epi.rb
51
37
  - lib/epi/cli.rb
52
38
  - lib/epi/cli/command.rb
39
+ - lib/epi/cli/commands/concerns/daemon.rb
53
40
  - lib/epi/cli/commands/config.rb
41
+ - lib/epi/cli/commands/daemon.rb
42
+ - lib/epi/cli/commands/help.rb
54
43
  - lib/epi/cli/commands/job.rb
55
- - lib/epi/cli/commands/server.rb
44
+ - lib/epi/cli/commands/restart.rb
45
+ - lib/epi/cli/commands/start.rb
56
46
  - lib/epi/cli/commands/status.rb
47
+ - lib/epi/cli/commands/stop.rb
57
48
  - lib/epi/configuration_file.rb
49
+ - lib/epi/connection.rb
58
50
  - lib/epi/core_ext.rb
59
51
  - lib/epi/core_ext/inflector.rb
52
+ - lib/epi/daemon.rb
53
+ - lib/epi/daemon/receiver.rb
54
+ - lib/epi/daemon/responder.rb
55
+ - lib/epi/daemon/responders/config.rb
56
+ - lib/epi/daemon/responders/job.rb
57
+ - lib/epi/daemon/responders/shutdown.rb
58
+ - lib/epi/daemon/responders/start.rb
59
+ - lib/epi/daemon/responders/status.rb
60
+ - lib/epi/daemon/responders/stop_all.rb
61
+ - lib/epi/daemon/sender.rb
60
62
  - lib/epi/data.rb
61
63
  - lib/epi/exceptions.rb
62
64
  - lib/epi/exceptions/base.rb
@@ -67,17 +69,14 @@ files:
67
69
  - lib/epi/job_description.rb
68
70
  - lib/epi/jobs.rb
69
71
  - lib/epi/launch.rb
72
+ - lib/epi/logging.rb
70
73
  - lib/epi/process_status.rb
71
74
  - lib/epi/running_process.rb
72
- - lib/epi/server.rb
73
- - lib/epi/server/receiver.rb
74
- - lib/epi/server/responder.rb
75
- - lib/epi/server/responders/command.rb
76
- - lib/epi/server/responders/config.rb
77
- - lib/epi/server/responders/job.rb
78
- - lib/epi/server/responders/shutdown.rb
79
- - lib/epi/server/responders/status.rb
80
- - lib/epi/server/sender.rb
75
+ - lib/epi/trigger.rb
76
+ - lib/epi/triggers/concerns/comparison.rb
77
+ - lib/epi/triggers/memory.rb
78
+ - lib/epi/triggers/touch.rb
79
+ - lib/epi/triggers/uptime.rb
81
80
  - lib/epi/version.rb
82
81
  homepage: https://github.com/hx/epi
83
82
  licenses:
@@ -104,3 +103,4 @@ signing_key:
104
103
  specification_version: 4
105
104
  summary: Epinephrine
106
105
  test_files: []
106
+ has_rdoc:
@@ -1,38 +0,0 @@
1
- module Epi
2
- module Cli
3
- module Commands
4
- class Server < Command
5
-
6
- def run
7
- process = Epi::Server.process
8
- raise Exceptions::Fatal, 'You need root privileges to manage this server' if
9
- process && process.was_alive? && process.root? && !Epi.root?
10
- case args.first
11
- when nil, 'start' then startup
12
- when 'run' then run_server
13
- when 'stop' then shutdown
14
- else raise Exceptions::Fatal, 'Unknown server command, use [ start | stop | restart ]'
15
- end
16
- end
17
-
18
- private
19
-
20
- def startup
21
- Epi::Server.ensure_running
22
- puts 'Server is running'
23
- end
24
-
25
- def shutdown
26
- Epi::Server.shutdown
27
- puts 'Server has shut down'
28
- end
29
-
30
- def run_server
31
- Epi::Server.run
32
- puts 'Server is running'
33
- end
34
-
35
- end
36
- end
37
- end
38
- end