Skip to content

Instantly share code, notes, and snippets.

@stuartcarnie
Last active February 25, 2024 11:07
  • Star 24 You must be signed in to star a gist
  • Fork 11 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save stuartcarnie/5523506 to your computer and use it in GitHub Desktop.
Self-contained php script to clear shared-memory cache in php5-fpm via command-line. In this instance it is clearing APC, but can be easily changed to clear other shared-memory caches.
#!/usr/bin/env php
<?php
/*
Copyright 2013 Stuart Carnie and other contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
if (!isset($_SERVER['GATEWAY_INTERFACE'])) {
$opts = getopt('h', [ 'port:', 'sock:', 'help' ]);
if (count($opts) === 0) {
$opts['port'] = 9000;
} elseif (isset($opts['h']) || isset($opts['help'])) {
fprintf(STDOUT, 'clear-apc.php 1.0'.PHP_EOL.'Usage: clear-apc.php --port=port|--sock=path'.PHP_EOL);
exit(1);
}
if (isset($opts['sock'])) {
$host = $opts['sock'];
$port = null;
} else {
$host = '127.0.0.1';
$port = $opts['port'];
}
$headers = [
'GATEWAY_INTERFACE' => 'FastCGI/1.0',
'SCRIPT_FILENAME' => realpath($_SERVER['PHP_SELF']),
'REQUEST_METHOD' => 'POST',
'REQUEST_URI' => '/'
];
try {
$client = new FastCgiClient($host, $port);
$client->request($headers, '');
$response = $client->response();
if ($response['statusCode'] !== 200) {
fprintf(STDERR, $response['stderr'].PHP_EOL);
exit(1);
} else {
fprintf(STDOUT, $response['body'].PHP_EOL);
}
} catch (CommunicationException $ex) {
fprintf(STDERR, $ex->getMessage().PHP_EOL);
exit(1);
}
} else {
$return = apc_clear_cache('opcode');
echo var_export($return, true) . PHP_EOL;
}
/**
* Note : Code is released under the GNU LGPL
*
* Please do not change the header of this file
*
* This library is free software; you can redistribute it and/or modify it under the terms of the GNU
* Lesser General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*
* See the GNU Lesser General Public License for more details.
*/
/**
* Handles communication with a FastCGI application
*
* @author Pierrick Charron <pierrick@webstart.fr>
* @author Daniel Aharon <dan@danielaharon.com>
* @author Erik Bernhardson <bernhardsonerik@gmail.com>
* @version 2.0
*/
class FastCgiClient
{
const VERSION_1 = 1;
const BEGIN_REQUEST = 1;
const ABORT_REQUEST = 2;
const END_REQUEST = 3;
const PARAMS = 4;
const STDIN = 5;
const STDOUT = 6;
const STDERR = 7;
const DATA = 8;
const GET_VALUES = 9;
const GET_VALUES_RESULT = 10;
const UNKNOWN_TYPE = 11;
const MAXTYPE = self::UNKNOWN_TYPE;
const RESPONDER = 1;
const AUTHORIZER = 2;
const FILTER = 3;
const REQUEST_COMPLETE = 0;
const CANT_MPX_CONN = 1;
const OVERLOADED = 2;
const UNKNOWN_ROLE = 3;
const MAX_CONNS = 'MAX_CONNS';
const MAX_REQS = 'MAX_REQS';
const MPXS_CONNS = 'MPXS_CONNS';
const HEADER_LEN = 8;
/**
* Socket
* @var Resource
*/
private $sock = null;
/**
* Host
* @var String
*/
private $host = null;
/**
* Port
* @var Integer
*/
private $port = null;
/**
* Unix socket path
* @var string
*/
private $socketPath = null;
/**
* Keep Alive
* @var Boolean
*/
private $keepAlive = false;
/**
* A request has been sent.
* @var Boolean
*/
private $awaitingResponse = false;
/**
* Constructor
*
* @param String $host Host of the FastCGI application or path to the FastCGI unix socket
* @param Integer $port Port of the FastCGI application or null for the FastCGI unix socket
*/
public function __construct($host, $port = null)
{
if ($port !== null) {
$this->host = $host;
$this->port = $port;
} else {
$this->socketPath = $host;
}
}
/**
* Destructor
*/
public function __destruct()
{
if ($this->sock) {
socket_close($this->sock);
}
}
/**
* Define whether or not the FastCGI application should keep the connection
* alive at the end of a request
*
* @param Boolean $b true if the connection should stay alive, false otherwise
*/
public function setKeepAlive($b)
{
$this->keepAlive = (boolean)$b;
if (!$this->keepAlive && $this->sock) {
$this->close();
}
}
/**
* Get the keep alive status
*
* @return Boolean true if the connection should stay alive, false otherwise
*/
public function getKeepAlive()
{
return $this->keepAlive;
}
/**
* Close the fastcgi connection
*/
public function close()
{
if ($this->sock) {
socket_close($this->sock);
$this->sock = null;
}
}
/**
* Create a connection to the FastCGI application
*/
private function connect()
{
if (!$this->sock) {
if($this->socketPath !== null) {
$this->sock = @socket_create(AF_UNIX, SOCK_STREAM, 0);
$address = $this->socketPath;
$port = 0;
} else {
$this->sock = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
$address = $this->host;
$port = $this->port;
}
if (!$this->sock) {
throw CommunicationException::socketCreate();
}
if (false === @socket_connect($this->sock, $address, $port)) {
throw CommunicationException::socketConnect($this->sock, $address, $port);
}
}
}
/**
* Build a FastCGI packet
*
* @param Integer $type Type of the packet
* @param String $content Content of the packet
* @param Integer $requestId RequestId
* @return string
*/
private function buildPacket($type, $content, $requestId = 1)
{
$clen = strlen($content);
return chr(self::VERSION_1) /* version */
. chr($type) /* type */
. chr(($requestId >> 8) & 0xFF) /* requestIdB1 */
. chr($requestId & 0xFF) /* requestIdB0 */
. chr(($clen >> 8 ) & 0xFF) /* contentLengthB1 */
. chr($clen & 0xFF) /* contentLengthB0 */
. chr(0) /* paddingLength */
. chr(0) /* reserved */
. $content; /* content */
}
/**
* Build an FastCGI Name value pair
*
* @param String $name Name
* @param String $value Value
* @return String FastCGI Name value pair
*/
private function buildNvpair($name, $value)
{
$nlen = strlen($name);
$vlen = strlen($value);
if ($nlen < 128) {
/* nameLengthB0 */
$nvpair = chr($nlen);
} else {
/* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */
$nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF);
}
if ($vlen < 128) {
/* valueLengthB0 */
$nvpair .= chr($vlen);
} else {
/* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */
$nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF);
}
/* nameData & valueData */
return $nvpair . $name . $value;
}
/**
* Read a set of FastCGI Name value pairs
*
* @param String $data Data containing the set of FastCGI NVPair
* @param null $length
* @return array of NVPair
*/
private function readNvpair($data, $length = null)
{
$array = array();
if ($length === null) {
$length = strlen($data);
}
$p = 0;
while ($p != $length) {
$nlen = ord($data{$p++});
if ($nlen >= 128) {
$nlen = ($nlen & 0x7F << 24);
$nlen |= (ord($data{$p++}) << 16);
$nlen |= (ord($data{$p++}) << 8);
$nlen |= (ord($data{$p++}));
}
$vlen = ord($data{$p++});
if ($vlen >= 128) {
$vlen = ($nlen & 0x7F << 24);
$vlen |= (ord($data{$p++}) << 16);
$vlen |= (ord($data{$p++}) << 8);
$vlen |= (ord($data{$p++}));
}
$array[substr($data, $p, $nlen)] = substr($data, $p+$nlen, $vlen);
$p += ($nlen + $vlen);
}
return $array;
}
/**
* Decode a FastCGI Packet
*
* @param String $data String containing all the packet
* @return array
*/
private function decodePacketHeader($data)
{
$ret = array();
$ret['version'] = ord($data{0});
$ret['type'] = ord($data{1});
$ret['requestId'] = (ord($data{2}) << 8) + ord($data{3});
$ret['contentLength'] = (ord($data{4}) << 8) + ord($data{5});
$ret['paddingLength'] = ord($data{6});
$ret['reserved'] = ord($data{7});
return $ret;
}
/**
* Read a FastCGI Packet
*
* @throws CommunicationException
* @return array
*/
private function readPacket()
{
$packet = @socket_read($this->sock, self::HEADER_LEN);
if ($packet === false) {
throw CommunicationException::socketRead($this->sock);
}
$resp = $this->decodePacketHeader($packet);
if ($len = $resp['contentLength'] + $resp['paddingLength']) {
$content = @socket_read($this->sock, $len);
if ($content === false) {
throw CommunicationException::socketRead($this->sock);
}
$resp['content'] = substr($content, 0, $resp['contentLength']);
} else {
$resp['content'] = '';
}
return $resp;
}
/**
* Get Informations on the FastCGI application
*
* @param array $requestedInfo information to retrieve
* @throws CommunicationException|\Exception
* @return array
*/
public function getValues(array $requestedInfo)
{
try {
return $this->doGetValues($requestedInfo);
} catch (CommunicationException $e) {
$this->close();
throw $e;
}
}
/**
* Get Informations on the FastCGI application
*
* @param array $requestedInfo information to retrieve
* @throws CommunicationException
* @return array
*/
protected function doGetValues(array $requestedInfo)
{
$this->connect();
$request = '';
foreach ($requestedInfo as $info) {
$request .= $this->buildNvpair($info, '');
}
if (false === @socket_write($this->sock, $this->buildPacket(self::GET_VALUES, $request, 0))) {
throw CommunicationException::socketWrite($this->sock);
}
$this->awaitingResponse = true;
$resp = $this->readPacket();
$this->awaitingResponse = false;
if ($resp['type'] == self::GET_VALUES_RESULT) {
return $this->readNvpair($resp['content'], $resp['length']);
} else {
throw new CommunicationException('Unexpected response type, expecting GET_VALUES_RESULT');
}
}
/**
* Execute a request to the FastCGI application
*
* @param array $params Array of parameters
* @param String $stdin Content
* @throws CommunicationException|\Exception
*/
public function request(array $params, $stdin)
{
try {
$this->doRequest($params, $stdin);
} catch (CommunicationException $e) {
$this->close();
throw $e;
}
}
/**
* Execute a request to the FastCGI application
*
* @param array $params Array of parameters
* @param String $stdin Content
* @throws CommunicationException
*/
protected function doRequest(array $params, $stdin)
{
$this->connect();
$request = $this->buildPacket(self::BEGIN_REQUEST, chr(0) . chr(self::RESPONDER) . chr((int) $this->keepAlive) . str_repeat(chr(0), 5));
$paramsRequest = '';
foreach ($params as $key => $value) {
$paramsRequest .= $this->buildNvpair($key, $value);
}
if ($paramsRequest) {
$request .= $this->buildPacket(self::PARAMS, $paramsRequest);
}
$request .= $this->buildPacket(self::PARAMS, '');
if ($stdin) {
$request .= $this->buildPacket(self::STDIN, $stdin);
}
$request .= $this->buildPacket(self::STDIN, '');
// Write the request and break.
if (false === @socket_write($this->sock, $request)) {
throw CommunicationException::socketWrite($this->sock);
}
$this->awaitingResponse = true;
}
/**
* FCGIClient::formatResponse()
*
* Format the response into an array with separate statusCode, headers, body, and error output.
*
* @param string $stdout The plain, unformatted response.
* @param string $stderr The plain, unformatted error output.
*
* @throws CommunicationException
* @return array An array containing the headers and body content.
*/
private static function formatResponse($stdout, $stderr) {
// Split the header from the body. Split on \n\n.
$doubleCr = strpos($stdout, "\r\n\r\n");
$rawHeader = substr($stdout, 0, $doubleCr);
$rawBody = substr($stdout, $doubleCr, strlen($stdout));
// Format the header.
$header = array();
$headerLines = explode("\n", $rawHeader);
foreach ($headerLines as $line) {
if (preg_match('/([\w-]+):\s*(.*)$/', $line, $matches)) {
// ['Content-type'] => 'text/plain'
$header[strtolower($matches[1])] = trim($matches[2]);
}
}
if (isset($header['status'])) {
$code = $header['status'];
if (false !== ($pos = strpos($code, ' '))) {
$code = substr($code, 0, $pos);
}
} else {
if (isset($header['location'])) {
$header['status'] = '302 Moved Temporarily';
$code = '302';
} else {
$header['status'] = '200 OK';
$code = '200';
}
}
if (false === ctype_digit($code)) {
throw new CommunicationException("Unrecognizable status code returned from fastcgi: $code");
}
return array(
'statusCode' => (int) $code,
'headers' => $header,
'body' => trim($rawBody),
'stderr' => $stderr,
);
}
/**
* Collect the response from a FastCGI request.
*
* @throws CommunicationException|\Exception
* @return String Return response.
*/
public function response()
{
try {
return $this->doResponse();
} catch (CommunicationException $e) {
$this->close();
throw $e;
}
}
/**
* Collect the response from a FastCGI request.
*
* @throws CommunicationException
* @return String Return response.
*/
protected function doResponse()
{
$stdout = $stderr = '';
$resp = false;
while ($this->awaitingResponse) {
$resp = $this->readPacket();
// Check for the end of the response.
if ($resp['type'] == self::END_REQUEST) {
$this->awaitingResponse = false;
// Check for response content.
} elseif ($resp['type'] == self::STDOUT) {
$stdout .= $resp['content'];
} elseif ($resp['type'] == self::STDERR) {
$stderr .= $resp['content'];
}
}
if (!is_array($resp)) {
throw new CommunicationException("Bad Request");
}
switch (ord($resp['content']{4})) {
case self::CANT_MPX_CONN:
throw new CommunicationException('This app can\'t multiplex [CANT_MPX_CONN]');
break;
case self::OVERLOADED:
throw new CommunicationException('New request rejected; too busy [OVERLOADED]');
break;
case self::UNKNOWN_ROLE:
throw new CommunicationException('Role value not known [UNKNOWN_ROLE]');
break;
case self::REQUEST_COMPLETE:
return static::formatResponse($stdout, $stderr);
}
return '';
}
public function __toString()
{
if ($this->awaitingResponse) {
$status = 'waiting for response';
} elseif ($this->sock) {
$status = 'ready for request';
} else {
$status = 'not connected';
}
$address = $this->socketPath;
if (!$address) {
$address = "{$this->host}:{$this->port}";
}
return "FCGIClient for $address : $status";
}
}
/**
* Handles communication with a FastCGI application
*
* @author Erik Bernhardson <bernhardsonerik@gmail.com>
* @version 2.0
*/
class CommunicationException extends \RuntimeException
{
static public function socketCreate()
{
$err = socket_last_error();
return new self("Couldn't create socket - $err - ".socket_strerror($err));
}
static public function socketConnect($socket, $host, $port)
{
if ($port) {
$host .= ":$port";
}
return self::socketError("Failure connecting to $host", $socket);
}
static public function socketRead($socket)
{
if ($socket === null) {
return self::requestAborted();
}
return self::socketError('Failure reading socket', $socket);
}
static public function socketWrite($socket)
{
if ($socket === null) {
return self::requestAborted();
}
return self::socketError('Failure writing socket', $socket);
}
static public function socketError($message, $socket)
{
$err = socket_last_error($socket);
return new self("$message - $err - ".socket_strerror($err));
}
static public function requestAborted()
{
return new self("The request was aborted.");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment