cognizant 0.0.1

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.
@@ -0,0 +1,70 @@
1
+ module Cognizant
2
+ class Process
3
+ module Actions
4
+ module Start
5
+ # Environment variables for process during start.
6
+ # @return [Hash] Defaults to {}
7
+ attr_accessor :start_env
8
+
9
+ # The command to run before the process is started. The exit status
10
+ # of this command determines whether or not to proceed.
11
+ # @return [String] Defaults to nil
12
+ attr_accessor :start_before_command
13
+
14
+ # The command to start the process with.
15
+ # e.g. "/usr/bin/redis-server"
16
+ # @return [String] Defaults to nil
17
+ attr_accessor :start_command
18
+
19
+ # Start the process with this in it's STDIN.
20
+ # e.g. "daemonize no"
21
+ # @return [String] Defaults to nil
22
+ attr_accessor :start_with_input
23
+
24
+ # Start the process with this file's data in it's STDIN.
25
+ # e.g. "/etc/redis/redis.conf"
26
+ # @return [String] Defaults to nil
27
+ attr_accessor :start_with_input_file
28
+
29
+ # Start the process with this command's output in it's (process') STDIN.
30
+ # e.g. "cat /etc/redis/redis.conf"
31
+ # @return [String] Defaults to nil
32
+ attr_accessor :start_with_input_command
33
+
34
+ # The grace time period in seconds for the process to start within.
35
+ # Covers the time period for the input and start command. After the
36
+ # timeout is over, the process the considered not started and it
37
+ # re-enters the auto start lifecycle based on conditions.
38
+ # @return [String] Defaults to 10
39
+ attr_accessor :start_timeout
40
+
41
+ # The command to run after the process is started.
42
+ # @return [String] Defaults to nil
43
+ attr_accessor :start_after_command
44
+
45
+ def start_process
46
+ result_handler = Proc.new do |result|
47
+ if result.respond_to?(:succeeded?) and result.succeeded?
48
+ write_pid(result.pid) if result.pid != 0
49
+ end
50
+ end
51
+ execute_action(
52
+ result_handler,
53
+ name: self.name,
54
+ daemonize: self.daemonize || true,
55
+ env: (self.env || {}).merge(self.start_env || {}),
56
+ logfile: self.logfile,
57
+ errfile: self.errfile,
58
+ before: self.start_before_command,
59
+ command: self.start_command,
60
+ input: self.start_with_input,
61
+ input_file: self.start_with_input_file,
62
+ input_command: self.start_with_input_command,
63
+ after: self.start_after_command,
64
+ timeout: self.start_timeout
65
+ )
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,59 @@
1
+ module Cognizant
2
+ class Process
3
+ module Actions
4
+ module Stop
5
+ # Environment variables for process during stop.
6
+ # @return [Hash] Defaults to {}
7
+ attr_accessor :stop_env
8
+
9
+ # The command to run before the process is stopped. The exit status
10
+ # of this command determines whether or not to proceed.
11
+ # @return [String] Defaults to nil
12
+ attr_accessor :stop_before_command
13
+
14
+ # The command to stop the process with.
15
+ # e.g. "/usr/bin/redis-server"
16
+ # @return [String] Defaults to nil
17
+ attr_accessor :stop_command
18
+
19
+ # The signals to pass to the process one by one attempting to stop it.
20
+ # Each signal is passed within the timeout period over equally
21
+ # distributed intervals (min. 2 seconds). Override with signals without
22
+ # "KILL" to never force kill the process.
23
+ # e.g. ["TERM", "INT"]
24
+ # @return [Array] Defaults to ["TERM", "INT", "KILL"]
25
+ attr_accessor :stop_signals
26
+
27
+ # The grace time period in seconds for the process to stop within.
28
+ # Covers the time period for the stop command or signals. After the
29
+ # timeout is over, the process is checked for running status and if
30
+ # not stopped, it re-enters the auto start lifecycle based on
31
+ # conditions.
32
+ # @return [String] Defaults to 10
33
+ attr_accessor :stop_timeout
34
+
35
+ # The command to run after the process is stopped.
36
+ # @return [String] Defaults to nil
37
+ attr_accessor :stop_after_command
38
+
39
+ def stop_process
40
+ result_handler = Proc.new do |result|
41
+ # If it is a boolean and value is true OR if it's an execution result and it succeeded.
42
+ if (!!result == result and result) or (result.respond_to?(:succeeded?) and result.succeeded?)
43
+ unlink_pid unless pid_running?
44
+ end
45
+ end
46
+ execute_action(
47
+ result_handler,
48
+ env: (self.env || {}).merge(self.stop_env || {}),
49
+ before: self.stop_before_command,
50
+ command: self.stop_command,
51
+ signals: self.stop_signals,
52
+ after: self.stop_after_command,
53
+ timeout: self.stop_timeout
54
+ )
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,96 @@
1
+ require "cognizant/process/actions/start"
2
+ require "cognizant/process/actions/stop"
3
+ require "cognizant/process/actions/restart"
4
+
5
+ module Cognizant
6
+ class Process
7
+ module Actions
8
+ include Cognizant::Process::Actions::Start
9
+ include Cognizant::Process::Actions::Stop
10
+ include Cognizant::Process::Actions::Restart
11
+
12
+ private
13
+
14
+ def execute_action(result_handler, options)
15
+ before_command = options[:before]
16
+ command = options[:command]
17
+ after_command = options[:after]
18
+ signals = options[:signals]
19
+ timeout = options[:timeout] || 10
20
+
21
+ # TODO: Works well but can some refactoring make it more declarative?
22
+ @action_thread = Thread.new do
23
+ result = false
24
+ queue = Queue.new
25
+ thread = Thread.new do
26
+ if before_command and not success = run(before_command).succeeded?
27
+ queue.push(success)
28
+ Thread.exit
29
+ end
30
+
31
+ if (command and success = run(command, options) and success.succeeded?)
32
+ run(after_command) if after_command
33
+ queue.push(success)
34
+ Thread.exit
35
+ end
36
+
37
+ # If the caller has attempted to set signals, then it can handle it's result.
38
+ if success = send_signals(signals: signals, timeout: timeout)
39
+ run(after_command) if after_command
40
+ queue.push(success)
41
+ Thread.exit
42
+ end
43
+
44
+ queue.push(false)
45
+ Thread.exit
46
+ end
47
+
48
+ timeout_left = timeout + 1
49
+ while (timeout_left -= 1) > 0 do
50
+ # If there is something in the queue, we have the required result.
51
+ if result = queue.pop
52
+ # Rollback the pending skips, since we finished before timeout.
53
+ skip_ticks_for(-timeout_left)
54
+ break
55
+ end
56
+ sleep 1
57
+ end
58
+
59
+ # Kill the nested thread.
60
+ thread.kill
61
+
62
+ # Action callback.
63
+ result_handler.call(result) if result_handler.respond_to?(:call)
64
+
65
+ # Kill self.
66
+ Thread.kill
67
+ end
68
+
69
+ # We skip so that we're not reinformed about the required transition by the tick.
70
+ skip_ticks_for(timeout)
71
+ end
72
+
73
+ def send_signals(options = {})
74
+ # Return if the process is already stopped.
75
+ return true unless pid_running?
76
+
77
+ signals = options[:signals] || ["TERM", "INT", "KILL"]
78
+ timeout = options[:timeout] || 10
79
+
80
+ catch :stopped do
81
+ signals.each do |stop_signal|
82
+ # Send the stop signal and wait for it to stop.
83
+ signal(stop_signal, @process_pid)
84
+
85
+ # Poll to see if it's stopped yet. Minimum 2 so that we check at least once again.
86
+ ([timeout / signals.size, 2].max).times do
87
+ throw :stopped unless pid_running?
88
+ sleep 1
89
+ end
90
+ end
91
+ end
92
+ not pid_running?
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,81 @@
1
+ module Cognizant
2
+ class Process
3
+ module Attributes
4
+ # Unique name for the process.
5
+ # @return [String]
6
+ attr_accessor :name
7
+
8
+ # Group classification for the process.
9
+ # @return [String] Defaults to nil
10
+ attr_accessor :group
11
+
12
+ # Whether or not to daemonize the process. It is recommended that
13
+ # cognizant managed your process completely by process not
14
+ # daemonizing itself. Find a non-daemonizing option in your process'
15
+ # documentation.
16
+ # @return [true, false] Defaults to true
17
+ attr_accessor :daemonize
18
+
19
+ # Whether or not to auto start the process on initial run. Afterwards,
20
+ # this attribute is overwritten by stop or restart request.
21
+ # @return [true, false] Defaults to true
22
+ attr_accessor :autostart
23
+
24
+ # The pid lock file for the process. Required when daemonize is set to
25
+ # false.
26
+ # @return [String] Defaults to value of pids_dir/name.pid
27
+ attr_accessor :pidfile
28
+
29
+ # The file to log the process' STDOUT stream into.
30
+ # @return [String] Defaults to value of logs_dir/name.log
31
+ attr_accessor :logfile
32
+
33
+ # The file to log the daemon's STDERR stream into.
34
+ # @return [String] Defaults to value of logfile
35
+ attr_accessor :errfile
36
+
37
+ # Environment variables for process.
38
+ # @return [Hash] Defaults to {}
39
+ attr_accessor :env
40
+
41
+ # The chroot directory to change the process' idea of the file system
42
+ # root.
43
+ # @return [String] Defaults to nil
44
+ attr_accessor :chroot
45
+
46
+ # The current working directory for the process to start with.
47
+ # @return [String] Defaults to nil
48
+ attr_accessor :chdir
49
+
50
+ # Limit the permission modes for files and directories created by the
51
+ # process.
52
+ # @return [Integer] Defaults to nil
53
+ attr_accessor :umask
54
+
55
+ # Run the process as the given user.
56
+ # e.g. "deploy", 1000
57
+ # @return [String] Defaults to nil
58
+ attr_accessor :uid
59
+
60
+ # Run the process as the given user group.
61
+ # e.g. "deploy"
62
+ # @return [String] Defaults to nil
63
+ attr_accessor :gid
64
+
65
+ # Supplementary user groups for the process.
66
+ # e.g. ["staff"]
67
+ # @return [Array] Defaults to []
68
+ attr_accessor :groups
69
+
70
+ # The command to check the running status of the process with. The exit
71
+ # status of the command is used to determine the status.
72
+ # e.g. "/usr/bin/redis-cli PING"
73
+ # @return [String] Defaults to nil
74
+ attr_accessor :ping_command
75
+
76
+ # The command that returns the pid of the process.
77
+ # @return [String] Defaults to nil
78
+ attr_accessor :pid_command
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,176 @@
1
+ module Cognizant
2
+ class Process
3
+ module Execution
4
+ ExecutionResult = Struct.new(
5
+ :pid,
6
+ :stdout,
7
+ :stderr,
8
+ :exit_code,
9
+ :succeeded
10
+ ) do alias succeeded? succeeded end
11
+
12
+ def execute(command, options = {})
13
+ options[:groups] ||= []
14
+ options[:env] ||= {}
15
+
16
+ pid, pid_w = IO.pipe
17
+
18
+ unless options[:daemonize]
19
+ # Stdout and stderr file descriptors.
20
+ out_r, out_w = IO.pipe
21
+ err_r, err_w = IO.pipe
22
+ end
23
+
24
+ # Run the following in a fork so that the privilege changes do not affect the parent process.
25
+ fork_pid = ::Process.fork do
26
+ # Set the user and groups for the process in context.
27
+ drop_privileges(options)
28
+
29
+ if options[:daemonize]
30
+ # Create a new session to detach from the controlling terminal.
31
+ unless ::Process.setsid
32
+ # raise Koth::RuntimeException.new('cannot detach from controlling terminal')
33
+ end
34
+
35
+ # TODO: Set pgroup: true so that the spawned process is the group leader, and it's death would kill all children as well.
36
+
37
+ # Prevent inheriting signals from parent process.
38
+ Signal.trap('TERM', 'SIG_DFL')
39
+ Signal.trap('INT', 'SIG_DFL')
40
+ Signal.trap('HUP', 'SIG_DFL')
41
+
42
+ # Give the process a name.
43
+ $0 = options[:name] if options[:name]
44
+
45
+ # Collect logs only when daemonizing.
46
+ stdout = [options[:logfile] || "/dev/null", "a"]
47
+ stderr = [options[:errfile] || options[:logfile] || "/dev/null", "a"]
48
+ else
49
+ stdout = out_w
50
+ stderr = err_w
51
+ end
52
+
53
+ # TODO: Run popen as spawned process before privileges are dropped for increased abilities?
54
+ stdin_data = options[:input] if options[:input]
55
+ stdin_data = IO.popen(options[:input_command]).gets if options[:input_command]
56
+
57
+ if stdin_data
58
+ stdin, stdin_w = IO.pipe
59
+ stdin_w.write stdin_data
60
+ stdin_w.close # stdin is closed by ::Process.spawn.
61
+ elsif options[:input_file] and File.exists?(options[:input_file])
62
+ stdin = options[:input_file]
63
+ else
64
+ stdin = "/dev/null"
65
+ end
66
+
67
+ # Merge spawn options.
68
+ spawn_options = construct_spawn_options(options, {
69
+ :in => stdin,
70
+ :out => stdout,
71
+ :err => stderr
72
+ })
73
+
74
+ # Spawn a process to execute the command.
75
+ process_pid = ::Process.spawn(options[:env], command, spawn_options)
76
+ # puts "process_pid: #{process_pid} (#{command})"
77
+ pid_w.write(process_pid)
78
+
79
+ if options[:daemonize]
80
+ # We do not care about actual status or output when daemonizing.
81
+ exit(0)
82
+ else
83
+ # TODO: Timeout here, in case the process doesn't daemonize itself.
84
+
85
+ # Wait (blocking) until the command has finished running.
86
+ status = ::Process.waitpid2(process_pid)[1]
87
+
88
+ # Pass on the exit status to the parent.
89
+ exit(status.exitstatus || 0) # TODO: This 0 or something else should be controlled by timeout handler.
90
+ end
91
+ end
92
+
93
+ # Close the pid file descriptor.
94
+ pid_w.close
95
+
96
+ if options[:daemonize]
97
+ # Detach (non blocking) the process executing the command and move on.
98
+ ::Process.detach(fork_pid)
99
+
100
+ return ExecutionResult.new(
101
+ pid.read.to_i,
102
+ nil,
103
+ nil,
104
+ 0,
105
+ true
106
+ )
107
+ else
108
+ # Wait until the fork has finished running and accept the exit status.
109
+ status = ::Process.waitpid2(fork_pid)[1]
110
+
111
+ # Timeout and try (detach + pid_running?)?
112
+
113
+ # Close the file descriptors.
114
+ out_w.close
115
+ err_w.close
116
+
117
+ # Collect and return stdout, stderr and exitcode.
118
+ return ExecutionResult.new(
119
+ pid.read.to_i,
120
+ out_r.read,
121
+ err_r.read,
122
+ status.exitstatus,
123
+ status.exitstatus.zero?
124
+ )
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ def drop_privileges(options = {})
131
+ # Cannot drop privileges unless we are superuser.
132
+ if ::Process.euid == 0
133
+ # Drop ~= decrease, since we can only decrease privileges.
134
+
135
+ # For clarity.
136
+ uid = options[:uid]
137
+ gid = options[:gid]
138
+ groups = options[:groups]
139
+
140
+ # Find the user and primary group in the password and group database.
141
+ user = (uid.is_a? Integer) ? Etc.getpwuid(uid) : Etc.getpwnam(uid) if uid
142
+ group = (gid.is_a? Integer) ? Etc.getgruid(gid) : Etc.getgrnam(gid) if gid
143
+
144
+ # Collect the secondary groups' GIDs.
145
+ group_ids = groups.map { |g| Etc.getgrnam(g).gid } if groups
146
+
147
+ # Set the fork's secondary user groups for the spawn process to inherit.
148
+ ::Process.groups = [group.gid] if group # Including the primary group.
149
+ ::Process.groups |= group_ids if groups and !group_ids.empty?
150
+
151
+ # Set the fork's user and primary group for the spawn process to inherit.
152
+ ::Process.uid = user.uid if user
153
+ ::Process.gid = group.gid if group
154
+
155
+ # Find and set the user's HOME environment variable for fun.
156
+ options[:env] = options[:env].merge({ 'HOME' => user.dir }) if user and user.dir
157
+
158
+ # Changes the process' idea of the file system root.
159
+ Dir.chroot(options[:chroot]) if options[:chroot]
160
+
161
+ # umask and chdir drops are managed by ::Process.spawn.
162
+ end
163
+ end
164
+
165
+ private
166
+
167
+ def construct_spawn_options(options, overrides = {})
168
+ spawn_options = {}
169
+ [:chdir, :umask].each do |o|
170
+ spawn_options[o] = options[o] if options[o]
171
+ end
172
+ spawn_options.merge(overrides)
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,28 @@
1
+ module Cognizant
2
+ class Process
3
+ module PID
4
+ def read_pid
5
+ if self.pid_command
6
+ str = execute(self.pid_command).stdout.to_i
7
+ @process_pid = str unless not str or str.zero? # TODO: Also check if the pid is alive.
8
+ elsif self.pidfile and File.exists?(self.pidfile)
9
+ str = File.read(self.pidfile).to_i
10
+ @process_pid = str unless not str or str.zero? # TODO: Also check if the pid is alive.
11
+ end
12
+ @process_pid
13
+ end
14
+
15
+ def write_pid(pid = nil)
16
+ @process_pid = pid if pid
17
+ File.open(self.pidfile, "w") { |f| f.write(@process_pid) } if self.pidfile and @process_pid
18
+ end
19
+
20
+ def unlink_pid
21
+ File.unlink(self.pidfile) if self.pidfile
22
+ rescue Errno::ENOENT
23
+ # It got deleted before we could. Perhaps it was a process managed pidfile.
24
+ true
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,27 @@
1
+ module Cognizant
2
+ class Process
3
+ module Status
4
+ def pid_running?
5
+ pid = read_pid
6
+ return false unless pid and pid != 0
7
+ signal(0, pid)
8
+ # It's running since no exception was raised.
9
+ true
10
+ rescue Errno::ESRCH
11
+ # No such process.
12
+ false
13
+ rescue Errno::EPERM
14
+ # Probably running, but we're not allowed to pass signals.
15
+ # TODO: Is this a sign of problems ahead?
16
+ true
17
+ else
18
+ # Possibly running.
19
+ true
20
+ end
21
+
22
+ def signal(signal, pid = nil)
23
+ ::Process.kill(signal, (pid || read_pid))
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,149 @@
1
+ require 'state_machine'
2
+
3
+ require "cognizant/process/pid"
4
+ require "cognizant/process/status"
5
+ require "cognizant/process/execution"
6
+ require "cognizant/process/attributes"
7
+ require "cognizant/process/actions"
8
+
9
+ module Cognizant
10
+ class Process
11
+ include Cognizant::Process::PID
12
+ include Cognizant::Process::Status
13
+ include Cognizant::Process::Execution
14
+ include Cognizant::Process::Attributes
15
+ include Cognizant::Process::Actions
16
+
17
+ state_machine :initial => :unmonitored do
18
+ # These are the idle states, i.e. only an event (either external or internal) will trigger a transition.
19
+ # The distinction between stopped and unmonitored is that stopped
20
+ # means we know it is not running and unmonitored is that we don't care if it's running.
21
+ state :unmonitored, :running, :stopped
22
+
23
+ # These are transitionary states, we expect the process to change state after a certain period of time.
24
+ state :starting, :stopping, :restarting
25
+
26
+ event :tick do
27
+ transition :starting => :running, :if => :process_running?
28
+ transition :starting => :stopped, :unless => :process_running?
29
+
30
+ transition :running => :stopped, :unless => :process_running?
31
+
32
+ # The process failed to die after entering the stopping state. Change the state to reflect reality.
33
+ transition :stopping => :running, :if => :process_running?
34
+ transition :stopping => :stopped, :unless => :process_running?
35
+
36
+ transition :stopped => :running, :if => :process_running?
37
+ transition :stopped => :starting, :if => lambda { |p| p.autostart and not p.process_running? }
38
+
39
+ transition :restarting => :running, :if => :process_running?
40
+ transition :restarting => :stopped, :unless => :process_running?
41
+ end
42
+
43
+ event :monitor do
44
+ transition :unmonitored => :stopped
45
+ end
46
+
47
+ event :start do
48
+ transition [:unmonitored, :stopped] => :starting
49
+ end
50
+
51
+ event :stop do
52
+ transition :running => :stopping
53
+ end
54
+
55
+ event :restart do
56
+ transition [:running, :stopped] => :restarting
57
+ end
58
+
59
+ event :unmonitor do
60
+ transition any => :unmonitored
61
+ end
62
+
63
+ after_transition any => :starting, :do => :start_process
64
+ before_transition :running => :stopping, :do => lambda { |p| p.autostart = false }
65
+ after_transition any => :stopping, :do => :stop_process
66
+ before_transition any => :restarting, :do => lambda { |p| p.autostart = true }
67
+ after_transition any => :restarting, :do => :restart_process
68
+
69
+ before_transition any => any, :do => :record_transition_start
70
+ after_transition any => any, :do => :record_transition_end
71
+ end
72
+
73
+ def initialize(process_name = nil, options = {})
74
+ # Default.
75
+ self.autostart = true
76
+ self.name = process_name if process_name
77
+
78
+ options.each do |attribute_name, value|
79
+ self.send("#{attribute_name}=", value) if self.respond_to?("#{attribute_name}=")
80
+ end
81
+
82
+ @ticks_to_skip = 0
83
+
84
+ yield(self) if block_given?
85
+
86
+ # Let state_machine initialize as well.
87
+ super
88
+ end
89
+
90
+ def tick
91
+ return if skip_tick?
92
+ @action_thread.kill if @action_thread # TODO: Ensure if this is really needed.
93
+
94
+ # Invoke the state_machine event.
95
+ super
96
+ end
97
+
98
+ def record_transition_start
99
+ print "#{name}: changing state from `#{state}`"
100
+ end
101
+
102
+ def record_transition_end
103
+ puts " to `#{state}`"
104
+ end
105
+
106
+ def process_running?
107
+ @process_running = begin
108
+ # Do not assume change when we're giving time to an execution by skipping ticks.
109
+ if @ticks_to_skip > 0
110
+ @process_running
111
+ elsif self.ping_command and run(self.ping_command).succeeded?
112
+ true
113
+ elsif pid_running?
114
+ true
115
+ else
116
+ false
117
+ end
118
+ end
119
+ end
120
+
121
+ def pidfile
122
+ @pidfile = @pidfile || File.join(Cognizant::Server.daemon.pids_dir, self.name + '.pid')
123
+ end
124
+
125
+ def logfile
126
+ @logfile = @logfile || File.join(Cognizant::Server.daemon.logs_dir, self.name + '.log')
127
+ end
128
+
129
+ private
130
+
131
+ def skip_ticks_for(skips)
132
+ # Accept negative skips with the result being >= 0.
133
+ @ticks_to_skip = [@ticks_to_skip + (skips.to_i + 1), 0].max # +1 so that we don't have to >= and ensure 0 in "skip_tick?".
134
+ end
135
+
136
+ def skip_tick?
137
+ (@ticks_to_skip -= 1) > 0 if @ticks_to_skip > 0
138
+ end
139
+
140
+ def run(command, action_overrides = {})
141
+ options = { daemonize: false }
142
+ # Options from daemon config.
143
+ [:uid, :gid, :groups, :chroot, :chdir, :umask].each do |attribute|
144
+ options[attribute] = self.send(attribute)
145
+ end
146
+ execute(command, options.merge(action_overrides))
147
+ end
148
+ end
149
+ end