module Eye::Process::Commands def start_process debug { 'start_process command' } switch :starting unless self[:start_command] warn 'no :start_command found, unmonitoring' switch :unmonitoring, reason: 'no_start_command' return :no_start_command end result = self[:daemonize] ? daemonize_process : execute_process if !result[:error] debug { "process <#{self.pid}> started successfully" } switch :started else error "process <#{self.pid}> failed to start (#{result[:error].inspect})" if process_really_running? warn "killing <#{self.pid}> due to error" send_signal(:KILL) sleep 0.2 # little grace end switch :crashed end result rescue StateMachines::InvalidTransition, Eye::Process::StateError => e warn "wrong switch '#{e.message}'" :state_error end def stop_process debug { 'stop_process command' } switch :stopping return unless check_identity kill_process if process_really_running? warn "process <#{self.pid}> was not stopped; try checking your command/signals or tuning the stop_timeout/stop_grace values" switch :unmonitoring, reason: 'not stopped (soft command)' nil else switch :stopped clear_pid_file if self[:clear_pid] # by default for all true end rescue StateMachines::InvalidTransition, Eye::Process::StateError => e warn "wrong switch '#{e.message}'" nil end def restart_process debug { 'restart_process command' } switch :restarting if self[:restart_command] return unless check_identity if execute_restart_command sleep_grace(:restart_grace) end result = process_really_running? || (load_external_pid_file == :ok) switch(result ? :restarted : :crashed) else stop_process start_process end true rescue StateMachines::InvalidTransition, Eye::Process::StateError => e warn "wrong switch '#{e.message}'" nil end private def kill_process unless self.pid error 'cannot stop a process without a pid' return end if self[:stop_command] cmd = prepare_command(self[:stop_command]) info "executing: `#{cmd}` with stop_timeout: #{self[:stop_timeout].to_f}s and stop_grace: #{self[:stop_grace].to_f}s" res = execute(cmd, config.merge(timeout: self[:stop_timeout])) if res[:error] if res[:error].class == Timeout::Error error "stop_command failed with #{res[:error].inspect}; try tuning the stop_timeout value" else error "stop_command failed with #{res[:error].inspect}" end end sleep_grace(:stop_grace) elsif self[:stop_signals] info "executing stop_signals #{self[:stop_signals].inspect}" stop_signals = self[:stop_signals].clone signal = stop_signals.shift send_signal(signal) while stop_signals.present? delay = stop_signals.shift signal = stop_signals.shift if wait_for_condition(delay.to_f, 0.1) { !process_really_running? } info 'has terminated' break end send_signal(signal) if signal end # little grace sleep 0.1 else # TODO, notify here? error 'stop_command or stop_signals should be' end end def execute_restart_command unless self.pid error 'cannot restart a process without a pid' return end cmd = prepare_command(self[:restart_command]) info "executing: `#{cmd}` with restart_timeout: #{self[:restart_timeout].to_f}s and restart_grace: #{self[:restart_grace].to_f}s" res = execute(cmd, config.merge(timeout: self[:restart_timeout])) if res[:error] if res[:error].class == Timeout::Error error "restart_command failed with #{res[:error].inspect}; try tuning the restart_timeout value" else error "restart_command failed with #{res[:error].inspect}" end end res end def daemonize_process res = daemonize(self[:start_command], config) info "daemonizing: `#{self[:start_command]}` with start_grace: #{self[:start_grace].to_f}s, env: '#{environment_string}'" \ ", <#{res[:pid]}> (in #{self[:working_dir]})" if res[:error] if res[:error].message == 'Permission denied - open' error "daemonize failed with #{res[:error].inspect}; make sure #{[self[:stdout], self[:stderr]]} are writable" else error "daemonize failed with #{res[:error].inspect}" end return { error: res[:error].inspect } end self.pid = res[:pid] unless self.pid error 'no pid was returned' return { error: :empty_pid } end sleep_grace(:start_grace) unless process_really_running? error "process <#{self.pid}> not found, it may have crashed (#{check_logs_str})" return { error: :not_really_running } end # if we using leaf child stratedy, pid should be used as last child process if self[:use_leaf_child] if lpid = Eye::SystemResources.leaf_child(self.pid) info "leaf child for <#{self.pid}> found: <#{lpid}>, accepting it!" self.parent_pid = self.pid self.pid = lpid else warn "leaf child not found for <#{self.pid}>, skipping it" end end if control_pid? && !failsafe_save_pid return { error: :cant_write_pid } end res end def execute_process info "executing: `#{self[:start_command]}` with start_timeout: #{config[:start_timeout].to_f}s" \ ", start_grace: #{self[:start_grace].to_f}s, env: '#{environment_string}' (in #{self[:working_dir]})" res = execute(self[:start_command], config.merge(timeout: config[:start_timeout])) if res[:error] if res[:error].message == 'Permission denied - open' error "execution failed with #{res[:error].inspect}; ensure that #{[self[:stdout], self[:stderr]]} are writable" elsif res[:error].class == Timeout::Error error "execution failed with #{res[:error].inspect}; try increasing the start_timeout value" \ "(the current value of #{self[:start_timeout]}s seems too short)" else error "execution failed with #{res[:error].inspect}" end return { error: res[:error].inspect } end sleep_grace(:start_grace) case load_external_pid_file when :ok res.merge(pid: self.pid) when :no_pid_file error "exit status #{res[:exitstatus]}, pid_file (#{self[:pid_file_ex]}) did not appear within the " \ "start_grace period (#{self[:start_grace].to_f}s); check your start_command, or tune the start_grace " \ 'value (eye expect process to create pid_file in self-daemonization mode)' { error: :pid_not_found } when :not_running error "exit status #{res[:exitstatus]}, process <#{@last_loaded_pid}> (from #{self[:pid_file_ex]}) was not found; " \ "ensure that the pid_file is being updated correctly (#{check_logs_str})" { error: :not_really_running } end end def check_logs_str if !self[:stdout] && !self[:stderr] 'you may want to configure stdout/err/all logs for this process' else "you should check the process logs #{[self[:stdout], self[:stderr]]}" end end def prepare_command(command) if self.pid command.to_s.gsub('{{PID}}', self.pid.to_s).gsub('{PID}', self.pid.to_s) else command end end def sleep_grace(grace_name) grace = self[grace_name].to_f info "sleeping for :#{grace_name} #{grace}" sleep grace end def execute_user_command(name, cmd) return unless check_identity info "executing user command #{name} #{cmd.inspect}" # cmd is string, or array of signals if cmd.is_a?(String) cmd = prepare_command(cmd) res = execute(cmd, config.merge(timeout: 120)) error "cmd #{cmd} error #{res.inspect}" if res[:error] elsif cmd.is_a?(Array) signals = cmd.clone signal = signals.shift send_signal(signal) while signals.present? delay = signals.shift signal = signals.shift if wait_for_condition(delay.to_f, 0.3) { !process_really_running? } info 'has terminated' break end send_signal(signal) if signal end else warn "unknown user command #{c}" end end end