├── README.md └── phprocksyd ├── Phprocksyd.php ├── Simple.php └── php-fd.patch /README.md: -------------------------------------------------------------------------------- 1 | # habr 2 | Materials for habrahabr articles 3 | -------------------------------------------------------------------------------- /phprocksyd/Phprocksyd.php: -------------------------------------------------------------------------------- 1 | stream) */ 48 | private $streams = []; 49 | /** @var string[] (client_id => read buffer) */ 50 | private $read_buf = []; 51 | 52 | /** @var string[] (client_id => write buffer) */ 53 | private $write_buf = []; 54 | /** @var resource[] (client_id => stream from which to read) */ 55 | private $read = []; 56 | /** @var resource[] (client_id => stream where to write) */ 57 | private $write = []; 58 | 59 | /** @var array (pid => hash) */ 60 | private $pid_to_hash = []; 61 | /** @var ScriptResult[] (hash => ScriptResult) */ 62 | private $hash_info; 63 | 64 | /** @var int Total connection count */ 65 | private $conn_count = 0; 66 | 67 | /** @var array we need to be able to close all "extra" file descriptors before exec so that we do not leak anything */ 68 | private $known_fds = [0 => true, 1 => true, 2 => true]; 69 | 70 | public function init() 71 | { 72 | foreach (['posix', 'pcntl'] as $ext) { 73 | if (!extension_loaded($ext) && !dl($ext . '.so')) { 74 | fwrite(STDERR, "Could not load $ext.so\n"); 75 | exit(1); 76 | } 77 | } 78 | 79 | $pid = pcntl_fork(); 80 | if ($pid < 0) { 81 | fwrite(STDERR, "Could not fork\n"); 82 | exit(1); 83 | } 84 | 85 | if ($pid == 0) { 86 | usleep(1000); 87 | exit(0); 88 | } 89 | 90 | $metadata = stream_get_meta_data(STDIN); 91 | if (!$metadata) { 92 | fwrite(STDERR, "stream_get_metadata broken: false returned for STDIN\n"); 93 | exit(1); 94 | } 95 | 96 | if (!isset($metadata['fd'])) { 97 | fwrite(STDERR, "stream_get_metadata broken: fd not present for STDIN\n"); 98 | exit(1); 99 | } 100 | } 101 | 102 | public function run() 103 | { 104 | $this->loadRestartFile(); 105 | $this->listen(); 106 | 107 | echo "Entering main loop\n"; 108 | $this->mainLoop(); 109 | return true; 110 | } 111 | 112 | protected function listen() 113 | { 114 | $ctx = stream_context_create(); 115 | $res = stream_context_set_option($ctx, 'socket', 'backlog', self::SOCKET_BACKLOG); 116 | if (!$res) { 117 | fwrite(STDERR, "Could not set backlog option on context\n"); 118 | exit(1); 119 | } 120 | 121 | $port = self::PORT; 122 | $ip_port = "0.0.0.0:$port"; 123 | $address = "tcp://$ip_port"; 124 | 125 | if (!isset($this->read[self::SERVER_KEY])) { 126 | $server = stream_socket_server($address, $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx); 127 | if (!$server) { 128 | fwrite(STDERR, "stream_socket_server failed: $errno $errstr\n"); 129 | exit(1); 130 | } 131 | 132 | $this->read[self::SERVER_KEY] = $server; 133 | echo "Listening on $address\n"; 134 | } else { 135 | echo "Reusing listen socket for $address\n"; 136 | } 137 | } 138 | 139 | protected function command($stream_id, $msg_name, $res, $res_json) 140 | { 141 | echo "stream$stream_id " . $msg_name . " " . $res_json . "\n"; 142 | 143 | switch ($msg_name) { 144 | case 'request_check': 145 | $this->requestCheck($stream_id, $res); 146 | break; 147 | 148 | case 'request_free': 149 | $this->requestFree($stream_id, $res); 150 | break; 151 | 152 | case 'request_terminate': 153 | $this->requestTerminate($stream_id, $res); 154 | break; 155 | 156 | case 'request_run': 157 | $this->requestRun($stream_id, $res); 158 | break; 159 | 160 | case 'request_restart': 161 | $this->requestRestart($stream_id, $res); 162 | break; 163 | 164 | case 'request_stop': 165 | $this->requestStop($stream_id, $res); 166 | break; 167 | 168 | case 'request_bye': 169 | $this->requestBye($stream_id, $res); 170 | break; 171 | 172 | default: 173 | $msg = "UNKNOWN REQUEST '$msg_name': $res_json"; 174 | echo $msg . "\n"; 175 | $this->generic($stream_id, $msg); 176 | return; 177 | } 178 | } 179 | 180 | public function response($stream_id, $response) 181 | { 182 | $json_resp = json_encode($response); 183 | echo "stream$stream_id " . $json_resp . "\n"; 184 | $this->write($stream_id, $json_resp . "\n"); 185 | } 186 | 187 | public function write($stream_id, $buf) 188 | { 189 | $this->write_buf[$stream_id] .= $buf; 190 | 191 | if (!isset($this->write[$stream_id])) { 192 | $this->write[$stream_id] = $this->streams[$stream_id]; 193 | } 194 | } 195 | 196 | public function accept($server) 197 | { 198 | echo "Accepting new connection\n"; 199 | 200 | $client = stream_socket_accept($server, self::ACCEPT_TIMEOUT, $peername); 201 | $stream_id = ($this->conn_count++); 202 | if (!$client) { 203 | fwrite(STDERR, "Accept failed\n"); 204 | return; 205 | } 206 | 207 | stream_set_read_buffer($client, 0); 208 | stream_set_write_buffer($client, 0); 209 | stream_set_blocking($client, 0); 210 | stream_set_timeout($client, self::CONN_TIMEOUT); 211 | 212 | $this->read_buf[$stream_id] = ''; 213 | $this->write_buf[$stream_id] = ''; 214 | $this->read[$stream_id] = $this->streams[$stream_id] = $client; 215 | 216 | echo "Connected stream$stream_id: $peername\n"; 217 | } 218 | 219 | private function disconnect($stream_id) 220 | { 221 | echo "Disconnect stream$stream_id\n"; 222 | unset($this->read_buf[$stream_id], $this->write_buf[$stream_id]); 223 | unset($this->streams[$stream_id]); 224 | unset($this->write[$stream_id], $this->read[$stream_id]); 225 | } 226 | 227 | private function cleanChildren() 228 | { 229 | while (true) { 230 | $status = null; 231 | $pid = pcntl_wait($status, WNOHANG); 232 | if ($pid <= 0) { 233 | return; 234 | } 235 | 236 | if (isset($this->pid_to_hash[$pid])) { 237 | $hash = $this->pid_to_hash[$pid]; 238 | unset($this->pid_to_hash[$pid]); 239 | 240 | if ($Res = $this->hash_info[$hash]) { 241 | $Res->retcode = pcntl_wexitstatus($status); 242 | echo "pid $pid (hash $hash) wexit = " . $Res->retcode . "\n"; 243 | } 244 | } 245 | } 246 | } 247 | 248 | private function handleRead($stream_id) 249 | { 250 | $buf = fread($this->streams[$stream_id], 8192); 251 | if ($buf === false || $buf === '') { 252 | echo "got EOF from stream$stream_id\n"; 253 | if (empty($this->write_buf[$stream_id])) { 254 | $this->disconnect($stream_id); 255 | } else { 256 | unset($this->read[$stream_id]); 257 | } 258 | return; 259 | } 260 | 261 | $this->read_buf[$stream_id] .= $buf; 262 | $this->processJSONRequests($stream_id); 263 | } 264 | 265 | private function processJSONRequests($stream_id) 266 | { 267 | if (!strpos($this->read_buf[$stream_id], "\n")) return; 268 | $requests = explode("\n", $this->read_buf[$stream_id]); 269 | $this->read_buf[$stream_id] = array_pop($requests); 270 | 271 | foreach ($requests as $req) { 272 | $req = rtrim($req); 273 | $parts = explode(" ", $req, 2); 274 | if (count($parts) != 2) { 275 | $parts[1] = '{}'; 276 | } 277 | list($msg_name_start, $msg) = $parts; 278 | $msg_name = 'request_' . $msg_name_start; 279 | $res = json_decode($msg, true); 280 | 281 | if ($res !== false) { 282 | $this->command($stream_id, $msg_name, $res, $msg); 283 | } else { 284 | $this->generic($stream_id, 'Invalid JSON'); 285 | } 286 | } 287 | } 288 | 289 | private function handleWrite($stream_id) 290 | { 291 | if (!isset($this->write_buf[$stream_id])) { 292 | return; 293 | } 294 | 295 | $wrote = fwrite($this->streams[$stream_id], substr($this->write_buf[$stream_id], 0, 65536)); 296 | if ($wrote === false) { 297 | fwrite(STDERR, "write failed into stream #$stream_id\n"); 298 | $this->disconnect($stream_id); 299 | return; 300 | } 301 | 302 | if ($wrote === strlen($this->write_buf[$stream_id])) { 303 | $this->write_buf[$stream_id] = ''; 304 | unset($this->write[$stream_id]); 305 | if (empty($this->read[$stream_id])) { 306 | $this->disconnect($stream_id); 307 | } 308 | } else { 309 | $this->write_buf[$stream_id] = substr($this->write_buf[$stream_id], $wrote); 310 | } 311 | } 312 | 313 | public function mainLoop() 314 | { 315 | while (true) { 316 | $read = $this->read; 317 | $write = $this->write; 318 | $except = null; 319 | 320 | echo "Selecting for " . count($read) . " reads, " . count($write) . " writes\n"; 321 | $n = stream_select($read, $write, $except, NULL); 322 | 323 | if (!$n) { 324 | fwrite(STDERR, "Could not stream_select()\n"); 325 | exit(1); 326 | } 327 | 328 | if (count($read)) { 329 | echo "Can read from " . count($read) . " streams\n"; 330 | } 331 | 332 | if (count($write)) { 333 | echo "Can write to " . count($write) . " streams\n"; 334 | } 335 | 336 | $this->cleanChildren(); 337 | 338 | if (isset($read[self::SERVER_KEY])) { 339 | $this->accept($read[self::SERVER_KEY]); 340 | unset($read[self::SERVER_KEY]); 341 | } 342 | 343 | // get rid of references to connection resources so that connections do not leak to children 344 | $read_keys = array_keys($read); 345 | $write_keys = array_keys($write); 346 | unset($read, $write); 347 | 348 | foreach ($read_keys as $stream_id) { 349 | $this->handleRead($stream_id); 350 | } 351 | 352 | foreach ($write_keys as $stream_id) { 353 | $this->handleWrite($stream_id); 354 | } 355 | } 356 | } 357 | 358 | private function msg_length($buf) 359 | { 360 | return ord($buf[0]) << 24 | ord($buf[1]) << 16 | ord($buf[2]) << 8 | ord($buf[3]); 361 | } 362 | 363 | /** 364 | * @param $stream_id 365 | * @param $req 366 | */ 367 | private function requestFree($stream_id, $req) 368 | { 369 | $hash = $req['hash']; 370 | 371 | if (!isset($this->hash_info[$hash])) { 372 | $this->generic($stream_id, "Hash not found"); 373 | return; 374 | } 375 | 376 | $this->free($hash); 377 | $this->generic($stream_id, "OK"); 378 | } 379 | 380 | private function free($hash) 381 | { 382 | if (isset($this->hash_info[$hash])) { 383 | $Res = $this->hash_info[$hash]; 384 | unset($this->pid_to_hash[$Res->pid], $this->hash_info[$hash]); 385 | } 386 | } 387 | 388 | /** 389 | * @param $stream_id 390 | * @param $req 391 | */ 392 | private function requestCheck($stream_id, $req) 393 | { 394 | $status = null; 395 | $hash = $req['hash']; 396 | 397 | if (!is_numeric($hash)) { 398 | $this->generic($stream_id, "Hash is not numeric"); 399 | return; 400 | } 401 | 402 | if (!isset($this->hash_info[$hash])) { 403 | echo "hash = $hash miss\n"; 404 | $this->generic($stream_id, "Hash not found"); 405 | return; 406 | } 407 | 408 | $Result = $this->hash_info[$hash]; 409 | 410 | if (!isset($Result->retcode)) { 411 | echo "hash = $hash didn't exit\n"; 412 | $this->generic($stream_id, "Still running"); 413 | return; 414 | } 415 | 416 | $resp = ['retcode' => $Result->retcode]; 417 | $this->response($stream_id, $resp); 418 | } 419 | 420 | /** 421 | * @param $stream_id 422 | * @param $req 423 | */ 424 | protected function requestTerminate($stream_id, $req) 425 | { 426 | if (!isset($this->hash_info[$req['hash']])) { 427 | $this->generic($stream_id, "Hash not found"); 428 | return; 429 | } 430 | 431 | $pid = $this->hash_info[$req['hash']]->pid; 432 | 433 | if (!isset($this->pid_to_hash[$pid])) { 434 | $this->generic($stream_id, "Script already not running"); 435 | return; 436 | } 437 | 438 | $result = posix_kill($pid, SIGTERM); 439 | $this->generic($stream_id, $result ? "OK" : "Failed to kill"); 440 | } 441 | 442 | /** 443 | * @param $stream_id 444 | * @param $req 445 | * @throws \Exception 446 | */ 447 | protected function requestRun($stream_id, $req) 448 | { 449 | $hash = $req['hash']; 450 | if (!is_numeric($hash)) { 451 | $this->generic($stream_id, "hash is not numeric"); 452 | return; 453 | } 454 | 455 | $pid = pcntl_fork(); 456 | if ($pid == -1) { 457 | fwrite(STDERR, "Cannot fork\n"); 458 | $this->generic($stream_id, "Cannot fork"); 459 | return; 460 | } 461 | 462 | if ($pid == 0) { 463 | try { 464 | /* clean memory and close all parent connections */ 465 | $this->streams = null; 466 | $this->read = null; 467 | $this->write = null; 468 | 469 | $args = array_merge([$req['class']], $req['params']); 470 | $title = "php " . implode(" ", $args); 471 | cli_set_process_title($title); 472 | 473 | $seed = floor(explode(" ", microtime())[0] * 1e6); 474 | srand($seed); 475 | mt_srand($seed); 476 | 477 | $class = $req['class']; 478 | $instance = new $class; 479 | $instance->run($req['params']); 480 | } finally { 481 | exit(0); 482 | } 483 | } 484 | 485 | echo "hash " . $hash . " ran as pid $pid\n"; 486 | 487 | $res = new ScriptResult; 488 | $res->is_running = true; 489 | $res->pid = $pid; 490 | 491 | $this->hash_info[$hash] = $res; 492 | $this->pid_to_hash[$pid] = $hash; 493 | 494 | $this->generic($stream_id, "OK"); 495 | } 496 | 497 | protected function generic($stream_id, $error_text) 498 | { 499 | $params = ['error_text' => $error_text]; 500 | $this->response($stream_id, $params); 501 | } 502 | 503 | protected function requestRestart($stream_id) 504 | { 505 | echo "Restarting...\n"; 506 | $this->generic($stream_id, "Restarted successfully"); 507 | $this->restart(); 508 | } 509 | 510 | protected function requestBye($stream_id) 511 | { 512 | $this->disconnect($stream_id); 513 | } 514 | 515 | private function getFdRestartData() 516 | { 517 | $res = []; 518 | 519 | foreach (self::$restart_fd_resources as $prop) { 520 | $res[$prop] = []; 521 | foreach ($this->$prop as $k => $v) { 522 | $meta = stream_get_meta_data($v); 523 | if (!isset($meta['fd'])) { 524 | fwrite(STDERR, "No fd in stream metadata for resource $v (key $k in $prop), got " . var_export($meta, true) . "\n"); 525 | return false; 526 | } 527 | $res[$prop][$k] = $meta['fd']; 528 | $this->known_fds[$meta['fd']] = true; 529 | } 530 | } 531 | 532 | foreach (self::$restart_fd_props as $prop) { 533 | $res[$prop] = $this->$prop; 534 | } 535 | 536 | return $res; 537 | } 538 | 539 | private function loadFdRestartData($res) 540 | { 541 | $fd_resources = []; 542 | 543 | foreach (self::$restart_fd_resources as $prop) { 544 | if (!isset($res[$prop])) { 545 | fwrite(STDERR, "Property '$prop' is not present in restart fd resources\n"); 546 | continue; 547 | } 548 | 549 | $pp = []; 550 | foreach ($res[$prop] as $k => $v) { 551 | if (isset($fd_resources[$v])) { 552 | $pp[$k] = $fd_resources[$v]; 553 | } else { 554 | $fp = fopen("php://fd/" . $v, 'r+'); 555 | if (!$fp) { 556 | fwrite(STDERR, "Failed to open fd = $v, exiting\n"); 557 | exit(1); 558 | } 559 | 560 | stream_set_read_buffer($fp, 0); 561 | stream_set_write_buffer($fp, 0); 562 | stream_set_blocking($fp, 0); 563 | stream_set_timeout($fp, self::CONN_TIMEOUT); 564 | 565 | $fd_resources[$v] = $fp; 566 | $pp[$k] = $fp; 567 | } 568 | } 569 | $this->$prop = $pp; 570 | } 571 | 572 | foreach (self::$restart_fd_props as $prop) { 573 | if (!isset($res[$prop])) { 574 | fwrite(STDERR, "Property '$prop' is not present in restart fd properties\n"); 575 | continue; 576 | } 577 | 578 | $this->$prop = $res[$prop]; 579 | } 580 | } 581 | 582 | private function restart() 583 | { 584 | echo "Creating restart file...\n"; 585 | $res = []; 586 | foreach (self::$restart_props as $prop) $res[$prop] = $this->$prop; 587 | 588 | if (!$add_res = $this->getFdRestartData()) { 589 | fwrite(STDERR, "Could not get restart FD data, exiting, graceful restart is not supported\n"); 590 | exit(0); 591 | } 592 | 593 | $res += $add_res; 594 | 595 | /* Close all extra file descriptors that we do not know of, including opendir() descriptor :) */ 596 | $dh = opendir("/proc/self/fd"); 597 | $fds = []; 598 | while (false !== ($file = readdir($dh))) { 599 | if ($file[0] === '.') continue; 600 | $fds[] = $file; 601 | } 602 | 603 | foreach ($fds as $fd) { 604 | if (!isset($this->known_fds[$fd])) { 605 | fclose(fopen("php://fd/" . $fd, 'r+')); 606 | } 607 | } 608 | 609 | $contents = serialize($res); 610 | 611 | if (file_put_contents(self::RESTART_DIR . self::RESTART_FILENAME, $contents) !== strlen($contents)) { 612 | fwrite(STDERR, "Could not fully write restart file\n"); 613 | unlink(self::RESTART_DIR . self::RESTART_FILENAME); 614 | } 615 | 616 | echo "Doing exec()\n"; 617 | pcntl_exec("/usr/bin/env", ["php", __FILE__], $_ENV); 618 | fwrite(STDERR, "exec() failed\n"); 619 | exit(1); 620 | } 621 | 622 | private function loadRestartFile() 623 | { 624 | if (!file_exists(self::RESTART_DIR . self::RESTART_FILENAME)) { 625 | return; 626 | } 627 | 628 | echo "Restart file found, trying to adopt it\n"; 629 | 630 | $contents = file_get_contents(self::RESTART_DIR . self::RESTART_FILENAME); 631 | unlink(self::RESTART_DIR . self::RESTART_FILENAME); 632 | 633 | if ($contents === false) { 634 | fwrite(STDERR, "Could not read restart file\n"); 635 | return; 636 | } 637 | 638 | $res = unserialize($contents); 639 | if (!$res) { 640 | fwrite(STDERR, "Could not unserialize restart file contents"); 641 | return; 642 | } 643 | 644 | foreach (self::$restart_props as $prop) { 645 | if (!array_key_exists($prop, $res)) { 646 | fwrite(STDERR, "No property $prop in restart file\n"); 647 | continue; 648 | } 649 | $this->$prop = $res[$prop]; 650 | } 651 | 652 | $this->loadFdRestartData($res); 653 | 654 | // We could lose some children on the way, clean them up 655 | $this->cleanChildren(); 656 | 657 | $our_pid = getmypid(); 658 | $clean_pids = []; 659 | 660 | foreach ($this->pid_to_hash as $pid => $hash) { 661 | $filename = "/proc/$pid/stat"; 662 | if (!file_exists($filename) || !is_readable($filename) || !($contents = file_get_contents($filename))) { 663 | $clean_pids[] = $pid; 664 | continue; 665 | } 666 | 667 | $parts = explode(' ', $contents); 668 | $ppid = $parts[3]; 669 | if ($ppid != $our_pid) { 670 | $clean_pids[] = $pid; 671 | } 672 | } 673 | 674 | foreach ($clean_pids as $pid) { 675 | $hash = $this->pid_to_hash[$pid]; 676 | unset($this->pid_to_hash[$pid]); 677 | 678 | echo "Lost info for pid = $pid, hash = " . $hash . "\n"; 679 | } 680 | } 681 | 682 | private function requestStop($stream_id, $res) 683 | { 684 | echo "Requested to stop, exiting\n"; 685 | exit(0); 686 | } 687 | } 688 | 689 | $instance = new Phprocksyd(); 690 | $instance->init(); 691 | $instance->run(); 692 | -------------------------------------------------------------------------------- /phprocksyd/Simple.php: -------------------------------------------------------------------------------- 1 | stream) */ 8 | private $streams = []; 9 | /** @var string[] (client_id => read buffer) */ 10 | private $read_buf = []; 11 | 12 | /** @var string[] (client_id => write buffer) */ 13 | private $write_buf = []; 14 | /** @var resource[] (client_id => stream from which to read) */ 15 | private $read = []; 16 | /** @var resource[] (client_id => stream where to write) */ 17 | private $write = []; 18 | 19 | /** @var int Total connection count */ 20 | private $conn_count = 0; 21 | 22 | public function run() 23 | { 24 | $this->listen(); 25 | echo "Entering main loop\n"; 26 | $this->mainLoop(); 27 | } 28 | 29 | protected function listen() 30 | { 31 | $port = self::PORT; 32 | $ip_port = "0.0.0.0:$port"; 33 | $address = "tcp://$ip_port"; 34 | 35 | $server = stream_socket_server($address, $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN); 36 | if (!$server) { 37 | fwrite(STDERR, "stream_socket_server failed: $errno $errstr\n"); 38 | exit(1); 39 | } 40 | 41 | $this->read[self::SERVER_KEY] = $server; 42 | echo "Listening on $address\n"; 43 | } 44 | 45 | public function response($stream_id, $response) 46 | { 47 | $json_resp = json_encode($response); 48 | echo "stream$stream_id " . $json_resp . "\n"; 49 | $this->write($stream_id, $json_resp . "\n"); 50 | } 51 | 52 | public function write($stream_id, $buf) 53 | { 54 | $this->write_buf[$stream_id] .= $buf; 55 | 56 | if (!isset($this->write[$stream_id])) { 57 | $this->write[$stream_id] = $this->streams[$stream_id]; 58 | } 59 | } 60 | 61 | public function accept($server) 62 | { 63 | echo "Accepting new connection\n"; 64 | 65 | $client = stream_socket_accept($server, 1, $peername); 66 | $stream_id = ($this->conn_count++); 67 | if (!$client) { 68 | fwrite(STDERR, "Accept failed\n"); 69 | return; 70 | } 71 | 72 | stream_set_read_buffer($client, 0); 73 | stream_set_write_buffer($client, 0); 74 | stream_set_blocking($client, 0); 75 | stream_set_timeout($client, 1); 76 | 77 | $this->read_buf[$stream_id] = ''; 78 | $this->write_buf[$stream_id] = ''; 79 | $this->read[$stream_id] = $this->streams[$stream_id] = $client; 80 | 81 | echo "Connected stream$stream_id: $peername\n"; 82 | } 83 | 84 | private function disconnect($stream_id) 85 | { 86 | echo "Disconnect stream$stream_id\n"; 87 | unset($this->read_buf[$stream_id], $this->write_buf[$stream_id]); 88 | unset($this->streams[$stream_id]); 89 | unset($this->write[$stream_id], $this->read[$stream_id]); 90 | } 91 | 92 | private function handleRead($stream_id) 93 | { 94 | $buf = fread($this->streams[$stream_id], 8192); 95 | if ($buf === false || $buf === '') { 96 | echo "got EOF from stream$stream_id\n"; 97 | if (empty($this->write_buf[$stream_id])) { 98 | $this->disconnect($stream_id); 99 | } else { 100 | unset($this->read[$stream_id]); 101 | } 102 | return; 103 | } 104 | 105 | $this->read_buf[$stream_id] .= $buf; 106 | $this->processJSONRequests($stream_id); 107 | } 108 | 109 | private function processJSONRequests($stream_id) 110 | { 111 | if (!strpos($this->read_buf[$stream_id], "\n")) return; 112 | $requests = explode("\n", $this->read_buf[$stream_id]); 113 | $this->read_buf[$stream_id] = array_pop($requests); 114 | 115 | foreach ($requests as $req) { 116 | $res = json_decode(rtrim($req), true); 117 | 118 | if ($res !== false) { 119 | $this->response($stream_id, "Request had " . count($res) . " keys"); 120 | } else { 121 | $this->response($stream_id, "Invalid JSON"); 122 | } 123 | } 124 | } 125 | 126 | private function handleWrite($stream_id) 127 | { 128 | if (!isset($this->write_buf[$stream_id])) { 129 | return; 130 | } 131 | 132 | $wrote = fwrite($this->streams[$stream_id], substr($this->write_buf[$stream_id], 0, 65536)); 133 | if ($wrote === false) { 134 | fwrite(STDERR, "write failed into stream #$stream_id\n"); 135 | $this->disconnect($stream_id); 136 | return; 137 | } 138 | 139 | if ($wrote === strlen($this->write_buf[$stream_id])) { 140 | $this->write_buf[$stream_id] = ''; 141 | unset($this->write[$stream_id]); 142 | if (empty($this->read[$stream_id])) { 143 | $this->disconnect($stream_id); 144 | } 145 | } else { 146 | $this->write_buf[$stream_id] = substr($this->write_buf[$stream_id], $wrote); 147 | } 148 | } 149 | 150 | public function mainLoop() 151 | { 152 | while (true) { 153 | $read = $this->read; 154 | $write = $this->write; 155 | $except = null; 156 | 157 | echo "Selecting for " . count($read) . " reads, " . count($write) . " writes\n"; 158 | $n = stream_select($read, $write, $except, NULL); 159 | 160 | if (!$n) { 161 | fwrite(STDERR, "Could not stream_select()\n"); 162 | } 163 | 164 | if (count($read)) { 165 | echo "Can read from " . count($read) . " streams\n"; 166 | } 167 | 168 | if (count($write)) { 169 | echo "Can write to " . count($write) . " streams\n"; 170 | } 171 | 172 | if (isset($read[self::SERVER_KEY])) { 173 | $this->accept($read[self::SERVER_KEY]); 174 | unset($read[self::SERVER_KEY]); 175 | } 176 | 177 | foreach ($read as $stream_id => $_) { 178 | $this->handleRead($stream_id); 179 | } 180 | 181 | foreach ($write as $stream_id => $_) { 182 | $this->handleWrite($stream_id); 183 | } 184 | } 185 | } 186 | } 187 | 188 | $instance = new Simple(); 189 | $instance->run(); 190 | -------------------------------------------------------------------------------- /phprocksyd/php-fd.patch: -------------------------------------------------------------------------------- 1 | diff --git a/ext/standard/php_fopen_wrapper.c b/ext/standard/php_fopen_wrapper.c 2 | index f8d7bda..fee964c 100644 3 | --- a/ext/standard/php_fopen_wrapper.c 4 | +++ b/ext/standard/php_fopen_wrapper.c 5 | @@ -24,6 +24,7 @@ 6 | #if HAVE_UNISTD_H 7 | #include 8 | #endif 9 | +#include 10 | 11 | #include "php.h" 12 | #include "php_globals.h" 13 | @@ -296,11 +297,11 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, char *path, ch 14 | "The file descriptors must be non-negative numbers smaller than %d", dtablesize); 15 | return NULL; 16 | } 17 | - 18 | - fd = dup(fildes_ori); 19 | - if (fd == -1) { 20 | + 21 | + fd = fildes_ori; 22 | + if (fcntl(fildes_ori, F_GETFD) == -1) { 23 | php_stream_wrapper_log_error(wrapper, options TSRMLS_CC, 24 | - "Error duping file descriptor %ld; possibly it doesn't exist: " 25 | + "File descriptor %ld invalid: " 26 | "[%d]: %s", fildes_ori, errno, strerror(errno)); 27 | return NULL; 28 | } 29 | diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c 30 | index 0610ecf..14fd3b0 100644 31 | --- a/ext/standard/streamsfuncs.c 32 | +++ b/ext/standard/streamsfuncs.c 33 | @@ -24,6 +24,7 @@ 34 | #include "ext/standard/flock_compat.h" 35 | #include "ext/standard/file.h" 36 | #include "ext/standard/php_filestat.h" 37 | +#include "ext/standard/php_fopen_wrappers.h" 38 | #include "php_open_temporary_file.h" 39 | #include "ext/standard/basic_functions.h" 40 | #include "php_ini.h" 41 | @@ -484,6 +485,7 @@ PHP_FUNCTION(stream_get_meta_data) 42 | zval *arg1; 43 | php_stream *stream; 44 | zval *newval; 45 | + int tmp_fd; 46 | 47 | if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &arg1) == FAILURE) { 48 | return; 49 | @@ -502,6 +504,9 @@ PHP_FUNCTION(stream_get_meta_data) 50 | add_assoc_string(return_value, "wrapper_type", (char *)stream->wrapper->wops->label, 1); 51 | } 52 | add_assoc_string(return_value, "stream_type", (char *)stream->ops->label, 1); 53 | + if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&tmp_fd, 1) && tmp_fd != -1) { 54 | + add_assoc_long(return_value, "fd", tmp_fd); 55 | + } 56 | 57 | add_assoc_string(return_value, "mode", stream->mode, 1); 58 | 59 | --------------------------------------------------------------------------------