Initial commit
This commit is contained in:
commit
86ff40274b
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/vendor/
|
||||
/composer.lock
|
27
README.md
Normal file
27
README.md
Normal file
@ -0,0 +1,27 @@
|
||||
noccylabs/ipc
|
||||
=============
|
||||
|
||||
This is a one-size-fits-all IPC library to facilitate communication between
|
||||
threads and processes. It contains the following features:
|
||||
|
||||
**Core:**
|
||||
|
||||
- [ ] Semaphore
|
||||
- [ ] Mutex
|
||||
- [x] Queue
|
||||
- [x] SharedMemory key-value store
|
||||
- [ ] SharedMemory blocks
|
||||
- [x] Signals
|
||||
- [x] Locks
|
||||
|
||||
**High-Level:**
|
||||
|
||||
- [ ] EventBridge
|
||||
- [ ] EventDispatcher
|
||||
- [ ] InterOp/Marshalling
|
||||
- [x] Asynchronous timers
|
||||
|
||||
**Transports:**
|
||||
|
||||
- [x] Stream channels
|
||||
|
27
composer.json
Normal file
27
composer.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "noccylabs/ipc",
|
||||
"description": "A complete set of IPC facilities",
|
||||
"type": "library",
|
||||
"license": "GPL-3.0",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christopher Vagnetoft",
|
||||
"email": "cvagnetoft@gmail.com"
|
||||
}
|
||||
],
|
||||
"require": {},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "composer",
|
||||
"url": "https://packages.noccylabs.info/composer/"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/signals.stub.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"NoccyLabs\\Ipc\\": "src/"
|
||||
}
|
||||
}
|
||||
}
|
15
examples/timers.php
Normal file
15
examples/timers.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__."/../vendor/autoload.php";
|
||||
|
||||
use NoccyLabs\Ipc\Interop\Async\Timer;
|
||||
|
||||
$timer1 = new Timer(function () { echo "Hello "; });
|
||||
$timer2 = new Timer(function () { echo "World! "; });
|
||||
|
||||
for ($n = 0; $n < 1000; $n++) {
|
||||
if ($n == 500) {
|
||||
unset($timer2);
|
||||
}
|
||||
usleep(10000);
|
||||
}
|
20
phpunit.xml
Normal file
20
phpunit.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/5.4/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
backupGlobals="false"
|
||||
beStrictAboutCoversAnnotation="true"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
beStrictAboutTestsThatDoNotTestAnything="true"
|
||||
beStrictAboutTodoAnnotatedTests="true"
|
||||
verbose="true">
|
||||
<testsuite>
|
||||
<directory suffix="Test.php">tests</directory>
|
||||
</testsuite>
|
||||
|
||||
<filter>
|
||||
<whitelist processUncoveredFilesFromWhitelist="true">
|
||||
<directory suffix=".php">src</directory>
|
||||
</whitelist>
|
||||
</filter>
|
||||
</phpunit>
|
80
src/Interop/Async/Timer.php
Normal file
80
src/Interop/Async/Timer.php
Normal file
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Ipc\Interop\Async;
|
||||
|
||||
use NoccyLabs\Ipc\Signal\SignalHandler;
|
||||
|
||||
|
||||
/**
|
||||
* Simple second-granularity timer for asynchronous events
|
||||
*
|
||||
*/
|
||||
class Timer
|
||||
{
|
||||
private static $timers = null;
|
||||
|
||||
private $callback;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param callable $callback
|
||||
*/
|
||||
public function __construct(callable $callback)
|
||||
{
|
||||
$this->callback = $callback;
|
||||
$this->seconds = $seconds;
|
||||
self::registerTimer($this->callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
self::clearTimer($this->callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a timer callback function
|
||||
*
|
||||
* @param callable $timer
|
||||
* @return void
|
||||
*/
|
||||
private static function registerTimer(callable $timer)
|
||||
{
|
||||
if (self::$timers === null) {
|
||||
$handler = new SignalHandler(SIGALRM, [ __CLASS__."::onSignal" ]);
|
||||
pcntl_alarm(1);
|
||||
}
|
||||
self::$timers[] = $timer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a timer callback function
|
||||
*
|
||||
* @param callable $timer
|
||||
* @return void
|
||||
*/
|
||||
private static function clearTimer(callable $timer)
|
||||
{
|
||||
self::$timers = array_filter(self::$timers, function ($t) use ($timer) {
|
||||
return $t !== $timer;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle signals when the alarm fires
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function onSignal()
|
||||
{
|
||||
foreach (self::$timers as $timer) {
|
||||
call_user_func($timer);
|
||||
}
|
||||
pcntl_alarm(1);
|
||||
}
|
||||
|
||||
}
|
||||
|
13
src/Interop/Channel/ChannelInterface.php
Normal file
13
src/Interop/Channel/ChannelInterface.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Ipc\Interop\Channel;
|
||||
|
||||
|
||||
interface ChannelInterface
|
||||
{
|
||||
public function isOpen():bool;
|
||||
|
||||
public function receive();
|
||||
|
||||
public function send($data);
|
||||
}
|
54
src/Interop/Channel/StreamChannel.php
Normal file
54
src/Interop/Channel/StreamChannel.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Ipc\Interop\Channel;
|
||||
|
||||
|
||||
class StreamChannel implements ChannelInterface
|
||||
{
|
||||
protected $stream;
|
||||
|
||||
public function __construct($stream)
|
||||
{
|
||||
if (!is_resource($stream)) {
|
||||
if (!is_string($stream)) {
|
||||
throw new \LogicException("Invalid stream");
|
||||
}
|
||||
$stream = stream_socket_client($stream, $errno, $errstr);
|
||||
if (!$stream) {
|
||||
throw new \RuntimeException(sprintf("%d %s", $errno, $errstr));
|
||||
}
|
||||
}
|
||||
|
||||
$this->stream = $stream;
|
||||
|
||||
$this->isOpen();
|
||||
}
|
||||
|
||||
public function send($data)
|
||||
{
|
||||
fwrite($this->stream, json_encode($data)."\0");
|
||||
}
|
||||
|
||||
public function receive()
|
||||
{
|
||||
$buf = fread($this->stream, 8192);
|
||||
|
||||
return json_decode(rtrim($buf, "\0"));
|
||||
}
|
||||
|
||||
public function isOpen(): bool
|
||||
{
|
||||
return is_resource($this->stream);
|
||||
}
|
||||
|
||||
public static function createPair()
|
||||
{
|
||||
$fd = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
|
||||
|
||||
return [
|
||||
new StreamChannel($fd[0]),
|
||||
new StreamChannel($fd[1])
|
||||
];
|
||||
}
|
||||
|
||||
}
|
84
src/Key/FileKey.php
Normal file
84
src/Key/FileKey.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Ipc\Key;
|
||||
|
||||
class FileKey implements KeyInterface
|
||||
{
|
||||
private $proj;
|
||||
|
||||
private $pathname;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $pathname
|
||||
* @param string $proj
|
||||
* @param boolean $create
|
||||
*/
|
||||
public function __construct(string $pathname, $proj="\0", bool $create=false)
|
||||
{
|
||||
if (!file_exists($pathname)) {
|
||||
if (!$create) {
|
||||
throw new \RuntimeException("Path does not exist: {$pathname}");
|
||||
}
|
||||
if (!is_dir(dirname($pathname))) {
|
||||
throw new \RuntimeException("The directory ".dirname($pathname)." does not exist");
|
||||
}
|
||||
touch($pathname);
|
||||
}
|
||||
|
||||
$this->pathname = $pathname;
|
||||
$this->proj = substr($proj,0,1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pathname used to generate the key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPathname():string
|
||||
{
|
||||
return $this->pathname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the project identifier. Not guaranteed to be printable
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getProjectIdentifier():string
|
||||
{
|
||||
return $this->proj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key value
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getKey():int
|
||||
{
|
||||
return ftok($this->pathname, $this->proj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone the FileKey, increasing the project identifier to create a new key for the
|
||||
* same file
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
$this->proj = chr(ord($this->proj) + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create printable representation of the key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->getKey();
|
||||
}
|
||||
}
|
9
src/Key/KeyInterface.php
Normal file
9
src/Key/KeyInterface.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Ipc\Key;
|
||||
|
||||
interface KeyInterface
|
||||
{
|
||||
public function getKey():int;
|
||||
public function __toString();
|
||||
}
|
88
src/Lock/FileLock.php
Normal file
88
src/Lock/FileLock.php
Normal file
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Ipc\Lock;
|
||||
|
||||
/**
|
||||
* Simple filesystem-based lock
|
||||
*
|
||||
*
|
||||
*/
|
||||
class FileLock
|
||||
{
|
||||
const DEFAULT_TIMEOUT = 5;
|
||||
|
||||
protected $pathname;
|
||||
|
||||
protected $resource;
|
||||
|
||||
protected $locked = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $pathname
|
||||
*/
|
||||
public function __construct(string $pathname)
|
||||
{
|
||||
if (!file_exists($pathname)) {
|
||||
throw new \RuntimeException(sprintf(
|
||||
"The file %s does not exist",
|
||||
$pathname
|
||||
));
|
||||
}
|
||||
|
||||
$this->pathname = $pathname;
|
||||
$this->resource = fopen($pathname, "r");
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->release();
|
||||
fclose($this->resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire an exclusive lock
|
||||
*
|
||||
* @param float $timeout
|
||||
* @return boolean
|
||||
*/
|
||||
public function acquire(float $timeout = self::DEFAULT_TIMEOUT):bool
|
||||
{
|
||||
if ($this->locked) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$expire = microtime(true) + $timeout;
|
||||
while (!flock($this->resource, LOCK_EX|LOCK_NB)) {
|
||||
if (microtime(true)>$expire) {
|
||||
return false;
|
||||
}
|
||||
usleep(1000);
|
||||
}
|
||||
|
||||
$this->locked = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release an acquired lock
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function release()
|
||||
{
|
||||
flock($this->resource, LOCK_UN);
|
||||
$this->locked = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the lock is acquired
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isLocked():bool
|
||||
{
|
||||
return $this->locked;
|
||||
}
|
||||
}
|
87
src/Msg/Queue.php
Normal file
87
src/Msg/Queue.php
Normal file
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Ipc\Msg;
|
||||
|
||||
use NoccyLabs\Ipc\Key\KeyInterface;
|
||||
|
||||
|
||||
/**
|
||||
* Message queue wrapper
|
||||
*
|
||||
*/
|
||||
class Queue
|
||||
{
|
||||
protected $resource;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param KeyInterface $key
|
||||
* @param integer $perms
|
||||
*/
|
||||
public function __construct(KeyInterface $key, int $perms = 0666)
|
||||
{
|
||||
$this->resource = msg_get_queue($key->getKey(), $perms);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Send to the queue
|
||||
*
|
||||
* @param integer $msgtype
|
||||
* @param mixed $message
|
||||
* @param boolean $blocking
|
||||
* @throws RuntimeException on error
|
||||
*/
|
||||
public function send(int $msgtype, $message, bool $blocking = true)
|
||||
{
|
||||
$ret = msg_send($this->resource, $msgtype, $message, true, $blocking, $errno);
|
||||
|
||||
if ($ret === false) {
|
||||
throw new \RuntimeException(sprintf(
|
||||
"msg_send error %d",
|
||||
$errno
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive from the queue
|
||||
*
|
||||
* @param integer $desiredmsgtype
|
||||
* @param integer $msgtype
|
||||
* @param integer $maxsize
|
||||
* @param boolean $blocking
|
||||
* @return mixed
|
||||
* @throws RuntimeException on error
|
||||
*/
|
||||
public function receive(int $desiredmsgtype, &$msgtype, int $maxsize=4096, bool $blocking = true)
|
||||
{
|
||||
$ret = msg_receive(
|
||||
$this->resource,
|
||||
$desiredmsgtype,
|
||||
$msgtype,
|
||||
$maxsize,
|
||||
$message,
|
||||
true,
|
||||
$blocking?MSG_IPC_NOWAIT:0,
|
||||
$errno
|
||||
);
|
||||
|
||||
if ($ret === false) {
|
||||
throw new \RuntimeException(sprintf(
|
||||
"msg_receive error %d",
|
||||
$errno
|
||||
));
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
}
|
15
src/Sem/Mutex.php
Normal file
15
src/Sem/Mutex.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Ipc\Sem;
|
||||
|
||||
use NoccyLabs\Ipc\Key\KeyInterface;
|
||||
|
||||
|
||||
|
||||
class Mutex extends Semaphore
|
||||
{
|
||||
public function __construct(KeyInterface $key)
|
||||
{
|
||||
parent::__construct($key, 1);
|
||||
}
|
||||
}
|
25
src/Sem/Semaphore.php
Normal file
25
src/Sem/Semaphore.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Ipc\Sem;
|
||||
|
||||
use NoccyLabs\Ipc\Key\KeyInterface;
|
||||
|
||||
|
||||
|
||||
class Semaphore
|
||||
{
|
||||
public function __construct(KeyInterface $key, int $max)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function acquire(float $timeout)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function release()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
133
src/Shm/SharedData.php
Normal file
133
src/Shm/SharedData.php
Normal file
@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Ipc\Shm;
|
||||
|
||||
/**
|
||||
* Shared data storage with a key-value interface
|
||||
*
|
||||
*/
|
||||
class SharedData extends SharedMemory
|
||||
{
|
||||
private const IDX_LOCK = 0;
|
||||
private const IDX_MAP = 1;
|
||||
private const IDX_DATA = 2;
|
||||
|
||||
private $checks = [];
|
||||
|
||||
/**
|
||||
* Acquire a lock
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function lock()
|
||||
{
|
||||
$expire = microtime(true) + 1;
|
||||
while ($this[self::IDX_LOCK] !== null) {
|
||||
usleep(1000);
|
||||
// Forcefully continue after 1 second
|
||||
if (microtime(true)>$expire) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this[self::IDX_LOCK] = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the lock
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function unlock()
|
||||
{
|
||||
$this[self::IDX_LOCK] = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of a key
|
||||
*
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function get(string $key)
|
||||
{
|
||||
$map = (array)$this[self::IDX_MAP];
|
||||
|
||||
// If the key doesn't exist, return null
|
||||
if (!array_key_exists($key, $map)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$index = $map[$key];
|
||||
$keyvar = $this[self::IDX_DATA + $index];
|
||||
|
||||
// If for some reason we don't get anything back, return null
|
||||
if (!$keyvar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Save the checksum for later and return the value
|
||||
$this->checks[$key] = $keyvar[0];
|
||||
return $keyvar[1];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a key to a specific value
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param boolean $check
|
||||
* @return boolean
|
||||
*/
|
||||
public function set(string $key, $value, $check=false):bool
|
||||
{
|
||||
// Acquire a lock and fetch the map
|
||||
$this->lock();
|
||||
$map = (array)$this[self::IDX_MAP];
|
||||
|
||||
// If the key doesn't exist in the map, we need to set it up
|
||||
if (!array_key_exists($key, $map)) {
|
||||
$free = 0;
|
||||
// Find first free index
|
||||
foreach ($map as $map_key=>$index) {
|
||||
if ($index != $free) { break; }
|
||||
$free++;
|
||||
}
|
||||
// Store the index in the map, and sort it by index.
|
||||
$map[$key] = $free;
|
||||
asort($map);
|
||||
$this[self::IDX_MAP] = $map;
|
||||
}
|
||||
|
||||
// Look up the key and fetch the data
|
||||
$index = $map[$key];
|
||||
$keyvar = $this[self::IDX_DATA + $index];
|
||||
|
||||
// Check the data to make sure the data hasn't changed since last read
|
||||
if ($check && $keyvar && array_key_exists($key, $this->checks)) {
|
||||
if ($keyvar[0] != $this->checks[$key]) {
|
||||
$this->unlock();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the data
|
||||
$this[self::IDX_DATA + $index] = [ md5($value), $value ];
|
||||
$this->unlock();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the map contains the key. This method doesn't acquire a lock
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function contains(string $key):bool
|
||||
{
|
||||
$map = (array)$this[self::IDX_MAP];
|
||||
|
||||
return array_key_exists($key, $map);
|
||||
}
|
||||
|
||||
}
|
94
src/Shm/SharedMemory.php
Normal file
94
src/Shm/SharedMemory.php
Normal file
@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Ipc\Shm;
|
||||
|
||||
use NoccyLabs\Ipc\Key\KeyInterface;
|
||||
|
||||
/**
|
||||
* Shared memory segment
|
||||
*/
|
||||
class SharedMemory implements \ArrayAccess
|
||||
{
|
||||
|
||||
protected $resource;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param KeyInterface $key
|
||||
* @param integer $memsize
|
||||
* @param integer $perm
|
||||
*/
|
||||
public function __construct(KeyInterface $key, int $memsize = 64000, int $perm = 0666)
|
||||
{
|
||||
if (!($shm_resource = shm_attach($key->getKey(), $memsize, $perm))) {
|
||||
throw new \RuntimeException("Unable to attach shm resource {$key}");
|
||||
}
|
||||
|
||||
$this->resource = $shm_resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
shm_detach($this->resource);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the shm segment
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function destroy()
|
||||
{
|
||||
shm_remove($this->resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @param int $offset
|
||||
* @return void
|
||||
*/
|
||||
public function offsetExists($offset)
|
||||
{
|
||||
return shm_has_var($this->resource, $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @param int $offset
|
||||
* @return mixed
|
||||
*/
|
||||
public function offsetGet($offset)
|
||||
{
|
||||
return shm_has_var($this->resource, $offset)?shm_get_var($this->resource, $offset):null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @param int $offset
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
public function offsetSet($offset, $value)
|
||||
{
|
||||
shm_put_var($this->resource, $offset, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @param int $offset
|
||||
* @return void
|
||||
*/
|
||||
public function offsetUnset($offset)
|
||||
{
|
||||
shm_remove_var($this->resource, $offset);
|
||||
}
|
||||
}
|
99
src/Shm/SharedMemoryBlock.php
Normal file
99
src/Shm/SharedMemoryBlock.php
Normal file
@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Ipc\Shm;
|
||||
|
||||
use NoccyLabs\Ipc\Key\KeyInterface;
|
||||
|
||||
/**
|
||||
* A block of shared memory that can be read and write like a string buffer
|
||||
*
|
||||
*
|
||||
*/
|
||||
class SharedMemoryBlock
|
||||
{
|
||||
|
||||
const FLAG_ACCESS = "a";
|
||||
const FLAG_CREATE = "c";
|
||||
const FLAG_WRITE = "w";
|
||||
const FLAG_NEW = "n";
|
||||
|
||||
protected $resource;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param KeyInterface $key
|
||||
* @param string $flags
|
||||
* @param integer $memsize
|
||||
* @param integer $perm
|
||||
*/
|
||||
public function __construct(KeyInterface $key, string $flags, int $memsize, int $perm = 0666)
|
||||
{
|
||||
if (!($shm_resource = shmop_open($key->getKey(), $memsize, $perm))) {
|
||||
throw new \RuntimeException("Unable to attach shm resource {$key}");
|
||||
}
|
||||
|
||||
$this->resource = $shm_resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if (!$this->resource) {
|
||||
return;
|
||||
}
|
||||
shmop_close($this->resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the memory block
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function destroy()
|
||||
{
|
||||
shmop_delete($this->resource);
|
||||
shmop_close($this->resource);
|
||||
$this->resource = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read bytes from the shared block
|
||||
*
|
||||
* @param integer $length
|
||||
* @param integer $offset
|
||||
* @return mixed
|
||||
*/
|
||||
public function read($length=0, $offset=0)
|
||||
{
|
||||
if ($length == 0) {
|
||||
$length = shmop_size($this->resource) - $offset;
|
||||
}
|
||||
return shmop_read($this->resource, $offset, $length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write bytes to the shared block
|
||||
*
|
||||
* @param mixed $data
|
||||
* @param integer $offset
|
||||
* @return integer
|
||||
*/
|
||||
public function write($data, $offset=0):int
|
||||
{
|
||||
return shmop_write($this->resource, $data, $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of the memory block
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getSize():int
|
||||
{
|
||||
return shmop_size($this->resource);
|
||||
}
|
||||
|
||||
}
|
24
src/Signal/Signal.php
Normal file
24
src/Signal/Signal.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Ipc\Signal;
|
||||
|
||||
|
||||
class Signal
|
||||
{
|
||||
private $signo;
|
||||
|
||||
public function __construct(int $signo)
|
||||
{
|
||||
$this->signo = $signo;
|
||||
}
|
||||
|
||||
public function setHandler(callable $handler):void
|
||||
{
|
||||
pcntl_signal($this->signo, $handler);
|
||||
}
|
||||
|
||||
public function dispatch($pid):bool
|
||||
{
|
||||
return posix_kill($pid, $this->signo);
|
||||
}
|
||||
}
|
37
src/Signal/SignalHandler.php
Normal file
37
src/Signal/SignalHandler.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Ipc\Signal;
|
||||
|
||||
|
||||
class SignalHandler
|
||||
{
|
||||
private $signo;
|
||||
|
||||
private $callbacks;
|
||||
|
||||
public function __construct(int $signo, array $callbacks=[])
|
||||
{
|
||||
$this->signo = $signo;
|
||||
$this->callbacks = $callbacks;
|
||||
pcntl_signal($this->signo, [ $this, "onSignal" ]);
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function addHandler(callable $handler):void
|
||||
{
|
||||
$this->callbacks[] = $handler;
|
||||
}
|
||||
|
||||
public function onSignal($signo)
|
||||
{
|
||||
foreach ($this->callbacks as $callback) {
|
||||
if (call_user_func($callback) === true)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
32
src/Signal/SignalTrap.php
Normal file
32
src/Signal/SignalTrap.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Ipc\Signal;
|
||||
|
||||
|
||||
class SignalTrap
|
||||
{
|
||||
protected $signal;
|
||||
|
||||
protected $trapped = false;
|
||||
|
||||
public function __construct(int $signo)
|
||||
{
|
||||
$this->signal = new Signal($signo);
|
||||
$this->signal->setHandler([ $this, "onSignal" ]);
|
||||
}
|
||||
|
||||
public function onSignal()
|
||||
{
|
||||
$this->trapped = true;
|
||||
}
|
||||
|
||||
public function isTrapped($reset=true):bool
|
||||
{
|
||||
if ($this->trapped) {
|
||||
$reset && ($this->trapped = false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
41
src/signals.stub.php
Normal file
41
src/signals.stub.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
if (function_exists('pcntl_async_signals')) {
|
||||
define("SIGNALS_ASYNC", true);
|
||||
pcntl_async_signals(true);
|
||||
} else {
|
||||
define("SIGNALS_ASYNC", false);
|
||||
}
|
||||
|
||||
// Signal shims
|
||||
defined("SIGHUP") || define("SIGHUP", 1); // Hangup (POSIX)
|
||||
defined("SIGINT") || define("SIGINT", 2); // Terminal interrupt (ANSI)
|
||||
defined("SIGQUIT") || define("SIGQUIT", 3); // Terminal quit (POSIX)
|
||||
defined("SIGILL") || define("SIGILL", 4); // Illegal instruction (ANSI)
|
||||
defined("SIGTRAP") || define("SIGTRAP", 5); // Trace trap (POSIX)
|
||||
defined("SIGIOT") || define("SIGIOT", 6); // IOT Trap (4.2 BSD)
|
||||
defined("SIGBUS") || define("SIGBUS", 7); // BUS error (4.2 BSD)
|
||||
defined("SIGFPE") || define("SIGFPE", 8); // Floating point exception (ANSI)
|
||||
defined("SIGKILL") || define("SIGKILL", 9); // Kill(can't be caught or ignored) (POSIX)
|
||||
defined("SIGUSR1") || define("SIGUSR1", 10); // User defined signal 1 (POSIX)
|
||||
defined("SIGSEGV") || define("SIGSEGV", 11); // Invalid memory segment access (ANSI)
|
||||
defined("SIGUSR2") || define("SIGUSR2", 12); // User defined signal 2 (POSIX)
|
||||
defined("SIGPIPE") || define("SIGPIPE", 13); // Write on a pipe with no reader, Broken pipe (POSIX)
|
||||
defined("SIGALRM") || define("SIGALRM", 14); // Alarm clock (POSIX)
|
||||
defined("SIGTERM") || define("SIGTERM", 15); // Termination (ANSI)
|
||||
defined("SIGSTKFLT") || define("SIGSTKFLT", 16); // Stack fault
|
||||
defined("SIGCHLD") || define("SIGCHLD", 17); // Child process has stopped or exited, changed (POSIX)
|
||||
defined("SIGCONT") || define("SIGCONT", 18); // Continue executing, if stopped (POSIX)
|
||||
defined("SIGSTOP") || define("SIGSTOP", 19); // Stop executing(can't be caught or ignored) (POSIX)
|
||||
defined("SIGTSTP") || define("SIGTSTP", 20); // Terminal stop signal (POSIX)
|
||||
defined("SIGTTIN") || define("SIGTTIN", 21); // Background process trying to read, from TTY (POSIX)
|
||||
defined("SIGTTOU") || define("SIGTTOU", 22); // Background process trying to write, to TTY (POSIX)
|
||||
defined("SIGURG") || define("SIGURG", 23); // Urgent condition on socket (4.2 BSD)
|
||||
defined("SIGXCPU") || define("SIGXCPU", 24); // CPU limit exceeded (4.2 BSD)
|
||||
defined("SIGXFSZ") || define("SIGXFSZ", 25); // File size limit exceeded (4.2 BSD)
|
||||
defined("SIGVTALRM") || define("SIGVTALRM", 26); // Virtual alarm clock (4.2 BSD)
|
||||
defined("SIGPROF") || define("SIGPROF", 27); // Profiling alarm clock (4.2 BSD)
|
||||
defined("SIGWINCH") || define("SIGWINCH", 28); // Window size change (4.3 BSD, Sun)
|
||||
defined("SIGIO") || define("SIGIO", 29); // I/O now possible (4.2 BSD)
|
||||
defined("SIGPWR") || define("SIGPWR", 30); // Power failure restart (System V)
|
||||
|
37
tests/Interop/Channel/StreamChannelTest.php
Normal file
37
tests/Interop/Channel/StreamChannelTest.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Ipc\Interop\Channel;
|
||||
|
||||
|
||||
|
||||
class StreamChannelTest extends \PhpUnit\Framework\TestCase
|
||||
{
|
||||
|
||||
public function testCreatingFromStreams()
|
||||
{
|
||||
$fd = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
|
||||
|
||||
$sc1 = new StreamChannel($fd[0]);
|
||||
$sc2 = new StreamChannel($fd[1]);
|
||||
|
||||
$frame = "Hello World";
|
||||
|
||||
$sc1->send($frame);
|
||||
$rcvd = $sc2->receive();
|
||||
|
||||
$this->assertEquals($frame, $rcvd);
|
||||
}
|
||||
|
||||
public function testCreatingPair()
|
||||
{
|
||||
[ $sc1, $sc2 ] = StreamChannel::createPair();
|
||||
|
||||
$frame = "Hello World";
|
||||
|
||||
$sc1->send($frame);
|
||||
$rcvd = $sc2->receive();
|
||||
|
||||
$this->assertEquals($frame, $rcvd);
|
||||
}
|
||||
|
||||
}
|
39
tests/Key/FileKeyTest.php
Normal file
39
tests/Key/FileKeyTest.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Ipc\Key;
|
||||
|
||||
class FileKeyTest extends \PhpUnit\Framework\TestCase
|
||||
{
|
||||
public function testKeyGenerationFromExistingFile()
|
||||
{
|
||||
$keyfile = new FileKey(__FILE__);
|
||||
|
||||
$key = $keyfile->getKey();
|
||||
|
||||
$this->assertGreaterThan(0, $key);
|
||||
}
|
||||
|
||||
public function testKeyGenerationFromTemporaryFile()
|
||||
{
|
||||
$tempfile = "/tmp/key.tmp";
|
||||
file_exists($tempfile) && unlink($tempfile);
|
||||
|
||||
$keyfile = new FileKey($tempfile, "a", true);
|
||||
|
||||
$key = $keyfile->getKey();
|
||||
|
||||
$this->assertGreaterThan(0, $key);
|
||||
|
||||
}
|
||||
|
||||
public function testCloningShouldIncreaseProject()
|
||||
{
|
||||
$keyfile = new FileKey(__FILE__, "a");
|
||||
|
||||
$this->assertEquals("a", $keyfile->getProjectIdentifier());
|
||||
|
||||
$keyfile2 = clone $keyfile;
|
||||
|
||||
$this->assertEquals("b", $keyfile2->getProjectIdentifier());
|
||||
}
|
||||
}
|
18
tests/Lock/FileLockTest.php
Normal file
18
tests/Lock/FileLockTest.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Ipc\Lock;
|
||||
|
||||
|
||||
class FileLockTest extends \PhpUnit\Framework\TestCase
|
||||
{
|
||||
|
||||
public function testTheLockingShouldBeExclusive()
|
||||
{
|
||||
$lock1 = new FileLock(__FILE__);
|
||||
$lock2 = new FileLock(__FILE__);
|
||||
|
||||
$this->assertEquals(true, $lock1->acquire(0));
|
||||
$this->assertEquals(false, $lock2->acquire(0));
|
||||
|
||||
}
|
||||
}
|
70
tests/Msg/QueueTest.php
Normal file
70
tests/Msg/QueueTest.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Ipc\Msg;
|
||||
|
||||
use NoccyLabs\Ipc\Key\FileKey;
|
||||
|
||||
|
||||
|
||||
|
||||
class QueueTest extends \PhpUnit\Framework\TestCase
|
||||
{
|
||||
|
||||
|
||||
public function testSendingAndReceiving()
|
||||
{
|
||||
$key = new FileKey(__FILE__);
|
||||
|
||||
$queue = new Queue($key);
|
||||
|
||||
$queue->send(1, "Hello World");
|
||||
|
||||
$ret = $queue->receive(1, $type);
|
||||
$this->assertEquals("Hello World", $ret);
|
||||
$this->assertEquals(1, $type);
|
||||
}
|
||||
|
||||
public function testSendingAndReceivingWithTypes()
|
||||
{
|
||||
$key = new FileKey(__FILE__);
|
||||
|
||||
$queue = new Queue($key);
|
||||
|
||||
$queue->send(1, "Hello World");
|
||||
$queue->send(2, "Hello World");
|
||||
$queue->send(3, "Hello World");
|
||||
|
||||
$ret = $queue->receive(1, $type);
|
||||
$this->assertEquals(1, $type);
|
||||
|
||||
$ret = $queue->receive(2, $type);
|
||||
$this->assertEquals(2, $type);
|
||||
|
||||
$ret = $queue->receive(3, $type);
|
||||
$this->assertEquals(3, $type);
|
||||
|
||||
}
|
||||
|
||||
public function testReceivingFromFront()
|
||||
{
|
||||
$key = new FileKey(__FILE__);
|
||||
|
||||
$queue = new Queue($key);
|
||||
|
||||
$queue->send(1, "Hello World");
|
||||
$queue->send(2, "Hello World");
|
||||
$queue->send(3, "Hello World");
|
||||
|
||||
$ret = $queue->receive(0, $type);
|
||||
$this->assertEquals(1, $type);
|
||||
|
||||
$ret = $queue->receive(0, $type);
|
||||
$this->assertEquals(2, $type);
|
||||
|
||||
$ret = $queue->receive(0, $type);
|
||||
$this->assertEquals(3, $type);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
31
tests/Shm/SharedDataTest.php
Normal file
31
tests/Shm/SharedDataTest.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Ipc\Shm;
|
||||
|
||||
use NoccyLabs\Ipc\Key\FileKey;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class SharedDataTest extends \PhpUnit\Framework\TestCase
|
||||
{
|
||||
public function testOpeningSharedData()
|
||||
{
|
||||
$key = new FileKey(__DIR__);
|
||||
$shm = new SharedData($key);
|
||||
|
||||
$this->assertNull($shm->get("foo"));
|
||||
|
||||
$this->assertTrue($shm->set("foo", "hello"));
|
||||
$this->assertEquals("hello", $shm->get("foo"));
|
||||
|
||||
$this->assertNull($shm->get("bar"));
|
||||
|
||||
$this->assertTrue($shm->set("bar", "world"));
|
||||
$this->assertEquals("hello", $shm->get("foo"));
|
||||
$this->assertEquals("world", $shm->get("bar"));
|
||||
|
||||
$shm->destroy();
|
||||
}
|
||||
}
|
85
tests/Signal/SignalHandlerTest.php
Normal file
85
tests/Signal/SignalHandlerTest.php
Normal file
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Ipc\Signal;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class SignalHandlerTest extends \PhpUnit\Framework\TestCase
|
||||
{
|
||||
|
||||
public function testSignalHandlerWithSingleHandlerInConstructor()
|
||||
{
|
||||
$callbacks = [
|
||||
function () use (&$handler1) { $handler1 = true; }
|
||||
];
|
||||
|
||||
$handler = new SignalHandler(SIGUSR1, $callbacks);
|
||||
|
||||
posix_kill(posix_getpid(), SIGUSR1);
|
||||
|
||||
usleep(1000);
|
||||
|
||||
$this->assertEquals(true, $handler1);
|
||||
}
|
||||
|
||||
public function testSignalHandlerWithMultipleHandlersInConstructor()
|
||||
{
|
||||
$callbacks = [
|
||||
function () use (&$handler1) { $handler1 = true; },
|
||||
function () use (&$handler2) { $handler2 = true; },
|
||||
function () use (&$handler3) { $handler3 = true; }
|
||||
];
|
||||
|
||||
$handler = new SignalHandler(SIGUSR1, $callbacks);
|
||||
|
||||
posix_kill(posix_getpid(), SIGUSR1);
|
||||
|
||||
usleep(1000);
|
||||
|
||||
$this->assertEquals(true, $handler1);
|
||||
$this->assertEquals(true, $handler2);
|
||||
$this->assertEquals(true, $handler3);
|
||||
}
|
||||
|
||||
public function testSignalHandlerWithAddedHandlers()
|
||||
{
|
||||
$callbacks = [
|
||||
function () use (&$handler1) { $handler1 = true; },
|
||||
function () use (&$handler2) { $handler2 = true; }
|
||||
];
|
||||
|
||||
$handler = new SignalHandler(SIGUSR1, $callbacks);
|
||||
|
||||
$handler->addHandler(function () use (&$handler3) { $handler3 = true; });
|
||||
|
||||
posix_kill(posix_getpid(), SIGUSR1);
|
||||
|
||||
usleep(1000);
|
||||
|
||||
$this->assertEquals(true, $handler1);
|
||||
$this->assertEquals(true, $handler2);
|
||||
$this->assertEquals(true, $handler3);
|
||||
}
|
||||
|
||||
public function testSignalHandlerWithBlockingHandler()
|
||||
{
|
||||
$callbacks = [
|
||||
function () use (&$handler1) { $handler1 = true; return true; },
|
||||
function () use (&$handler2) { $handler2 = true; },
|
||||
function () use (&$handler3) { $handler3 = true; }
|
||||
];
|
||||
|
||||
$handler = new SignalHandler(SIGUSR1, $callbacks);
|
||||
|
||||
posix_kill(posix_getpid(), SIGUSR1);
|
||||
|
||||
usleep(1000);
|
||||
|
||||
$this->assertEquals(true, $handler1);
|
||||
$this->assertEquals(false, $handler2);
|
||||
$this->assertEquals(false, $handler3);
|
||||
}
|
||||
|
||||
}
|
40
tests/Signal/SignalTest.php
Normal file
40
tests/Signal/SignalTest.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Ipc\Signal;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class SignalTest extends \PhpUnit\Framework\TestCase
|
||||
{
|
||||
|
||||
public function testSimpleSignalHandler()
|
||||
{
|
||||
$callback = function () use (&$handler1) { $handler1 = true; };
|
||||
|
||||
$handler = new Signal(SIGUSR1);
|
||||
$handler->setHandler($callback);
|
||||
|
||||
posix_kill(posix_getpid(), SIGUSR1);
|
||||
|
||||
usleep(1000);
|
||||
|
||||
$this->assertEquals(true, $handler1);
|
||||
}
|
||||
|
||||
public function testSimpleSignalDispatcher()
|
||||
{
|
||||
$callback = function () use (&$handler1) { $handler1 = true; };
|
||||
|
||||
$handler = new Signal(SIGUSR1);
|
||||
$handler->setHandler($callback);
|
||||
|
||||
$handler->dispatch(posix_getpid());
|
||||
|
||||
usleep(1000);
|
||||
|
||||
$this->assertEquals(true, $handler1);
|
||||
}
|
||||
|
||||
}
|
42
tests/Signal/SignalTrapTest.php
Normal file
42
tests/Signal/SignalTrapTest.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Ipc\Signal;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class SignalTrapTest extends \PhpUnit\Framework\TestCase
|
||||
{
|
||||
|
||||
public function testSignalTrap()
|
||||
{
|
||||
$trap = new SignalTrap(SIGUSR1);
|
||||
|
||||
$this->assertEquals(false, $trap->isTrapped());
|
||||
|
||||
posix_kill(posix_getpid(), SIGUSR1);
|
||||
|
||||
usleep(1000);
|
||||
|
||||
$this->assertEquals(true, $trap->isTrapped());
|
||||
|
||||
$this->assertEquals(false, $trap->isTrapped());
|
||||
}
|
||||
|
||||
public function testSignalTrapWithoutResetting()
|
||||
{
|
||||
$trap = new SignalTrap(SIGUSR1);
|
||||
|
||||
$this->assertEquals(false, $trap->isTrapped());
|
||||
|
||||
posix_kill(posix_getpid(), SIGUSR1);
|
||||
|
||||
usleep(1000);
|
||||
|
||||
$this->assertEquals(true, $trap->isTrapped(false));
|
||||
|
||||
$this->assertEquals(true, $trap->isTrapped());
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user