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.
- data/.gitignore +18 -0
- data/.yardopts +1 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +160 -0
- data/Rakefile +2 -0
- data/bin/cognizant +58 -0
- data/bin/cognizantd +124 -0
- data/cognizant.gemspec +24 -0
- data/examples/cognizantd.yml +23 -0
- data/examples/redis-server.rb +17 -0
- data/examples/resque.rb +10 -0
- data/images/logo-small.png +0 -0
- data/images/logo.png +0 -0
- data/images/logo.pxm +0 -0
- data/lib/cognizant/client/cli.rb +9 -0
- data/lib/cognizant/client/interface.rb +33 -0
- data/lib/cognizant/logging.rb +33 -0
- data/lib/cognizant/process/actions/restart.rb +60 -0
- data/lib/cognizant/process/actions/start.rb +70 -0
- data/lib/cognizant/process/actions/stop.rb +59 -0
- data/lib/cognizant/process/actions.rb +96 -0
- data/lib/cognizant/process/attributes.rb +81 -0
- data/lib/cognizant/process/execution.rb +176 -0
- data/lib/cognizant/process/pid.rb +28 -0
- data/lib/cognizant/process/status.rb +27 -0
- data/lib/cognizant/process.rb +149 -0
- data/lib/cognizant/server/commands.rb +53 -0
- data/lib/cognizant/server/daemon.rb +239 -0
- data/lib/cognizant/server/interface.rb +60 -0
- data/lib/cognizant/server.rb +14 -0
- data/lib/cognizant/validations.rb +142 -0
- data/lib/cognizant/version.rb +3 -0
- data/lib/cognizant.rb +7 -0
- metadata +169 -0
@@ -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
|