3 if ( php_sapi_name() !== 'cli' ) {
4 die( "This is not a valid entry point.\n" );
6 error_reporting( E_ALL
);
10 LockServerDaemon
::init(
12 'address:', 'port:', 'authKey:',
13 'connTimeout::', 'lockTimeout::', 'maxClients::', 'maxBacklog::', 'maxLocks::',
18 * Simple lock server daemon that accepts lock/unlock requests.
19 * This should not require MediaWiki setup or PHP files.
21 class LockServerDaemon
{
23 protected $sock; // socket to listen/accept on
25 protected $shLocks = array(); // (key => session => 1)
27 protected $exLocks = array(); // (key => session)
29 protected $sessions = array(); // (session => resource)
31 protected $deadSessions = array(); // (session => UNIX timestamp)
34 protected $sessionIndexSh = array(); // (session => key => 1)
36 protected $sessionIndexEx = array(); // (session => key => 1)
38 protected $address; // string (IP/hostname)
39 protected $port; // integer
40 protected $authKey; // string key
41 protected $connTimeout; // array ( 'sec' => integer, 'usec' => integer )
42 protected $lockTimeout; // integer number of seconds
43 protected $maxLocks; // integer
44 protected $maxClients; // integer
45 protected $maxBacklog; // integer
47 protected $startTime; // integer UNIX timestamp
48 protected $lockCount = 0; // integer
49 protected $ticks = 0; // integer counter
51 protected static $instance = null;
54 * @params $config Array
55 * @return LockServerDaemon
57 public static function init( array $config ) {
58 if ( self
::$instance ) {
59 throw new Exception( 'LockServer already initialized.' );
61 foreach ( array( 'address', 'port', 'authKey' ) as $par ) {
62 if ( !isset( $config[$par] ) ) {
63 die( "Usage: php LockServerDaemon.php " .
64 "--address <address> --port <port> --authkey <key> " .
65 "[--connTimeout <seconds>] [--lockTimeout <seconds>] " .
66 "[--maxLocks <integer>] [--maxClients <integer>] [--maxBacklog <integer>]"
70 self
::$instance = new self( $config );
71 return self
::$instance;
75 * @params $config Array
77 protected function __construct( array $config ) {
78 // Required parameters...
79 $this->address
= $config['address'];
80 $this->port
= $config['port'];
81 $this->authKey
= $config['authKey'];
82 // Parameters with defaults...
83 $connTimeout = isset( $config['connTimeout'] )
84 ?
$config['connTimeout']
86 $this->connTimeout
= array(
87 'sec' => floor( $connTimeout ),
88 'usec' => floor( ( $connTimeout - floor( $connTimeout ) ) * 1e6
)
90 $this->lockTimeout
= isset( $config['lockTimeout'] )
91 ?
$config['lockTimeout']
93 $this->maxLocks
= isset( $config['maxLocks'] )
96 $this->maxClients
= isset( $config['maxClients'] )
97 ?
$config['maxClients']
98 : 1000; // less than default FD_SETSIZE
99 $this->maxBacklog
= isset( $config['maxBacklog'] )
100 ?
$config['maxBacklog']
104 protected function setupSocket() {
105 if ( !function_exists( 'socket_create' ) ) {
106 throw new Exception( "PHP sockets extension missing from PHP CLI mode." );
108 $sock = socket_create( AF_INET
, SOCK_STREAM
, SOL_TCP
);
109 if ( $sock === false ) {
110 throw new Exception( "socket_create(): " . socket_strerror( socket_last_error() ) );
112 socket_set_option( $sock, SOL_SOCKET
, SO_REUSEADDR
, 1 ); // bypass 2MLS
113 if ( socket_bind( $sock, $this->address
, $this->port
) === false ) {
114 throw new Exception( "socket_bind(): " .
115 socket_strerror( socket_last_error( $sock ) ) );
116 } elseif ( socket_listen( $sock, $this->maxBacklog
) === false ) {
117 throw new Exception( "socket_listen(): " .
118 socket_strerror( socket_last_error( $sock ) ) );
122 $this->startTime
= time();
125 public function main() {
126 // Setup socket and start listing
127 $this->setupSocket();
128 // Create a list of all the clients that will be connected to us.
129 $clients = array( $this->sock
); // start off with listening socket
131 // Create a copy, so $clients doesn't get modified by socket_select()
132 $read = $clients; // clients-with-data
133 // Get a list of all the clients that have data to be read from
134 $changed = socket_select( $read, $write = NULL, $except = NULL, NULL );
135 if ( $changed === false ) {
136 trigger_error( 'socket_listen(): ' . socket_strerror( socket_last_error() ) );
138 } elseif ( $changed < 1 ) {
141 // Check if there is a client trying to connect...
142 if ( in_array( $this->sock
, $read ) && count( $clients ) < $this->maxClients
) {
143 // Accept the new client...
144 $newsock = socket_accept( $this->sock
);
145 socket_set_option( $newsock, SOL_SOCKET
, SO_RCVTIMEO
, $this->connTimeout
);
146 socket_set_option( $newsock, SOL_SOCKET
, SO_SNDTIMEO
, $this->connTimeout
);
147 $clients[] = $newsock;
148 // Remove the listening socket from the clients-with-data array...
149 $key = array_search( $this->sock
, $read );
150 unset( $read[$key] );
152 // Loop through all the clients that have data to read...
153 foreach ( $read as $read_sock ) {
154 // Read until newline or 65535 bytes are recieved.
155 // socket_read show errors when the client is disconnected.
156 $data = @socket_read
( $read_sock, 65535, PHP_NORMAL_READ
);
157 // Check if the client is disconnected
158 if ( $data === false ) {
159 // Remove client from $clients list
160 $key = array_search( $read_sock, $clients );
161 unset( $clients[$key] );
162 // Remove socket's session from tracking (if it exists)
163 $session = array_search( $read_sock, $this->sessions
);
164 if ( $session !== false ) {
165 unset( $this->sessions
[$session] );
166 // Record recently killed sessions that still have locks
167 if ( isset( $this->sessionIndexSh
[$session] )
168 ||
isset( $this->sessionIndexEx
[$session] ) )
170 $this->deadSessions
[$session] = time();
174 // Perform the requested command...
175 $response = $this->doCommand( trim( $data ), $read_sock );
176 // Send the response to the client...
177 if ( socket_write( $read_sock, "$response\n" ) === false ) {
178 trigger_error( 'socket_write(): ' .
179 socket_strerror( socket_last_error( $read_sock ) ) );
183 // Prune dead locks every 10 socket events...
184 if ( ++
$this->ticks
>= 9 ) {
186 $this->purgeExpiredLocks();
192 * @param $data string
193 * @param $sourceSock resource
196 protected function doCommand( $data, $sourceSock ) {
197 $cmdArr = $this->getCommand( $data );
198 if ( is_string( $cmdArr ) ) {
199 return $cmdArr; // error
201 list( $function, $session, $type, $resources ) = $cmdArr;
202 // On first command, track the session => sock correspondence
203 if ( !isset( $this->sessions
[$session] ) ) {
204 $this->sessions
[$session] = $sourceSock;
206 if ( $function === 'ACQUIRE' ) {
207 return $this->lock( $session, $type, $resources );
208 } elseif ( $function === 'RELEASE' ) {
209 return $this->unlock( $session, $type, $resources );
210 } elseif ( $function === 'RELEASE_ALL' ) {
211 return $this->release( $session );
212 } elseif ( $function === 'STAT' ) {
213 return $this->stat();
215 return 'INTERNAL_ERROR';
219 * @param $data string
222 protected function getCommand( $data ) {
223 $m = explode( ':', $data ); // <session, key, command, type, values>
224 if ( count( $m ) == 5 ) {
225 list( $session, $key, $command, $type, $values ) = $m;
226 if ( sha1( $session . $command . $type . $values . $this->authKey
) !== $key ) {
228 } elseif ( strlen( $session ) !== 31 ) {
229 return 'BAD_SESSION';
231 $values = explode( '|', $values );
232 if ( $command === 'ACQUIRE' ) {
233 $needsLockArgs = true;
234 } elseif ( $command === 'RELEASE' ) {
235 $needsLockArgs = true;
236 } elseif ( $command === 'RELEASE_ALL' ) {
237 $needsLockArgs = false;
238 } elseif ( $command === 'STAT' ) {
239 $needsLockArgs = false;
241 return 'BAD_COMMAND';
243 if ( $needsLockArgs ) {
244 if ( $type !== 'SH' && $type !== 'EX' ) {
247 foreach ( $values as $value ) {
248 if ( strlen( $value ) !== 31 ) {
253 return array( $command, $session, $type, $values );
259 * @param $session string
260 * @param $type string
264 protected function lock( $session, $type, $keys ) {
265 if ( $this->lockCount
>= $this->maxLocks
) {
266 return 'TOO_MANY_LOCKS';
268 if ( $type === 'SH' ) {
269 // Check if any keys are already write-locked...
270 foreach ( $keys as $key ) {
271 if ( isset( $this->exLocks
[$key] ) && $this->exLocks
[$key] !== $session ) {
272 return 'CANT_ACQUIRE';
275 // Acquire the read-locks...
276 foreach ( $keys as $key ) {
277 $this->set_sh_lock( $key, $session );
280 } elseif ( $type === 'EX' ) {
281 // Check if any keys are already read-locked or write-locked...
282 foreach ( $keys as $key ) {
283 if ( isset( $this->exLocks
[$key] ) && $this->exLocks
[$key] !== $session ) {
284 return 'CANT_ACQUIRE';
286 if ( isset( $this->shLocks
[$key] ) ) {
287 foreach ( $this->shLocks
[$key] as $otherSession => $x ) {
288 if ( $otherSession !== $session ) {
289 return 'CANT_ACQUIRE';
294 // Acquire the write-locks...
295 foreach ( $keys as $key ) {
296 $this->set_ex_lock( $key, $session );
300 return 'INTERNAL_ERROR';
304 * @param $session string
305 * @param $type string
309 protected function unlock( $session, $type, $keys ) {
310 if ( $type === 'SH' ) {
311 foreach ( $keys as $key ) {
312 $this->unset_sh_lock( $key, $session );
315 } elseif ( $type === 'EX' ) {
316 foreach ( $keys as $key ) {
317 $this->unset_ex_lock( $key, $session );
321 return 'INTERNAL_ERROR';
325 * @param $session string
328 protected function release( $session ) {
329 if ( isset( $this->sessionIndexSh
[$session] ) ) {
330 foreach ( $this->sessionIndexSh
[$session] as $key => $x ) {
331 $this->unset_sh_lock( $key, $session );
334 if ( isset( $this->sessionIndexEx
[$session] ) ) {
335 foreach ( $this->sessionIndexEx
[$session] as $key => $x ) {
336 $this->unset_ex_lock( $key, $session );
339 return 'RELEASED_ALL';
345 protected function stat() {
346 return ( time() - $this->startTime
) . ':' . memory_get_usage();
350 * Clear locks for sessions that have been dead for a while
352 protected function purgeExpiredLocks() {
354 foreach ( $this->deadSessions
as $session => $timestamp ) {
355 if ( ( $now - $timestamp ) > $this->lockTimeout
) {
356 $this->release( $session );
357 unset( $this->deadSessions
[$session] );
364 * @param $session string
366 protected function set_sh_lock( $key, $session ) {
367 if ( !isset( $this->shLocks
[$key][$session] ) ) {
368 $this->shLocks
[$key][$session] = 1;
369 $this->sessionIndexSh
[$session][$key] = 1;
370 ++
$this->lockCount
; // we are adding a lock
376 * @param $session string
378 protected function set_ex_lock( $key, $session ) {
379 if ( !isset( $this->exLocks
[$key][$session] ) ) {
380 $this->exLocks
[$key] = $session;
381 $this->sessionIndexEx
[$session][$key] = 1;
382 ++
$this->lockCount
; // we are adding a lock
388 * @param $session string
390 protected function unset_sh_lock( $key, $session ) {
391 if ( isset( $this->shLocks
[$key][$session] ) ) {
392 unset( $this->shLocks
[$key][$session] );
393 if ( !count( $this->shLocks
[$key] ) ) {
394 unset( $this->shLocks
[$key] );
396 unset( $this->sessionIndexSh
[$session][$key] );
397 if ( !count( $this->sessionIndexSh
[$session] ) ) {
398 unset( $this->sessionIndexSh
[$session] );
406 * @param $session string
408 protected function unset_ex_lock( $key, $session ) {
409 if ( isset( $this->exLocks
[$key] ) && $this->exLocks
[$key] === $session ) {
410 unset( $this->exLocks
[$key] );
411 unset( $this->sessionIndexEx
[$session][$key] );
412 if ( !count( $this->sessionIndexEx
[$session] ) ) {
413 unset( $this->sessionIndexEx
[$session] );