1: <?php
2:
3: App::uses('Scheduling', 'Scheduling.Lib');
4:
5: class CheckAndRunShell extends Shell {
6:
7: const SCHEDULING_STATE_RUN = 'run';
8: const SCHEDULING_STATE_PASS = 'pass';
9: const SCHEDULING_STATE_SET_NEXT_RUN = 'set_next_run';
10: const SCHEDULING_STATE_TIMEOUT = 'timeout';
11: const PROCESS_TIMEOUT = 3600;
12:
13: public $uses = array(
14: 'Scheduling.SchedulingShellCallLog',
15: );
16:
17: public function getOptionParser() {
18: $parser = parent::getOptionParser();
19: $parser->addOption('ignore-scheduling', array(
20: 'boolean' => true
21: ));
22: return $parser;
23: }
24:
25: public function main() {
26: foreach (Scheduling::shellCalls() as $shellCall) {
27: $this->_checkShellCall($shellCall);
28: }
29: }
30:
31: private function _checkShellCall($shellCall) {
32: $this->out('<info>Shell call: </info>' . $this->_shellCallToString($shellCall));
33: $schedulingShellCallLog = Scheduling::findLog($shellCall);
34: $this->out(" * Next run: " . (empty($schedulingShellCallLog['SchedulingShellCallLog']['next_run']) ? 'Not set' : $schedulingShellCallLog['SchedulingShellCallLog']['next_run']));
35: $currentTimestamp = time();
36: $this->out(" * Current: " . date('Y-m-d H:i:s', $currentTimestamp));
37: switch ($this->_schedulingState($schedulingShellCallLog, $currentTimestamp)) {
38: case self::SCHEDULING_STATE_RUN:
39: $this->_runShellCall($schedulingShellCallLog);
40: break;
41:
42: case self::SCHEDULING_STATE_TIMEOUT:
43: $this->_killProcess($schedulingShellCallLog);
44: $this->_runShellCall($schedulingShellCallLog);
45: break;
46:
47: case self::SCHEDULING_STATE_SET_NEXT_RUN:
48: $this->out(' * Setting next run...');
49: Scheduling::setNextRun($schedulingShellCallLog['SchedulingShellCallLog']);
50: break;
51:
52: case self::SCHEDULING_STATE_PASS:
53: $this->out(' * Pass');
54: break;
55: }
56: $this->out(' * Done');
57: }
58:
59: private function _schedulingState($schedulingShellCallLog, $currentTimestamp) {
60: if (!$schedulingShellCallLog['SchedulingShellCallLog']['next_run']) {
61: return self::SCHEDULING_STATE_SET_NEXT_RUN;
62: }
63: $nextRun = strtotime($schedulingShellCallLog['SchedulingShellCallLog']['next_run']);
64: if ($nextRun <= $currentTimestamp || $this->params['ignore-scheduling']) {
65: $currentPid = $schedulingShellCallLog['SchedulingShellCallLog']['current_pid'];
66: if ($currentPid) {
67: $processAge = $this->_currentProcessLifeTime($currentPid);
68: if ($processAge === false) {
69: return self::SCHEDULING_STATE_RUN;
70: } else if ($processAge > self::PROCESS_TIMEOUT) {
71: return self::SCHEDULING_STATE_TIMEOUT;
72: } else {
73: return self::SCHEDULING_STATE_PASS;
74: }
75: }
76: return self::SCHEDULING_STATE_RUN;
77: }
78: return self::SCHEDULING_STATE_PASS;
79: }
80:
81: private function _runShellCall($schedulingShellCallLog) {
82: $this->out(' * Running...');
83: $command = self::_shellCallCommand($schedulingShellCallLog);
84: $this->out(" * Command: $command");
85: $pid = $this->_execInBackground($command);
86: $this->out(" * PID: $pid");
87: $this->out(" * Storing process id...");
88: $this->_storeProcessId($schedulingShellCallLog, $pid);
89: }
90:
91: private function _storeProcessId($schedulingShellCallLog, $pid) {
92: ClassRegistry::init('SchedulingShellCallLog')->saveOrThrowException(array(
93: 'SchedulingShellCallLog' => array(
94: 'id' => $schedulingShellCallLog['SchedulingShellCallLog']['id'],
95: 'current_pid' => $pid
96: )
97: )
98: );
99: }
100:
101: private function _currentProcessLifeTime($pid) {
102: if (substr(php_uname(), 0, 7) == "Windows") {
103: return $this->_currentProcessLifeTimeWindows($pid);
104: } else {
105: return $this->_currentProcessLifeTimeDefault($pid);
106: }
107: }
108:
109: private function _currentProcessLifeTimeDefault($pid) {
110: exec("ps -p $pid -o lstart=", $output);
111: if (empty($output)) {
112: return false;
113: } else {
114: return time() - strtotime($output[0]);
115: }
116: }
117:
118: private function _killProcess($schedulingShellCallLog) {
119: $this->out(' * Killing process ' . $schedulingShellCallLog['SchedulingShellCallLog']['current_pid'] . '...');
120: system('kill -9 ' . $schedulingShellCallLog['SchedulingShellCallLog']['current_pid']);
121: }
122:
123: private function _execInBackground($command) {
124: if (substr(php_uname(), 0, 7) == "Windows") {
125: return $this->_execInBackgroundWindows($command);
126: } else {
127: return $this->_execInBackgroundDefault($command);
128: }
129: }
130:
131: private function _execInBackgroundDefault($command) {
132: $descriptorspec = array(
133: 0 => array("pipe", "r"),
134: 1 => array("file", "/dev/null", "a"),
135: 2 => array("file", "/dev/null", "a")
136: );
137: $pipes = array();
138: $process = proc_open($command, $descriptorspec, $pipes);
139: if ($process) {
140: $info = proc_get_status($process);
141: return $info['pid'];
142: } else {
143: throw new Exception("Process was not created");
144: }
145: }
146:
147: private function _execInBackgroundWindows($command) {
148: throw new Exception("Not yet implemented: " . __METHOD__);
149:
150: }
151:
152: private function _shellCallCommand($schedulingShellCallLog) {
153: return $this->_consolePath() . ' Scheduling.run_shell_call ' .
154: $schedulingShellCallLog['SchedulingShellCallLog']['id'];
155: }
156:
157: private function _consolePath() {
158: return APP . 'Console' . DS . 'cake';
159: }
160:
161: private function _shellCallToString($shellCall) {
162: return trim($shellCall['scheduling'] . ' ' . $shellCall['shell'] . ' ' . Scheduling::serializeArgs($shellCall['args']));
163: }
164:
165: }
166: