epi 0.0.1 → 0.1.0

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.
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