epi 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,159 @@
1
+ require 'time'
2
+
3
+ module Epi
4
+ # noinspection RubyTooManyInstanceVariablesInspection
5
+ class RunningProcess
6
+
7
+ DEFAULT_TIMEOUT = 20
8
+
9
+ @users = {}
10
+
11
+ class << self
12
+
13
+ def user_name(uid)
14
+ @users[uid.to_i] ||= `id -un #{uid}`.chomp
15
+ end
16
+
17
+ def group_name(gid)
18
+ groups[gid.to_i]
19
+ end
20
+
21
+ private
22
+
23
+ def groups
24
+ @groups ||= read_groups
25
+ end
26
+
27
+ def read_groups
28
+ {}.tap do |result|
29
+ File.readlines('/etc/group').each do |line|
30
+ result[$2.to_i] = $1 if line =~ /^([^:]+):[^:]+:(-?\d+):/
31
+ end
32
+ end
33
+ end
34
+
35
+ end
36
+
37
+ PS_FORMAT = 'pid,%cpu,%mem,rss,vsz,lstart,uid,gid,command'
38
+
39
+ attr_reader :pid
40
+
41
+ def logger
42
+ Epi.logger
43
+ end
44
+
45
+ def initialize(pid, ps_line = nil)
46
+ @pid = pid
47
+ @ps_line = ps_line
48
+ @props = {}
49
+ reload! unless ps_line
50
+ end
51
+
52
+ def reload!
53
+ @props = {}
54
+ @parts = nil
55
+ @ps_line = `ps -p #{pid} -o #{PS_FORMAT}`.lines[1]
56
+ end
57
+
58
+ # Returns `true` if the process was running when this instance was created
59
+ def was_alive?
60
+ !@ps_line.nil?
61
+ end
62
+
63
+ # CPU usage as a percentage
64
+ # @return [Float]
65
+ def cpu_percentage
66
+ @cpu_percentage ||= parts[1].to_f
67
+ end
68
+
69
+ # Physical memory usage as a percentage
70
+ # @return [Float]
71
+ def memory_percentage
72
+ @memory_percentage ||= parts[2].to_f
73
+ end
74
+
75
+ # Physical memory usage in bytes (rounded to the nearest kilobyte)
76
+ # @return [Fixnum]
77
+ def physical_memory
78
+ @physical_memory ||= parts[3].to_i * 1024
79
+ end
80
+
81
+ # Virtual memory usage in bytes (rounded to the nearest kilobyte)
82
+ # @return [Fixnum]
83
+ def virtual_memory
84
+ @virtual_memory ||= parts[4].to_i * 1024
85
+ end
86
+
87
+ # Sum of {#physical_memory} and {#total_memory}
88
+ # @return [Fixnum]
89
+ def total_memory
90
+ @total_memory ||= physical_memory + virtual_memory
91
+ end
92
+
93
+ # Time at which the process was started
94
+ # @return [Time]
95
+ def started_at
96
+ @started_at ||= Time.parse parts[5..9].join ' '
97
+ end
98
+
99
+ # Name of the user that owns the process
100
+ # @return [String]
101
+ def user
102
+ @user ||= self.class.user_name parts[10]
103
+ end
104
+
105
+ # Name of the group that owns the process
106
+ # @return [String]
107
+ def group
108
+ @group ||= self.class.group_name parts[11]
109
+ end
110
+
111
+ # The command that was used to start the process, including its arguments
112
+ # @return [String]
113
+ def command
114
+ @command ||= parts[12]
115
+ end
116
+
117
+ # Whether the process is root-owned
118
+ # @return [TrueClass|FalseClass]
119
+ def root?
120
+ user == 'root'
121
+ end
122
+
123
+ # Kill a running process
124
+ # @param timeout [TrueClass|FalseClass|Numeric] `true` to kill immediately (KILL),
125
+ # `false` to kill gracefully (TERM), or a number of seconds to wait between trying
126
+ # both (first TERM, then KILL).
127
+ # @return [RunningProcess]
128
+ def kill(timeout = DEFAULT_TIMEOUT)
129
+ if timeout.is_a? Numeric
130
+ begin
131
+ logger.info "Will wait #{timeout} second#{timeout != 1 && 's'} for process to terminate gracefully"
132
+ Timeout::timeout(timeout) { kill false }
133
+ rescue Timeout::Error
134
+ kill true
135
+ end
136
+ else
137
+ signal = timeout ? 'KILL' : 'TERM'
138
+ logger.info "Sending #{signal} to process #{pid}"
139
+ Process.kill signal, pid
140
+ sleep 0.2 while `ps -p #{pid} > /dev/null 2>&1; echo $?`.chomp.to_i == 0
141
+ logger.info "Process #{pid} terminated by signal #{signal}"
142
+ end
143
+ self
144
+ end
145
+
146
+ def kill!
147
+ kill true
148
+ end
149
+
150
+ private
151
+
152
+ def parts
153
+ raise 'Tried to access details of a non-running process' unless String === @ps_line
154
+ @parts ||= @ps_line.strip.split(/\s+/, 13)
155
+ end
156
+
157
+ end
158
+
159
+ end
data/lib/epi/server.rb ADDED
@@ -0,0 +1,104 @@
1
+ require 'eventmachine'
2
+
3
+ require_relative 'server/sender'
4
+ require_relative 'server/receiver'
5
+ require_relative 'server/responder'
6
+
7
+ module Epi
8
+ module Server
9
+
10
+ class << self
11
+
12
+ attr_reader :start_time
13
+
14
+ def logger
15
+ Epi.logger
16
+ end
17
+
18
+ def ensure_running
19
+ should_run_as_root = Data.root?
20
+
21
+ if running? && should_run_as_root && !process.root?
22
+ logger.info "Server needs to run as root, but is running as #{process.user}"
23
+ shutdown
24
+ end
25
+
26
+ unless running?
27
+ if should_run_as_root && !Epi.root?
28
+ raise Exceptions::Fatal, 'Found root data but not running as root. Either run again as root, ' +
29
+ 'or specify EPI_HOME as a directory other than /etc/epi'
30
+ end
31
+
32
+ logger.info 'Starting server'
33
+ Epi.launch [$0, 'server', 'run'],
34
+ stdout: Data.home + 'server.log',
35
+ stderr: Data.home + 'server_errors.log'
36
+
37
+ begin
38
+ Timeout::timeout(5) { sleep 0.05 until socket_path.exist? }
39
+ rescue Timeout::Error
40
+ raise Exceptions::Fatal, 'Server not started after 5 seconds'
41
+ end unless socket_path.exist?
42
+ end
43
+ end
44
+
45
+ def socket_path
46
+ Data.home + 'socket'
47
+ end
48
+
49
+ def run
50
+ raise Exceptions::Fatal, 'Server already running' if running?
51
+
52
+ # Save the server PID
53
+ Data.server_pid = Process.pid
54
+
55
+ # Run an initial beat
56
+ Jobs.beat!
57
+
58
+ # Start a server
59
+ EventMachine.start_unix_domain_server socket_path.to_s, Receiver
60
+ logger.info "Listening on socket #{socket_path}"
61
+
62
+ # Make sure other users can connect to the server
63
+ socket_path.chmod 0777 #TODO: make configurable
64
+
65
+ # Ensure the socket is destroyed when the server exits
66
+ EventMachine.add_shutdown_hook { socket_path.delete }
67
+
68
+ @start_time = Time.now
69
+ end
70
+
71
+ def send(*args)
72
+ ensure_running
73
+ Sender.send *args
74
+ end
75
+
76
+ def shutdown(process = nil)
77
+ process ||= self.process
78
+ raise Exceptions::Fatal, 'Attempted to shut down server when no server is running' unless running?
79
+ if process.pid == Process.pid
80
+ EventMachine.next_tick do
81
+ EventMachine.stop_event_loop
82
+ Data.server_pid = nil
83
+ logger.info 'Server has shut down'
84
+ end
85
+ else
86
+ logger.info 'Server will shut down'
87
+ send :shutdown
88
+ end
89
+ end
90
+
91
+ def running?
92
+ process && process.was_alive?
93
+ end
94
+
95
+ def process
96
+ server_pid = Data.server_pid
97
+ @process = nil if @process && @process.pid != server_pid
98
+ @process ||= server_pid && RunningProcess.new(server_pid)
99
+ end
100
+
101
+ end
102
+
103
+ end
104
+ end
@@ -0,0 +1,46 @@
1
+ require 'eventmachine'
2
+ require 'bson'
3
+
4
+ module Epi
5
+ module Server
6
+ class Receiver < EventMachine::Connection
7
+
8
+ def logger
9
+ Epi.logger
10
+ end
11
+
12
+ def receive_data(data)
13
+ response = begin
14
+ data = Hash.from_bson StringIO.new data
15
+ logger.debug "Received message of type '#{data['type']}'"
16
+ {result: Responder.run(self, data.delete('type').to_s, data)}
17
+ rescue Exceptions::Shutdown
18
+ self.should_shut_down = true
19
+ {result: 'Server is shutting down'}
20
+ rescue => error
21
+ {error: {
22
+ class: error.class.name,
23
+ message: error.message,
24
+ backtrace: error.backtrace
25
+ }}
26
+ end
27
+ response[:complete] = true
28
+ send_data response.to_bson
29
+ Server.shutdown if should_shut_down
30
+ end
31
+
32
+ def puts(text)
33
+ data = {
34
+ result: "#{text}\n",
35
+ complete: false
36
+ }
37
+ send_data data.to_bson
38
+ end
39
+
40
+ private
41
+
42
+ attr_accessor :should_shut_down
43
+
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,44 @@
1
+ module Epi
2
+ module Server
3
+ unless defined? Responder
4
+ class Responder
5
+ include Exceptions
6
+
7
+ # Runs a responder by name.
8
+ #
9
+ # @param receiver [Receiver] The receiver that is running the responder
10
+ # @param name [String] Name of the responder to invoke, e.g. 'command'
11
+ # @param data [Hash] Data included in the message, to be extracted onto the responder before it is run
12
+ def self.run(receiver, name, data)
13
+ klass_name = name.camelize.to_sym
14
+ klass = Responders.const_defined?(klass_name) && Responders.const_get(klass_name)
15
+ raise Fatal, 'Unknown message type' unless Class === klass && klass < Responder
16
+ responder = klass.new(receiver)
17
+ data.each { |key, value| responder.__send__ :"#{key}=", value }
18
+ responder.run
19
+ end
20
+
21
+ attr_reader :receiver
22
+
23
+ def logger
24
+ Epi.logger
25
+ end
26
+
27
+ def initialize(receiver)
28
+ @receiver = receiver
29
+ end
30
+
31
+ def run
32
+ raise NotImplementedError, "You need to define #run for class #{self.class.name}"
33
+ end
34
+
35
+ def puts(text)
36
+ receiver.puts text
37
+ end
38
+
39
+ end
40
+
41
+ Dir[File.expand_path '../responders/*.rb', __FILE__].each { |f| require f }
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,15 @@
1
+ module Epi
2
+ module Server
3
+ module Responders
4
+ class Command < Responder
5
+
6
+ attr_accessor :command, :arguments
7
+
8
+ def run
9
+ Cli::Command.run command, arguments
10
+ end
11
+
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,30 @@
1
+ module Epi
2
+ module Server
3
+ module Responders
4
+ class Config < Responder
5
+
6
+ attr_accessor :add_paths
7
+
8
+ def run
9
+ result = []
10
+ configs = Data.configuration_paths
11
+ add_paths.each do |path|
12
+ path = path.to_s
13
+ if configs.include?(path)
14
+ logger.warn "Tried to re-add config path: #{path}"
15
+ result << "Config path already loaded: #{path}"
16
+ else
17
+ logger.info "Adding config path: #{path}"
18
+ configs << path
19
+ result << "Added config path: #{path}"
20
+ end
21
+ end if add_paths
22
+ Data.save
23
+ Jobs.beat!
24
+ result.join ' '
25
+ end
26
+
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,70 @@
1
+ module Epi
2
+ module Server
3
+ module Responders
4
+ class Job < Responder
5
+
6
+ attr_accessor :id, :instruction
7
+
8
+ def run
9
+ Jobs.beat!
10
+ raise Exceptions::Fatal, 'Unknown job ID' unless Epi::Job === job
11
+ case instruction
12
+ when /^\d+$/ then set instruction.to_i
13
+ when /^(\d+ )?(more|less)$/ then __send__ $2, ($1 || 1).to_i
14
+ else __send__ instruction
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def job
21
+ @job ||= Jobs[id]
22
+ end
23
+
24
+ def set(count, validate = true)
25
+ allowed = job.allowed_processes
26
+ raise Exceptions::Fatal, "Requested count #{count} is outside allowed range #{allowed}" unless !validate || allowed === count
27
+ original = job.expected_count
28
+ raise Exceptions::Fatal, "Already running #{count} process#{count != 1 ? 'es' : ''}" unless !validate || original != count
29
+ job.expected_count = count
30
+ job.sync!
31
+ "#{count < original ? 'De' : 'In'}creasing '#{job.name}' processes by #{(original - count).abs} (from #{original} to #{count})"
32
+ end
33
+
34
+ def more(increase)
35
+ set job.expected_count + increase
36
+ end
37
+
38
+ def less(decrease)
39
+ set job.expected_count - decrease
40
+ end
41
+
42
+ def max
43
+ set job.allowed_processes.max
44
+ end
45
+
46
+ def min
47
+ set job.allowed_processes.min
48
+ end
49
+
50
+ def pause
51
+ set 0
52
+ end
53
+
54
+ def resume
55
+ set job.job_description.initial_processes
56
+ end
57
+ alias_method :reset, :resume
58
+
59
+ def restart
60
+ count = job.expected_count
61
+ raise Exceptions::Fatal, 'This job has no processes to restart' if count == 0
62
+ set 0, false
63
+ set count
64
+ "Replacing #{count} '#{job.name}' process#{count != 1 ? 'es' : ''}"
65
+ end
66
+
67
+ end
68
+ end
69
+ end
70
+ end