|
7 | 7 | define('MSG_TIMEOUT', ZM_WEB_AJAX_TIMEOUT/2); |
8 | 8 | define('MSG_DATA_SIZE', 4+256); |
9 | 9 |
|
| 10 | +/* Failure classes reported to the client as 'reason'. The client needs to |
| 11 | + * tell "zms is gone, start a new one" apart from "this particular exchange |
| 12 | + * failed", because restarting the stream replaces the connkey and makes any |
| 13 | + * still-running zms unreachable. |
| 14 | + * |
| 15 | + * no_socket - zms is not listening: it never started, or it has exited. |
| 16 | + * The only class where restarting the stream is the right answer. |
| 17 | + * timeout - the command went out but no reply came back in time. zms is |
| 18 | + * most likely alive and busy. |
| 19 | + * transient - a failure local to this php process (socket setup, a short |
| 20 | + * read). Says nothing at all about zms. |
| 21 | + * invalid - bad request or an unexpected reply. Retrying will not help. |
| 22 | + */ |
| 23 | +define('STREAM_ERR_NO_SOCKET', 'no_socket'); |
| 24 | +define('STREAM_ERR_TIMEOUT', 'timeout'); |
| 25 | +define('STREAM_ERR_TRANSIENT', 'transient'); |
| 26 | +define('STREAM_ERR_INVALID', 'invalid'); |
| 27 | + |
10 | 28 | if ( !($_REQUEST['connkey'] && $_REQUEST['command']) ) { |
11 | | - ajaxError('No connkey or no command in stream ajax'); |
| 29 | + ajaxError('No connkey or no command in stream ajax', HTTP_STATUS_OK, STREAM_ERR_INVALID); |
12 | 30 | } |
13 | 31 |
|
14 | 32 | @mkdir(ZM_PATH_SOCKS); |
|
38 | 56 |
|
39 | 57 | if (!($socket = @socket_create(AF_UNIX, SOCK_DGRAM, 0))) { |
40 | 58 | if ($semaphore) sem_release($semaphore); |
41 | | - ajaxError('socket_create() failed: '.socket_strerror(socket_last_error())); |
| 59 | + ajaxError('socket_create() failed: '.socket_strerror(socket_last_error()), HTTP_STATUS_OK, STREAM_ERR_TRANSIENT); |
42 | 60 | } |
43 | 61 |
|
44 | 62 | $localSocketFile = ZM_PATH_SOCKS.'/zms-'.$connkey.'w.sock'; |
45 | 63 | if (!socket_bind($socket, $localSocketFile)) { |
46 | 64 | if ($semaphore) sem_release($semaphore); |
47 | | - ajaxError("socket_bind( $localSocketFile ) failed: ".socket_strerror(socket_last_error())); |
| 65 | + ajaxError("socket_bind( $localSocketFile ) failed: ".socket_strerror(socket_last_error()), HTTP_STATUS_OK, STREAM_ERR_TRANSIENT); |
48 | 66 | } |
49 | 67 |
|
50 | 68 | switch ($_REQUEST['command']) { |
|
99 | 117 |
|
100 | 118 | if (!file_exists($remSockFile)) { |
101 | 119 | if ($semaphore) sem_release($semaphore); |
102 | | - ajaxError("Socket $remSockFile does not exist. This file is created by zms, and since it does not exist, either zms did not run, or zms exited early. Please check your zms logs and ensure that CGI is enabled in apache and check that the PATH_ZMS is set correctly. Make sure that ZM is actually recording. If you are trying to view a live stream and the capture process (zmc) is not running then zms will exit. Please go to http://zoneminder.readthedocs.io/en/latest/faq.html#why-can-t-i-see-streamed-images-when-i-can-see-stills-in-the-zone-window-etc for more information."); |
| 120 | + ajaxError("Socket $remSockFile does not exist. This file is created by zms, and since it does not exist, either zms did not run, or zms exited early. Please check your zms logs and ensure that CGI is enabled in apache and check that the PATH_ZMS is set correctly. Make sure that ZM is actually recording. If you are trying to view a live stream and the capture process (zmc) is not running then zms will exit. Please go to http://zoneminder.readthedocs.io/en/latest/faq.html#why-can-t-i-see-streamed-images-when-i-can-see-stills-in-the-zone-window-etc for more information.", HTTP_STATUS_OK, STREAM_ERR_NO_SOCKET); |
103 | 121 | } else { |
104 | 122 | if (!@socket_sendto($socket, $msg, strlen($msg), 0, $remSockFile)) { |
105 | 123 | if ($semaphore) sem_release($semaphore); |
106 | | - ajaxError("socket_sendto( $remSockFile ) failed: ".socket_strerror(socket_last_error())); |
| 124 | + ajaxError("socket_sendto( $remSockFile ) failed: ".socket_strerror(socket_last_error()), HTTP_STATUS_OK, STREAM_ERR_NO_SOCKET); |
107 | 125 | } |
108 | 126 | } |
109 | 127 |
|
110 | 128 | $rSockets = array($socket); |
111 | 129 | $wSockets = NULL; |
112 | 130 | $eSockets = NULL; |
| 131 | +$select_timed_out = false; |
113 | 132 |
|
114 | 133 | $timeout = MSG_TIMEOUT - ( time() - $start_time ); |
115 | 134 |
|
116 | 135 | $numSockets = socket_select($rSockets, $wSockets, $eSockets, intval($timeout/1000), ($timeout%1000)*1000); |
117 | 136 |
|
118 | 137 | if ( $numSockets === false ) { |
119 | 138 | if ($semaphore) sem_release($semaphore); |
120 | | - ajaxError('socket_select failed: '.socket_strerror(socket_last_error())); |
| 139 | + ajaxError('socket_select failed: '.socket_strerror(socket_last_error()), HTTP_STATUS_OK, STREAM_ERR_TRANSIENT); |
121 | 140 | } else if ( $numSockets < 0 ) { |
122 | 141 | if ($semaphore) sem_release($semaphore); |
123 | | - ajaxError("Socket closed $remSockFile"); |
| 142 | + ajaxError("Socket closed $remSockFile", HTTP_STATUS_OK, STREAM_ERR_NO_SOCKET); |
124 | 143 | } else if ( $numSockets == 0 ) { |
125 | 144 | ZM\Error("Timed out waiting for msg $remSockFile after waiting $timeout milliseconds"); |
126 | 145 | socket_set_nonblock($socket); |
| 146 | + // Not an error on its own: the socket is now non-blocking, so the recvfrom |
| 147 | + // below returns immediately and reports this as a timeout rather than |
| 148 | + // pretending zms had nothing to say. |
| 149 | + $select_timed_out = true; |
127 | 150 | #ajaxError("Timed out waiting for msg $remSockFile"); |
128 | 151 | } else if ( $numSockets > 0 ) { |
129 | 152 | if ( count($rSockets) != 1 ) { |
130 | 153 | if ($semaphore) sem_release($semaphore); |
131 | | - ajaxError('Bogus return from select, '.count($rSockets).' sockets available'); |
| 154 | + ajaxError('Bogus return from select, '.count($rSockets).' sockets available', HTTP_STATUS_OK, STREAM_ERR_TRANSIENT); |
132 | 155 | } |
133 | 156 | } |
134 | 157 |
|
135 | 158 | $nbytes = @socket_recvfrom($socket, $msg, MSG_DATA_SIZE, 0, $remSockFile); |
136 | 159 | if ($semaphore) sem_release($semaphore); |
137 | 160 | switch ($nbytes) { |
138 | 161 | case -1 : |
139 | | - ajaxError("socket_recvfrom( $remSockFile ) failed: ".socket_strerror(socket_last_error())); |
| 162 | + ajaxError("socket_recvfrom( $remSockFile ) failed: ".socket_strerror(socket_last_error()), HTTP_STATUS_OK, STREAM_ERR_TRANSIENT); |
140 | 163 | break; |
141 | 164 | case 0 : |
142 | | - ajaxError('No data to read from socket'); |
| 165 | + // socket_recvfrom() returns false on error, and false == 0 under switch's |
| 166 | + // loose comparison, so a timed-out select lands here rather than on -1. |
| 167 | + if ($select_timed_out) { |
| 168 | + ajaxError("Timed out waiting for msg $remSockFile after waiting $timeout milliseconds", |
| 169 | + HTTP_STATUS_OK, STREAM_ERR_TIMEOUT); |
| 170 | + } |
| 171 | + ajaxError('No data to read from socket', HTTP_STATUS_OK, STREAM_ERR_TRANSIENT); |
143 | 172 | break; |
144 | 173 | default : |
145 | 174 | if ( $nbytes != MSG_DATA_SIZE ) { |
146 | | - ajaxError("Got unexpected message size, got $nbytes, expected ".MSG_DATA_SIZE); |
| 175 | + ajaxError("Got unexpected message size, got $nbytes, expected ".MSG_DATA_SIZE, HTTP_STATUS_OK, STREAM_ERR_TRANSIENT); |
147 | 176 | } |
148 | 177 | break; |
149 | 178 | } |
|
197 | 226 | ajaxResponse(array('status'=>$data)); |
198 | 227 | break; |
199 | 228 | default : |
200 | | - ajaxError('Unexpected received message type '.$data['type']); |
| 229 | + ajaxError('Unexpected received message type '.$data['type'], HTTP_STATUS_OK, STREAM_ERR_INVALID); |
201 | 230 | } |
202 | | -ajaxError('Unrecognised action or insufficient permissions in ajax/stream'); |
| 231 | +ajaxError('Unrecognised action or insufficient permissions in ajax/stream', HTTP_STATUS_OK, STREAM_ERR_INVALID); |
203 | 232 |
|
204 | 233 | function ajaxCleanup() { |
205 | 234 | global $socket, $localSocketFile; |
|
0 commit comments