init
This commit is contained in:
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
// autoload.php @generated by Composer
|
||||
|
||||
if (PHP_VERSION_ID < 50600) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, $err);
|
||||
} elseif (!headers_sent()) {
|
||||
echo $err;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException($err);
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/composer/autoload_real.php';
|
||||
|
||||
return ComposerAutoloaderInit54847d6030d29731b0e767d050d22a36::getLoader();
|
||||
Vendored
+579
@@ -0,0 +1,579 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\Autoload;
|
||||
|
||||
/**
|
||||
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
|
||||
*
|
||||
* $loader = new \Composer\Autoload\ClassLoader();
|
||||
*
|
||||
* // register classes with namespaces
|
||||
* $loader->add('Symfony\Component', __DIR__.'/component');
|
||||
* $loader->add('Symfony', __DIR__.'/framework');
|
||||
*
|
||||
* // activate the autoloader
|
||||
* $loader->register();
|
||||
*
|
||||
* // to enable searching the include path (eg. for PEAR packages)
|
||||
* $loader->setUseIncludePath(true);
|
||||
*
|
||||
* In this example, if you try to use a class in the Symfony\Component
|
||||
* namespace or one of its children (Symfony\Component\Console for instance),
|
||||
* the autoloader will first look for the class under the component/
|
||||
* directory, and it will then fallback to the framework/ directory if not
|
||||
* found before giving up.
|
||||
*
|
||||
* This class is loosely based on the Symfony UniversalClassLoader.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
* @see https://www.php-fig.org/psr/psr-0/
|
||||
* @see https://www.php-fig.org/psr/psr-4/
|
||||
*/
|
||||
class ClassLoader
|
||||
{
|
||||
/** @var \Closure(string):void */
|
||||
private static $includeFile;
|
||||
|
||||
/** @var string|null */
|
||||
private $vendorDir;
|
||||
|
||||
// PSR-4
|
||||
/**
|
||||
* @var array<string, array<string, int>>
|
||||
*/
|
||||
private $prefixLengthsPsr4 = array();
|
||||
/**
|
||||
* @var array<string, list<string>>
|
||||
*/
|
||||
private $prefixDirsPsr4 = array();
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private $fallbackDirsPsr4 = array();
|
||||
|
||||
// PSR-0
|
||||
/**
|
||||
* List of PSR-0 prefixes
|
||||
*
|
||||
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
|
||||
*
|
||||
* @var array<string, array<string, list<string>>>
|
||||
*/
|
||||
private $prefixesPsr0 = array();
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private $fallbackDirsPsr0 = array();
|
||||
|
||||
/** @var bool */
|
||||
private $useIncludePath = false;
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private $classMap = array();
|
||||
|
||||
/** @var bool */
|
||||
private $classMapAuthoritative = false;
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
private $missingClasses = array();
|
||||
|
||||
/** @var string|null */
|
||||
private $apcuPrefix;
|
||||
|
||||
/**
|
||||
* @var array<string, self>
|
||||
*/
|
||||
private static $registeredLoaders = array();
|
||||
|
||||
/**
|
||||
* @param string|null $vendorDir
|
||||
*/
|
||||
public function __construct($vendorDir = null)
|
||||
{
|
||||
$this->vendorDir = $vendorDir;
|
||||
self::initializeIncludeClosure();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, list<string>>
|
||||
*/
|
||||
public function getPrefixes()
|
||||
{
|
||||
if (!empty($this->prefixesPsr0)) {
|
||||
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, list<string>>
|
||||
*/
|
||||
public function getPrefixesPsr4()
|
||||
{
|
||||
return $this->prefixDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getFallbackDirs()
|
||||
{
|
||||
return $this->fallbackDirsPsr0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getFallbackDirsPsr4()
|
||||
{
|
||||
return $this->fallbackDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string> Array of classname => path
|
||||
*/
|
||||
public function getClassMap()
|
||||
{
|
||||
return $this->classMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $classMap Class to filename map
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addClassMap(array $classMap)
|
||||
{
|
||||
if ($this->classMap) {
|
||||
$this->classMap = array_merge($this->classMap, $classMap);
|
||||
} else {
|
||||
$this->classMap = $classMap;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-0 directories for a given prefix, either
|
||||
* appending or prepending to the ones previously set for this prefix.
|
||||
*
|
||||
* @param string $prefix The prefix
|
||||
* @param list<string>|string $paths The PSR-0 root directories
|
||||
* @param bool $prepend Whether to prepend the directories
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add($prefix, $paths, $prepend = false)
|
||||
{
|
||||
$paths = (array) $paths;
|
||||
if (!$prefix) {
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
$paths,
|
||||
$this->fallbackDirsPsr0
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
$this->fallbackDirsPsr0,
|
||||
$paths
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$first = $prefix[0];
|
||||
if (!isset($this->prefixesPsr0[$first][$prefix])) {
|
||||
$this->prefixesPsr0[$first][$prefix] = $paths;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($prepend) {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
$paths,
|
||||
$this->prefixesPsr0[$first][$prefix]
|
||||
);
|
||||
} else {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
$this->prefixesPsr0[$first][$prefix],
|
||||
$paths
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-4 directories for a given namespace, either
|
||||
* appending or prepending to the ones previously set for this namespace.
|
||||
*
|
||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||
* @param list<string>|string $paths The PSR-4 base directories
|
||||
* @param bool $prepend Whether to prepend the directories
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addPsr4($prefix, $paths, $prepend = false)
|
||||
{
|
||||
$paths = (array) $paths;
|
||||
if (!$prefix) {
|
||||
// Register directories for the root namespace.
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
$paths,
|
||||
$this->fallbackDirsPsr4
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
$this->fallbackDirsPsr4,
|
||||
$paths
|
||||
);
|
||||
}
|
||||
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
|
||||
// Register directories for a new namespace.
|
||||
$length = strlen($prefix);
|
||||
if ('\\' !== $prefix[$length - 1]) {
|
||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||
}
|
||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||
$this->prefixDirsPsr4[$prefix] = $paths;
|
||||
} elseif ($prepend) {
|
||||
// Prepend directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
$paths,
|
||||
$this->prefixDirsPsr4[$prefix]
|
||||
);
|
||||
} else {
|
||||
// Append directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
$this->prefixDirsPsr4[$prefix],
|
||||
$paths
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-0 directories for a given prefix,
|
||||
* replacing any others previously set for this prefix.
|
||||
*
|
||||
* @param string $prefix The prefix
|
||||
* @param list<string>|string $paths The PSR-0 base directories
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set($prefix, $paths)
|
||||
{
|
||||
if (!$prefix) {
|
||||
$this->fallbackDirsPsr0 = (array) $paths;
|
||||
} else {
|
||||
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-4 directories for a given namespace,
|
||||
* replacing any others previously set for this namespace.
|
||||
*
|
||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||
* @param list<string>|string $paths The PSR-4 base directories
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setPsr4($prefix, $paths)
|
||||
{
|
||||
if (!$prefix) {
|
||||
$this->fallbackDirsPsr4 = (array) $paths;
|
||||
} else {
|
||||
$length = strlen($prefix);
|
||||
if ('\\' !== $prefix[$length - 1]) {
|
||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||
}
|
||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||
$this->prefixDirsPsr4[$prefix] = (array) $paths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns on searching the include path for class files.
|
||||
*
|
||||
* @param bool $useIncludePath
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setUseIncludePath($useIncludePath)
|
||||
{
|
||||
$this->useIncludePath = $useIncludePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to check if the autoloader uses the include path to check
|
||||
* for classes.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getUseIncludePath()
|
||||
{
|
||||
return $this->useIncludePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns off searching the prefix and fallback directories for classes
|
||||
* that have not been registered with the class map.
|
||||
*
|
||||
* @param bool $classMapAuthoritative
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setClassMapAuthoritative($classMapAuthoritative)
|
||||
{
|
||||
$this->classMapAuthoritative = $classMapAuthoritative;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should class lookup fail if not found in the current class map?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isClassMapAuthoritative()
|
||||
{
|
||||
return $this->classMapAuthoritative;
|
||||
}
|
||||
|
||||
/**
|
||||
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
|
||||
*
|
||||
* @param string|null $apcuPrefix
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setApcuPrefix($apcuPrefix)
|
||||
{
|
||||
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The APCu prefix in use, or null if APCu caching is not enabled.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getApcuPrefix()
|
||||
{
|
||||
return $this->apcuPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers this instance as an autoloader.
|
||||
*
|
||||
* @param bool $prepend Whether to prepend the autoloader or not
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register($prepend = false)
|
||||
{
|
||||
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
|
||||
|
||||
if (null === $this->vendorDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($prepend) {
|
||||
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
|
||||
} else {
|
||||
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||
self::$registeredLoaders[$this->vendorDir] = $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters this instance as an autoloader.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function unregister()
|
||||
{
|
||||
spl_autoload_unregister(array($this, 'loadClass'));
|
||||
|
||||
if (null !== $this->vendorDir) {
|
||||
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the given class or interface.
|
||||
*
|
||||
* @param string $class The name of the class
|
||||
* @return true|null True if loaded, null otherwise
|
||||
*/
|
||||
public function loadClass($class)
|
||||
{
|
||||
if ($file = $this->findFile($class)) {
|
||||
$includeFile = self::$includeFile;
|
||||
$includeFile($file);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the path to the file where the class is defined.
|
||||
*
|
||||
* @param string $class The name of the class
|
||||
*
|
||||
* @return string|false The path if found, false otherwise
|
||||
*/
|
||||
public function findFile($class)
|
||||
{
|
||||
// class map lookup
|
||||
if (isset($this->classMap[$class])) {
|
||||
return $this->classMap[$class];
|
||||
}
|
||||
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
|
||||
return false;
|
||||
}
|
||||
if (null !== $this->apcuPrefix) {
|
||||
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
|
||||
if ($hit) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
$file = $this->findFileWithExtension($class, '.php');
|
||||
|
||||
// Search for Hack files if we are running on HHVM
|
||||
if (false === $file && defined('HHVM_VERSION')) {
|
||||
$file = $this->findFileWithExtension($class, '.hh');
|
||||
}
|
||||
|
||||
if (null !== $this->apcuPrefix) {
|
||||
apcu_add($this->apcuPrefix.$class, $file);
|
||||
}
|
||||
|
||||
if (false === $file) {
|
||||
// Remember that this class does not exist.
|
||||
$this->missingClasses[$class] = true;
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently registered loaders keyed by their corresponding vendor directories.
|
||||
*
|
||||
* @return array<string, self>
|
||||
*/
|
||||
public static function getRegisteredLoaders()
|
||||
{
|
||||
return self::$registeredLoaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $class
|
||||
* @param string $ext
|
||||
* @return string|false
|
||||
*/
|
||||
private function findFileWithExtension($class, $ext)
|
||||
{
|
||||
// PSR-4 lookup
|
||||
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
|
||||
|
||||
$first = $class[0];
|
||||
if (isset($this->prefixLengthsPsr4[$first])) {
|
||||
$subPath = $class;
|
||||
while (false !== $lastPos = strrpos($subPath, '\\')) {
|
||||
$subPath = substr($subPath, 0, $lastPos);
|
||||
$search = $subPath . '\\';
|
||||
if (isset($this->prefixDirsPsr4[$search])) {
|
||||
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
|
||||
foreach ($this->prefixDirsPsr4[$search] as $dir) {
|
||||
if (file_exists($file = $dir . $pathEnd)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-4 fallback dirs
|
||||
foreach ($this->fallbackDirsPsr4 as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 lookup
|
||||
if (false !== $pos = strrpos($class, '\\')) {
|
||||
// namespaced class name
|
||||
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
|
||||
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
|
||||
} else {
|
||||
// PEAR-like class name
|
||||
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
|
||||
}
|
||||
|
||||
if (isset($this->prefixesPsr0[$first])) {
|
||||
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
|
||||
if (0 === strpos($class, $prefix)) {
|
||||
foreach ($dirs as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 fallback dirs
|
||||
foreach ($this->fallbackDirsPsr0 as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 include paths.
|
||||
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private static function initializeIncludeClosure()
|
||||
{
|
||||
if (self::$includeFile !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope isolated include.
|
||||
*
|
||||
* Prevents access to $this/self from included files.
|
||||
*
|
||||
* @param string $file
|
||||
* @return void
|
||||
*/
|
||||
self::$includeFile = \Closure::bind(static function($file) {
|
||||
include $file;
|
||||
}, null, null);
|
||||
}
|
||||
}
|
||||
+396
@@ -0,0 +1,396 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
use Composer\Autoload\ClassLoader;
|
||||
use Composer\Semver\VersionParser;
|
||||
|
||||
/**
|
||||
* This class is copied in every Composer installed project and available to all
|
||||
*
|
||||
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
|
||||
*
|
||||
* To require its presence, you can require `composer-runtime-api ^2.0`
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class InstalledVersions
|
||||
{
|
||||
/**
|
||||
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
|
||||
* @internal
|
||||
*/
|
||||
private static $selfDir = null;
|
||||
|
||||
/**
|
||||
* @var mixed[]|null
|
||||
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
|
||||
*/
|
||||
private static $installed;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private static $installedIsLocalDir;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
private static $canGetVendors;
|
||||
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
private static $installedByVendor = array();
|
||||
|
||||
/**
|
||||
* Returns a list of all package names which are present, either by being installed, replaced or provided
|
||||
*
|
||||
* @return string[]
|
||||
* @psalm-return list<string>
|
||||
*/
|
||||
public static function getInstalledPackages()
|
||||
{
|
||||
$packages = array();
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
$packages[] = array_keys($installed['versions']);
|
||||
}
|
||||
|
||||
if (1 === \count($packages)) {
|
||||
return $packages[0];
|
||||
}
|
||||
|
||||
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all package names with a specific type e.g. 'library'
|
||||
*
|
||||
* @param string $type
|
||||
* @return string[]
|
||||
* @psalm-return list<string>
|
||||
*/
|
||||
public static function getInstalledPackagesByType($type)
|
||||
{
|
||||
$packagesByType = array();
|
||||
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
foreach ($installed['versions'] as $name => $package) {
|
||||
if (isset($package['type']) && $package['type'] === $type) {
|
||||
$packagesByType[] = $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $packagesByType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given package is installed
|
||||
*
|
||||
* This also returns true if the package name is provided or replaced by another package
|
||||
*
|
||||
* @param string $packageName
|
||||
* @param bool $includeDevRequirements
|
||||
* @return bool
|
||||
*/
|
||||
public static function isInstalled($packageName, $includeDevRequirements = true)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (isset($installed['versions'][$packageName])) {
|
||||
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given package satisfies a version constraint
|
||||
*
|
||||
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
|
||||
*
|
||||
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
|
||||
*
|
||||
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
|
||||
* @param string $packageName
|
||||
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
|
||||
* @return bool
|
||||
*/
|
||||
public static function satisfies(VersionParser $parser, $packageName, $constraint)
|
||||
{
|
||||
$constraint = $parser->parseConstraints((string) $constraint);
|
||||
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
|
||||
|
||||
return $provided->matches($constraint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a version constraint representing all the range(s) which are installed for a given package
|
||||
*
|
||||
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
|
||||
* whether a given version of a package is installed, and not just whether it exists
|
||||
*
|
||||
* @param string $packageName
|
||||
* @return string Version constraint usable with composer/semver
|
||||
*/
|
||||
public static function getVersionRanges($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ranges = array();
|
||||
if (isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
|
||||
}
|
||||
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
|
||||
}
|
||||
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
|
||||
}
|
||||
if (array_key_exists('provided', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
|
||||
}
|
||||
|
||||
return implode(' || ', $ranges);
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||
*/
|
||||
public static function getVersion($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['version'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['version'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||
*/
|
||||
public static function getPrettyVersion($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['pretty_version'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
|
||||
*/
|
||||
public static function getReference($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['reference'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['reference'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
|
||||
*/
|
||||
public static function getInstallPath($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
|
||||
*/
|
||||
public static function getRootPackage()
|
||||
{
|
||||
$installed = self::getInstalled();
|
||||
|
||||
return $installed[0]['root'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw installed.php data for custom implementations
|
||||
*
|
||||
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
|
||||
* @return array[]
|
||||
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
|
||||
*/
|
||||
public static function getRawData()
|
||||
{
|
||||
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
|
||||
|
||||
if (null === self::$installed) {
|
||||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||
self::$installed = include __DIR__ . '/installed.php';
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
|
||||
return self::$installed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw data of all installed.php which are currently loaded for custom implementations
|
||||
*
|
||||
* @return array[]
|
||||
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
public static function getAllRawData()
|
||||
{
|
||||
return self::getInstalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lets you reload the static array from another file
|
||||
*
|
||||
* This is only useful for complex integrations in which a project needs to use
|
||||
* this class but then also needs to execute another project's autoloader in process,
|
||||
* and wants to ensure both projects have access to their version of installed.php.
|
||||
*
|
||||
* A typical case would be PHPUnit, where it would need to make sure it reads all
|
||||
* the data it needs from this class, then call reload() with
|
||||
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
|
||||
* the project in which it runs can then also use this class safely, without
|
||||
* interference between PHPUnit's dependencies and the project's dependencies.
|
||||
*
|
||||
* @param array[] $data A vendor/composer/installed.php data set
|
||||
* @return void
|
||||
*
|
||||
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
|
||||
*/
|
||||
public static function reload($data)
|
||||
{
|
||||
self::$installed = $data;
|
||||
self::$installedByVendor = array();
|
||||
|
||||
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
|
||||
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
|
||||
// so we have to assume it does not, and that may result in duplicate data being returned when listing
|
||||
// all installed packages for example
|
||||
self::$installedIsLocalDir = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private static function getSelfDir()
|
||||
{
|
||||
if (self::$selfDir === null) {
|
||||
self::$selfDir = strtr(__DIR__, '\\', '/');
|
||||
}
|
||||
|
||||
return self::$selfDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
private static function getInstalled()
|
||||
{
|
||||
if (null === self::$canGetVendors) {
|
||||
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
|
||||
}
|
||||
|
||||
$installed = array();
|
||||
$copiedLocalDir = false;
|
||||
|
||||
if (self::$canGetVendors) {
|
||||
$selfDir = self::getSelfDir();
|
||||
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
|
||||
$vendorDir = strtr($vendorDir, '\\', '/');
|
||||
if (isset(self::$installedByVendor[$vendorDir])) {
|
||||
$installed[] = self::$installedByVendor[$vendorDir];
|
||||
} elseif (is_file($vendorDir.'/composer/installed.php')) {
|
||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||
$required = require $vendorDir.'/composer/installed.php';
|
||||
self::$installedByVendor[$vendorDir] = $required;
|
||||
$installed[] = $required;
|
||||
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
|
||||
self::$installed = $required;
|
||||
self::$installedIsLocalDir = true;
|
||||
}
|
||||
}
|
||||
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
|
||||
$copiedLocalDir = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (null === self::$installed) {
|
||||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||
$required = require __DIR__ . '/installed.php';
|
||||
self::$installed = $required;
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
|
||||
if (self::$installed !== array() && !$copiedLocalDir) {
|
||||
$installed[] = self::$installed;
|
||||
}
|
||||
|
||||
return $installed;
|
||||
}
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
|
||||
Copyright (c) Nils Adermann, Jordi Boggiano
|
||||
|
||||
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.
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// autoload_classmap.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
|
||||
);
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
// autoload_namespaces.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
);
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
// autoload_psr4.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'Workerman\\Coroutine\\' => array($vendorDir . '/workerman/coroutine/src'),
|
||||
'Workerman\\' => array($vendorDir . '/workerman/workerman/src', $vendorDir . '/workerman/coroutine/src'),
|
||||
'LayLink\\' => array($baseDir . '/src'),
|
||||
);
|
||||
Vendored
+38
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
// autoload_real.php @generated by Composer
|
||||
|
||||
class ComposerAutoloaderInit54847d6030d29731b0e767d050d22a36
|
||||
{
|
||||
private static $loader;
|
||||
|
||||
public static function loadClassLoader($class)
|
||||
{
|
||||
if ('Composer\Autoload\ClassLoader' === $class) {
|
||||
require __DIR__ . '/ClassLoader.php';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Composer\Autoload\ClassLoader
|
||||
*/
|
||||
public static function getLoader()
|
||||
{
|
||||
if (null !== self::$loader) {
|
||||
return self::$loader;
|
||||
}
|
||||
|
||||
require __DIR__ . '/platform_check.php';
|
||||
|
||||
spl_autoload_register(array('ComposerAutoloaderInit54847d6030d29731b0e767d050d22a36', 'loadClassLoader'), true, true);
|
||||
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
|
||||
spl_autoload_unregister(array('ComposerAutoloaderInit54847d6030d29731b0e767d050d22a36', 'loadClassLoader'));
|
||||
|
||||
require __DIR__ . '/autoload_static.php';
|
||||
call_user_func(\Composer\Autoload\ComposerStaticInit54847d6030d29731b0e767d050d22a36::getInitializer($loader));
|
||||
|
||||
$loader->register(true);
|
||||
|
||||
return $loader;
|
||||
}
|
||||
}
|
||||
Vendored
+50
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
// autoload_static.php @generated by Composer
|
||||
|
||||
namespace Composer\Autoload;
|
||||
|
||||
class ComposerStaticInit54847d6030d29731b0e767d050d22a36
|
||||
{
|
||||
public static $prefixLengthsPsr4 = array (
|
||||
'W' =>
|
||||
array (
|
||||
'Workerman\\Coroutine\\' => 20,
|
||||
'Workerman\\' => 10,
|
||||
),
|
||||
'L' =>
|
||||
array (
|
||||
'LayLink\\' => 8,
|
||||
),
|
||||
);
|
||||
|
||||
public static $prefixDirsPsr4 = array (
|
||||
'Workerman\\Coroutine\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/workerman/coroutine/src',
|
||||
),
|
||||
'Workerman\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/workerman/workerman/src',
|
||||
1 => __DIR__ . '/..' . '/workerman/coroutine/src',
|
||||
),
|
||||
'LayLink\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/../..' . '/src',
|
||||
),
|
||||
);
|
||||
|
||||
public static $classMap = array (
|
||||
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
|
||||
);
|
||||
|
||||
public static function getInitializer(ClassLoader $loader)
|
||||
{
|
||||
return \Closure::bind(function () use ($loader) {
|
||||
$loader->prefixLengthsPsr4 = ComposerStaticInit54847d6030d29731b0e767d050d22a36::$prefixLengthsPsr4;
|
||||
$loader->prefixDirsPsr4 = ComposerStaticInit54847d6030d29731b0e767d050d22a36::$prefixDirsPsr4;
|
||||
$loader->classMap = ComposerStaticInit54847d6030d29731b0e767d050d22a36::$classMap;
|
||||
|
||||
}, null, ClassLoader::class);
|
||||
}
|
||||
}
|
||||
Vendored
+128
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"packages": [
|
||||
{
|
||||
"name": "workerman/coroutine",
|
||||
"version": "v1.1.5",
|
||||
"version_normalized": "1.1.5.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/workerman-php/coroutine.git",
|
||||
"reference": "b60e44267b90d398dbfa7a320f3e97b46357ac9f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/workerman-php/coroutine/zipball/b60e44267b90d398dbfa7a320f3e97b46357ac9f",
|
||||
"reference": "b60e44267b90d398dbfa7a320f3e97b46357ac9f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"workerman/workerman": "^5.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^11.0",
|
||||
"psr/log": "*"
|
||||
},
|
||||
"time": "2026-03-12T02:07:37+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Workerman\\": "src",
|
||||
"Workerman\\Coroutine\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "Workerman coroutine",
|
||||
"support": {
|
||||
"issues": "https://github.com/workerman-php/coroutine/issues",
|
||||
"source": "https://github.com/workerman-php/coroutine/tree/v1.1.5"
|
||||
},
|
||||
"install-path": "../workerman/coroutine"
|
||||
},
|
||||
{
|
||||
"name": "workerman/workerman",
|
||||
"version": "v5.2.0",
|
||||
"version_normalized": "5.2.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/walkor/workerman.git",
|
||||
"reference": "1d8694c945bc64a5bc11ad753ec7220bcba37cb1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/walkor/workerman/zipball/1d8694c945bc64a5bc11ad753ec7220bcba37cb1",
|
||||
"reference": "1d8694c945bc64a5bc11ad753ec7220bcba37cb1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"php": ">=8.1",
|
||||
"workerman/coroutine": "^1.1 || dev-main"
|
||||
},
|
||||
"conflict": {
|
||||
"ext-swow": "<v1.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"guzzlehttp/guzzle": "^7.10",
|
||||
"mockery/mockery": "^1.6",
|
||||
"pestphp/pest": "^2.36 || ^3 || ^4",
|
||||
"phpstan/phpstan": "^2.1"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-event": "For better performance. "
|
||||
},
|
||||
"time": "2026-05-05T14:33:37+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Workerman\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "walkor",
|
||||
"email": "walkor@workerman.net",
|
||||
"homepage": "https://www.workerman.net",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "An asynchronous event driven PHP framework for easily building fast, scalable network applications.",
|
||||
"homepage": "https://www.workerman.net",
|
||||
"keywords": [
|
||||
"asynchronous",
|
||||
"event-loop",
|
||||
"framework",
|
||||
"http"
|
||||
],
|
||||
"support": {
|
||||
"email": "walkor@workerman.net",
|
||||
"forum": "https://www.workerman.net/questions",
|
||||
"issues": "https://github.com/walkor/workerman/issues",
|
||||
"source": "https://github.com/walkor/workerman",
|
||||
"wiki": "https://www.workerman.net/doc/workerman/"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://opencollective.com/workerman",
|
||||
"type": "open_collective"
|
||||
},
|
||||
{
|
||||
"url": "https://www.patreon.com/walkor",
|
||||
"type": "patreon"
|
||||
}
|
||||
],
|
||||
"install-path": "../workerman/workerman"
|
||||
}
|
||||
],
|
||||
"dev": true,
|
||||
"dev-package-names": []
|
||||
}
|
||||
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
<?php return array(
|
||||
'root' => array(
|
||||
'name' => '__root__',
|
||||
'pretty_version' => '1.0.0+no-version-set',
|
||||
'version' => '1.0.0.0',
|
||||
'reference' => null,
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'dev' => true,
|
||||
),
|
||||
'versions' => array(
|
||||
'__root__' => array(
|
||||
'pretty_version' => '1.0.0+no-version-set',
|
||||
'version' => '1.0.0.0',
|
||||
'reference' => null,
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'workerman/coroutine' => array(
|
||||
'pretty_version' => 'v1.1.5',
|
||||
'version' => '1.1.5.0',
|
||||
'reference' => 'b60e44267b90d398dbfa7a320f3e97b46357ac9f',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../workerman/coroutine',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'workerman/workerman' => array(
|
||||
'pretty_version' => 'v5.2.0',
|
||||
'version' => '5.2.0.0',
|
||||
'reference' => '1d8694c945bc64a5bc11ad753ec7220bcba37cb1',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../workerman/workerman',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
),
|
||||
);
|
||||
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
// platform_check.php @generated by Composer
|
||||
|
||||
$issues = array();
|
||||
|
||||
if (!(PHP_VERSION_ID >= 80100)) {
|
||||
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.';
|
||||
}
|
||||
|
||||
if ($issues) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
|
||||
} elseif (!headers_sent()) {
|
||||
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
|
||||
}
|
||||
}
|
||||
throw new \RuntimeException(
|
||||
'Composer detected issues in your platform: ' . implode(' ', $issues)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
composer.lock
|
||||
vendor
|
||||
.idea
|
||||
tests/.phpunit.result.cache
|
||||
tests/workerman.log
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 workerman
|
||||
|
||||
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.
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
# Workerman coroutine library
|
||||
|
||||
This is Workerman's coroutine library, which includes `Coroutine` `Channel` `Barrier` `Parallel` `Pool`.
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "workerman/coroutine",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"description": "Workerman coroutine",
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"workerman/workerman": "^5.1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Workerman\\Coroutine\\": "src",
|
||||
"Workerman\\": "src"
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^11.0",
|
||||
"psr/log": "*"
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Workerman\\Coroutine\\": "src",
|
||||
"Workerman\\": "src",
|
||||
"tests\\": "tests"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "php tests/start.php start"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Coroutine;
|
||||
|
||||
use Workerman\Coroutine\Barrier\BarrierInterface;
|
||||
use Workerman\Events\Swoole;
|
||||
use Workerman\Events\Swow;
|
||||
use Workerman\Worker;
|
||||
|
||||
/**
|
||||
* Class Barrier
|
||||
*/
|
||||
class Barrier implements BarrierInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected static string $driver;
|
||||
|
||||
/**
|
||||
* Get driver.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getDriver(): string
|
||||
{
|
||||
return static::$driver ??= match (Worker::$eventLoopClass) {
|
||||
Swoole::class => Barrier\Swoole::class,
|
||||
Swow::class => Barrier\Swow::class,
|
||||
default=> Barrier\Fiber::class,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function wait(object &$barrier, int $timeout = -1): void
|
||||
{
|
||||
static::getDriver()::wait($barrier, $timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function create(): object
|
||||
{
|
||||
return static::getDriver()::create();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Coroutine\Barrier;
|
||||
|
||||
/**
|
||||
* Interface BarrierInterface
|
||||
*/
|
||||
interface BarrierInterface
|
||||
{
|
||||
/**
|
||||
* Wait for the barrier to be released.
|
||||
*
|
||||
* @param object $barrier
|
||||
* @param int $timeout
|
||||
* @return void
|
||||
*/
|
||||
public static function wait(object &$barrier, int $timeout = -1): void;
|
||||
|
||||
/**
|
||||
* Create a new barrier instance.
|
||||
*
|
||||
* @return BarrierInterface
|
||||
*/
|
||||
public static function create(): object;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Coroutine\Barrier;
|
||||
|
||||
use Revolt\EventLoop;
|
||||
use RuntimeException;
|
||||
use Workerman\Coroutine\Utils\DestructionWatcher;
|
||||
use Workerman\Timer;
|
||||
use Fiber as BaseFiber;
|
||||
use Workerman\Worker;
|
||||
|
||||
/**
|
||||
* Class Fiber
|
||||
*/
|
||||
class Fiber implements BarrierInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function wait(object &$barrier, int $timeout = -1): void
|
||||
{
|
||||
$coroutine = BaseFiber::getCurrent();
|
||||
$resumed = false;
|
||||
$timerId = null;
|
||||
|
||||
if ($timeout > 0 && $coroutine) {
|
||||
$timerId = Timer::delay($timeout, function() use ($coroutine, &$resumed) {
|
||||
if (!$resumed) {
|
||||
$resumed = true;
|
||||
$coroutine->resume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$coroutine && DestructionWatcher::watch($barrier, function() use ($coroutine, &$resumed, &$timerId) {
|
||||
if (!$resumed) {
|
||||
$resumed = true;
|
||||
if ($timerId !== null) {
|
||||
Timer::del($timerId);
|
||||
}
|
||||
// In PHP 8.4.0 and earlier,
|
||||
// switching fibers during the execution of an object's destructor method is not allowed,
|
||||
// so we implemented a delay.
|
||||
if ($coroutine instanceof BaseFiber) {
|
||||
Timer::delay(0.00001, function() use ($coroutine) {
|
||||
$coroutine->resume();
|
||||
});
|
||||
return;
|
||||
}
|
||||
EventLoop::defer(function () use ($coroutine) {
|
||||
$coroutine->resume();
|
||||
});
|
||||
}
|
||||
});
|
||||
$barrier = null;
|
||||
$coroutine && BaseFiber::suspend();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function create(): object
|
||||
{
|
||||
if (!Worker::isRunning()) {
|
||||
throw new RuntimeException('Fiber barrier only support in workerman runtime');
|
||||
}
|
||||
return new self();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Coroutine\Barrier;
|
||||
|
||||
use Swoole\Coroutine\Barrier as SwooleBarrier;
|
||||
class Swoole implements BarrierInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function wait(object &$barrier, int $timeout = -1): void
|
||||
{
|
||||
SwooleBarrier::wait($barrier, $timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function create(): object
|
||||
{
|
||||
return SwooleBarrier::make();
|
||||
}
|
||||
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Coroutine\Barrier;
|
||||
|
||||
use Swow\Sync\WaitReference;
|
||||
|
||||
class Swow implements BarrierInterface
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function wait(object &$barrier, int $timeout = -1): void
|
||||
{
|
||||
WaitReference::wait($barrier, $timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function create(): object
|
||||
{
|
||||
return new WaitReference();
|
||||
}
|
||||
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Coroutine;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Workerman\Coroutine\Channel\ChannelInterface;
|
||||
use Workerman\Coroutine\Channel\Memory as ChannelMemory;
|
||||
use Workerman\Coroutine\Channel\Swoole as ChannelSwoole;
|
||||
use Workerman\Coroutine\Channel\Swow as ChannelSwow;
|
||||
use Workerman\Coroutine\Channel\Fiber as ChannelFiber;
|
||||
use Workerman\Events\Fiber;
|
||||
use Workerman\Events\Swoole;
|
||||
use Workerman\Events\Swow;
|
||||
use Workerman\Worker;
|
||||
|
||||
/**
|
||||
* Class Channel
|
||||
*/
|
||||
class Channel implements ChannelInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @var ChannelInterface
|
||||
*/
|
||||
protected ChannelInterface $driver;
|
||||
|
||||
/**
|
||||
* Channel constructor.
|
||||
*
|
||||
* @param int $capacity
|
||||
*/
|
||||
public function __construct(int $capacity = 1)
|
||||
{
|
||||
if ($capacity < 1) {
|
||||
throw new InvalidArgumentException("The capacity must be greater than 0");
|
||||
}
|
||||
$this->driver = match (Worker::$eventLoopClass) {
|
||||
Swoole::class => new ChannelSwoole($capacity),
|
||||
Swow::class => new ChannelSwow($capacity),
|
||||
Fiber::class => new ChannelFiber($capacity),
|
||||
default => new ChannelMemory($capacity),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function push(mixed $data, float $timeout = -1): bool
|
||||
{
|
||||
return $this->driver->push($data, $timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function pop(float $timeout = -1): mixed
|
||||
{
|
||||
return $this->driver->pop($timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function length(): int
|
||||
{
|
||||
return $this->driver->length();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getCapacity(): int
|
||||
{
|
||||
return $this->driver->getCapacity();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function hasConsumers(): bool
|
||||
{
|
||||
return $this->driver->hasConsumers();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function hasProducers(): bool
|
||||
{
|
||||
return $this->driver->hasProducers();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function close(): void
|
||||
{
|
||||
$this->driver->close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Coroutine\Channel;
|
||||
|
||||
/**
|
||||
* ChannelInterface
|
||||
*/
|
||||
interface ChannelInterface
|
||||
{
|
||||
/**
|
||||
* Push data to channel.
|
||||
*
|
||||
* @param mixed $data
|
||||
* @param float $timeout
|
||||
* @return bool
|
||||
*/
|
||||
public function push(mixed $data, float $timeout = -1): bool;
|
||||
|
||||
/**
|
||||
* Pop data from channel.
|
||||
*
|
||||
* @param float $timeout
|
||||
* @return mixed
|
||||
*/
|
||||
public function pop(float $timeout = -1): mixed;
|
||||
|
||||
/**
|
||||
* Get the length of channel.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function length(): int;
|
||||
|
||||
/**
|
||||
* Get the capacity of channel.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getCapacity(): int;
|
||||
|
||||
/**
|
||||
* Check if there are consumers waiting to pop data from the channel.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasConsumers(): bool;
|
||||
|
||||
/**
|
||||
* Check if there are producers waiting to push data to the channel.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasProducers(): bool;
|
||||
|
||||
/**
|
||||
* Close the channel.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function close(): void;
|
||||
|
||||
}
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Coroutine\Channel;
|
||||
|
||||
use Fiber as BaseFiber;
|
||||
use RuntimeException;
|
||||
use Workerman\Timer;
|
||||
use WeakMap;
|
||||
use Workerman\Worker;
|
||||
|
||||
/**
|
||||
* Channel
|
||||
*/
|
||||
class Fiber implements ChannelInterface
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private array $queue = [];
|
||||
|
||||
/**
|
||||
* @var WeakMap
|
||||
*/
|
||||
private WeakMap $waitingPush;
|
||||
|
||||
/**
|
||||
* @var WeakMap
|
||||
*/
|
||||
private WeakMap $waitingPop;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private int $capacity;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private bool $closed = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param int $capacity
|
||||
*/
|
||||
public function __construct(int $capacity = 1)
|
||||
{
|
||||
$this->capacity = $capacity;
|
||||
$this->waitingPush = new WeakMap();
|
||||
$this->waitingPop = new WeakMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function push(mixed $data, float $timeout = -1): bool
|
||||
{
|
||||
if ($this->closed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (count($this->queue) >= $this->capacity) {
|
||||
|
||||
if ($timeout == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$fiber = BaseFiber::getCurrent();
|
||||
if ($fiber === null) {
|
||||
throw new RuntimeException("Fiber::getCurrent() returned null. Ensure this method is called within a Fiber context.");
|
||||
}
|
||||
|
||||
$this->waitingPush[$fiber] = true;
|
||||
|
||||
$timedOut = false;
|
||||
$timerId = null;
|
||||
if ($timeout > 0 && Worker::isRunning()) {
|
||||
$timerId = Timer::delay($timeout, function () use ($fiber, &$timedOut) {
|
||||
$timedOut = true;
|
||||
if ($fiber->isSuspended()) {
|
||||
unset($this->waitingPush[$fiber]);
|
||||
$fiber->resume(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
BaseFiber::suspend();
|
||||
unset($this->waitingPush[$fiber]);
|
||||
|
||||
if (!$timedOut && $timerId) {
|
||||
Timer::del($timerId);
|
||||
}
|
||||
|
||||
if ($timedOut) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the channel is closed while waiting, return false.
|
||||
if ($this->closed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
foreach ($this->waitingPop as $popFiber => $_) {
|
||||
unset($this->waitingPop[$popFiber]);
|
||||
if ($popFiber->isSuspended()) {
|
||||
$popFiber->resume($data);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$this->queue[] = $data;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function pop(float $timeout = -1): mixed
|
||||
{
|
||||
if ($this->closed && empty($this->queue)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($this->queue)) {
|
||||
if ($timeout == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$fiber = BaseFiber::getCurrent();
|
||||
if ($fiber === null) {
|
||||
throw new RuntimeException("Fiber::getCurrent() returned null. Ensure this method is called within a Fiber context.");
|
||||
}
|
||||
|
||||
$this->waitingPop[$fiber] = true;
|
||||
|
||||
$timedOut = false;
|
||||
$timerId = null;
|
||||
if ($timeout > 0) {
|
||||
Worker::isRunning() && $timerId = Timer::delay($timeout, function () use ($fiber, &$timedOut) {
|
||||
$timedOut = true;
|
||||
if ($fiber->isSuspended()) {
|
||||
unset($this->waitingPop[$fiber]);
|
||||
$fiber->resume(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$data = BaseFiber::suspend();
|
||||
|
||||
unset($this->waitingPop[$fiber]);
|
||||
|
||||
if (!$timedOut && $timerId !== null) {
|
||||
Timer::del($timerId);
|
||||
}
|
||||
|
||||
if ($timedOut) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($data === false && $this->closed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
$value = array_shift($this->queue);
|
||||
|
||||
foreach ($this->waitingPush as $pushFiber => $_) {
|
||||
unset($this->waitingPush[$pushFiber]);
|
||||
if ($pushFiber->isSuspended()) {
|
||||
$pushFiber->resume();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function length(): int
|
||||
{
|
||||
return count($this->queue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getCapacity(): int
|
||||
{
|
||||
return $this->capacity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function hasConsumers(): bool
|
||||
{
|
||||
return count($this->waitingPop) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function hasProducers(): bool
|
||||
{
|
||||
return count($this->waitingPush) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function close(): void
|
||||
{
|
||||
$this->closed = true;
|
||||
|
||||
foreach ($this->waitingPush as $fiber => $_) {
|
||||
unset($this->waitingPush[$fiber]);
|
||||
if ($fiber->isSuspended()) {
|
||||
$fiber->resume(false);
|
||||
}
|
||||
}
|
||||
$this->waitingPush = new WeakMap();
|
||||
|
||||
foreach ($this->waitingPop as $fiber => $_) {
|
||||
unset($this->waitingPop[$fiber]);
|
||||
if ($fiber->isSuspended()) {
|
||||
$fiber->resume(false);
|
||||
}
|
||||
}
|
||||
$this->waitingPop = new WeakMap();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Coroutine\Channel;
|
||||
|
||||
class Memory implements ChannelInterface
|
||||
{
|
||||
private array $data = [];
|
||||
private int $capacity;
|
||||
private bool $closed = false;
|
||||
|
||||
public function __construct(int $capacity = 0)
|
||||
{
|
||||
$this->capacity = $capacity;
|
||||
}
|
||||
|
||||
public function push(mixed $data, float $timeout = -1): bool
|
||||
{
|
||||
if ($this->closed) {
|
||||
return false;
|
||||
}
|
||||
if ($this->capacity > 0 && count($this->data) >= $this->capacity) {
|
||||
// Channel is full
|
||||
return false;
|
||||
}
|
||||
$this->data[] = $data;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function pop(float $timeout = -1): mixed
|
||||
{
|
||||
if (count($this->data) > 0) {
|
||||
return array_shift($this->data);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function length(): int
|
||||
{
|
||||
return count($this->data);
|
||||
}
|
||||
|
||||
public function getCapacity(): int
|
||||
{
|
||||
return $this->capacity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function hasConsumers(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function hasProducers(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function close(): void
|
||||
{
|
||||
$this->closed = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Coroutine\Channel;
|
||||
|
||||
use Swoole\Coroutine\Channel;
|
||||
|
||||
/**
|
||||
* Class Swoole
|
||||
*/
|
||||
class Swoole implements ChannelInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Channel
|
||||
*/
|
||||
protected Channel $channel;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param int $capacity
|
||||
*/
|
||||
public function __construct(protected int $capacity = 1)
|
||||
{
|
||||
$this->channel = new Channel($capacity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function push(mixed $data, float $timeout = -1): bool
|
||||
{
|
||||
return $this->channel->push($data, $timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function pop(float $timeout = -1): mixed
|
||||
{
|
||||
return $this->channel->pop($timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function length(): int
|
||||
{
|
||||
return $this->channel->length();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getCapacity(): int
|
||||
{
|
||||
return $this->channel->capacity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function hasConsumers(): bool
|
||||
{
|
||||
return $this->channel->stats()['consumer_num'] > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function hasProducers(): bool
|
||||
{
|
||||
return $this->channel->stats()['producer_num'] > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function close(): void
|
||||
{
|
||||
$this->channel->close();
|
||||
}
|
||||
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Coroutine\Channel;
|
||||
|
||||
use Swow\Channel;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Class Swow
|
||||
*/
|
||||
class Swow implements ChannelInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Channel
|
||||
*/
|
||||
protected Channel $channel;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param int $capacity
|
||||
*/
|
||||
public function __construct(protected int $capacity = 1)
|
||||
{
|
||||
$this->channel = new Channel($capacity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function push(mixed $data, float $timeout = -1): bool
|
||||
{
|
||||
try {
|
||||
$this->channel->push($data, $timeout == -1 ? -1 : (int)($timeout * 1000));
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function pop(float $timeout = -1): mixed
|
||||
{
|
||||
try {
|
||||
return $this->channel->pop($timeout == -1 ? -1 : (int)($timeout * 1000));
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function length(): int
|
||||
{
|
||||
return $this->channel->getLength();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getCapacity(): int
|
||||
{
|
||||
return $this->channel->getCapacity();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function hasConsumers(): bool
|
||||
{
|
||||
return $this->channel->hasConsumers();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function hasProducers(): bool
|
||||
{
|
||||
return $this->channel->hasProducers();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function close(): void
|
||||
{
|
||||
$this->channel->close();
|
||||
}
|
||||
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Coroutine;
|
||||
|
||||
use ArrayObject;
|
||||
use Workerman\Coroutine\Context\ContextInterface;
|
||||
use Workerman\Events\Swoole;
|
||||
use Workerman\Events\Swow;
|
||||
use Workerman\Worker;
|
||||
|
||||
/**
|
||||
* Class Context
|
||||
*/
|
||||
class Context implements ContextInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @var class-string<ContextInterface>
|
||||
*/
|
||||
protected static string $driver;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function get(?string $name = null, mixed $default = null): mixed
|
||||
{
|
||||
return static::$driver::get($name, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function set(string $name, $value): void
|
||||
{
|
||||
static::$driver::set($name, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function has(string $name): bool
|
||||
{
|
||||
return static::$driver::has($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function reset(?ArrayObject $data = null): void
|
||||
{
|
||||
static::$driver::reset($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function destroy(): void
|
||||
{
|
||||
static::$driver::destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public static function initDriver(): void
|
||||
{
|
||||
static::$driver ??= match (Worker::$eventLoopClass) {
|
||||
Swoole::class => Context\Swoole::class,
|
||||
Swow::class => Context\Swow::class,
|
||||
default=> Context\Fiber::class,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Context::initDriver();
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Workerman\Coroutine\Context;
|
||||
|
||||
use ArrayObject;
|
||||
|
||||
/**
|
||||
* Interface ContextInterface
|
||||
*/
|
||||
interface ContextInterface
|
||||
{
|
||||
/**
|
||||
* Get the value from the context with the specified name.
|
||||
* If the name does not exist, return the default value.
|
||||
*
|
||||
* @param string|null $name The name of the value to get.
|
||||
* @param mixed $default The default value to return if the name does not exist.
|
||||
* @return mixed The value from the context or the default value.
|
||||
*/
|
||||
public static function get(?string $name = null, mixed $default = null): mixed;
|
||||
|
||||
/**
|
||||
* Set the value in the context with the specified name.
|
||||
*
|
||||
* @param string $name The name of the value to set.
|
||||
* @param mixed $value The value to set.
|
||||
*/
|
||||
public static function set(string $name, mixed $value): void;
|
||||
|
||||
/**
|
||||
* Check if the specified name exists in the context.
|
||||
*
|
||||
* @param string $name The name to check.
|
||||
* @return bool True if the name exists, otherwise false.
|
||||
*/
|
||||
public static function has(string $name): bool;
|
||||
|
||||
/**
|
||||
* Initialize the context with an array of data.
|
||||
*
|
||||
* @param ArrayObject|null $data The array of data to initialize the context.
|
||||
*/
|
||||
public static function reset(?ArrayObject $data = null): void;
|
||||
|
||||
/**
|
||||
* Destroy the context.
|
||||
*/
|
||||
public static function destroy(): void;
|
||||
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace Workerman\Coroutine\Context;
|
||||
|
||||
use ArrayObject;
|
||||
use WeakMap;
|
||||
use Fiber as BaseFiber;
|
||||
|
||||
/**
|
||||
* Class Fiber
|
||||
*/
|
||||
class Fiber implements ContextInterface
|
||||
{
|
||||
/**
|
||||
* @var WeakMap
|
||||
*/
|
||||
private static WeakMap $contexts;
|
||||
|
||||
/**
|
||||
* @var ArrayObject
|
||||
*/
|
||||
private static ArrayObject $nonFiberContext;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function get(?string $name = null, mixed $default = null): mixed
|
||||
{
|
||||
$fiber = BaseFiber::getCurrent();
|
||||
if ($fiber === null) {
|
||||
return $name !== null ? (static::$nonFiberContext[$name] ?? $default) : static::$nonFiberContext;
|
||||
}
|
||||
if ($name === null) {
|
||||
return static::$contexts[$fiber] ??= new ArrayObject([], ArrayObject::ARRAY_AS_PROPS);
|
||||
}
|
||||
return static::$contexts[$fiber][$name] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function set(string $name, $value): void
|
||||
{
|
||||
$fiber = BaseFiber::getCurrent();
|
||||
if ($fiber === null) {
|
||||
static::$nonFiberContext[$name] = $value;
|
||||
return;
|
||||
}
|
||||
static::$contexts[$fiber] ??= new ArrayObject([], ArrayObject::ARRAY_AS_PROPS);
|
||||
static::$contexts[$fiber][$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function has(string $name): bool
|
||||
{
|
||||
$fiber = BaseFiber::getCurrent();
|
||||
if ($fiber === null) {
|
||||
return static::$nonFiberContext->offsetExists($name);
|
||||
}
|
||||
return isset(static::$contexts[$fiber]) && static::$contexts[$fiber]->offsetExists($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function reset(?ArrayObject $data = null): void
|
||||
{
|
||||
if ($data) {
|
||||
$data->setFlags(ArrayObject::ARRAY_AS_PROPS);
|
||||
} else {
|
||||
$data = new ArrayObject([], ArrayObject::ARRAY_AS_PROPS);
|
||||
}
|
||||
$fiber = BaseFiber::getCurrent();
|
||||
if ($fiber === null) {
|
||||
static::$nonFiberContext = $data;
|
||||
return;
|
||||
}
|
||||
static::$contexts[$fiber] = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function destroy(): void
|
||||
{
|
||||
$fiber = BaseFiber::getCurrent();
|
||||
if ($fiber === null) {
|
||||
static::$nonFiberContext = new ArrayObject([], ArrayObject::ARRAY_AS_PROPS);
|
||||
return;
|
||||
}
|
||||
unset(static::$contexts[$fiber]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the weakMap.
|
||||
*/
|
||||
public static function initContext(): void
|
||||
{
|
||||
static::$contexts = new WeakMap();
|
||||
static::$nonFiberContext = new ArrayObject([], ArrayObject::ARRAY_AS_PROPS);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Fiber::initContext();
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Workerman\Coroutine\Context;
|
||||
|
||||
use ArrayObject;
|
||||
use Swoole\Coroutine;
|
||||
|
||||
class Swoole implements ContextInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function get(?string $name = null, mixed $default = null): mixed
|
||||
{
|
||||
$context = Coroutine::getContext();
|
||||
if (!$context) {
|
||||
return $default;
|
||||
}
|
||||
$context->setFlags(ArrayObject::ARRAY_AS_PROPS);
|
||||
if ($name === null) {
|
||||
return $context;
|
||||
}
|
||||
return $context[$name] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function set(string $name, $value): void
|
||||
{
|
||||
Coroutine::getContext()[$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function has(string $name): bool
|
||||
{
|
||||
$context = Coroutine::getContext();
|
||||
return $context->offsetExists($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function reset(?ArrayObject $data = null): void
|
||||
{
|
||||
$context = Coroutine::getContext();
|
||||
$context->setFlags(ArrayObject::ARRAY_AS_PROPS);
|
||||
$context->exchangeArray($data ? $data->getArrayCopy() : []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function destroy(): void
|
||||
{
|
||||
$context = Coroutine::getContext();
|
||||
$context->exchangeArray([]);
|
||||
}
|
||||
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Workerman\Coroutine\Context;
|
||||
|
||||
use ArrayObject;
|
||||
use Swow\Coroutine;
|
||||
use WeakMap;
|
||||
|
||||
class Swow implements ContextInterface
|
||||
{
|
||||
/**
|
||||
* @var WeakMap
|
||||
*/
|
||||
public static WeakMap $contexts;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function get(?string $name = null, mixed $default = null): mixed
|
||||
{
|
||||
$fiber = Coroutine::getCurrent();
|
||||
if ($name === null) {
|
||||
static::$contexts[$fiber] ??= new ArrayObject([], ArrayObject::ARRAY_AS_PROPS);
|
||||
return static::$contexts[$fiber];
|
||||
}
|
||||
return static::$contexts[$fiber][$name] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function set(string $name, $value): void
|
||||
{
|
||||
$coroutine = Coroutine::getCurrent();
|
||||
static::$contexts[$coroutine] ??= new ArrayObject([], ArrayObject::ARRAY_AS_PROPS);
|
||||
static::$contexts[$coroutine][$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function has(string $name): bool
|
||||
{
|
||||
$fiber = Coroutine::getCurrent();
|
||||
return isset(static::$contexts[$fiber]) && static::$contexts[$fiber]->offsetExists($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function reset(?ArrayObject $data = null): void
|
||||
{
|
||||
$coroutine = Coroutine::getCurrent();
|
||||
$data->setFlags(ArrayObject::ARRAY_AS_PROPS);
|
||||
static::$contexts[$coroutine] = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function destroy(): void
|
||||
{
|
||||
unset(static::$contexts[Coroutine::getCurrent()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the weakMap.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function initContext(): void
|
||||
{
|
||||
self::$contexts = new WeakMap();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Swow::initContext();
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman;
|
||||
|
||||
use Workerman\Coroutine\Coroutine\CoroutineInterface;
|
||||
use Workerman\Coroutine\Coroutine\Fiber;
|
||||
use Workerman\Worker;
|
||||
use Workerman\Coroutine\Coroutine\Swoole as SwooleCoroutine;
|
||||
use Workerman\Coroutine\Coroutine\Swow as SwowCoroutine;
|
||||
use Workerman\Events\Swoole as SwooleEvent;
|
||||
use Workerman\Events\Swow as SwowEvent;
|
||||
|
||||
/**
|
||||
* Class Coroutine
|
||||
*/
|
||||
class Coroutine implements CoroutineInterface
|
||||
{
|
||||
/**
|
||||
* @var class-string<CoroutineInterface>
|
||||
*/
|
||||
protected static string $driverClass;
|
||||
|
||||
/**
|
||||
* @var CoroutineInterface
|
||||
*/
|
||||
public CoroutineInterface $driver;
|
||||
|
||||
/**
|
||||
* Coroutine constructor.
|
||||
*
|
||||
* @param callable $callable
|
||||
*/
|
||||
public function __construct(callable $callable)
|
||||
{
|
||||
$this->driver = new static::$driverClass($callable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function create(callable $callable, ...$args): CoroutineInterface
|
||||
{
|
||||
return static::$driverClass::create($callable, ...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function start(mixed ...$args): mixed
|
||||
{
|
||||
return $this->driver->start(...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function resume(mixed ...$args): mixed
|
||||
{
|
||||
return $this->driver->resume(...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function id(): int
|
||||
{
|
||||
return $this->driver->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function defer(callable $callable): void
|
||||
{
|
||||
static::$driverClass::defer($callable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function suspend(mixed $value = null): mixed
|
||||
{
|
||||
return static::$driverClass::suspend($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function getCurrent(): CoroutineInterface
|
||||
{
|
||||
return static::$driverClass::getCurrent();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function isCoroutine(): bool
|
||||
{
|
||||
return static::$driverClass::isCoroutine();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public static function init(): void
|
||||
{
|
||||
static::$driverClass = match (Worker::$eventLoopClass ?? null) {
|
||||
SwooleEvent::class => SwooleCoroutine::class,
|
||||
SwowEvent::class => SwowCoroutine::class,
|
||||
default => Fiber::class,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
Coroutine::init();
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Coroutine\Coroutine;
|
||||
|
||||
use Fiber;
|
||||
use Swow\Coroutine as SwowCoroutine;
|
||||
|
||||
/**
|
||||
* Interface CoroutineInterface
|
||||
*/
|
||||
interface CoroutineInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* Create a coroutine.
|
||||
*
|
||||
* @param callable $callable
|
||||
* @param ...$data
|
||||
* @return CoroutineInterface
|
||||
*/
|
||||
public static function create(callable $callable, ...$data): CoroutineInterface;
|
||||
|
||||
/**
|
||||
* Start a coroutine.
|
||||
*
|
||||
* @param mixed ...$args
|
||||
* @return mixed
|
||||
*/
|
||||
public function start(mixed ...$args): mixed;
|
||||
|
||||
/**
|
||||
* Resume a coroutine.
|
||||
*
|
||||
* @param mixed ...$args
|
||||
* @return mixed
|
||||
*/
|
||||
public function resume(mixed ...$args): mixed;
|
||||
|
||||
/**
|
||||
* Get the id of the coroutine.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function id(): int;
|
||||
|
||||
/**
|
||||
* Register a callable to be executed when the current fiber is destroyed
|
||||
*
|
||||
* @param callable $callable
|
||||
* @return void
|
||||
*/
|
||||
public static function defer(callable $callable): void;
|
||||
|
||||
/**
|
||||
* Yield the coroutine.
|
||||
*
|
||||
* @param mixed|null $value
|
||||
* @return mixed
|
||||
*/
|
||||
public static function suspend(mixed $value = null): mixed;
|
||||
|
||||
/**
|
||||
* Get the current coroutine.
|
||||
*
|
||||
* @return CoroutineInterface|Fiber|SwowCoroutine|static
|
||||
*/
|
||||
public static function getCurrent(): CoroutineInterface|Fiber|SwowCoroutine|static;
|
||||
|
||||
/**
|
||||
* Check if the current coroutine is in a coroutine.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isCoroutine(): bool;
|
||||
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Coroutine\Coroutine;
|
||||
|
||||
use Fiber as BaseFiber;
|
||||
use RuntimeException;
|
||||
use WeakMap;
|
||||
use Workerman\Coroutine\Utils\DestructionWatcher;
|
||||
|
||||
/**
|
||||
* Class Fiber
|
||||
*/
|
||||
class Fiber implements CoroutineInterface
|
||||
{
|
||||
/**
|
||||
* @var BaseFiber|null
|
||||
*/
|
||||
private ?BaseFiber $fiber;
|
||||
|
||||
/**
|
||||
* @var WeakMap
|
||||
*/
|
||||
private static WeakMap $instances;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private int $id;
|
||||
|
||||
/**
|
||||
* @param callable|null $callable
|
||||
*/
|
||||
public function __construct(?callable $callable = null)
|
||||
{
|
||||
static $id = 0;
|
||||
$this->id = ++$id;
|
||||
if ($callable) {
|
||||
$callable = function(...$args) use ($callable) {
|
||||
try {
|
||||
$callable(...$args);
|
||||
} finally {
|
||||
$this->fiber = null;
|
||||
}
|
||||
};
|
||||
$this->fiber = new BaseFiber($callable);
|
||||
self::$instances[$this->fiber] = $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function create(callable $callable, ...$args): CoroutineInterface
|
||||
{
|
||||
$fiber = new Fiber($callable);
|
||||
$fiber->start(...$args);
|
||||
return $fiber;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function start(mixed ...$args): mixed
|
||||
{
|
||||
return $this->fiber->start(...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function resume(mixed ...$args): mixed
|
||||
{
|
||||
return $this->fiber->resume(...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function suspend(mixed $value = null): mixed
|
||||
{
|
||||
return BaseFiber::suspend($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function id(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function defer(callable $callable): void
|
||||
{
|
||||
$baseFiber = BaseFiber::getCurrent();
|
||||
if ($baseFiber === null) {
|
||||
throw new RuntimeException('Cannot defer outside of a fiber.');
|
||||
}
|
||||
DestructionWatcher::watch($baseFiber, $callable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function getCurrent(): CoroutineInterface
|
||||
{
|
||||
if (!$baseFiber = BaseFiber::getCurrent()) {
|
||||
throw new RuntimeException('Not in fiber context');
|
||||
}
|
||||
if (!isset(self::$instances[$baseFiber])) {
|
||||
$fiber = new Fiber();
|
||||
$fiber->fiber = $baseFiber;
|
||||
self::$instances[$baseFiber] = $fiber;
|
||||
}
|
||||
return self::$instances[$baseFiber];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function isCoroutine(): bool
|
||||
{
|
||||
return BaseFiber::getCurrent() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the fiber.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init(): void
|
||||
{
|
||||
self::$instances = new WeakMap();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Fiber::init();
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Coroutine\Coroutine;
|
||||
|
||||
use RuntimeException;
|
||||
use Swoole\Coroutine;
|
||||
use WeakReference;
|
||||
|
||||
class Swoole implements CoroutineInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private static array $instances = [];
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private int $id = 0;
|
||||
|
||||
/**
|
||||
* @var callable|null
|
||||
*/
|
||||
private $callable;
|
||||
|
||||
/**
|
||||
* Coroutine constructor.
|
||||
*
|
||||
* @param callable|null $callable
|
||||
*/
|
||||
public function __construct(?callable $callable = null)
|
||||
{
|
||||
$this->callable = $callable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function create(callable $callable, ...$args): CoroutineInterface
|
||||
{
|
||||
$id = Coroutine::create($callable, ...$args);
|
||||
if (isset(self::$instances[$id]) && $coroutine = self::$instances[$id]->get()) {
|
||||
return $coroutine;
|
||||
}
|
||||
$coroutine = new self($callable);
|
||||
$coroutine->id = $id;
|
||||
self::$instances[$id] = WeakReference::create($coroutine);
|
||||
return $coroutine;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function start(mixed ...$args): CoroutineInterface
|
||||
{
|
||||
if ($this->id) {
|
||||
throw new RuntimeException('Coroutine has already started');
|
||||
}
|
||||
$this->id = Coroutine::create($this->callable, ...$args);
|
||||
$this->callable = null;
|
||||
if (isset(self::$instances[$this->id]) && $coroutine = self::$instances[$this->id]->get()) {
|
||||
return $coroutine;
|
||||
}
|
||||
self::$instances[$this->id] = WeakReference::create($this);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function resume(mixed ...$args): mixed
|
||||
{
|
||||
return Coroutine::resume($this->id, ...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function id(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function defer(callable $callable): void
|
||||
{
|
||||
Coroutine::defer($callable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function suspend(mixed $value = null): mixed
|
||||
{
|
||||
return Coroutine::suspend($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function getCurrent(): CoroutineInterface
|
||||
{
|
||||
$id = Coroutine::getCid();
|
||||
if ($id === -1) {
|
||||
throw new RuntimeException('Not in coroutine');
|
||||
}
|
||||
if (!isset(self::$instances[$id])) {
|
||||
$coroutine = new self();
|
||||
$coroutine->id = $id;
|
||||
self::$instances[$id] = WeakReference::create($coroutine);
|
||||
}
|
||||
return self::$instances[$id]->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function isCoroutine(): bool
|
||||
{
|
||||
return Coroutine::getCid() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
unset(self::$instances[$this->id]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Coroutine\Coroutine;
|
||||
|
||||
use Swow\Coroutine;
|
||||
|
||||
/**
|
||||
* Class Swow
|
||||
*/
|
||||
class Swow extends Coroutine implements CoroutineInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private array $callbacks = [];
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function defer(callable $callable): void
|
||||
{
|
||||
$coroutine = static::getCurrent();
|
||||
$coroutine->callbacks[] = $callable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function create(callable $callable, ...$args): CoroutineInterface
|
||||
{
|
||||
return static::run($callable, ...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function start(mixed ...$args): mixed
|
||||
{
|
||||
return $this->resume(...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function id(): int
|
||||
{
|
||||
return $this->getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function suspend(mixed $value = null): mixed
|
||||
{
|
||||
return Coroutine::yield($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function isCoroutine(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
foreach (array_reverse($this->callbacks) as $callable) {
|
||||
$callable();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Workerman\Coroutine\Exception;
|
||||
|
||||
class PoolException extends \RuntimeException
|
||||
{
|
||||
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Coroutine;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Class Locker
|
||||
*/
|
||||
class Locker
|
||||
{
|
||||
/**
|
||||
* @var Channel[]
|
||||
*/
|
||||
protected static array $channels = [];
|
||||
|
||||
/**
|
||||
* Lock.
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public static function lock(string $key): bool
|
||||
{
|
||||
if (!isset(static::$channels[$key])) {
|
||||
static::$channels[$key] = new Channel(1);
|
||||
}
|
||||
return static::$channels[$key]->push(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock.
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public static function unlock(string $key): bool
|
||||
{
|
||||
if ($channel = static::$channels[$key] ?? null) {
|
||||
// Must check hasProducers before pop, because pop in swow will wake up the producer, leading to inaccurate judgment.
|
||||
$hasProducers = $channel->hasProducers();
|
||||
$result = $channel->pop();
|
||||
if (!$hasProducers) {
|
||||
$channel->close();
|
||||
unset(static::$channels[$key]);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
throw new RuntimeException("Unlock failed, because the key $key is not locked");
|
||||
}
|
||||
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Coroutine;
|
||||
|
||||
use Throwable;
|
||||
use Workerman\Coroutine;
|
||||
|
||||
/**
|
||||
* Class Parallel
|
||||
*/
|
||||
class Parallel
|
||||
{
|
||||
/**
|
||||
* @var Channel|null
|
||||
*/
|
||||
protected ?Channel $channel = null;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $callbacks = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $results = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $exceptions = [];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param int $concurrent
|
||||
*/
|
||||
public function __construct(int $concurrent = -1)
|
||||
{
|
||||
if ($concurrent > 0) {
|
||||
$this->channel = new Channel($concurrent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a coroutine.
|
||||
*
|
||||
* @param callable $callable
|
||||
* @param string|null $key
|
||||
* @return void
|
||||
*/
|
||||
public function add(callable $callable, ?string $key = null): void
|
||||
{
|
||||
if ($key === null) {
|
||||
$this->callbacks[] = $callable;
|
||||
} else {
|
||||
$this->callbacks[$key] = $callable;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait all coroutines complete and return results.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function wait(): array
|
||||
{
|
||||
$barrier = Barrier::create();
|
||||
foreach ($this->callbacks as $key => $callback) {
|
||||
$this->channel?->push(true);
|
||||
Coroutine::create(function () use ($callback, $key, $barrier) {
|
||||
try {
|
||||
$this->results[$key] = $callback();
|
||||
} catch (Throwable $throwable) {
|
||||
$this->exceptions[$key] = $throwable;
|
||||
} finally {
|
||||
$this->channel?->pop();
|
||||
}
|
||||
});
|
||||
}
|
||||
Barrier::wait($barrier);
|
||||
return $this->results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get failed results.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getExceptions(): array
|
||||
{
|
||||
return $this->exceptions;
|
||||
}
|
||||
|
||||
}
|
||||
+386
@@ -0,0 +1,386 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Coroutine;
|
||||
|
||||
use Closure;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use stdClass;
|
||||
use Throwable;
|
||||
use WeakMap;
|
||||
use Workerman\Coroutine;
|
||||
use Workerman\Coroutine\Exception\PoolException;
|
||||
use Workerman\Coroutine\Utils\DestructionWatcher;
|
||||
use Workerman\Timer;
|
||||
use Workerman\Worker;
|
||||
|
||||
/**
|
||||
* Class Pool
|
||||
*/
|
||||
class Pool implements PoolInterface
|
||||
{
|
||||
/**
|
||||
* @var Channel
|
||||
*/
|
||||
protected Channel $channel;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected int $minConnections = 1;
|
||||
|
||||
/**
|
||||
* @var WeakMap
|
||||
*/
|
||||
protected WeakMap $connections;
|
||||
|
||||
/**
|
||||
* @var ?object
|
||||
*/
|
||||
protected ?object $nonCoroutineConnection = null;
|
||||
|
||||
/**
|
||||
* @var WeakMap
|
||||
*/
|
||||
protected WeakMap $lastUsedTimes;
|
||||
|
||||
/**
|
||||
* @var WeakMap
|
||||
*/
|
||||
protected WeakMap $lastHeartbeatTimes;
|
||||
|
||||
/**
|
||||
* @var Closure|null
|
||||
*/
|
||||
protected ?Closure $connectionCreateHandler = null;
|
||||
|
||||
/**
|
||||
* @var Closure|null
|
||||
*/
|
||||
protected ?Closure $connectionDestroyHandler = null;
|
||||
|
||||
/**
|
||||
* @var Closure|null
|
||||
*/
|
||||
protected ?Closure $connectionHeartbeatHandler = null;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
protected float $idleTimeout = 60;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
protected float $heartbeatInterval = 50;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
protected float $waitTimeout = 10;
|
||||
|
||||
/**
|
||||
* @var LoggerInterface|Closure|null
|
||||
*/
|
||||
protected LoggerInterface|Closure|null $logger = null;
|
||||
|
||||
/**
|
||||
* @var array|string[]
|
||||
*/
|
||||
private array $configurableProperties = [
|
||||
'minConnections',
|
||||
'idleTimeout',
|
||||
'heartbeatInterval',
|
||||
'waitTimeout',
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param int $maxConnections
|
||||
* @param array $config
|
||||
*/
|
||||
public function __construct(protected int $maxConnections = 1, protected array $config = [])
|
||||
{
|
||||
foreach ($config as $key => $value) {
|
||||
$camelCaseKey = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $key))));
|
||||
if (in_array($camelCaseKey, $this->configurableProperties, true)) {
|
||||
$this->$camelCaseKey = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$this->channel = new Channel($maxConnections);
|
||||
$this->lastUsedTimes = new WeakMap();
|
||||
$this->lastHeartbeatTimes = new WeakMap();
|
||||
$this->connections = new WeakMap();
|
||||
|
||||
if (Worker::isRunning()) {
|
||||
Timer::repeat(1, function () {
|
||||
$this->checkConnections();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the connection creator.
|
||||
*
|
||||
* @param callable $connectionCreateHandler
|
||||
* @return $this
|
||||
*/
|
||||
public function setConnectionCreator(callable $connectionCreateHandler): self
|
||||
{
|
||||
$this->connectionCreateHandler = $connectionCreateHandler;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the connection closer.
|
||||
*
|
||||
* @param callable $connectionDestroyHandler
|
||||
* @return $this
|
||||
*/
|
||||
public function setConnectionCloser(callable $connectionDestroyHandler): self
|
||||
{
|
||||
$this->connectionDestroyHandler = $connectionDestroyHandler;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the connection heartbeat checker.
|
||||
*
|
||||
* @param callable $connectionHeartbeatHandler
|
||||
* @return $this
|
||||
*/
|
||||
public function setHeartbeatChecker(callable $connectionHeartbeatHandler): self
|
||||
{
|
||||
$this->connectionHeartbeatHandler = $connectionHeartbeatHandler;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection.
|
||||
*
|
||||
* @return object
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function get(): object
|
||||
{
|
||||
if (!Coroutine::isCoroutine()) {
|
||||
if (!$this->nonCoroutineConnection) {
|
||||
$this->nonCoroutineConnection = $this->createConnection();
|
||||
}
|
||||
return $this->nonCoroutineConnection;
|
||||
}
|
||||
$num = $this->channel->length();
|
||||
if ($num === 0 && $this->getConnectionCount() < $this->maxConnections) {
|
||||
return $this->createConnection();
|
||||
}
|
||||
$connection = $this->channel->pop($this->waitTimeout);
|
||||
if (!$connection) {
|
||||
throw new PoolException("Failed to get a connection from the pool within the wait timeout ($this->waitTimeout seconds). The connection pool is exhausted.");
|
||||
}
|
||||
$this->lastUsedTimes[$connection] = time();
|
||||
return $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put connection to pool.
|
||||
*
|
||||
* @param object $connection
|
||||
* @return void
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function put(object $connection): void
|
||||
{
|
||||
// This connection does not belong to the connection pool.
|
||||
// It may have been closed by $this->closeConnection($connection).
|
||||
if (!isset($this->connections[$connection])) {
|
||||
throw new PoolException('The connection does not belong to the connection pool.');
|
||||
}
|
||||
if ($connection === $this->nonCoroutineConnection) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$this->channel->push($connection);
|
||||
} catch (Throwable $throwable) {
|
||||
$this->closeConnection($connection);
|
||||
throw $throwable;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the connection is valid.
|
||||
*
|
||||
* @param $connection
|
||||
* @return bool
|
||||
*/
|
||||
protected function isValidConnection($connection): bool
|
||||
{
|
||||
return is_object($connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create connection.
|
||||
*
|
||||
* @return object
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function createConnection(): object
|
||||
{
|
||||
if ($this->getConnectionCount() >= $this->maxConnections) {
|
||||
throw new PoolException('CreateConnection failed, maximum connection limit reached.');
|
||||
}
|
||||
// Create a placeholder to ensure the correct value of getConnectionCount().
|
||||
$placeholder = new stdClass;
|
||||
$this->connections[$placeholder] = 0;
|
||||
try {
|
||||
// Coroutines will switch here, so we need $placeholder to ensure the correct value of getConnectionCount().
|
||||
$connection = ($this->connectionCreateHandler)();
|
||||
if (!$this->isValidConnection($connection)) {
|
||||
throw new PoolException('CreateConnection failed, expected a connection object, but got ' . gettype($connection) . '.');
|
||||
}
|
||||
unset($this->connections[$placeholder]);
|
||||
$this->connections[$connection] = $this->lastUsedTimes[$connection] = $this->lastHeartbeatTimes[$connection] = time();
|
||||
} catch (Throwable $throwable) {
|
||||
unset($this->connections[$placeholder]);
|
||||
throw $throwable;
|
||||
}
|
||||
return $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the connection and remove the connection from the connection pool.
|
||||
*
|
||||
* @param object $connection
|
||||
* @return void
|
||||
*/
|
||||
public function closeConnection(object $connection): void
|
||||
{
|
||||
if (!isset($this->connections[$connection])) {
|
||||
return;
|
||||
}
|
||||
// Mark this connection as no longer belonging to the connection pool.
|
||||
unset($this->lastUsedTimes[$connection], $this->lastHeartbeatTimes[$connection], $this->connections[$connection]);
|
||||
if ($this->nonCoroutineConnection === $connection) {
|
||||
$this->nonCoroutineConnection = null;
|
||||
}
|
||||
if (!$this->connectionDestroyHandler) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
($this->connectionDestroyHandler)($connection);
|
||||
} catch (Throwable $throwable) {
|
||||
$this->log($throwable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup idle connections.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function checkConnections(): void
|
||||
{
|
||||
$num = $this->channel->length();
|
||||
$time = time();
|
||||
for($i = $num; $i > 0; $i--) {
|
||||
$connection = $this->channel->pop(0.001);
|
||||
if (!$connection) {
|
||||
return;
|
||||
}
|
||||
$lastUsedTime = $this->lastUsedTimes[$connection];
|
||||
if ($time - $lastUsedTime > $this->idleTimeout && $this->channel->length() >= $this->minConnections) {
|
||||
$this->closeConnection($connection);
|
||||
continue;
|
||||
}
|
||||
$this->trySendHeartbeat($connection) && $this->channel->push($connection);
|
||||
}
|
||||
if ($this->nonCoroutineConnection) {
|
||||
$this->trySendHeartbeat($this->nonCoroutineConnection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to send heartbeat.
|
||||
*
|
||||
* @param $connection
|
||||
* @return bool
|
||||
*/
|
||||
private function trySendHeartbeat($connection): bool
|
||||
{
|
||||
$lastHeartbeatTime = $this->lastHeartbeatTimes[$connection] ?? 0;
|
||||
$time = time();
|
||||
if ($this->connectionHeartbeatHandler && $time - $lastHeartbeatTime >= $this->heartbeatInterval) {
|
||||
try {
|
||||
($this->connectionHeartbeatHandler)($connection);
|
||||
$this->lastHeartbeatTimes[$connection] = $time;
|
||||
} catch (Throwable $throwable) {
|
||||
$this->log($throwable);
|
||||
$this->closeConnection($connection);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of connections in the connection pool.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getConnectionCount(): int
|
||||
{
|
||||
return count($this->connections);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close connections.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function closeConnections(): void
|
||||
{
|
||||
$num = $this->channel->length();
|
||||
for ($i = $num; $i > 0; $i--) {
|
||||
$connection = $this->channel->pop(0.001);
|
||||
if (!$connection) {
|
||||
return;
|
||||
}
|
||||
$this->closeConnection($connection);
|
||||
}
|
||||
$this->nonCoroutineConnection && $this->closeConnection($this->nonCoroutineConnection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log.
|
||||
*
|
||||
* @param $message
|
||||
* @return void
|
||||
*/
|
||||
protected function log($message): void
|
||||
{
|
||||
if (!$this->logger) {
|
||||
echo $message . PHP_EOL;
|
||||
return;
|
||||
}
|
||||
if ($this->logger instanceof Closure) {
|
||||
($this->logger)($message);
|
||||
return;
|
||||
}
|
||||
$this->logger->info((string)$message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Coroutine;
|
||||
|
||||
/**
|
||||
* Interface PoolInterface
|
||||
*/
|
||||
interface PoolInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* Get a connection from the pool.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get(): mixed;
|
||||
|
||||
/**
|
||||
* Put a connection back to the pool.
|
||||
*
|
||||
* @param object $connection
|
||||
* @return void
|
||||
*/
|
||||
public function put(object $connection): void;
|
||||
|
||||
/**
|
||||
* Create a connection.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function createConnection(): object;
|
||||
|
||||
/**
|
||||
* Close the connection and remove the connection from the connection pool.
|
||||
*
|
||||
* @param object $connection
|
||||
* @return void
|
||||
*/
|
||||
public function closeConnection(object $connection): void;
|
||||
|
||||
/**
|
||||
* Get the number of connections in the connection pool.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getConnectionCount(): int;
|
||||
|
||||
/**
|
||||
* Close connections in the connection pool.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function closeConnections(): void;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace Workerman\Coroutine\Utils;
|
||||
|
||||
use WeakMap;
|
||||
|
||||
class DestructionWatcher
|
||||
{
|
||||
/**
|
||||
* @var WeakMap
|
||||
*/
|
||||
protected static WeakMap $objects;
|
||||
|
||||
/**
|
||||
* @var callable[]
|
||||
*/
|
||||
protected array $callbacks = [];
|
||||
|
||||
/**
|
||||
* DestructionWatcher constructor.
|
||||
*
|
||||
* @param callable|null $callback
|
||||
*/
|
||||
public function __construct(?callable $callback = null)
|
||||
{
|
||||
if ($callback) {
|
||||
$this->callbacks[] = $callback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DestructionWatcher destructor.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
foreach (array_reverse($this->callbacks) as $callback) {
|
||||
$callback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch object destruction.
|
||||
*
|
||||
* @param object $object
|
||||
* @param callable $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function watch(object $object, callable $callback): void
|
||||
{
|
||||
static::$objects ??= new WeakMap();
|
||||
static::$objects[$object] ??= new static();
|
||||
static::$objects[$object]->callbacks[] = $callback;
|
||||
}
|
||||
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
/**
|
||||
* @author workbunny/Chaz6chez
|
||||
* @email chaz6chez1993@outlook.com
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Coroutine;
|
||||
|
||||
use BadMethodCallException;
|
||||
use Workerman\Worker;
|
||||
use Workerman\Coroutine\Coroutine\CoroutineInterface;
|
||||
use Workerman\Coroutine\WaitGroup\Fiber as FiberWaitGroup;
|
||||
use Workerman\Coroutine\WaitGroup\Swoole as SwooleWaitGroup;
|
||||
use Workerman\Coroutine\WaitGroup\Swow as SwowWaitGroup;
|
||||
use Workerman\Coroutine\WaitGroup\WaitGroupInterface;
|
||||
use Workerman\Events\Swoole;
|
||||
use Workerman\Events\Swow;
|
||||
|
||||
/**
|
||||
* @method bool add(int $delta = 1)
|
||||
* @method bool done()
|
||||
* @method int count()
|
||||
* @method bool wait(int|float $timeout = -1)
|
||||
*/
|
||||
class WaitGroup
|
||||
{
|
||||
/**
|
||||
* @var class-string<CoroutineInterface>
|
||||
*/
|
||||
protected static string $driverClass;
|
||||
|
||||
/**
|
||||
* @var WaitGroupInterface
|
||||
*/
|
||||
protected WaitGroupInterface $driver;
|
||||
|
||||
/**
|
||||
* 构造方法
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->driver = new (self::driverClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get driver class.
|
||||
*
|
||||
* @return class-string<CoroutineInterface>
|
||||
*/
|
||||
protected static function driverClass(): string
|
||||
{
|
||||
return static::$driverClass ??= match (Worker::$eventLoopClass ?? null) {
|
||||
Swoole::class => SwooleWaitGroup::class,
|
||||
Swow::class => SwowWaitGroup::class,
|
||||
default => FiberWaitGroup::class,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 代理调用WaitGroupInterface方法
|
||||
*
|
||||
* @codeCoverageIgnore 系统魔术方法,忽略覆盖
|
||||
* @param string $name
|
||||
* @param array $arguments
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call(string $name, array $arguments): mixed
|
||||
{
|
||||
if (!method_exists($this->driver, $name)) {
|
||||
throw new BadMethodCallException("Method $name not exists. ");
|
||||
}
|
||||
|
||||
return $this->driver->$name(...$arguments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
/**
|
||||
* @author workbunny/Chaz6chez
|
||||
* @email chaz6chez1993@outlook.com
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Coroutine\WaitGroup;
|
||||
|
||||
use Workerman\Coroutine\Channel\Fiber as Channel;
|
||||
|
||||
class Fiber implements WaitGroupInterface
|
||||
{
|
||||
|
||||
/** @var int */
|
||||
protected int $count;
|
||||
|
||||
/**
|
||||
* @var Channel
|
||||
*/
|
||||
protected Channel $channel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->count = 0;
|
||||
$this->channel = new Channel(1);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function add(int $delta = 1): bool
|
||||
{
|
||||
$this->count += max($delta, 1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function done(): bool
|
||||
{
|
||||
$this->count--;
|
||||
if ($this->count <= 0) {
|
||||
$this->channel->push(true);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function count(): int
|
||||
{
|
||||
return $this->count;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function wait(int|float $timeout = -1): bool
|
||||
{
|
||||
if ($this->count() > 0) {
|
||||
return $this->channel->pop($timeout);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
/**
|
||||
* @author workbunny/Chaz6chez
|
||||
* @email chaz6chez1993@outlook.com
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Coroutine\WaitGroup;
|
||||
|
||||
use Swoole\Coroutine\WaitGroup;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Class Swoole
|
||||
*/
|
||||
class Swoole implements WaitGroupInterface
|
||||
{
|
||||
|
||||
/** @var WaitGroup */
|
||||
protected WaitGroup $waitGroup;
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->waitGroup = new WaitGroup();
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function add(int $delta = 1): bool
|
||||
{
|
||||
$this->waitGroup->add(max($delta, 1));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function done(): bool
|
||||
{
|
||||
if ($this->count() > 0) {
|
||||
$this->waitGroup->done();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function count(): int
|
||||
{
|
||||
return $this->waitGroup->count();
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function wait(int|float $timeout = -1): bool
|
||||
{
|
||||
try {
|
||||
$this->waitGroup->wait(max($timeout, $timeout > 0 ? 0.001 : -1));
|
||||
return true;
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
/**
|
||||
* @author workbunny/Chaz6chez
|
||||
* @email chaz6chez1993@outlook.com
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Coroutine\WaitGroup;
|
||||
|
||||
use Swow\Sync\WaitGroup;
|
||||
use Throwable;
|
||||
|
||||
class Swow implements WaitGroupInterface
|
||||
{
|
||||
|
||||
/** @var WaitGroup */
|
||||
protected WaitGroup $waitGroup;
|
||||
|
||||
/** @var int count */
|
||||
protected int $count;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->waitGroup = new WaitGroup();
|
||||
$this->count = 0;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function add(int $delta = 1): bool
|
||||
{
|
||||
$this->waitGroup->add($delta = max($delta, 1));
|
||||
$this->count += $delta;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function done(): bool
|
||||
{
|
||||
if ($this->count() > 0) {
|
||||
$this->count--;
|
||||
$this->waitGroup->done();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function count(): int
|
||||
{
|
||||
return $this->count;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function wait(int|float $timeout = -1): bool
|
||||
{
|
||||
try {
|
||||
$this->waitGroup->wait($timeout > 0 ? (int) ($timeout * 1000) : $timeout);
|
||||
return true;
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* @author workbunny/Chaz6chez
|
||||
* @email chaz6chez1993@outlook.com
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Coroutine\WaitGroup;
|
||||
|
||||
interface WaitGroupInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* Increment count
|
||||
*
|
||||
* @param int $delta
|
||||
* @return bool
|
||||
*/
|
||||
public function add(int $delta = 1): bool;
|
||||
|
||||
/**
|
||||
* Complete count
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function done(): bool;
|
||||
|
||||
/**
|
||||
* Return count
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int;
|
||||
|
||||
/**
|
||||
* Wait
|
||||
*
|
||||
* @param int|float $timeout second
|
||||
* @return bool timeout:false success:true
|
||||
*/
|
||||
public function wait(int|float $timeout = -1): bool;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Swow;
|
||||
|
||||
class Coroutine
|
||||
{
|
||||
public function resume(mixed ...$args): mixed
|
||||
{
|
||||
// Stub for PHPStorm
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function getCurrent(): static
|
||||
{
|
||||
// Stub for PHPStorm
|
||||
return new Coroutine;
|
||||
}
|
||||
|
||||
public static function yield (mixed ...$args) : mixed
|
||||
{
|
||||
// Stub for PHPStorm
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getId() : int
|
||||
{
|
||||
// Stub for PHPStorm
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static function run(callable $callable , mixed ... $args): static
|
||||
{
|
||||
// Stub for PHPStorm
|
||||
return new Coroutine;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Workerman\Coroutine\Barrier;
|
||||
use Workerman\Coroutine;
|
||||
use Workerman\Timer;
|
||||
|
||||
/**
|
||||
* Class FiberBarrierTest
|
||||
*
|
||||
* Tests for the Fiber Barrier implementation.
|
||||
*/
|
||||
class BarrierTest extends TestCase
|
||||
{
|
||||
|
||||
/**
|
||||
* Test that the barrier is set to null after calling wait.
|
||||
*/
|
||||
public function testWaitSetsBarrierToNull()
|
||||
{
|
||||
$barrier = Barrier::create();
|
||||
$results = [0];
|
||||
Coroutine::create(function () use ($barrier, &$results) {
|
||||
Timer::sleep(0.1);
|
||||
$results[] = 1;
|
||||
});
|
||||
Coroutine::create(function () use ($barrier, &$results) {
|
||||
Timer::sleep(0.2);
|
||||
$results[] = 2;
|
||||
});
|
||||
Coroutine::create(function () use ($barrier, &$results) {
|
||||
Timer::sleep(0.3);
|
||||
$results[] = 3;
|
||||
});
|
||||
Barrier::wait($barrier);
|
||||
$this->assertNull($barrier, 'Barrier should be null after wait is called.');
|
||||
$this->assertEquals([0, 1, 2, 3], $results, 'All coroutines should have been executed.');
|
||||
}
|
||||
}
|
||||
+313
@@ -0,0 +1,313 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tests;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use stdClass;
|
||||
use Workerman\Coroutine\Channel;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Workerman\Coroutine\Channel\Memory;
|
||||
use Workerman\Coroutine;
|
||||
|
||||
class ChannelTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Test initializing channel with valid capacity.
|
||||
*/
|
||||
public function testInitializeWithValidCapacity()
|
||||
{
|
||||
$channel = new Channel(1);
|
||||
$this->assertInstanceOf(Channel::class, $channel);
|
||||
$this->assertEquals(1, $channel->getCapacity());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test initializing channel with invalid capacities.
|
||||
*/
|
||||
#[DataProvider('invalidCapacitiesProvider')]
|
||||
public function testInitializeWithInvalidCapacity($capacity)
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
new Channel($capacity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for invalid capacities.
|
||||
*/
|
||||
public static function invalidCapacitiesProvider(): array
|
||||
{
|
||||
return [
|
||||
[0],
|
||||
[-1],
|
||||
[-100]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test pushing and popping data.
|
||||
*/
|
||||
public function testPushAndPop()
|
||||
{
|
||||
$channel = new Channel(2);
|
||||
$data1 = 'test data 1';
|
||||
$data2 = 'test data 2';
|
||||
|
||||
// Push data into the channel
|
||||
$this->assertTrue($channel->push($data1));
|
||||
$this->assertTrue($channel->push($data2));
|
||||
|
||||
// Verify the length of the channel
|
||||
$this->assertEquals(2, $channel->length());
|
||||
|
||||
// Pop data from the channel
|
||||
$this->assertEquals($data1, $channel->pop());
|
||||
$this->assertEquals($data2, $channel->pop());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test pushing data when the channel is full.
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function testPushWhenFull()
|
||||
{
|
||||
// Memory driver does not support push with timeout
|
||||
if ($this->driverIsMemory()) {
|
||||
$this->assertTrue(true);
|
||||
return;
|
||||
}
|
||||
$channel = new Channel(1);
|
||||
$this->assertTrue($channel->push('data1'));
|
||||
|
||||
$timeout = 0.5;
|
||||
// Attempt to push when the channel is full with a timeout
|
||||
$startTime = microtime(true);
|
||||
$this->assertFalse($channel->push('data2', $timeout));
|
||||
$elapsedTime = microtime(true) - $startTime;
|
||||
|
||||
// Verify that the push operation timed out
|
||||
$this->assertTrue(0.1 > abs($elapsedTime - $timeout));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test popping data when the channel is empty.
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function testPopWhenEmpty()
|
||||
{
|
||||
// Memory driver does not support push with timeout
|
||||
if ($this->driverIsMemory()) {
|
||||
$this->assertTrue(true);
|
||||
return;
|
||||
}
|
||||
$channel = new Channel(1);
|
||||
|
||||
// Attempt to pop when the channel is empty with a timeout
|
||||
$startTime = microtime(true);
|
||||
$this->assertFalse($channel->pop(0.1));
|
||||
$elapsedTime = microtime(true) - $startTime;
|
||||
|
||||
// Verify that the pop operation timed out
|
||||
$this->assertGreaterThanOrEqual(0.09, $elapsedTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test closing the channel and its effects.
|
||||
*/
|
||||
public function testCloseChannel()
|
||||
{
|
||||
$channel = new Channel(1);
|
||||
$this->assertTrue($channel->push('data'));
|
||||
|
||||
// Close the channel
|
||||
$channel->close();
|
||||
|
||||
// Attempt to push after closing
|
||||
$this->assertFalse($channel->push('new data'));
|
||||
|
||||
// Pop the remaining data
|
||||
$this->assertEquals('data', $channel->pop());
|
||||
|
||||
// Attempt to pop after channel is empty and closed
|
||||
$this->assertFalse($channel->pop());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that push and pop return false when channel is closed.
|
||||
*/
|
||||
public function testPushAndPopReturnFalseWhenClosed()
|
||||
{
|
||||
$channel = new Channel(1);
|
||||
$channel->close();
|
||||
|
||||
$this->assertFalse($channel->push('data'));
|
||||
$this->assertFalse($channel->pop());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the length and capacity methods.
|
||||
*/
|
||||
public function testLengthAndCapacity()
|
||||
{
|
||||
$channel = new Channel(5);
|
||||
$this->assertEquals(0, $channel->length());
|
||||
$this->assertEquals(5, $channel->getCapacity());
|
||||
|
||||
$channel->push('data1');
|
||||
$channel->push('data2');
|
||||
|
||||
$this->assertEquals(2, $channel->length());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test pushing and popping with different data types.
|
||||
*/
|
||||
#[DataProvider('dataTypesProvider')]
|
||||
public function testPushAndPopWithDifferentDataTypes($data)
|
||||
{
|
||||
$channel = new Channel(1);
|
||||
$this->assertTrue($channel->push($data));
|
||||
$this->assertSame($data, $channel->pop());
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for different data types.
|
||||
*/
|
||||
public static function dataTypesProvider(): array
|
||||
{
|
||||
return [
|
||||
['string'],
|
||||
[123],
|
||||
[123.456],
|
||||
[true],
|
||||
[false],
|
||||
[null],
|
||||
[[]],
|
||||
[['key' => 'value']],
|
||||
[new stdClass()],
|
||||
[fopen('php://memory', 'r')],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test pushing to a closed channel immediately returns false.
|
||||
*/
|
||||
public function testPushToClosedChannel()
|
||||
{
|
||||
$channel = new Channel(1);
|
||||
$channel->close();
|
||||
$this->assertFalse($channel->push('data', 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test popping from a closed and empty channel immediately returns false.
|
||||
*/
|
||||
public function testPopFromClosedAndEmptyChannel()
|
||||
{
|
||||
$channel = new Channel(1);
|
||||
$channel->close();
|
||||
$this->assertFalse($channel->pop(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
protected function driverIsMemory(): bool
|
||||
{
|
||||
$reflectionClass = new ReflectionClass(Channel::class);
|
||||
$instance = $reflectionClass->newInstance();
|
||||
$property = $reflectionClass->getProperty('driver');
|
||||
$driverValue = $property->getValue($instance);
|
||||
return $driverValue instanceof Memory;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 hasConsumers 当没有消费者时返回 false
|
||||
*/
|
||||
public function testHasConsumersWhenNoConsumers()
|
||||
{
|
||||
if (!Coroutine::isCoroutine()) {
|
||||
$this->assertTrue(true);
|
||||
return;
|
||||
}
|
||||
$channel = new Channel(1);
|
||||
$this->assertFalse($channel->hasConsumers());
|
||||
$channel->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 hasConsumers 当有消费者等待时返回 true
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function testHasConsumersWhenConsumersWaiting()
|
||||
{
|
||||
if ($this->driverIsMemory()) {
|
||||
$this->assertTrue(true);
|
||||
return;
|
||||
}
|
||||
$channel = new Channel(1);
|
||||
$sync = new Channel(1);
|
||||
|
||||
Coroutine::create(function () use ($channel, $sync) {
|
||||
$sync->push(true);
|
||||
$channel->pop();
|
||||
});
|
||||
|
||||
$sync->pop();
|
||||
|
||||
$this->assertTrue($channel->hasConsumers());
|
||||
|
||||
Coroutine::create(function () use ($channel) {
|
||||
$channel->push('data');
|
||||
});
|
||||
$channel->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 hasProducers 当没有生产者时返回 false
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function testHasProducersWhenNoProducers()
|
||||
{
|
||||
if ($this->driverIsMemory()) {
|
||||
$this->assertTrue(true);
|
||||
return;
|
||||
}
|
||||
$channel = new Channel(1);
|
||||
$this->assertFalse($channel->hasProducers());
|
||||
$channel->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 hasProducers 当有生产者等待时返回 true
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function testHasProducersWhenProducersWaiting()
|
||||
{
|
||||
if ($this->driverIsMemory()) {
|
||||
$this->assertTrue(true);
|
||||
return;
|
||||
}
|
||||
$channel = new Channel(1);
|
||||
$channel->push('data1');
|
||||
|
||||
$sync = new Channel(1);
|
||||
|
||||
Coroutine::create(function () use ($channel, $sync) {
|
||||
$sync->push(true);
|
||||
$channel->push('data2');
|
||||
});
|
||||
|
||||
$sync->pop();
|
||||
|
||||
$this->assertTrue($channel->hasProducers());
|
||||
|
||||
$channel->pop();
|
||||
$channel->close();
|
||||
}
|
||||
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace tests;
|
||||
|
||||
use ArrayObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Workerman\Coroutine\Context;
|
||||
use Workerman\Coroutine;
|
||||
|
||||
// Now, the test cases
|
||||
class ContextTest extends TestCase
|
||||
{
|
||||
public function testContextSetAndGetWithinCoroutine()
|
||||
{
|
||||
Coroutine::create(function () {
|
||||
$key = 'testContextSetAndGetWithinCoroutine';
|
||||
Context::set($key, 'value');
|
||||
$this->assertEquals('value', Context::get($key));
|
||||
});
|
||||
}
|
||||
|
||||
public function testContextGet()
|
||||
{
|
||||
Context::reset(new ArrayObject(['not_exist' => 'value']));
|
||||
$key = 'testContextGet';
|
||||
Context::reset(new ArrayObject([$key => 'value']));
|
||||
$context = Context::get();
|
||||
$this->assertArrayNotHasKey('not_exist', $context);
|
||||
$this->assertObjectNotHasProperty('not_exist', $context);
|
||||
$this->assertArrayHasKey($key, $context);
|
||||
$this->assertObjectHasProperty($key, $context);
|
||||
$this->assertEquals('value', $context[$key]);
|
||||
$this->assertEquals('value', $context->$key);
|
||||
$this->assertInstanceOf('ArrayObject', $context);
|
||||
unset($context[$key]);
|
||||
$this->assertNull(Context::get($key));
|
||||
$context[$key] = 'value';
|
||||
$this->assertEquals('value', Context::get($key));
|
||||
unset($context->$key);
|
||||
$this->assertNull(Context::get($key));
|
||||
$context->$key = 'value';
|
||||
$this->assertEquals('value', Context::get($key));
|
||||
}
|
||||
|
||||
public function testContextIsolationBetweenCoroutines()
|
||||
{
|
||||
$values = [];
|
||||
|
||||
Coroutine::create(function () use (&$values) {
|
||||
Context::set('key', 'value1');
|
||||
$values[] = Context::get('key');
|
||||
// Ensure the value is not available after coroutine ends
|
||||
Context::destroy();
|
||||
});
|
||||
|
||||
Coroutine::create(function () use (&$values) {
|
||||
Context::set('key', 'value2');
|
||||
$values[] = Context::get('key');
|
||||
// Ensure the value is not available after coroutine ends
|
||||
Context::destroy();
|
||||
});
|
||||
|
||||
$this->assertEquals(['value1', 'value2'], $values);
|
||||
}
|
||||
|
||||
public function testContextDestroyedAfterCoroutineEnds()
|
||||
{
|
||||
Coroutine::create(function () {
|
||||
Context::set('key', 'value');
|
||||
$this->assertTrue(Context::has('key'));
|
||||
// Simulate coroutine end and context destruction
|
||||
Context::destroy();
|
||||
});
|
||||
|
||||
// After coroutine ends, the context should be destroyed
|
||||
// Need to simulate this by trying to access context outside coroutine
|
||||
$this->assertNull(Context::get('key'));
|
||||
$this->assertFalse(Context::has('key'));
|
||||
}
|
||||
|
||||
public function testContextHasMethod()
|
||||
{
|
||||
Coroutine::create(function () {
|
||||
$this->assertFalse(Context::has('key'));
|
||||
Context::set('key', 'value');
|
||||
$this->assertTrue(Context::has('key'));
|
||||
});
|
||||
}
|
||||
|
||||
public function testContextResetMethod()
|
||||
{
|
||||
Coroutine::create(function () {
|
||||
Context::reset(new ArrayObject(['key3' => 'value1']));
|
||||
Context::reset(new ArrayObject(['key1' => 'value1', 'key2' => 'value2']));
|
||||
$this->assertEquals('value1', Context::get('key1'));
|
||||
$this->assertEquals('value2', Context::get('key2'));
|
||||
// Test that other keys are not set
|
||||
$this->assertNull(Context::get('key3'));
|
||||
});
|
||||
}
|
||||
|
||||
public function testContextDataNotSharedBetweenCoroutines()
|
||||
{
|
||||
$result = [];
|
||||
|
||||
Coroutine::create(function () use (&$result) {
|
||||
Context::set('counter', 1);
|
||||
$result[] = Context::get('counter');
|
||||
Context::destroy();
|
||||
});
|
||||
|
||||
Coroutine::create(function () use (&$result) {
|
||||
$this->assertNull(Context::get('counter'));
|
||||
Context::set('counter', 2);
|
||||
$result[] = Context::get('counter');
|
||||
Context::destroy();
|
||||
});
|
||||
|
||||
$this->assertEquals([1, 2], $result);
|
||||
}
|
||||
|
||||
public function testContextDefaultValues()
|
||||
{
|
||||
Coroutine::create(function () {
|
||||
$this->assertEquals('default', Context::get('non_existing_key', 'default'));
|
||||
});
|
||||
}
|
||||
|
||||
public function testContextSetOverrideValue()
|
||||
{
|
||||
Coroutine::create(function () {
|
||||
Context::set('key', 'initial');
|
||||
$this->assertEquals('initial', Context::get('key'));
|
||||
Context::set('key', 'overridden');
|
||||
$this->assertEquals('overridden', Context::get('key'));
|
||||
});
|
||||
}
|
||||
|
||||
public function testContextMultipleKeys()
|
||||
{
|
||||
Coroutine::create(function () {
|
||||
Context::set('key1', 'value1');
|
||||
Context::set('key2', 'value2');
|
||||
$this->assertEquals('value1', Context::get('key1'));
|
||||
$this->assertEquals('value2', Context::get('key2'));
|
||||
});
|
||||
}
|
||||
|
||||
public function testContextPersistenceWithinCoroutine()
|
||||
{
|
||||
Coroutine::create(function () {
|
||||
Context::set('key', 'value');
|
||||
|
||||
// Simulate asynchronous operation within coroutine
|
||||
$this->someAsyncOperation(function () {
|
||||
$this->assertEquals('value', Context::get('key'));
|
||||
});
|
||||
|
||||
// Context should persist throughout the coroutine
|
||||
$this->assertEquals('value', Context::get('key'));
|
||||
});
|
||||
}
|
||||
|
||||
private function someAsyncOperation(callable $callback)
|
||||
{
|
||||
// Simulate async operation
|
||||
$callback();
|
||||
}
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Workerman\Coroutine;
|
||||
use Workerman\Coroutine\Coroutine\CoroutineInterface;
|
||||
use Workerman\Events\Swoole;
|
||||
use Workerman\Worker;
|
||||
|
||||
class CoroutineTest extends TestCase
|
||||
{
|
||||
public function testCreateReturnsCoroutineInterface()
|
||||
{
|
||||
$callable = function() {};
|
||||
$coroutine = Coroutine::create($callable);
|
||||
$this->assertInstanceOf(CoroutineInterface::class, $coroutine);
|
||||
}
|
||||
|
||||
public function testStartExecutesCoroutine()
|
||||
{
|
||||
$value = null;
|
||||
Coroutine::create(function() use (&$value) {
|
||||
$value = 'started';
|
||||
});
|
||||
$this->assertEquals('started', $value);
|
||||
}
|
||||
|
||||
public function testSuspendAndResumeCoroutine()
|
||||
{
|
||||
if (Worker::$eventLoopClass === Swoole::class) {
|
||||
// Swoole does not support suspend and resume
|
||||
$this->assertTrue(true);
|
||||
return;
|
||||
}
|
||||
$value = [];
|
||||
$coroutine = Coroutine::create(function() use (&$value) {
|
||||
$value[] = 'before suspend';
|
||||
$resumedValue = Coroutine::suspend();
|
||||
$value[] = 'after resume';
|
||||
$value[] = $resumedValue;
|
||||
});
|
||||
$this->assertEquals(['before suspend'], $value);
|
||||
$coroutine->resume('resumed data');
|
||||
unset($coroutine);
|
||||
gc_collect_cycles();
|
||||
$this->assertEquals(['before suspend', 'after resume', 'resumed data'], $value);
|
||||
}
|
||||
|
||||
public function testGetCurrentReturnsCurrentCoroutine()
|
||||
{
|
||||
$currentCoroutine = null;
|
||||
$coroutine = Coroutine::create(function() use (&$currentCoroutine) {
|
||||
$currentCoroutine = Coroutine::getCurrent();
|
||||
});
|
||||
$this->assertSame($coroutine, $currentCoroutine);
|
||||
}
|
||||
|
||||
public function testCoroutineIdIsInteger()
|
||||
{
|
||||
$coroutine = Coroutine::create(function() {});
|
||||
$id = $coroutine->id();
|
||||
$this->assertIsInt($id);
|
||||
}
|
||||
|
||||
public function testDeferExecutesAfterCoroutineDestruction()
|
||||
{
|
||||
$value = [];
|
||||
$coroutine = Coroutine::create(function() use (&$value) {
|
||||
Coroutine::defer(function() use (&$value) {
|
||||
$value[] = 'defer1';
|
||||
});
|
||||
Coroutine::defer(function() use (&$value) {
|
||||
$value[] = 'defer2';
|
||||
});
|
||||
$value[] = 'before suspend';
|
||||
Coroutine::suspend();
|
||||
$value[] = 'after resume';
|
||||
});
|
||||
$this->assertEquals(['before suspend'], $value);
|
||||
$coroutine->resume();
|
||||
unset($coroutine);
|
||||
gc_collect_cycles();
|
||||
$this->assertEquals(['before suspend', 'after resume', 'defer2', 'defer1'], $value);
|
||||
}
|
||||
|
||||
public function testMultipleCoroutines()
|
||||
{
|
||||
$sequence = [];
|
||||
$coroutine1 = Coroutine::create(function() use (&$sequence) {
|
||||
$sequence[] = 'coroutine1 start';
|
||||
Coroutine::suspend();
|
||||
$sequence[] = 'coroutine1 resumed';
|
||||
});
|
||||
$coroutine2 = Coroutine::create(function() use (&$sequence) {
|
||||
$sequence[] = 'coroutine2 start';
|
||||
Coroutine::suspend();
|
||||
$sequence[] = 'coroutine2 resumed';
|
||||
});
|
||||
$this->assertEquals(['coroutine1 start', 'coroutine2 start'], $sequence);
|
||||
$coroutine1->resume();
|
||||
$coroutine2->resume();
|
||||
$this->assertEquals(
|
||||
['coroutine1 start', 'coroutine2 start', 'coroutine1 resumed', 'coroutine2 resumed'],
|
||||
$sequence
|
||||
);
|
||||
}
|
||||
|
||||
public function testCoroutineWithArguments()
|
||||
{
|
||||
$result = null;
|
||||
$coroutine = new Coroutine(function($a, $b) use (&$result) {
|
||||
$result = $a + $b;
|
||||
});
|
||||
$coroutine->start(2, 3);
|
||||
$this->assertEquals(5, $result);
|
||||
}
|
||||
|
||||
public function testSuspendReturnsValue()
|
||||
{
|
||||
if (Worker::$eventLoopClass === Swoole::class) {
|
||||
// Swoole does not support suspend and resume
|
||||
$this->assertTrue(true);
|
||||
return;
|
||||
}
|
||||
$coroutine = new Coroutine(function() {
|
||||
$valueFromResume = Coroutine::suspend('first suspend');
|
||||
Coroutine::suspend($valueFromResume);
|
||||
});
|
||||
$first_suspend = $coroutine->start();
|
||||
$this->assertEquals('first suspend', $first_suspend);
|
||||
$result = $coroutine->resume('value from resume');
|
||||
$this->assertEquals('value from resume', $result);
|
||||
}
|
||||
|
||||
public function testNestedCoroutines()
|
||||
{
|
||||
$sequence = [];
|
||||
$coroutine = Coroutine::create(function() use (&$sequence) {
|
||||
$sequence[] = 'outer start';
|
||||
$inner = Coroutine::create(function() use (&$sequence) {
|
||||
$sequence[] = 'inner start';
|
||||
Coroutine::suspend();
|
||||
$sequence[] = 'inner resumed';
|
||||
});
|
||||
Coroutine::suspend();
|
||||
$sequence[] = 'outer resumed';
|
||||
$inner->resume();
|
||||
$sequence[] = 'outer end';
|
||||
});
|
||||
$this->assertEquals(['outer start', 'inner start'], $sequence);
|
||||
$coroutine->resume();
|
||||
$this->assertEquals(['outer start', 'inner start', 'outer resumed', 'inner resumed', 'outer end'], $sequence);
|
||||
}
|
||||
|
||||
/*public function testCoroutineExceptionHandling()
|
||||
{
|
||||
$this->expectException(\Exception::class);
|
||||
$this->expectExceptionMessage('Test exception');
|
||||
Coroutine::create(function() {
|
||||
throw new \Exception('Test exception');
|
||||
});
|
||||
}*/
|
||||
|
||||
public function testDeferOrder()
|
||||
{
|
||||
$value = [];
|
||||
$coroutine = Coroutine::create(function() use (&$value) {
|
||||
Coroutine::defer(function() use (&$value) {
|
||||
$value[] = 'defer1';
|
||||
});
|
||||
Coroutine::defer(function() use (&$value) {
|
||||
$value[] = 'defer2';
|
||||
});
|
||||
$value[] = 'coroutine body';
|
||||
});
|
||||
unset($coroutine);
|
||||
// Force garbage collection
|
||||
gc_collect_cycles();
|
||||
$this->assertEquals(['coroutine body', 'defer2', 'defer1'], $value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Workerman\Coroutine\Channel\Fiber as Channel;
|
||||
use Workerman\Timer;
|
||||
use Fiber as BaseFiber;
|
||||
|
||||
class FiberChannelTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Test basic push and pop operations.
|
||||
*/
|
||||
public function testBasicPushPop()
|
||||
{
|
||||
$channel = new Channel();
|
||||
|
||||
$fiber = new BaseFiber(function() use ($channel) {
|
||||
$channel->push('test data');
|
||||
});
|
||||
|
||||
$fiber->start();
|
||||
|
||||
$this->assertEquals('test data', $channel->pop());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that pop will block until data is available or timeout occurs.
|
||||
*/
|
||||
public function testPopWithTimeout()
|
||||
{
|
||||
$channel = new Channel();
|
||||
|
||||
$fiber = new BaseFiber(function() use ($channel) {
|
||||
$result = $channel->pop(0.5);
|
||||
$this->assertFalse($result);
|
||||
});
|
||||
|
||||
$startTime = microtime(true);
|
||||
|
||||
$fiber->start();
|
||||
|
||||
// Allow time for the fiber to suspend and wait
|
||||
Timer::sleep(0.2); // 200 ms
|
||||
|
||||
// Ensure that the fiber is still waiting (not timed out yet)
|
||||
$this->assertTrue($fiber->isSuspended());
|
||||
|
||||
// Wait until the timeout should have occurred
|
||||
Timer::sleep(0.4); // 400 ms
|
||||
|
||||
$endTime = microtime(true);
|
||||
|
||||
$this->assertTrue($fiber->isTerminated());
|
||||
$this->assertGreaterThanOrEqual(0.5, $endTime - $startTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that push will block when capacity is reached and timeout occurs.
|
||||
*/
|
||||
public function testPushWithTimeout()
|
||||
{
|
||||
$channel = new Channel(1);
|
||||
|
||||
$this->assertTrue($channel->push('data1'));
|
||||
|
||||
$fiber = new BaseFiber(function() use ($channel) {
|
||||
$result = $channel->push('data2', 0.5);
|
||||
$this->assertFalse($result);
|
||||
});
|
||||
|
||||
$startTime = microtime(true);
|
||||
|
||||
$fiber->start();
|
||||
|
||||
// Allow time for the fiber to suspend and wait
|
||||
Timer::sleep(0.2); // 200 ms
|
||||
|
||||
// Ensure that the fiber is still waiting (not timed out yet)
|
||||
$this->assertTrue($fiber->isSuspended());
|
||||
|
||||
// Wait until the timeout should have occurred
|
||||
Timer::sleep(0.4); // 400 ms
|
||||
|
||||
$endTime = microtime(true);
|
||||
|
||||
$this->assertTrue($fiber->isTerminated());
|
||||
$this->assertGreaterThanOrEqual(0.5, $endTime - $startTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that push returns false immediately if capacity is full and timeout is zero.
|
||||
*/
|
||||
public function testPushNonBlockingWhenFull()
|
||||
{
|
||||
$channel = new Channel(1);
|
||||
|
||||
$this->assertTrue($channel->push('data1'));
|
||||
|
||||
$result = $channel->push('data2', 0);
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that pop returns false immediately if the channel is empty and timeout is zero.
|
||||
*/
|
||||
public function testPopNonBlockingWhenEmpty()
|
||||
{
|
||||
$channel = new Channel();
|
||||
|
||||
$result = $channel->pop(0);
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test closing the channel.
|
||||
*/
|
||||
public function testCloseChannel()
|
||||
{
|
||||
$channel = new Channel();
|
||||
|
||||
$channel->close();
|
||||
|
||||
$this->assertFalse($channel->push('data'));
|
||||
$this->assertFalse($channel->pop());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that waiting pushers and poppers are resumed when the channel is closed.
|
||||
*/
|
||||
public function testWaitersAreResumedOnClose()
|
||||
{
|
||||
$channelPush = new Channel(1);
|
||||
$channelPop = new Channel(1);
|
||||
|
||||
$pushFiber = new BaseFiber(function() use ($channelPush) {
|
||||
$channelPush->push('data', 1);
|
||||
$result = $channelPush->push('data', 1);
|
||||
$this->assertFalse($result);
|
||||
});
|
||||
|
||||
$popFiber = new BaseFiber(function() use ($channelPop) {
|
||||
$result = $channelPop->pop(1);
|
||||
$this->assertFalse($result);
|
||||
});
|
||||
|
||||
$pushFiber->start();
|
||||
$popFiber->start();
|
||||
|
||||
// Allow time for fibers to suspend
|
||||
Timer::sleep(0.1); // 100 ms
|
||||
|
||||
// Close the channel to resume fibers
|
||||
$channelPush->close();
|
||||
$channelPop->close();
|
||||
|
||||
// Allow time for fibers to process after resuming
|
||||
Timer::sleep(0.1); // 100 ms
|
||||
|
||||
$this->assertTrue($pushFiber->isTerminated());
|
||||
$this->assertTrue($popFiber->isTerminated());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that length and getCapacity methods return correct values.
|
||||
*/
|
||||
public function testLengthAndCapacity()
|
||||
{
|
||||
$capacity = 2;
|
||||
$channel = new Channel($capacity);
|
||||
|
||||
$this->assertEquals(0, $channel->length());
|
||||
$this->assertEquals($capacity, $channel->getCapacity());
|
||||
|
||||
$channel->push('data1');
|
||||
$this->assertEquals(1, $channel->length());
|
||||
|
||||
$channel->push('data2');
|
||||
$this->assertEquals(2, $channel->length());
|
||||
|
||||
$channel->pop();
|
||||
$this->assertEquals(1, $channel->length());
|
||||
|
||||
$channel->pop();
|
||||
$this->assertEquals(0, $channel->length());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test pushing to a closed channel.
|
||||
*/
|
||||
public function testPushToClosedChannel()
|
||||
{
|
||||
$channel = new Channel();
|
||||
|
||||
$channel->close();
|
||||
|
||||
$result = $channel->push('data');
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test popping from a closed channel.
|
||||
*/
|
||||
public function testPopFromClosedChannel()
|
||||
{
|
||||
$channel = new Channel();
|
||||
|
||||
$channel->push('data');
|
||||
|
||||
$channel->close();
|
||||
|
||||
$this->assertEquals('data', $channel->pop());
|
||||
$this->assertFalse($channel->pop());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test multiple push and pop operations with fibers.
|
||||
*/
|
||||
public function testMultiplePushPopWithFibers()
|
||||
{
|
||||
$channel = new Channel(2);
|
||||
|
||||
$results = [];
|
||||
|
||||
$producerFiber = new BaseFiber(function() use ($channel) {
|
||||
$channel->push('data1');
|
||||
$channel->push('data2');
|
||||
$channel->push('data3');
|
||||
});
|
||||
|
||||
$consumerFiber = new BaseFiber(function() use ($channel, &$results) {
|
||||
$results[] = $channel->pop();
|
||||
$results[] = $channel->pop();
|
||||
$results[] = $channel->pop();
|
||||
});
|
||||
|
||||
$producerFiber->start();
|
||||
$consumerFiber->start();
|
||||
|
||||
// Allow time for fibers to execute
|
||||
usleep(500000); // 500 ms
|
||||
|
||||
$this->assertEquals(['data1', 'data2', 'data3'], $results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that fibers are properly blocked and resumed in push and pop operations.
|
||||
*/
|
||||
public function testFiberBlockingAndResuming()
|
||||
{
|
||||
$channel = new Channel(1);
|
||||
|
||||
$pushFiber = new BaseFiber(function() use ($channel) {
|
||||
$channel->push('data1');
|
||||
$channel->push('data2');
|
||||
$channel->push('data3');
|
||||
});
|
||||
|
||||
$popFiber = new BaseFiber(function() use ($channel) {
|
||||
$this->assertEquals('data1', $channel->pop());
|
||||
$this->assertEquals('data2', $channel->pop());
|
||||
$this->assertEquals('data3', $channel->pop());
|
||||
});
|
||||
|
||||
$pushFiber->start();
|
||||
$popFiber->start();
|
||||
|
||||
// Allow time for fibers to execute
|
||||
Timer::sleep(0.5); // 500 ms
|
||||
|
||||
$this->assertTrue($pushFiber->isTerminated());
|
||||
$this->assertTrue($popFiber->isTerminated());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that pushing data after capacity is reached blocks until space is available.
|
||||
*/
|
||||
public function testPushBlocksWhenFull()
|
||||
{
|
||||
$channel = new Channel(1);
|
||||
|
||||
$channel->push('data1');
|
||||
|
||||
$pushFiber = new BaseFiber(function() use ($channel) {
|
||||
$channel->push('data2');
|
||||
});
|
||||
|
||||
$popFiber = new BaseFiber(function() use ($channel) {
|
||||
Timer::sleep(0.2); // Wait before popping
|
||||
$this->assertEquals('data1', $channel->pop());
|
||||
});
|
||||
|
||||
$pushFiber->start();
|
||||
$popFiber->start();
|
||||
|
||||
// Allow time for fibers to execute
|
||||
Timer::sleep(0.5); // 500 ms
|
||||
|
||||
$this->assertTrue($pushFiber->isTerminated());
|
||||
$this->assertTrue($popFiber->isTerminated());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that popping data from an empty channel blocks until data is available.
|
||||
*/
|
||||
public function testPopBlocksWhenEmpty()
|
||||
{
|
||||
$channel = new Channel();
|
||||
|
||||
$popFiber = new BaseFiber(function() use ($channel) {
|
||||
$this->assertEquals('data1', $channel->pop());
|
||||
});
|
||||
|
||||
$pushFiber = new BaseFiber(function() use ($channel) {
|
||||
Timer::sleep(0.2); // Wait before pushing
|
||||
$channel->push('data1');
|
||||
});
|
||||
|
||||
$popFiber->start();
|
||||
$pushFiber->start();
|
||||
|
||||
// Allow time for fibers to execute
|
||||
Timer::sleep(0.5); // 500 ms
|
||||
|
||||
$this->assertTrue($pushFiber->isTerminated());
|
||||
$this->assertTrue($popFiber->isTerminated());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test pushing and popping with zero timeout.
|
||||
*/
|
||||
public function testPushPopWithZeroTimeout()
|
||||
{
|
||||
$channel = new Channel(1);
|
||||
|
||||
$this->assertTrue($channel->push('data1'));
|
||||
|
||||
$result = $channel->push('data2', 0);
|
||||
$this->assertFalse($result);
|
||||
|
||||
$result = $channel->pop(0);
|
||||
$this->assertEquals('data1', $result);
|
||||
|
||||
$result = $channel->pop(0);
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Workerman\Coroutine\Locker;
|
||||
use RuntimeException;
|
||||
use Workerman\Coroutine;
|
||||
use ReflectionClass;
|
||||
use Workerman\Timer;
|
||||
|
||||
class LockerTest extends TestCase
|
||||
{
|
||||
|
||||
public function testLock()
|
||||
{
|
||||
$key = 'testLock';
|
||||
Locker::lock($key);
|
||||
$timeStart = microtime(true);
|
||||
$timeDiff2 = 0;
|
||||
Coroutine::create(function () use ($key, $timeStart, &$timeDiff2) {
|
||||
$this->assertChannelExists($key);
|
||||
Locker::lock($key);
|
||||
$timeDiff = microtime(true) - $timeStart;
|
||||
$this->assertGreaterThan($timeDiff2, $timeDiff);
|
||||
Locker::unlock($key);
|
||||
});
|
||||
usleep(100000);
|
||||
$timeDiff2 = microtime(true) - $timeStart;
|
||||
Locker::unlock($key);
|
||||
}
|
||||
|
||||
public function testLockAndUnlock()
|
||||
{
|
||||
$key = 'testLockAndUnlock';
|
||||
$this->assertTrue(Locker::lock($key));
|
||||
$this->assertTrue(Locker::unlock($key));
|
||||
$this->assertChannelRemoved($key);
|
||||
}
|
||||
|
||||
public function testUnlockWithoutLockThrowsException()
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
Locker::unlock('non_existent_key');
|
||||
}
|
||||
|
||||
public function testRelockAfterUnlock()
|
||||
{
|
||||
$key = 'testRelockAfterUnlock';
|
||||
Locker::lock($key);
|
||||
Locker::unlock($key);
|
||||
|
||||
$this->assertTrue(Locker::lock($key));
|
||||
Locker::unlock($key);
|
||||
$this->assertChannelRemoved($key);
|
||||
}
|
||||
|
||||
public function testMultipleCoroutinesLocking()
|
||||
{
|
||||
$key = 'testMultipleCoroutinesLocking';
|
||||
$results = [];
|
||||
Coroutine::create(function () use ($key, &$results) {
|
||||
Coroutine::create(function () use ($key, &$results) {
|
||||
Locker::lock($key);
|
||||
$results[] = 'A';
|
||||
Timer::sleep(0.1);
|
||||
usleep(100000);
|
||||
Locker::unlock($key);
|
||||
});
|
||||
|
||||
Coroutine::create(function () use ($key, &$results) {
|
||||
Timer::sleep(0.05);
|
||||
Locker::lock($key);
|
||||
$results[] = 'B';
|
||||
Locker::unlock($key);
|
||||
});
|
||||
|
||||
Coroutine::create(function () use ($key, &$results) {
|
||||
Timer::sleep(0.05);
|
||||
Locker::lock($key);
|
||||
$results[] = 'C';
|
||||
Locker::unlock($key);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Timer::sleep(0.3);
|
||||
$this->assertEquals(['A', 'B', 'C'], $results);
|
||||
$this->assertChannelRemoved($key);
|
||||
}
|
||||
|
||||
public function testChannelRemainsWhenWaiting()
|
||||
{
|
||||
$key = 'testChannelRemainsWhenWaiting';
|
||||
Locker::lock($key);
|
||||
|
||||
Coroutine::create(function () use ($key) {
|
||||
Coroutine::create(function () use ($key) {
|
||||
Locker::lock($key);
|
||||
Locker::unlock($key);
|
||||
});
|
||||
|
||||
Locker::unlock($key);
|
||||
|
||||
$this->assertChannelRemoved($key);
|
||||
});
|
||||
}
|
||||
|
||||
private function assertChannelExists(string $key): void
|
||||
{
|
||||
$channels = $this->getChannels();
|
||||
$this->assertArrayHasKey($key, $channels, "Channel for key '$key' should exist");
|
||||
}
|
||||
|
||||
private function assertChannelRemoved(string $key): void
|
||||
{
|
||||
$channels = $this->getChannels();
|
||||
$this->assertArrayNotHasKey($key, $channels, "Channel for key '$key' should be removed");
|
||||
}
|
||||
|
||||
private function getChannels(): array
|
||||
{
|
||||
$reflector = new ReflectionClass(Locker::class);
|
||||
$property = $reflector->getProperty('channels');
|
||||
return $property->getValue();
|
||||
}
|
||||
|
||||
}
|
||||
+302
@@ -0,0 +1,302 @@
|
||||
<?php
|
||||
|
||||
namespace tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Workerman\Coroutine\Parallel;
|
||||
use Workerman\Coroutine;
|
||||
use Workerman\Timer;
|
||||
|
||||
/**
|
||||
* Test cases for the Workerman\Coroutine\Parallel class.
|
||||
*/
|
||||
class ParallelTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Test that callables are added and executed, and results are collected properly.
|
||||
*/
|
||||
public function testAddAndWait()
|
||||
{
|
||||
$parallel = new Parallel();
|
||||
|
||||
$parallel->add(function () {
|
||||
// Simulate some work.
|
||||
Timer::sleep(0.01);
|
||||
return 1;
|
||||
}, 'task1');
|
||||
|
||||
$parallel->add(function () {
|
||||
// Simulate some work.
|
||||
Timer::sleep(0.005);
|
||||
return 2;
|
||||
}, 'task2');
|
||||
|
||||
$results = $parallel->wait();
|
||||
|
||||
$this->assertEquals(['task1' => 1, 'task2' => 2], $results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that exceptions thrown in callables are caught and can be retrieved.
|
||||
*/
|
||||
public function testExceptions()
|
||||
{
|
||||
$parallel = new Parallel();
|
||||
|
||||
$parallel->add(function () {
|
||||
throw new \Exception('Test exception');
|
||||
}, 'task_with_exception');
|
||||
|
||||
$parallel->add(function () {
|
||||
return 'normal result';
|
||||
}, 'normal_task');
|
||||
|
||||
$results = $parallel->wait();
|
||||
$exceptions = $parallel->getExceptions();
|
||||
|
||||
// Check that the normal task result is present.
|
||||
$this->assertEquals(['normal_task' => 'normal result'], $results);
|
||||
|
||||
// Check that the exception is captured for the failing task.
|
||||
$this->assertArrayHasKey('task_with_exception', $exceptions);
|
||||
$this->assertInstanceOf(\Exception::class, $exceptions['task_with_exception']);
|
||||
$this->assertEquals('Test exception', $exceptions['task_with_exception']->getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test concurrency control by limiting the number of concurrent tasks.
|
||||
*/
|
||||
public function testConcurrencyLimit()
|
||||
{
|
||||
$concurrentLimit = 2;
|
||||
$parallel = new Parallel($concurrentLimit);
|
||||
|
||||
$startTimes = [];
|
||||
$endTimes = [];
|
||||
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$parallel->add(function () use (&$startTimes, &$endTimes, $i) {
|
||||
$startTimes[$i] = microtime(true);
|
||||
// Simulate some work.
|
||||
Timer::sleep(0.1); // 100 milliseconds
|
||||
$endTimes[$i] = microtime(true);
|
||||
return $i;
|
||||
}, "task{$i}");
|
||||
}
|
||||
|
||||
$parallel->wait();
|
||||
|
||||
// Since we limited concurrency to 2, tasks should finish in batches.
|
||||
// We'll check that at no point more than $concurrentLimit tasks were running simultaneously.
|
||||
|
||||
// Collect start and end times into an array of intervals.
|
||||
$intervals = [];
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$intervals[] = ['start' => $startTimes[$i], 'end' => $endTimes[$i]];
|
||||
}
|
||||
|
||||
// Check the maximum number of overlapping intervals does not exceed the concurrency limit.
|
||||
$maxConcurrent = $this->getMaxConcurrentIntervals($intervals);
|
||||
|
||||
$this->assertLessThanOrEqual($concurrentLimit, $maxConcurrent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to determine the maximum number of overlapping intervals.
|
||||
*
|
||||
* @param array $intervals
|
||||
* @return int
|
||||
*/
|
||||
private function getMaxConcurrentIntervals(array $intervals)
|
||||
{
|
||||
$events = [];
|
||||
foreach ($intervals as $interval) {
|
||||
$events[] = ['time' => $interval['start'], 'type' => 'start'];
|
||||
$events[] = ['time' => $interval['end'], 'type' => 'end'];
|
||||
}
|
||||
|
||||
// Sort events by time, 'start' before 'end' if times are equal.
|
||||
usort($events, function ($a, $b) {
|
||||
if ($a['time'] == $b['time']) {
|
||||
return $a['type'] === 'start' ? -1 : 1;
|
||||
}
|
||||
return $a['time'] < $b['time'] ? -1 : 1;
|
||||
});
|
||||
|
||||
$maxConcurrent = 0;
|
||||
$currentConcurrent = 0;
|
||||
|
||||
foreach ($events as $event) {
|
||||
if ($event['type'] === 'start') {
|
||||
$currentConcurrent++;
|
||||
if ($currentConcurrent > $maxConcurrent) {
|
||||
$maxConcurrent = $currentConcurrent;
|
||||
}
|
||||
} else {
|
||||
$currentConcurrent--;
|
||||
}
|
||||
}
|
||||
|
||||
return $maxConcurrent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that callables are executed in parallel when no concurrency limit is set.
|
||||
*/
|
||||
public function testParallelExecutionWithoutConcurrencyLimit()
|
||||
{
|
||||
$parallel = new Parallel();
|
||||
|
||||
$startTimes = [];
|
||||
$endTimes = [];
|
||||
|
||||
$parallel->add(function () use (&$startTimes, &$endTimes) {
|
||||
$startTimes[] = microtime(true);
|
||||
Timer::sleep(0.1); // 100 milliseconds
|
||||
$endTimes[] = microtime(true);
|
||||
return 'task1';
|
||||
}, 'task1');
|
||||
|
||||
$parallel->add(function () use (&$startTimes, &$endTimes) {
|
||||
$startTimes[] = microtime(true);
|
||||
Timer::sleep(0.1);// 100 milliseconds
|
||||
$endTimes[] = microtime(true);
|
||||
return 'task2';
|
||||
}, 'task2');
|
||||
|
||||
$parallel->wait();
|
||||
|
||||
// Calculate total elapsed time.
|
||||
$totalTime = max($endTimes) - min($startTimes);
|
||||
|
||||
// The total time should be approximately the duration of one task, not the sum of both.
|
||||
$this->assertLessThan(0.2, $totalTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test adding callables without specifying keys and ensure results are correctly indexed.
|
||||
*/
|
||||
public function testAddWithoutKeys()
|
||||
{
|
||||
$parallel = new Parallel();
|
||||
|
||||
$parallel->add(function () {
|
||||
return 'result1';
|
||||
});
|
||||
|
||||
$parallel->add(function () {
|
||||
return 'result2';
|
||||
});
|
||||
|
||||
$results = $parallel->wait();
|
||||
|
||||
// Since no keys were specified, indices should be 0 and 1.
|
||||
$this->assertEquals(['result1', 'result2'], $results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the Parallel class can handle a large number of tasks.
|
||||
*/
|
||||
public function testLargeNumberOfTasks()
|
||||
{
|
||||
$parallel = new Parallel();
|
||||
|
||||
$taskCount = 100;
|
||||
for ($i = 0; $i < $taskCount; $i++) {
|
||||
$parallel->add(function () use ($i) {
|
||||
return $i * $i;
|
||||
}, "task{$i}");
|
||||
}
|
||||
|
||||
$results = $parallel->wait();
|
||||
|
||||
// Verify that all tasks have been completed and results are correct.
|
||||
for ($i = 0; $i < $taskCount; $i++) {
|
||||
$this->assertEquals($i * $i, $results["task{$i}"]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that adding a non-callable throws a TypeError.
|
||||
*/
|
||||
public function testAddNonCallable()
|
||||
{
|
||||
$this->expectException(\TypeError::class);
|
||||
|
||||
$parallel = new Parallel();
|
||||
$parallel->add('not a callable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the wait method can be called multiple times safely.
|
||||
*/
|
||||
public function testMultipleWaitCalls()
|
||||
{
|
||||
$parallel = new Parallel();
|
||||
|
||||
$parallel->add(function () {
|
||||
return 'first call';
|
||||
}, 'task1');
|
||||
|
||||
$resultsFirst = $parallel->wait();
|
||||
|
||||
$this->assertEquals(['task1' => 'first call'], $resultsFirst);
|
||||
|
||||
// Add another task after first wait.
|
||||
$parallel->add(function () {
|
||||
return 'second call';
|
||||
}, 'task2');
|
||||
|
||||
$resultsSecond = $parallel->wait();
|
||||
|
||||
// Since the callbacks array is not cleared after wait, results should include both tasks.
|
||||
$this->assertEquals(['task1' => 'first call', 'task2' => 'second call'], $resultsSecond);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the class properly handles empty tasks (no callables added).
|
||||
*/
|
||||
public function testNoTasks()
|
||||
{
|
||||
$parallel = new Parallel();
|
||||
|
||||
$results = $parallel->wait();
|
||||
|
||||
$this->assertEmpty($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the class handles tasks that return null.
|
||||
*/
|
||||
public function testTasksReturningNull()
|
||||
{
|
||||
$parallel = new Parallel();
|
||||
|
||||
$parallel->add(function () {
|
||||
// No return statement, implicitly returns null.
|
||||
}, 'nullTask');
|
||||
|
||||
$results = $parallel->wait();
|
||||
|
||||
$this->assertArrayHasKey('nullTask', $results);
|
||||
$this->assertNull($results['nullTask']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test defer can be used in tasks.
|
||||
*/
|
||||
public function testWithDefer()
|
||||
{
|
||||
$parallel = new Parallel();
|
||||
$results = [];
|
||||
$parallel->add(function () use (&$results) {
|
||||
Coroutine::defer(function () use (&$results) {
|
||||
$results[] = 'defer1';
|
||||
});
|
||||
});
|
||||
$parallel->wait();
|
||||
$this->assertEquals(['defer1'], $results);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+394
@@ -0,0 +1,394 @@
|
||||
<?php
|
||||
|
||||
namespace test;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionMethod;
|
||||
use ReflectionProperty;
|
||||
use Workerman\Coroutine;
|
||||
use Workerman\Coroutine\Exception\PoolException;
|
||||
use Workerman\Coroutine\Pool;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use ReflectionClass;
|
||||
use stdClass;
|
||||
use Exception;
|
||||
use Workerman\Events\Event;
|
||||
use Workerman\Events\Select;
|
||||
use Workerman\Timer;
|
||||
use Workerman\Worker;
|
||||
|
||||
class PoolTest extends TestCase
|
||||
{
|
||||
|
||||
public function testConstructorWithConfig()
|
||||
{
|
||||
$config = [
|
||||
'min_connections' => 2,
|
||||
'idle_timeout' => 30,
|
||||
'heartbeat_interval' => 10,
|
||||
'wait_timeout' => 5,
|
||||
];
|
||||
$pool = new Pool(10, $config);
|
||||
|
||||
$this->assertEquals(10, $this->getPrivateProperty($pool, 'maxConnections'));
|
||||
$this->assertEquals(2, $this->getPrivateProperty($pool, 'minConnections'));
|
||||
$this->assertEquals(30, $this->getPrivateProperty($pool, 'idleTimeout'));
|
||||
$this->assertEquals(10, $this->getPrivateProperty($pool, 'heartbeatInterval'));
|
||||
$this->assertEquals(5, $this->getPrivateProperty($pool, 'waitTimeout'));
|
||||
}
|
||||
|
||||
public function testSetConnectionCreator()
|
||||
{
|
||||
$pool = new Pool(5);
|
||||
$connectionCreator = function () {
|
||||
return new stdClass();
|
||||
};
|
||||
$pool->setConnectionCreator($connectionCreator);
|
||||
$this->assertSame($connectionCreator, $this->getPrivateProperty($pool, 'connectionCreateHandler'));
|
||||
}
|
||||
|
||||
public function testSetConnectionCloser()
|
||||
{
|
||||
$pool = new Pool(5);
|
||||
$connectionCloser = function ($conn) {
|
||||
// Close connection.
|
||||
};
|
||||
$pool->setConnectionCloser($connectionCloser);
|
||||
$this->assertSame($connectionCloser, $this->getPrivateProperty($pool, 'connectionDestroyHandler'));
|
||||
}
|
||||
|
||||
public function testGetConnection()
|
||||
{
|
||||
$pool = new Pool(5);
|
||||
|
||||
$connectionMock = $this->createMock(stdClass::class);
|
||||
|
||||
// 设置连接创建器
|
||||
$pool->setConnectionCreator(function () use ($connectionMock) {
|
||||
return $connectionMock;
|
||||
});
|
||||
|
||||
$connection = $pool->get();
|
||||
|
||||
$this->assertSame($connectionMock, $connection);
|
||||
$this->assertEquals(1, $this->getCurrentConnections($pool));
|
||||
|
||||
// 检查 WeakMap 是否更新
|
||||
$connections = $this->getPrivateProperty($pool, 'connections');
|
||||
$lastUsedTimes = $this->getPrivateProperty($pool, 'lastUsedTimes');
|
||||
$lastHeartbeatTimes = $this->getPrivateProperty($pool, 'lastHeartbeatTimes');
|
||||
|
||||
$this->assertTrue($connections->offsetExists($connection));
|
||||
$this->assertTrue($lastUsedTimes->offsetExists($connection));
|
||||
$this->assertTrue($lastHeartbeatTimes->offsetExists($connection));
|
||||
}
|
||||
|
||||
public function testPutConnection()
|
||||
{
|
||||
$pool = new Pool(5);
|
||||
|
||||
$connectionMock = $this->createMock(stdClass::class);
|
||||
|
||||
$pool->setConnectionCreator(function () use ($connectionMock) {
|
||||
return $connectionMock;
|
||||
});
|
||||
|
||||
$connection = $pool->get();
|
||||
|
||||
$pool->put($connection);
|
||||
|
||||
if (Coroutine::isCoroutine()) {
|
||||
$channel = $this->getPrivateProperty($pool, 'channel');
|
||||
$this->assertEquals(1, $channel->length());
|
||||
}
|
||||
|
||||
$this->assertEquals(1, $pool->getConnectionCount());
|
||||
}
|
||||
|
||||
public function testPutConnectionDoesNotBelong()
|
||||
{
|
||||
$this->expectException(PoolException::class);
|
||||
$this->expectExceptionMessage('The connection does not belong to the connection pool.');
|
||||
|
||||
$pool = new Pool(5);
|
||||
$connection = new stdClass();
|
||||
|
||||
$pool->put($connection);
|
||||
}
|
||||
|
||||
public function testCreateConnection()
|
||||
{
|
||||
$pool = new Pool(5);
|
||||
$connectionMock = $this->createMock(stdClass::class);
|
||||
|
||||
$pool->setConnectionCreator(function () use ($connectionMock) {
|
||||
return $connectionMock;
|
||||
});
|
||||
|
||||
$connection = $pool->createConnection();
|
||||
|
||||
$this->assertSame($connectionMock, $connection);
|
||||
|
||||
// 确保 currentConnections 增加
|
||||
$this->assertEquals(1, $this->getCurrentConnections($pool));
|
||||
|
||||
// 检查 WeakMap 是否更新
|
||||
$connections = $this->getPrivateProperty($pool, 'connections');
|
||||
$lastUsedTimes = $this->getPrivateProperty($pool, 'lastUsedTimes');
|
||||
$lastHeartbeatTimes = $this->getPrivateProperty($pool, 'lastHeartbeatTimes');
|
||||
|
||||
$this->assertTrue($connections->offsetExists($connection));
|
||||
$this->assertTrue($lastUsedTimes->offsetExists($connection));
|
||||
$this->assertTrue($lastHeartbeatTimes->offsetExists($connection));
|
||||
}
|
||||
|
||||
public function testCreateMaxConnections()
|
||||
{
|
||||
if (in_array(Worker::$eventLoopClass, [Select::class, Event::class])) {
|
||||
$this->assertTrue(true);
|
||||
return;
|
||||
}
|
||||
$maxConnections = 2;
|
||||
$pool = new Pool($maxConnections);
|
||||
|
||||
$pool->setConnectionCreator(function () {
|
||||
Timer::sleep(0.01);
|
||||
return $this->createMock(stdClass::class);
|
||||
});
|
||||
|
||||
$connections = [];
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
Coroutine::create(function () use ($pool, &$connections) {
|
||||
$connections[] = $pool->get();
|
||||
});
|
||||
}
|
||||
|
||||
Timer::sleep(0.1);
|
||||
$this->assertEquals($maxConnections, $this->getCurrentConnections($pool));
|
||||
|
||||
$lastUsedTimes = $this->getPrivateProperty($pool, 'lastUsedTimes');
|
||||
$lastHeartbeatTimes = $this->getPrivateProperty($pool, 'lastHeartbeatTimes');
|
||||
|
||||
$this->assertCount($maxConnections, $lastUsedTimes);
|
||||
$this->assertCount($maxConnections, $lastHeartbeatTimes);
|
||||
|
||||
foreach ($connections as $connection) {
|
||||
$pool->put($connection);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function testCreateConnectionThrowsException()
|
||||
{
|
||||
$pool = new Pool(5);
|
||||
|
||||
$pool->setConnectionCreator(function () {
|
||||
throw new Exception('Failed to create connection');
|
||||
});
|
||||
|
||||
$this->expectException(Exception::class);
|
||||
$this->expectExceptionMessage('Failed to create connection');
|
||||
|
||||
try {
|
||||
$pool->createConnection();
|
||||
} finally {
|
||||
// 确保 currentConnections 减少
|
||||
$this->assertEquals(0, $this->getCurrentConnections($pool));
|
||||
}
|
||||
}
|
||||
|
||||
public function testCloseConnection()
|
||||
{
|
||||
$pool = new Pool(5);
|
||||
|
||||
$connection = $this->createMock(ConnectionMock::class);
|
||||
|
||||
// 模拟连接属于连接池
|
||||
$connections = $this->getPrivateProperty($pool, 'connections');
|
||||
$connections[$connection] = time();
|
||||
|
||||
$connection->expects($this->once())->method('close');
|
||||
$pool->setConnectionCloser(function ($conn) {
|
||||
$conn->close();
|
||||
});
|
||||
|
||||
$pool->closeConnection($connection);
|
||||
|
||||
// 确保 currentConnections 减少
|
||||
$this->assertEquals(0, $this->getCurrentConnections($pool));
|
||||
|
||||
// 确保连接从 WeakMap 中移除
|
||||
$this->assertFalse($connections->offsetExists($connection));
|
||||
}
|
||||
|
||||
public function testCloseConnections()
|
||||
{
|
||||
$maxConnections = 5;
|
||||
|
||||
$pool = new Pool($maxConnections);
|
||||
|
||||
$pool->setConnectionCreator(function () {
|
||||
$connection = $this->createMock(ConnectionMock::class);
|
||||
$connection->expects($this->once())->method('close');
|
||||
return $connection;
|
||||
});
|
||||
|
||||
$pool->setConnectionCloser(function ($conn) {
|
||||
$conn->close();
|
||||
});
|
||||
|
||||
$connections = [];
|
||||
for ($i = 0; $i < $maxConnections; $i++) {
|
||||
$connections[] = $pool->get();
|
||||
}
|
||||
|
||||
$this->assertEquals(Coroutine::isCoroutine() ? $maxConnections : 1, $this->getCurrentConnections($pool));
|
||||
|
||||
$pool->closeConnections();
|
||||
$this->assertEquals(Coroutine::isCoroutine() ? $maxConnections : 0, $this->getCurrentConnections($pool));
|
||||
if (!Coroutine::isCoroutine()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($connections as $connection) {
|
||||
$pool->put($connection);
|
||||
}
|
||||
$this->assertEquals($maxConnections, $this->getCurrentConnections($pool));
|
||||
$pool->closeConnections();
|
||||
$this->assertEquals(0, $this->getCurrentConnections($pool));
|
||||
|
||||
$connections = [];
|
||||
for ($i = 0; $i < $maxConnections; $i++) {
|
||||
$connections[] = $pool->get();
|
||||
}
|
||||
$this->assertEquals($maxConnections, $this->getCurrentConnections($pool));
|
||||
foreach ($connections as $connection) {
|
||||
$pool->put($connection);
|
||||
}
|
||||
$pool->closeConnections();
|
||||
unset($connections);
|
||||
$this->assertEquals(0, $this->getCurrentConnections($pool));
|
||||
}
|
||||
|
||||
public function testCloseConnectionWithExceptionInDestroyHandler()
|
||||
{
|
||||
$pool = new Pool(5);
|
||||
|
||||
$connection = $this->createMock(stdClass::class);
|
||||
|
||||
// 模拟连接属于连接池
|
||||
$connections = $this->getPrivateProperty($pool, 'connections');
|
||||
$connections[$connection] = time();
|
||||
|
||||
$exception = new Exception('Error closing connection');
|
||||
|
||||
$pool->setConnectionCloser(function ($conn) use ($exception) {
|
||||
throw $exception;
|
||||
});
|
||||
|
||||
// 设置日志记录器
|
||||
$loggerMock = $this->createMock(LoggerInterface::class);
|
||||
$loggerMock->expects($this->once())
|
||||
->method('info')
|
||||
->with($this->stringContains('Error closing connection'));
|
||||
|
||||
$this->setPrivateProperty($pool, 'logger', $loggerMock);
|
||||
|
||||
$pool->closeConnection($connection);
|
||||
|
||||
// 确保 currentConnections 减少
|
||||
$this->assertEquals(0, $this->getCurrentConnections($pool));
|
||||
|
||||
// 确保连接从 WeakMap 中移除
|
||||
$this->assertFalse($connections->offsetExists($connection));
|
||||
}
|
||||
|
||||
public function testHeartbeatChecker()
|
||||
{
|
||||
$pool = $this->getMockBuilder(Pool::class)
|
||||
->setConstructorArgs([5])
|
||||
->onlyMethods(['closeConnection'])
|
||||
->getMock();
|
||||
|
||||
$connection = $this->createMock(stdClass::class);
|
||||
|
||||
// 设置连接心跳检测器
|
||||
$pool->setHeartbeatChecker(function ($conn) {
|
||||
// 模拟心跳检测
|
||||
});
|
||||
|
||||
// 模拟连接在通道中
|
||||
$channel = $this->getPrivateProperty($pool, 'channel');
|
||||
$channel->push($connection);
|
||||
|
||||
// 设置连接的上次使用时间和心跳时间
|
||||
$connections = $this->getPrivateProperty($pool, 'connections');
|
||||
$connections[$connection] = time();
|
||||
|
||||
$lastUsedTimes = $this->getPrivateProperty($pool, 'lastUsedTimes');
|
||||
$lastUsedTimes[$connection] = time();
|
||||
|
||||
$lastHeartbeatTimes = $this->getPrivateProperty($pool, 'lastHeartbeatTimes');
|
||||
$lastHeartbeatTimes[$connection] = time() - 100; // 超过心跳间隔
|
||||
|
||||
// 调用受保护的 checkConnections 方法
|
||||
$reflectedMethod = new ReflectionMethod($pool, 'checkConnections');
|
||||
$reflectedMethod->invoke($pool);
|
||||
|
||||
// 检查心跳时间是否更新
|
||||
$lastHeartbeatTimes = $this->getPrivateProperty($pool, 'lastHeartbeatTimes');
|
||||
$this->assertGreaterThan(time() - 2, $lastHeartbeatTimes[$connection]);
|
||||
}
|
||||
|
||||
public function testConnectionDestroyedWithoutReturn()
|
||||
{
|
||||
$pool = new Pool(5);
|
||||
|
||||
// 设置连接创建器
|
||||
$pool->setConnectionCreator(function () {
|
||||
return new stdClass;
|
||||
});
|
||||
|
||||
// 获取初始的 currentConnections
|
||||
$initialConnections = $this->getCurrentConnections($pool);
|
||||
|
||||
// 从连接池获取一个连接
|
||||
$connection = $pool->get();
|
||||
|
||||
// 检查 currentConnections 是否增加
|
||||
$this->assertEquals(Coroutine::isCoroutine() ? $initialConnections + 1 : 1, $this->getCurrentConnections($pool));
|
||||
|
||||
// 不归还连接,并销毁连接对象
|
||||
unset($connection);
|
||||
|
||||
// 检查 currentConnections 是否减少
|
||||
$this->assertEquals(Coroutine::isCoroutine() ? $initialConnections : 1, $this->getCurrentConnections($pool));
|
||||
}
|
||||
|
||||
private function getPrivateProperty($object, string $property)
|
||||
{
|
||||
$prop = new ReflectionProperty($object, $property);
|
||||
return $prop->getValue($object);
|
||||
}
|
||||
|
||||
private function setPrivateProperty($object, string $property, $value)
|
||||
{
|
||||
$prop = new ReflectionProperty($object, $property);
|
||||
$prop->setValue($object, $value);
|
||||
}
|
||||
|
||||
private function getCurrentConnections($object): int
|
||||
{
|
||||
return $object->getConnectionCount();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 定义 ConnectionMock 类用于测试
|
||||
class ConnectionMock
|
||||
{
|
||||
public function close()
|
||||
{
|
||||
// 模拟关闭连接
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Workerman\Coroutine;
|
||||
use Workerman\Timer;
|
||||
use Workerman\Coroutine\WaitGroup;
|
||||
|
||||
/**
|
||||
* Class WaitGroupTest
|
||||
*
|
||||
* Tests for the Fiber WaitGroup implementation.
|
||||
*/
|
||||
class WaitGroupTest extends TestCase
|
||||
{
|
||||
|
||||
public function testWaitWaitGroupDone()
|
||||
{
|
||||
$waitGroup = new WaitGroup();
|
||||
$this->assertEquals(0, $waitGroup->count());
|
||||
$results = [0];
|
||||
$this->assertTrue($waitGroup->add());
|
||||
Coroutine::create(function () use ($waitGroup, &$results) {
|
||||
try {
|
||||
Timer::sleep(0.1);
|
||||
$results[] = 1;
|
||||
} finally {
|
||||
$this->assertTrue($waitGroup->done());
|
||||
}
|
||||
});
|
||||
$this->assertTrue($waitGroup->add());
|
||||
Coroutine::create(function () use ($waitGroup, &$results) {
|
||||
try {
|
||||
Timer::sleep(0.2);
|
||||
$results[] = 2;
|
||||
} finally {
|
||||
$this->assertTrue($waitGroup->done());
|
||||
}
|
||||
});
|
||||
$this->assertTrue($waitGroup->add());
|
||||
Coroutine::create(function () use ($waitGroup, &$results) {
|
||||
try {
|
||||
Timer::sleep(0.3);
|
||||
$results[] = 3;
|
||||
} finally {
|
||||
$this->assertTrue($waitGroup->done());
|
||||
}
|
||||
});
|
||||
$this->assertTrue($waitGroup->wait());
|
||||
$this->assertEquals(0, $waitGroup->count(), 'WaitGroup count should be 0 after wait is called.');
|
||||
$this->assertEquals([0, 1, 2, 3], $results, 'All coroutines should have been executed.');
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
use Workerman\Events\Event;
|
||||
use Workerman\Events\Select;
|
||||
use Workerman\Events\Swow;
|
||||
use Workerman\Events\Swoole;
|
||||
use Workerman\Events\Fiber;
|
||||
use Workerman\Timer;
|
||||
use Workerman\Worker;
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
$phpunitDisplayOptions = [
|
||||
'--colors=always',
|
||||
'--display-deprecations',
|
||||
'--display-phpunit-deprecations',
|
||||
'--display-errors',
|
||||
'--display-notices',
|
||||
'--display-warnings',
|
||||
'--display-incomplete',
|
||||
'--display-skipped',
|
||||
];
|
||||
|
||||
if (DIRECTORY_SEPARATOR === '/' || (!extension_loaded('swow') && !class_exists(Revolt\EventLoop::class))) {
|
||||
create_test_worker(function () use ($phpunitDisplayOptions) {
|
||||
(new PHPUnit\TextUI\Application)->run([
|
||||
__DIR__ . '/../vendor/bin/phpunit',
|
||||
...$phpunitDisplayOptions,
|
||||
__DIR__ . '/ChannelTest.php',
|
||||
__DIR__ . '/PoolTest.php',
|
||||
__DIR__ . '/BarrierTest.php',
|
||||
__DIR__ . '/ContextTest.php',
|
||||
__DIR__ . '/WaitGroupTest.php',
|
||||
]);
|
||||
}, Select::class);
|
||||
}
|
||||
|
||||
if (extension_loaded('event')) {
|
||||
create_test_worker(function () use ($phpunitDisplayOptions) {
|
||||
(new PHPUnit\TextUI\Application)->run([
|
||||
__DIR__ . '/../vendor/bin/phpunit',
|
||||
...$phpunitDisplayOptions,
|
||||
__DIR__ . '/ChannelTest.php',
|
||||
__DIR__ . '/PoolTest.php',
|
||||
__DIR__ . '/BarrierTest.php',
|
||||
__DIR__ . '/ContextTest.php',
|
||||
__DIR__ . '/WaitGroupTest.php',
|
||||
]);
|
||||
}, Event::class);
|
||||
}
|
||||
|
||||
if (class_exists(Revolt\EventLoop::class) && (DIRECTORY_SEPARATOR === '/' || !extension_loaded('swow'))) {
|
||||
create_test_worker(function () use ($phpunitDisplayOptions) {
|
||||
(new PHPUnit\TextUI\Application)->run([
|
||||
__DIR__ . '/../vendor/bin/phpunit',
|
||||
...$phpunitDisplayOptions,
|
||||
...glob(__DIR__ . '/*Test.php')
|
||||
]);
|
||||
}, Fiber::class);
|
||||
}
|
||||
|
||||
if (extension_loaded('Swoole')) {
|
||||
create_test_worker(function () use ($phpunitDisplayOptions) {
|
||||
(new PHPUnit\TextUI\Application)->run([
|
||||
__DIR__ . '/../vendor/bin/phpunit',
|
||||
...$phpunitDisplayOptions,
|
||||
...glob(__DIR__ . '/*Test.php')
|
||||
]);
|
||||
}, Swoole::class);
|
||||
}
|
||||
|
||||
if (extension_loaded('Swow')) {
|
||||
create_test_worker(function () use ($phpunitDisplayOptions) {
|
||||
(new PHPUnit\TextUI\Application)->run([
|
||||
__DIR__ . '/../vendor/bin/phpunit',
|
||||
...$phpunitDisplayOptions,
|
||||
...glob(__DIR__ . '/*Test.php')
|
||||
]);
|
||||
}, Swow::class);
|
||||
}
|
||||
|
||||
function create_test_worker(Closure $callable, $eventLoopClass): void
|
||||
{
|
||||
$worker = new Worker();
|
||||
$worker->eventLoop = $eventLoopClass;
|
||||
$worker->onWorkerStart = function () use ($callable, $eventLoopClass) {
|
||||
$fp = fopen(__FILE__, 'r+');
|
||||
flock($fp, LOCK_EX);
|
||||
echo PHP_EOL . PHP_EOL. PHP_EOL . '[TEST EVENT-LOOP: ' . basename(str_replace('\\', '/', $eventLoopClass)) . ']' . PHP_EOL;
|
||||
try {
|
||||
$callable();
|
||||
} catch (Throwable $e) {
|
||||
echo $e;
|
||||
} finally {
|
||||
flock($fp, LOCK_UN);
|
||||
}
|
||||
Timer::repeat(1, function () use ($fp) {
|
||||
if (flock($fp, LOCK_EX | LOCK_NB)) {
|
||||
if(function_exists('posix_kill')) {
|
||||
posix_kill(posix_getppid(), SIGINT);
|
||||
} else {
|
||||
Worker::stopAll();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
Worker::runAll();
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2009-2025 walkor<walkor@workerman.net> and contributors (see https://github.com/walkor/workerman/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.
|
||||
Vendored
+477
@@ -0,0 +1,477 @@
|
||||
# Workerman
|
||||
[](https://gitter.im/walkor/Workerman?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=body_badge)
|
||||
[](https://packagist.org/packages/workerman/workerman)
|
||||
[](https://packagist.org/packages/workerman/workerman)
|
||||
[](https://packagist.org/packages/workerman/workerman)
|
||||
[](https://packagist.org/packages/workerman/workerman)
|
||||
[](https://packagist.org/packages/workerman/workerman)
|
||||
|
||||
## What is it
|
||||
Workerman is an asynchronous event-driven PHP framework with high performance to build fast and scalable network applications. It supports HTTP, WebSocket, custom protocols, coroutines, and connection pools, making it ideal for handling high-concurrency scenarios efficiently.
|
||||
|
||||
## Requires
|
||||
A POSIX compatible operating system (Linux, OSX, BSD)
|
||||
POSIX and PCNTL extensions required
|
||||
Event/Swoole/Swow extension recommended for better performance
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
composer require workerman/workerman
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
[https://manual.workerman.net](https://manual.workerman.net)
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### A websocket server
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Workerman\Worker;
|
||||
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
// Create a Websocket server
|
||||
$ws_worker = new Worker('websocket://0.0.0.0:2346');
|
||||
|
||||
// Emitted when new connection come
|
||||
$ws_worker->onConnect = function ($connection) {
|
||||
echo "New connection\n";
|
||||
};
|
||||
|
||||
// Emitted when data received
|
||||
$ws_worker->onMessage = function ($connection, $data) {
|
||||
// Send hello $data
|
||||
$connection->send('Hello ' . $data);
|
||||
};
|
||||
|
||||
// Emitted when connection closed
|
||||
$ws_worker->onClose = function ($connection) {
|
||||
echo "Connection closed\n";
|
||||
};
|
||||
|
||||
// Run worker
|
||||
Worker::runAll();
|
||||
```
|
||||
|
||||
### An http server
|
||||
```php
|
||||
use Workerman\Worker;
|
||||
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
// #### http worker ####
|
||||
$http_worker = new Worker('http://0.0.0.0:2345');
|
||||
|
||||
// 4 processes
|
||||
$http_worker->count = 4;
|
||||
|
||||
// Emitted when data received
|
||||
$http_worker->onMessage = function ($connection, $request) {
|
||||
//$request->get();
|
||||
//$request->post();
|
||||
//$request->header();
|
||||
//$request->cookie();
|
||||
//$request->session();
|
||||
//$request->uri();
|
||||
//$request->path();
|
||||
//$request->method();
|
||||
|
||||
// Send data to client
|
||||
$connection->send("Hello World");
|
||||
};
|
||||
|
||||
// Run all workers
|
||||
Worker::runAll();
|
||||
```
|
||||
|
||||
### A tcp server
|
||||
```php
|
||||
use Workerman\Worker;
|
||||
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
// #### create socket and listen 1234 port ####
|
||||
$tcp_worker = new Worker('tcp://0.0.0.0:1234');
|
||||
|
||||
// 4 processes
|
||||
$tcp_worker->count = 4;
|
||||
|
||||
// Emitted when new connection come
|
||||
$tcp_worker->onConnect = function ($connection) {
|
||||
echo "New Connection\n";
|
||||
};
|
||||
|
||||
// Emitted when data received
|
||||
$tcp_worker->onMessage = function ($connection, $data) {
|
||||
// Send data to client
|
||||
$connection->send("Hello $data \n");
|
||||
};
|
||||
|
||||
// Emitted when connection is closed
|
||||
$tcp_worker->onClose = function ($connection) {
|
||||
echo "Connection closed\n";
|
||||
};
|
||||
|
||||
Worker::runAll();
|
||||
```
|
||||
|
||||
### Enable SSL
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Workerman\Worker;
|
||||
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
// SSL context.
|
||||
$context = [
|
||||
'ssl' => [
|
||||
'local_cert' => '/your/path/of/server.pem',
|
||||
'local_pk' => '/your/path/of/server.key',
|
||||
'verify_peer' => false,
|
||||
]
|
||||
];
|
||||
|
||||
// Create a Websocket server with ssl context.
|
||||
$ws_worker = new Worker('websocket://0.0.0.0:2346', $context);
|
||||
|
||||
// Enable SSL. WebSocket+SSL means that Secure WebSocket (wss://).
|
||||
// The similar approaches for Https etc.
|
||||
$ws_worker->transport = 'ssl';
|
||||
|
||||
$ws_worker->onMessage = function ($connection, $data) {
|
||||
// Send hello $data
|
||||
$connection->send('Hello ' . $data);
|
||||
};
|
||||
|
||||
Worker::runAll();
|
||||
```
|
||||
|
||||
### AsyncTcpConnection (tcp/ws/text/frame etc...)
|
||||
```php
|
||||
|
||||
use Workerman\Worker;
|
||||
use Workerman\Connection\AsyncTcpConnection;
|
||||
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
$worker = new Worker();
|
||||
$worker->onWorkerStart = function () {
|
||||
// Websocket protocol for client.
|
||||
$ws_connection = new AsyncTcpConnection('ws://echo.websocket.org:80');
|
||||
$ws_connection->onConnect = function ($connection) {
|
||||
$connection->send('Hello');
|
||||
};
|
||||
$ws_connection->onMessage = function ($connection, $data) {
|
||||
echo "Recv: $data\n";
|
||||
};
|
||||
$ws_connection->onError = function ($connection, $code, $msg) {
|
||||
echo "Error: $msg\n";
|
||||
};
|
||||
$ws_connection->onClose = function ($connection) {
|
||||
echo "Connection closed\n";
|
||||
};
|
||||
$ws_connection->connect();
|
||||
};
|
||||
|
||||
Worker::runAll();
|
||||
```
|
||||
|
||||
### Coroutine
|
||||
|
||||
Coroutine is used to create coroutines, enabling the execution of asynchronous tasks to improve concurrency performance.
|
||||
|
||||
```php
|
||||
<?php
|
||||
use Workerman\Connection\TcpConnection;
|
||||
use Workerman\Coroutine;
|
||||
use Workerman\Events\Swoole;
|
||||
use Workerman\Protocols\Http\Request;
|
||||
use Workerman\Worker;
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
$worker = new Worker('http://0.0.0.0:8001');
|
||||
|
||||
$worker->eventLoop = Swoole::class; // Or Swow::class or Fiber::class
|
||||
|
||||
$worker->onMessage = function (TcpConnection $connection, Request $request) {
|
||||
Coroutine::create(function () {
|
||||
echo file_get_contents("http://www.example.com/event/notify");
|
||||
});
|
||||
$connection->send('ok');
|
||||
};
|
||||
|
||||
Worker::runAll();
|
||||
```
|
||||
|
||||
> Note: Coroutine require Swoole extension or Swow extension or [Fiber revolt/event-loop](https://github.com/revoltphp/event-loop), and the same applies below
|
||||
|
||||
### Barrier
|
||||
Barrier is used to manage concurrency and synchronization in coroutines. It allows tasks to run concurrently and waits until all tasks are completed, ensuring process synchronization.
|
||||
|
||||
```php
|
||||
<?php
|
||||
use Workerman\Connection\TcpConnection;
|
||||
use Workerman\Coroutine\Barrier;
|
||||
use Workerman\Coroutine;
|
||||
use Workerman\Events\Swoole;
|
||||
use Workerman\Protocols\Http\Request;
|
||||
use Workerman\Worker;
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
// Http Server
|
||||
$worker = new Worker('http://0.0.0.0:8001');
|
||||
$worker->eventLoop = Swoole::class; // Or Swow::class or Fiber::class
|
||||
$worker->onMessage = function (TcpConnection $connection, Request $request) {
|
||||
$barrier = Barrier::create();
|
||||
for ($i=1; $i<5; $i++) {
|
||||
Coroutine::create(function () use ($barrier, $i) {
|
||||
file_get_contents("http://127.0.0.1:8002?task_id=$i");
|
||||
});
|
||||
}
|
||||
// Wait all coroutine done
|
||||
Barrier::wait($barrier);
|
||||
$connection->send('All Task Done');
|
||||
};
|
||||
|
||||
// Task Server
|
||||
$task = new Worker('http://0.0.0.0:8002');
|
||||
$task->onMessage = function (TcpConnection $connection, Request $request) {
|
||||
$task_id = $request->get('task_id');
|
||||
$message = "Task $task_id Done";
|
||||
echo $message . PHP_EOL;
|
||||
$connection->close($message);
|
||||
};
|
||||
|
||||
Worker::runAll();
|
||||
```
|
||||
|
||||
### Parallel
|
||||
Parallel executes multiple tasks concurrently and collects results. Use add to add tasks and wait to wait for completion and get results. Unlike Barrier, Parallel directly returns the results of each task.
|
||||
|
||||
```php
|
||||
<?php
|
||||
use Workerman\Connection\TcpConnection;
|
||||
use Workerman\Coroutine\Parallel;
|
||||
use Workerman\Events\Swoole;
|
||||
use Workerman\Protocols\Http\Request;
|
||||
use Workerman\Worker;
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
// Http Server
|
||||
$worker = new Worker('http://0.0.0.0:8001');
|
||||
$worker->eventLoop = Swoole::class; // Or Swow::class or Fiber::class
|
||||
$worker->onMessage = function (TcpConnection $connection, Request $request) {
|
||||
$parallel = new Parallel();
|
||||
for ($i=1; $i<5; $i++) {
|
||||
$parallel->add(function () use ($i) {
|
||||
return file_get_contents("http://127.0.0.1:8002?task_id=$i");
|
||||
});
|
||||
}
|
||||
$results = $parallel->wait();
|
||||
$connection->send(json_encode($results)); // Response: ["Task 1 Done","Task 2 Done","Task 3 Done","Task 4 Done"]
|
||||
};
|
||||
|
||||
// Task Server
|
||||
$task = new Worker('http://0.0.0.0:8002');
|
||||
$task->onMessage = function (TcpConnection $connection, Request $request) {
|
||||
$task_id = $request->get('task_id');
|
||||
$message = "Task $task_id Done";
|
||||
$connection->close($message);
|
||||
};
|
||||
|
||||
Worker::runAll();
|
||||
```
|
||||
|
||||
### Channel
|
||||
|
||||
Channel is a mechanism for communication between coroutines. One coroutine can push data into the channel, while another can pop data from it, enabling synchronization and data sharing between coroutines.
|
||||
|
||||
```php
|
||||
<?php
|
||||
use Workerman\Connection\TcpConnection;
|
||||
use Workerman\Coroutine\Channel;
|
||||
use Workerman\Coroutine;
|
||||
use Workerman\Events\Swoole;
|
||||
use Workerman\Protocols\Http\Request;
|
||||
use Workerman\Worker;
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
// Http Server
|
||||
$worker = new Worker('http://0.0.0.0:8001');
|
||||
$worker->eventLoop = Swoole::class; // Or Swow::class or Fiber::class
|
||||
$worker->onMessage = function (TcpConnection $connection, Request $request) {
|
||||
$channel = new Channel(2);
|
||||
Coroutine::create(function () use ($channel) {
|
||||
$channel->push('Task 1 Done');
|
||||
});
|
||||
Coroutine::create(function () use ($channel) {
|
||||
$channel->push('Task 2 Done');
|
||||
});
|
||||
$result = [];
|
||||
for ($i = 0; $i < 2; $i++) {
|
||||
$result[] = $channel->pop();
|
||||
}
|
||||
$connection->send(json_encode($result)); // Response: ["Task 1 Done","Task 2 Done"]
|
||||
};
|
||||
Worker::runAll();
|
||||
```
|
||||
|
||||
### Pool
|
||||
|
||||
Pool is used to manage connection or resource pools, improving performance by reusing resources (e.g., database connections). It supports acquiring, returning, creating, and destroying resources.
|
||||
|
||||
```php
|
||||
<?php
|
||||
use Workerman\Connection\TcpConnection;
|
||||
use Workerman\Coroutine\Pool;
|
||||
use Workerman\Events\Swoole;
|
||||
use Workerman\Protocols\Http\Request;
|
||||
use Workerman\Worker;
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
class RedisPool
|
||||
{
|
||||
private Pool $pool;
|
||||
public function __construct($host, $port, $max_connections = 10)
|
||||
{
|
||||
$pool = new Pool($max_connections);
|
||||
$pool->setConnectionCreator(function () use ($host, $port) {
|
||||
$redis = new \Redis();
|
||||
$redis->connect($host, $port);
|
||||
return $redis;
|
||||
});
|
||||
$pool->setConnectionCloser(function ($redis) {
|
||||
$redis->close();
|
||||
});
|
||||
$pool->setHeartbeatChecker(function ($redis) {
|
||||
$redis->ping();
|
||||
});
|
||||
$this->pool = $pool;
|
||||
}
|
||||
public function get(): \Redis
|
||||
{
|
||||
return $this->pool->get();
|
||||
}
|
||||
public function put($redis): void
|
||||
{
|
||||
$this->pool->put($redis);
|
||||
}
|
||||
}
|
||||
|
||||
// Http Server
|
||||
$worker = new Worker('http://0.0.0.0:8001');
|
||||
$worker->eventLoop = Swoole::class; // Or Swow::class or Fiber::class
|
||||
$worker->onMessage = function (TcpConnection $connection, Request $request) {
|
||||
static $pool;
|
||||
if (!$pool) {
|
||||
$pool = new RedisPool('127.0.0.1', 6379, 10);
|
||||
}
|
||||
$redis = $pool->get();
|
||||
$redis->set('key', 'hello');
|
||||
$value = $redis->get('key');
|
||||
$pool->put($redis);
|
||||
$connection->send($value);
|
||||
};
|
||||
|
||||
Worker::runAll();
|
||||
```
|
||||
|
||||
|
||||
### Pool for automatic acquisition and release
|
||||
|
||||
```php
|
||||
<?php
|
||||
use Workerman\Connection\TcpConnection;
|
||||
use Workerman\Coroutine\Context;
|
||||
use Workerman\Coroutine;
|
||||
use Workerman\Coroutine\Pool;
|
||||
use Workerman\Events\Swoole;
|
||||
use Workerman\Protocols\Http\Request;
|
||||
use Workerman\Worker;
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
class Db
|
||||
{
|
||||
private static ?Pool $pool = null;
|
||||
public static function __callStatic($name, $arguments)
|
||||
{
|
||||
if (self::$pool === null) {
|
||||
self::initializePool();
|
||||
}
|
||||
// Get the connection from the coroutine context
|
||||
// to ensure the same connection is used within the same coroutine
|
||||
$pdo = Context::get('pdo');
|
||||
if (!$pdo) {
|
||||
// If no connection is retrieved, get one from the connection pool
|
||||
$pdo = self::$pool->get();
|
||||
Context::set('pdo', $pdo);
|
||||
// When the coroutine is destroyed, return the connection to the pool
|
||||
Coroutine::defer(function () use ($pdo) {
|
||||
self::$pool->put($pdo);
|
||||
});
|
||||
}
|
||||
return call_user_func_array([$pdo, $name], $arguments);
|
||||
}
|
||||
private static function initializePool(): void
|
||||
{
|
||||
self::$pool = new Pool(10);
|
||||
self::$pool->setConnectionCreator(function () {
|
||||
return new \PDO('mysql:host=127.0.0.1;dbname=your_database', 'your_username', 'your_password');
|
||||
});
|
||||
self::$pool->setConnectionCloser(function ($pdo) {
|
||||
$pdo = null;
|
||||
});
|
||||
self::$pool->setHeartbeatChecker(function ($pdo) {
|
||||
$pdo->query('SELECT 1');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Http Server
|
||||
$worker = new Worker('http://0.0.0.0:8001');
|
||||
$worker->eventLoop = Swoole::class; // Or Swow::class or Fiber::class
|
||||
$worker->onMessage = function (TcpConnection $connection, Request $request) {
|
||||
$value = Db::query('SELECT NOW() as now')->fetchAll();
|
||||
$connection->send(json_encode($value));
|
||||
};
|
||||
|
||||
Worker::runAll();
|
||||
```
|
||||
|
||||
## Available commands
|
||||
```php start.php start ```
|
||||
```php start.php start -d ```
|
||||
```php start.php status ```
|
||||
```php start.php status -d ```
|
||||
```php start.php connections```
|
||||
```php start.php stop ```
|
||||
```php start.php stop -g ```
|
||||
```php start.php restart ```
|
||||
```php start.php reload ```
|
||||
```php start.php reload -g ```
|
||||
|
||||
# Benchmarks
|
||||
https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext&l=zik073-1r
|
||||
|
||||
|
||||
### Supported by
|
||||
|
||||
[](https://jb.gg/OpenSourceSupport)
|
||||
|
||||
|
||||
## Other links with workerman
|
||||
|
||||
[webman](https://github.com/walkor/webman)
|
||||
[AdapterMan](https://github.com/joanhey/AdapterMan)
|
||||
|
||||
## Donate
|
||||
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=UQGGS9UB35WWG">PayPal</a>
|
||||
|
||||
## LICENSE
|
||||
|
||||
Workerman is released under the [MIT license](https://github.com/walkor/workerman/blob/master/MIT-LICENSE.txt).
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Security Policy
|
||||
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please contact by email walkor@workerman.net
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "workerman/workerman",
|
||||
"type": "library",
|
||||
"keywords": [
|
||||
"event-loop",
|
||||
"asynchronous",
|
||||
"http",
|
||||
"framework"
|
||||
],
|
||||
"homepage": "https://www.workerman.net",
|
||||
"license": "MIT",
|
||||
"description": "An asynchronous event driven PHP framework for easily building fast, scalable network applications.",
|
||||
"authors": [
|
||||
{
|
||||
"name": "walkor",
|
||||
"email": "walkor@workerman.net",
|
||||
"homepage": "https://www.workerman.net",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"support": {
|
||||
"email": "walkor@workerman.net",
|
||||
"issues": "https://github.com/walkor/workerman/issues",
|
||||
"forum": "https://www.workerman.net/questions",
|
||||
"wiki": "https://www.workerman.net/doc/workerman/",
|
||||
"source": "https://github.com/walkor/workerman"
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"ext-json": "*",
|
||||
"workerman/coroutine": "^1.1 || dev-main"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-event": "For better performance. "
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Workerman\\": "src"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"conflict": {
|
||||
"ext-swow": "<v1.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"pestphp/pest": "^2.36 || ^3 || ^4",
|
||||
"mockery/mockery": "^1.6",
|
||||
"guzzlehttp/guzzle": "^7.10",
|
||||
"phpstan/phpstan": "^2.1"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"analyze": "php -d memory_limit=1G vendor/phpstan/phpstan/phpstan.phar",
|
||||
"test": "pest --colors=always"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,478 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Connection;
|
||||
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
use stdClass;
|
||||
use Throwable;
|
||||
use Workerman\Timer;
|
||||
use Workerman\Worker;
|
||||
use function class_exists;
|
||||
use function explode;
|
||||
use function function_exists;
|
||||
use function is_resource;
|
||||
use function method_exists;
|
||||
use function microtime;
|
||||
use function parse_url;
|
||||
use function socket_import_stream;
|
||||
use function socket_set_option;
|
||||
use function stream_context_create;
|
||||
use function stream_set_blocking;
|
||||
use function stream_set_read_buffer;
|
||||
use function stream_socket_client;
|
||||
use function stream_socket_get_name;
|
||||
use function ucfirst;
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use const PHP_INT_MAX;
|
||||
use const SO_KEEPALIVE;
|
||||
use const SOL_SOCKET;
|
||||
use const SOL_TCP;
|
||||
use const STREAM_CLIENT_ASYNC_CONNECT;
|
||||
use const TCP_NODELAY;
|
||||
|
||||
/**
|
||||
* AsyncTcpConnection.
|
||||
*/
|
||||
class AsyncTcpConnection extends TcpConnection
|
||||
{
|
||||
/**
|
||||
* PHP built-in protocols.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public const BUILD_IN_TRANSPORTS = [
|
||||
'tcp' => 'tcp',
|
||||
'udp' => 'udp',
|
||||
'unix' => 'unix',
|
||||
'ssl' => 'ssl',
|
||||
'sslv2' => 'sslv2',
|
||||
'sslv3' => 'sslv3',
|
||||
'tls' => 'tls'
|
||||
];
|
||||
|
||||
/**
|
||||
* Emitted when socket connection is successfully established.
|
||||
*
|
||||
* @var ?callable
|
||||
*/
|
||||
public $onConnect = null;
|
||||
|
||||
/**
|
||||
* Emitted when websocket handshake completed (Only work when protocol is ws).
|
||||
*
|
||||
* @var ?callable
|
||||
*/
|
||||
public $onWebSocketConnect = null;
|
||||
|
||||
/**
|
||||
* Transport layer protocol.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $transport = 'tcp';
|
||||
|
||||
/**
|
||||
* Socks5 proxy.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $proxySocks5 = '';
|
||||
|
||||
/**
|
||||
* Http proxy.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $proxyHttp = '';
|
||||
|
||||
/**
|
||||
* Http proxy authorization header value.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $proxyAuthorization = '';
|
||||
|
||||
/**
|
||||
* Status.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected int $status = self::STATUS_INITIAL;
|
||||
|
||||
/**
|
||||
* Remote host.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $remoteHost = '';
|
||||
|
||||
/**
|
||||
* Remote port.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected int $remotePort = 80;
|
||||
|
||||
/**
|
||||
* Connect start time.
|
||||
*
|
||||
* @var float
|
||||
*/
|
||||
protected float $connectStartTime = 0;
|
||||
|
||||
/**
|
||||
* Remote URI.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $remoteURI = '';
|
||||
|
||||
/**
|
||||
* Context option.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $socketContext = [];
|
||||
|
||||
/**
|
||||
* Reconnect timer.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected int $reconnectTimer = 0;
|
||||
|
||||
/**
|
||||
* Construct.
|
||||
*
|
||||
* @param string $remoteAddress
|
||||
* @param array $socketContext
|
||||
*/
|
||||
public function __construct(string $remoteAddress, array $socketContext = [])
|
||||
{
|
||||
$addressInfo = parse_url($remoteAddress);
|
||||
if (!$addressInfo) {
|
||||
[$scheme, $this->remoteAddress] = explode(':', $remoteAddress, 2);
|
||||
if ('unix' === strtolower($scheme)) {
|
||||
$this->remoteAddress = substr($remoteAddress, strpos($remoteAddress, '/') + 2);
|
||||
}
|
||||
if (!$this->remoteAddress) {
|
||||
throw new RuntimeException('Bad remoteAddress');
|
||||
}
|
||||
} else {
|
||||
$addressInfo['port'] ??= 0;
|
||||
$addressInfo['path'] ??= '/';
|
||||
if (!isset($addressInfo['query'])) {
|
||||
$addressInfo['query'] = '';
|
||||
} else {
|
||||
$addressInfo['query'] = '?' . $addressInfo['query'];
|
||||
}
|
||||
$this->remoteHost = $addressInfo['host'];
|
||||
$this->remotePort = $addressInfo['port'];
|
||||
$this->remoteURI = "{$addressInfo['path']}{$addressInfo['query']}";
|
||||
$scheme = $addressInfo['scheme'] ?? 'tcp';
|
||||
$this->remoteAddress = 'unix' === strtolower($scheme)
|
||||
? substr($remoteAddress, strpos($remoteAddress, '/') + 2)
|
||||
: $this->remoteHost . ':' . $this->remotePort;
|
||||
}
|
||||
|
||||
$this->id = $this->realId = self::$idRecorder++;
|
||||
if (PHP_INT_MAX === self::$idRecorder) {
|
||||
self::$idRecorder = 0;
|
||||
}
|
||||
// Check application layer protocol class.
|
||||
if (!isset(self::BUILD_IN_TRANSPORTS[$scheme])) {
|
||||
// Validate scheme contains only safe characters for class name resolution.
|
||||
if (!preg_match('/^[a-zA-Z][a-zA-Z0-9]*$/', $scheme)) {
|
||||
throw new RuntimeException("Invalid protocol scheme '$scheme'");
|
||||
}
|
||||
$scheme = ucfirst($scheme);
|
||||
$this->protocol = '\\Protocols\\' . $scheme;
|
||||
if (!class_exists($this->protocol)) {
|
||||
$this->protocol = "\\Workerman\\Protocols\\$scheme";
|
||||
if (!class_exists($this->protocol)) {
|
||||
throw new RuntimeException("class \\Protocols\\$scheme not exist");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->transport = self::BUILD_IN_TRANSPORTS[$scheme];
|
||||
}
|
||||
|
||||
// For statistics.
|
||||
++self::$statistics['connection_count'];
|
||||
$this->maxSendBufferSize = self::$defaultMaxSendBufferSize;
|
||||
$this->maxPackageSize = self::$defaultMaxPackageSize;
|
||||
$this->socketContext = $socketContext;
|
||||
static::$connections[$this->realId] = $this;
|
||||
$this->context = new stdClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconnect.
|
||||
*
|
||||
* @param int $after
|
||||
* @return void
|
||||
*/
|
||||
public function reconnect(int $after = 0): void
|
||||
{
|
||||
$this->status = self::STATUS_INITIAL;
|
||||
static::$connections[$this->realId] = $this;
|
||||
if ($this->reconnectTimer) {
|
||||
Timer::del($this->reconnectTimer);
|
||||
}
|
||||
if ($after > 0) {
|
||||
$this->reconnectTimer = Timer::add($after, $this->connect(...), null, false);
|
||||
return;
|
||||
}
|
||||
$this->connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Do connect.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function connect(): void
|
||||
{
|
||||
if ($this->status !== self::STATUS_INITIAL && $this->status !== self::STATUS_CLOSING &&
|
||||
$this->status !== self::STATUS_CLOSED) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->eventLoop ??= Worker::getEventLoop();
|
||||
|
||||
$this->status = self::STATUS_CONNECTING;
|
||||
$this->connectStartTime = microtime(true);
|
||||
set_error_handler(fn() => false);
|
||||
if ($this->transport !== 'unix') {
|
||||
if (!$this->remotePort) {
|
||||
$this->remotePort = $this->transport === 'ssl' ? 443 : 80;
|
||||
$this->remoteAddress = $this->remoteHost . ':' . $this->remotePort;
|
||||
}
|
||||
// Open socket connection asynchronously.
|
||||
if ($this->proxySocks5) {
|
||||
$this->socketContext['ssl']['peer_name'] = $this->remoteHost;
|
||||
$context = stream_context_create($this->socketContext);
|
||||
$this->socket = stream_socket_client("tcp://$this->proxySocks5", $errno, $err_str, 0, STREAM_CLIENT_ASYNC_CONNECT, $context);
|
||||
} else if ($this->proxyHttp) {
|
||||
$this->socketContext['ssl']['peer_name'] = $this->remoteHost;
|
||||
$context = stream_context_create($this->socketContext);
|
||||
$this->socket = stream_socket_client("tcp://$this->proxyHttp", $errno, $err_str, 0, STREAM_CLIENT_ASYNC_CONNECT, $context);
|
||||
} else if ($this->socketContext) {
|
||||
$context = stream_context_create($this->socketContext);
|
||||
$this->socket = stream_socket_client("tcp://$this->remoteHost:$this->remotePort",
|
||||
$errno, $err_str, 0, STREAM_CLIENT_ASYNC_CONNECT, $context);
|
||||
} else {
|
||||
$this->socket = stream_socket_client("tcp://$this->remoteHost:$this->remotePort",
|
||||
$errno, $err_str, 0, STREAM_CLIENT_ASYNC_CONNECT);
|
||||
}
|
||||
} else {
|
||||
$this->socket = stream_socket_client("$this->transport://$this->remoteAddress", $errno, $err_str, 0,
|
||||
STREAM_CLIENT_ASYNC_CONNECT);
|
||||
}
|
||||
restore_error_handler();
|
||||
// If failed attempt to emit onError callback.
|
||||
if (!$this->socket || !is_resource($this->socket)) {
|
||||
$this->emitError(static::CONNECT_FAIL, $err_str);
|
||||
if ($this->status === self::STATUS_CLOSING) {
|
||||
$this->destroy();
|
||||
}
|
||||
if ($this->status === self::STATUS_CLOSED) {
|
||||
$this->onConnect = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$this->eventLoop ??= Worker::getEventLoop();
|
||||
|
||||
// Add socket to global event loop waiting connection is successfully established or failed.
|
||||
$this->eventLoop->onWritable($this->socket, $this->checkConnection(...));
|
||||
// For windows.
|
||||
if (DIRECTORY_SEPARATOR === '\\' && method_exists($this->eventLoop, 'onExcept')) {
|
||||
$this->eventLoop->onExcept($this->socket, $this->checkConnection(...));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to emit onError callback.
|
||||
*
|
||||
* @param int $code
|
||||
* @param mixed $msg
|
||||
* @return void
|
||||
*/
|
||||
protected function emitError(int $code, mixed $msg): void
|
||||
{
|
||||
$this->status = self::STATUS_CLOSING;
|
||||
if ($this->onError) {
|
||||
try {
|
||||
($this->onError)($this, $code, $msg);
|
||||
} catch (Throwable $e) {
|
||||
$this->error($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CancelReconnect.
|
||||
*/
|
||||
public function cancelReconnect(): void
|
||||
{
|
||||
if ($this->reconnectTimer) {
|
||||
Timer::del($this->reconnectTimer);
|
||||
$this->reconnectTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remote address.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRemoteHost(): string
|
||||
{
|
||||
return $this->remoteHost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remote URI.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRemoteURI(): string
|
||||
{
|
||||
return $this->remoteURI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check connection is successfully established or failed.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function checkConnection(): void
|
||||
{
|
||||
// Remove EV_EXPECT for windows.
|
||||
if (DIRECTORY_SEPARATOR === '\\' && method_exists($this->eventLoop, 'offExcept')) {
|
||||
$this->eventLoop->offExcept($this->socket);
|
||||
}
|
||||
// Remove write listener.
|
||||
$this->eventLoop->offWritable($this->socket);
|
||||
|
||||
if ($this->status !== self::STATUS_CONNECTING) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check socket state.
|
||||
if ($address = stream_socket_get_name($this->socket, true)) {
|
||||
// Proxy
|
||||
if ($this->proxySocks5) {
|
||||
fwrite($this->socket, chr(5) . chr(1) . chr(0));
|
||||
fread($this->socket, 512);
|
||||
fwrite($this->socket, chr(5) . chr(1) . chr(0) . chr(3) . chr(strlen($this->remoteHost)) . $this->remoteHost . pack("n", $this->remotePort));
|
||||
fread($this->socket, 512);
|
||||
} elseif ($this->proxyHttp) {
|
||||
$str = "CONNECT $this->remoteHost:$this->remotePort HTTP/1.1\r\n";
|
||||
$str .= "Host: $this->remoteHost:$this->remotePort\r\n";
|
||||
if ($this->proxyAuthorization !== '') {
|
||||
$str .= "Proxy-Authorization: $this->proxyAuthorization\r\n";
|
||||
}
|
||||
$str .= "Proxy-Connection: keep-alive\r\n\r\n";
|
||||
fwrite($this->socket, $str);
|
||||
$proxyResponse = fread($this->socket, 512);
|
||||
if ($proxyResponse && preg_match('/^HTTP\/\d\.\d\s+(\d{3})(?:\s+([^\r\n]+))?/i', $proxyResponse, $match)) {
|
||||
if ((int)$match[1] !== 200) {
|
||||
$reason = $match[2] ?? 'Proxy CONNECT failed';
|
||||
$this->emitError(static::CONNECT_FAIL, "Proxy CONNECT failed: {$match[1]} $reason");
|
||||
if ($this->status === self::STATUS_CLOSING) {
|
||||
$this->destroy();
|
||||
}
|
||||
if ($this->status === self::STATUS_CLOSED) {
|
||||
$this->onConnect = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!is_resource($this->socket)) {
|
||||
$this->emitError(static::CONNECT_FAIL, 'connect ' . $this->remoteAddress . ' fail after ' . round(microtime(true) - $this->connectStartTime, 4) . ' seconds');
|
||||
if ($this->status === self::STATUS_CLOSING) {
|
||||
$this->destroy();
|
||||
}
|
||||
if ($this->status === self::STATUS_CLOSED) {
|
||||
$this->onConnect = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Nonblocking.
|
||||
stream_set_blocking($this->socket, false);
|
||||
stream_set_read_buffer($this->socket, 0);
|
||||
// Try to open keepalive for tcp and disable Nagle algorithm.
|
||||
if (function_exists('socket_import_stream') && $this->transport === 'tcp') {
|
||||
$socket = socket_import_stream($this->socket);
|
||||
socket_set_option($socket, SOL_SOCKET, SO_KEEPALIVE, 1);
|
||||
socket_set_option($socket, SOL_TCP, TCP_NODELAY, 1);
|
||||
if (defined('TCP_KEEPIDLE') && defined('TCP_KEEPINTVL') && defined('TCP_KEEPCNT')) {
|
||||
socket_set_option($socket, SOL_TCP, TCP_KEEPIDLE, static::TCP_KEEPALIVE_INTERVAL);
|
||||
socket_set_option($socket, SOL_TCP, TCP_KEEPINTVL, static::TCP_KEEPALIVE_INTERVAL);
|
||||
socket_set_option($socket, SOL_TCP, TCP_KEEPCNT, 1);
|
||||
}
|
||||
}
|
||||
// SSL handshake.
|
||||
if ($this->transport === 'ssl') {
|
||||
$this->sslHandshakeCompleted = $this->doSslHandshake($this->socket);
|
||||
if ($this->sslHandshakeCompleted === false) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// There are some data waiting to send.
|
||||
if ($this->sendBuffer) {
|
||||
$this->eventLoop->onWritable($this->socket, $this->baseWrite(...));
|
||||
}
|
||||
}
|
||||
// Register a listener waiting read event.
|
||||
$this->eventLoop->onReadable($this->socket, $this->baseRead(...));
|
||||
|
||||
$this->status = self::STATUS_ESTABLISHED;
|
||||
$this->remoteAddress = $address;
|
||||
|
||||
// Try to emit onConnect callback.
|
||||
if ($this->onConnect) {
|
||||
try {
|
||||
($this->onConnect)($this);
|
||||
} catch (Throwable $e) {
|
||||
$this->error($e);
|
||||
}
|
||||
}
|
||||
// Try to emit protocol::onConnect
|
||||
if ($this->protocol && method_exists($this->protocol, 'onConnect')) {
|
||||
try {
|
||||
$this->protocol::onConnect($this);
|
||||
} catch (Throwable $e) {
|
||||
$this->error($e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Connection failed.
|
||||
$this->emitError(static::CONNECT_FAIL, 'connect ' . $this->remoteAddress . ' fail after ' . round(microtime(true) - $this->connectStartTime, 4) . ' seconds');
|
||||
if ($this->status === self::STATUS_CLOSING) {
|
||||
$this->destroy();
|
||||
}
|
||||
if ($this->status === self::STATUS_CLOSED) {
|
||||
$this->onConnect = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Connection;
|
||||
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
use Workerman\Protocols\ProtocolInterface;
|
||||
use Workerman\Worker;
|
||||
use function class_exists;
|
||||
use function is_resource;
|
||||
use function explode;
|
||||
use function fclose;
|
||||
use function stream_context_create;
|
||||
use function stream_set_blocking;
|
||||
use function stream_socket_client;
|
||||
use function stream_socket_recvfrom;
|
||||
use function stream_socket_sendto;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
use function ucfirst;
|
||||
use const STREAM_CLIENT_CONNECT;
|
||||
|
||||
/**
|
||||
* AsyncUdpConnection.
|
||||
*/
|
||||
class AsyncUdpConnection extends UdpConnection
|
||||
{
|
||||
/**
|
||||
* Emitted when socket connection is successfully established.
|
||||
*
|
||||
* @var ?callable
|
||||
*/
|
||||
public $onConnect = null;
|
||||
|
||||
/**
|
||||
* Emitted when socket connection closed.
|
||||
*
|
||||
* @var ?callable
|
||||
*/
|
||||
public $onClose = null;
|
||||
|
||||
/**
|
||||
* Connected or not.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $connected = false;
|
||||
|
||||
/**
|
||||
* Context option.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $contextOption = [];
|
||||
|
||||
/**
|
||||
* Construct.
|
||||
*
|
||||
* @param string $remoteAddress
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function __construct($remoteAddress, $contextOption = [])
|
||||
{
|
||||
// Get the application layer communication protocol and listening address.
|
||||
[$scheme, $address] = explode(':', $remoteAddress, 2);
|
||||
// Check application layer protocol class.
|
||||
if ($scheme !== 'udp') {
|
||||
// Validate scheme contains only safe characters for class name resolution.
|
||||
if (!preg_match('/^[a-zA-Z][a-zA-Z0-9]*$/', $scheme)) {
|
||||
throw new RuntimeException("Invalid protocol scheme '$scheme'");
|
||||
}
|
||||
$scheme = ucfirst($scheme);
|
||||
$this->protocol = '\\Protocols\\' . $scheme;
|
||||
if (!class_exists($this->protocol)) {
|
||||
$this->protocol = "\\Workerman\\Protocols\\$scheme";
|
||||
if (!class_exists($this->protocol)) {
|
||||
throw new RuntimeException("class \\Protocols\\$scheme not exist");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->remoteAddress = substr($address, 2);
|
||||
$this->contextOption = $contextOption;
|
||||
}
|
||||
|
||||
/**
|
||||
* For udp package.
|
||||
*
|
||||
* @param resource $socket
|
||||
* @return void
|
||||
*/
|
||||
public function baseRead($socket): void
|
||||
{
|
||||
$recvBuffer = stream_socket_recvfrom($socket, static::MAX_UDP_PACKAGE_SIZE, 0, $remoteAddress);
|
||||
if (false === $recvBuffer || empty($remoteAddress)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->onMessage) {
|
||||
if ($this->protocol) {
|
||||
$recvBuffer = $this->protocol::decode($recvBuffer, $this);
|
||||
}
|
||||
++ConnectionInterface::$statistics['total_request'];
|
||||
try {
|
||||
($this->onMessage)($this, $recvBuffer);
|
||||
} catch (Throwable $e) {
|
||||
$this->error($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close connection.
|
||||
*
|
||||
* @param mixed $data
|
||||
* @param bool $raw
|
||||
* @return void
|
||||
*/
|
||||
public function close(mixed $data = null, bool $raw = false): void
|
||||
{
|
||||
if ($data !== null) {
|
||||
$this->send($data, $raw);
|
||||
}
|
||||
if ($this->eventLoop) {
|
||||
$this->eventLoop->offReadable($this->socket);
|
||||
}
|
||||
if (is_resource($this->socket)) {
|
||||
fclose($this->socket);
|
||||
}
|
||||
$this->socket = null; // intentionally nullable to mark closed state
|
||||
$this->connected = false;
|
||||
// Try to emit onClose callback.
|
||||
if ($this->onClose) {
|
||||
try {
|
||||
($this->onClose)($this);
|
||||
} catch (Throwable $e) {
|
||||
$this->error($e);
|
||||
}
|
||||
}
|
||||
$this->onConnect = $this->onMessage = $this->onClose = $this->eventLoop = $this->errorHandler = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends data on the connection.
|
||||
*
|
||||
* @param mixed $sendBuffer
|
||||
* @param bool $raw
|
||||
* @return bool|null
|
||||
*/
|
||||
public function send(mixed $sendBuffer, bool $raw = false): bool|null
|
||||
{
|
||||
if (false === $raw && $this->protocol) {
|
||||
$sendBuffer = $this->protocol::encode($sendBuffer, $this);
|
||||
if ($sendBuffer === '') {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if ($this->connected === false) {
|
||||
$this->connect();
|
||||
}
|
||||
return strlen($sendBuffer) === stream_socket_sendto($this->socket, $sendBuffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function connect(): void
|
||||
{
|
||||
if ($this->connected === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->eventLoop ??= Worker::getEventLoop();
|
||||
|
||||
if ($this->contextOption) {
|
||||
$context = stream_context_create($this->contextOption);
|
||||
$this->socket = stream_socket_client("udp://$this->remoteAddress", $errno, $errmsg,
|
||||
30, STREAM_CLIENT_CONNECT, $context);
|
||||
} else {
|
||||
$this->socket = stream_socket_client("udp://$this->remoteAddress", $errno, $errmsg);
|
||||
}
|
||||
|
||||
if (!$this->socket) {
|
||||
Worker::safeEcho((string)(new Exception($errmsg)));
|
||||
$this->eventLoop = null;
|
||||
return;
|
||||
}
|
||||
|
||||
$this->eventLoop ??= Worker::getEventLoop();
|
||||
|
||||
stream_set_blocking($this->socket, false);
|
||||
if ($this->onMessage) {
|
||||
$this->eventLoop->onReadable($this->socket, $this->baseRead(...));
|
||||
}
|
||||
$this->connected = true;
|
||||
// Try to emit onConnect callback.
|
||||
if ($this->onConnect) {
|
||||
try {
|
||||
($this->onConnect)($this);
|
||||
} catch (Throwable $e) {
|
||||
$this->error($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Connection;
|
||||
|
||||
use Throwable;
|
||||
use Workerman\Events\Event;
|
||||
use Workerman\Events\EventInterface;
|
||||
use Workerman\Worker;
|
||||
use AllowDynamicProperties;
|
||||
|
||||
/**
|
||||
* ConnectionInterface.
|
||||
*/
|
||||
#[AllowDynamicProperties]
|
||||
abstract class ConnectionInterface
|
||||
{
|
||||
/**
|
||||
* Connect failed.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public const CONNECT_FAIL = 1;
|
||||
|
||||
/**
|
||||
* Send failed.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public const SEND_FAIL = 2;
|
||||
|
||||
/**
|
||||
* Statistics for status command.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static array $statistics = [
|
||||
'connection_count' => 0,
|
||||
'total_request' => 0,
|
||||
'throw_exception' => 0,
|
||||
'send_fail' => 0,
|
||||
];
|
||||
|
||||
/**
|
||||
* Application layer protocol.
|
||||
* The format is like this Workerman\\Protocols\\Http.
|
||||
*
|
||||
* @var ?class-string
|
||||
*/
|
||||
public ?string $protocol = null;
|
||||
|
||||
/**
|
||||
* Emitted when data is received.
|
||||
*
|
||||
* @var ?callable
|
||||
*/
|
||||
public $onMessage = null;
|
||||
|
||||
/**
|
||||
* Emitted when the other end of the socket sends a FIN packet.
|
||||
*
|
||||
* @var ?callable
|
||||
*/
|
||||
public $onClose = null;
|
||||
|
||||
/**
|
||||
* Emitted when an error occurs with connection.
|
||||
*
|
||||
* @var ?callable
|
||||
*/
|
||||
public $onError = null;
|
||||
|
||||
/**
|
||||
* @var ?EventInterface
|
||||
*/
|
||||
public ?EventInterface $eventLoop = null;
|
||||
|
||||
/**
|
||||
* @var ?callable
|
||||
*/
|
||||
public $errorHandler = null;
|
||||
|
||||
/**
|
||||
* Sends data on the connection.
|
||||
*
|
||||
* @param mixed $sendBuffer
|
||||
* @param bool $raw
|
||||
* @return bool|null
|
||||
*/
|
||||
abstract public function send(mixed $sendBuffer, bool $raw = false): bool|null;
|
||||
|
||||
/**
|
||||
* Get remote IP.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getRemoteIp(): string;
|
||||
|
||||
/**
|
||||
* Get remote port.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
abstract public function getRemotePort(): int;
|
||||
|
||||
/**
|
||||
* Get remote address.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getRemoteAddress(): string;
|
||||
|
||||
/**
|
||||
* Get local IP.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getLocalIp(): string;
|
||||
|
||||
/**
|
||||
* Get local port.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
abstract public function getLocalPort(): int;
|
||||
|
||||
/**
|
||||
* Get local address.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getLocalAddress(): string;
|
||||
|
||||
/**
|
||||
* Close connection.
|
||||
*
|
||||
* @param mixed $data
|
||||
* @param bool $raw
|
||||
* @return void
|
||||
*/
|
||||
abstract public function close(mixed $data = null, bool $raw = false): void;
|
||||
|
||||
/**
|
||||
* Is ipv4.
|
||||
*
|
||||
* return bool.
|
||||
*/
|
||||
abstract public function isIpV4(): bool;
|
||||
|
||||
/**
|
||||
* Is ipv6.
|
||||
*
|
||||
* return bool.
|
||||
*/
|
||||
abstract public function isIpV6(): bool;
|
||||
|
||||
/**
|
||||
* @param Throwable $exception
|
||||
* @return void
|
||||
*/
|
||||
public function error(Throwable $exception): void
|
||||
{
|
||||
if (!$this->errorHandler) {
|
||||
Worker::stopAll(250, $exception);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
($this->errorHandler)($exception);
|
||||
} catch (Throwable $exception) {
|
||||
Worker::stopAll(250, $exception);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Connection;
|
||||
|
||||
use JsonSerializable;
|
||||
use Workerman\Protocols\ProtocolInterface;
|
||||
use function stream_socket_get_name;
|
||||
use function stream_socket_sendto;
|
||||
use function strlen;
|
||||
use function strrchr;
|
||||
use function strrpos;
|
||||
use function substr;
|
||||
use function trim;
|
||||
|
||||
/**
|
||||
* UdpConnection.
|
||||
*/
|
||||
class UdpConnection extends ConnectionInterface implements JsonSerializable
|
||||
{
|
||||
/**
|
||||
* Max udp package size.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public const MAX_UDP_PACKAGE_SIZE = 65535;
|
||||
|
||||
/**
|
||||
* Transport layer protocol.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $transport = 'udp';
|
||||
|
||||
/**
|
||||
* Whether the socket is connected (created via stream_socket_client).
|
||||
* On BSD/macOS, sendto() on a connected UDP socket with a destination address
|
||||
* returns EISCONN(-1). We must omit the address for connected sockets.
|
||||
*/
|
||||
protected bool $connected = false;
|
||||
|
||||
/**
|
||||
* @param resource|null $socket
|
||||
*/
|
||||
public function __construct(
|
||||
/** @var resource|null */ protected $socket,
|
||||
protected string $remoteAddress)
|
||||
{
|
||||
if (is_resource($socket) && stream_socket_get_name($socket, true) !== false) {
|
||||
$this->connected = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends data on the connection.
|
||||
*
|
||||
* @param mixed $sendBuffer
|
||||
* @param bool $raw
|
||||
* @return bool|null
|
||||
*/
|
||||
public function send(mixed $sendBuffer, bool $raw = false): bool|null
|
||||
{
|
||||
if (false === $raw && $this->protocol) {
|
||||
$sendBuffer = $this->protocol::encode($sendBuffer, $this);
|
||||
if ($sendBuffer === '') {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if ($this->connected) {
|
||||
return strlen($sendBuffer) === stream_socket_sendto($this->socket, $sendBuffer);
|
||||
}
|
||||
return strlen($sendBuffer) === stream_socket_sendto($this->socket, $sendBuffer, 0, $this->isIpV6() ? '[' . $this->getRemoteIp() . ']:' . $this->getRemotePort() : $this->remoteAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remote IP.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRemoteIp(): string
|
||||
{
|
||||
$pos = strrpos($this->remoteAddress, ':');
|
||||
if ($pos) {
|
||||
return trim(substr($this->remoteAddress, 0, $pos), '[]');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remote port.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getRemotePort(): int
|
||||
{
|
||||
if ($this->remoteAddress) {
|
||||
return (int)substr(strrchr($this->remoteAddress, ':'), 1);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remote address.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRemoteAddress(): string
|
||||
{
|
||||
return $this->remoteAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get local IP.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocalIp(): string
|
||||
{
|
||||
$address = $this->getLocalAddress();
|
||||
$pos = strrpos($address, ':');
|
||||
if (!$pos) {
|
||||
return '';
|
||||
}
|
||||
return substr($address, 0, $pos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get local port.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getLocalPort(): int
|
||||
{
|
||||
$address = $this->getLocalAddress();
|
||||
$pos = strrpos($address, ':');
|
||||
if (!$pos) {
|
||||
return 0;
|
||||
}
|
||||
return (int)substr(strrchr($address, ':'), 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get local address.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocalAddress(): string
|
||||
{
|
||||
return is_resource($this->socket) ? (string)@stream_socket_get_name($this->socket, false) : '';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Close connection.
|
||||
*
|
||||
* @param mixed $data
|
||||
* @param bool $raw
|
||||
* @return void
|
||||
*/
|
||||
public function close(mixed $data = null, bool $raw = false): void
|
||||
{
|
||||
if ($data !== null) {
|
||||
$this->send($data, $raw);
|
||||
}
|
||||
if ($this->eventLoop) {
|
||||
$this->eventLoop->offReadable($this->socket);
|
||||
}
|
||||
if (is_resource($this->socket)) {
|
||||
@fclose($this->socket);
|
||||
}
|
||||
$this->socket = null;
|
||||
$this->eventLoop = $this->errorHandler = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is ipv4.
|
||||
*
|
||||
* return bool.
|
||||
*/
|
||||
public function isIpV4(): bool
|
||||
{
|
||||
if ($this->transport === 'unix') {
|
||||
return false;
|
||||
}
|
||||
return !str_contains($this->getRemoteIp(), ':');
|
||||
}
|
||||
|
||||
/**
|
||||
* Is ipv6.
|
||||
*
|
||||
* return bool.
|
||||
*/
|
||||
public function isIpV6(): bool
|
||||
{
|
||||
if ($this->transport === 'unix') {
|
||||
return false;
|
||||
}
|
||||
return str_contains($this->getRemoteIp(), ':');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the real socket.
|
||||
*
|
||||
* @return resource
|
||||
*/
|
||||
/**
|
||||
* @return resource|null
|
||||
*/
|
||||
public function getSocket()
|
||||
{
|
||||
return $this->socket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the json_encode information.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'transport' => $this->transport,
|
||||
'getRemoteIp' => $this->getRemoteIp(),
|
||||
'remotePort' => $this->getRemotePort(),
|
||||
'getRemoteAddress' => $this->getRemoteAddress(),
|
||||
'getLocalIp' => $this->getLocalIp(),
|
||||
'getLocalPort' => $this->getLocalPort(),
|
||||
'getLocalAddress' => $this->getLocalAddress(),
|
||||
'isIpV4' => $this->isIpV4(),
|
||||
'isIpV6' => $this->isIpV6(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Events;
|
||||
|
||||
/**
|
||||
* Ev eventloop
|
||||
*/
|
||||
final class Ev implements EventInterface
|
||||
{
|
||||
/**
|
||||
* All listeners for read event.
|
||||
*
|
||||
* @var array<int, \EvIo>
|
||||
*/
|
||||
private array $readEvents = [];
|
||||
|
||||
/**
|
||||
* All listeners for write event.
|
||||
*
|
||||
* @var array<int, \EvIo>
|
||||
*/
|
||||
private array $writeEvents = [];
|
||||
|
||||
/**
|
||||
* Event listeners of signal.
|
||||
*
|
||||
* @var array<int, \EvSignal>
|
||||
*/
|
||||
private array $eventSignal = [];
|
||||
|
||||
/**
|
||||
* All timer event listeners.
|
||||
*
|
||||
* @var array<int, \EvTimer>
|
||||
*/
|
||||
private array $eventTimer = [];
|
||||
|
||||
/**
|
||||
* @var ?callable
|
||||
*/
|
||||
private $errorHandler = null;
|
||||
|
||||
/**
|
||||
* Timer id.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private static int $timerId = 1;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function delay(float $delay, callable $func, array $args = []): int
|
||||
{
|
||||
$timerId = self::$timerId;
|
||||
$event = new \EvTimer($delay, 0, function () use ($func, $args, $timerId) {
|
||||
unset($this->eventTimer[$timerId]);
|
||||
$this->safeCall($func, $args);
|
||||
});
|
||||
$this->eventTimer[self::$timerId] = $event;
|
||||
return self::$timerId++;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offDelay(int $timerId): bool
|
||||
{
|
||||
if (isset($this->eventTimer[$timerId])) {
|
||||
$this->eventTimer[$timerId]->stop();
|
||||
unset($this->eventTimer[$timerId]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offRepeat(int $timerId): bool
|
||||
{
|
||||
return $this->offDelay($timerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function repeat(float $interval, callable $func, array $args = []): int
|
||||
{
|
||||
$event = new \EvTimer($interval, $interval, fn () => $this->safeCall($func, $args));
|
||||
$this->eventTimer[self::$timerId] = $event;
|
||||
return self::$timerId++;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onReadable($stream, callable $func): void
|
||||
{
|
||||
$fdKey = (int)$stream;
|
||||
$event = new \EvIo($stream, \Ev::READ, fn () => $this->safeCall($func, [$stream]));
|
||||
$this->readEvents[$fdKey] = $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offReadable($stream): bool
|
||||
{
|
||||
$fdKey = (int)$stream;
|
||||
if (isset($this->readEvents[$fdKey])) {
|
||||
$this->readEvents[$fdKey]->stop();
|
||||
unset($this->readEvents[$fdKey]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onWritable($stream, callable $func): void
|
||||
{
|
||||
$fdKey = (int)$stream;
|
||||
$event = new \EvIo($stream, \Ev::WRITE, fn () => $this->safeCall($func, [$stream]));
|
||||
$this->writeEvents[$fdKey] = $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offWritable($stream): bool
|
||||
{
|
||||
$fdKey = (int)$stream;
|
||||
if (isset($this->writeEvents[$fdKey])) {
|
||||
$this->writeEvents[$fdKey]->stop();
|
||||
unset($this->writeEvents[$fdKey]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onSignal(int $signal, callable $func): void
|
||||
{
|
||||
$event = new \EvSignal($signal, fn () => $this->safeCall($func, [$signal]));
|
||||
$this->eventSignal[$signal] = $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offSignal(int $signal): bool
|
||||
{
|
||||
if (isset($this->eventSignal[$signal])) {
|
||||
$this->eventSignal[$signal]->stop();
|
||||
unset($this->eventSignal[$signal]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function deleteAllTimer(): void
|
||||
{
|
||||
foreach ($this->eventTimer as $event) {
|
||||
$event->stop();
|
||||
}
|
||||
$this->eventTimer = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
\Ev::run();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function stop(): void
|
||||
{
|
||||
\Ev::stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTimerCount(): int
|
||||
{
|
||||
return count($this->eventTimer);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setErrorHandler(callable $errorHandler): void
|
||||
{
|
||||
$this->errorHandler = $errorHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable $func
|
||||
* @param array $args
|
||||
* @return void
|
||||
*/
|
||||
private function safeCall(callable $func, array $args = []): void
|
||||
{
|
||||
try {
|
||||
$func(...$args);
|
||||
} catch (\Throwable $e) {
|
||||
if ($this->errorHandler === null) {
|
||||
echo $e;
|
||||
} else {
|
||||
($this->errorHandler)($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Events;
|
||||
|
||||
/**
|
||||
* libevent eventloop
|
||||
*/
|
||||
final class Event implements EventInterface
|
||||
{
|
||||
/**
|
||||
* Event base.
|
||||
*
|
||||
* @var \EventBase
|
||||
*/
|
||||
private \EventBase $eventBase;
|
||||
|
||||
/**
|
||||
* All listeners for read event.
|
||||
*
|
||||
* @var array<int, \Event>
|
||||
*/
|
||||
private array $readEvents = [];
|
||||
|
||||
/**
|
||||
* All listeners for write event.
|
||||
*
|
||||
* @var array<int, \Event>
|
||||
*/
|
||||
private array $writeEvents = [];
|
||||
|
||||
/**
|
||||
* Event listeners of signal.
|
||||
*
|
||||
* @var array<int, \Event>
|
||||
*/
|
||||
private array $eventSignal = [];
|
||||
|
||||
/**
|
||||
* All timer event listeners.
|
||||
*
|
||||
* @var array<int, \Event>
|
||||
*/
|
||||
private array $eventTimer = [];
|
||||
|
||||
/**
|
||||
* Timer id.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private int $timerId = 0;
|
||||
|
||||
/**
|
||||
* Event class name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private string $eventClassName = '';
|
||||
|
||||
/**
|
||||
* @var ?callable
|
||||
*/
|
||||
private $errorHandler = null;
|
||||
|
||||
/**
|
||||
* Construct.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
if (\class_exists('\\\\Event', false)) {
|
||||
$className = '\\\\Event';
|
||||
} else {
|
||||
$className = '\Event';
|
||||
}
|
||||
$this->eventClassName = $className;
|
||||
if (\class_exists('\\\\EventBase', false)) {
|
||||
$className = '\\\\EventBase';
|
||||
} else {
|
||||
$className = '\EventBase';
|
||||
}
|
||||
$this->eventBase = new $className();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function delay(float $delay, callable $func, array $args = []): int
|
||||
{
|
||||
$className = $this->eventClassName;
|
||||
$timerId = $this->timerId++;
|
||||
$event = new $className($this->eventBase, -1, $className::TIMEOUT, function () use ($func, $args, $timerId) {
|
||||
unset($this->eventTimer[$timerId]);
|
||||
$this->safeCall($func, $args);
|
||||
});
|
||||
if (!$event->addTimer($delay)) {
|
||||
throw new \RuntimeException("Event::addTimer($delay) failed");
|
||||
}
|
||||
$this->eventTimer[$timerId] = $event;
|
||||
return $timerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offDelay(int $timerId): bool
|
||||
{
|
||||
if (isset($this->eventTimer[$timerId])) {
|
||||
$this->eventTimer[$timerId]->del();
|
||||
unset($this->eventTimer[$timerId]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offRepeat(int $timerId): bool
|
||||
{
|
||||
return $this->offDelay($timerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function repeat(float $interval, callable $func, array $args = []): int
|
||||
{
|
||||
$className = $this->eventClassName;
|
||||
$timerId = $this->timerId++;
|
||||
$event = new $className($this->eventBase, -1, $className::TIMEOUT | $className::PERSIST, function () use ($func, $args) {
|
||||
$this->safeCall($func, $args);
|
||||
});
|
||||
if (!$event->addTimer($interval)) {
|
||||
throw new \RuntimeException("Event::addTimer($interval) failed");
|
||||
}
|
||||
$this->eventTimer[$timerId] = $event;
|
||||
return $timerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onReadable($stream, callable $func): void
|
||||
{
|
||||
$className = $this->eventClassName;
|
||||
$fdKey = (int)$stream;
|
||||
$event = new $className($this->eventBase, $stream, $className::READ | $className::PERSIST, $func);
|
||||
if ($event->add()) {
|
||||
$this->readEvents[$fdKey] = $event;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offReadable($stream): bool
|
||||
{
|
||||
$fdKey = (int)$stream;
|
||||
if (isset($this->readEvents[$fdKey])) {
|
||||
$this->readEvents[$fdKey]->del();
|
||||
unset($this->readEvents[$fdKey]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onWritable($stream, callable $func): void
|
||||
{
|
||||
$className = $this->eventClassName;
|
||||
$fdKey = (int)$stream;
|
||||
$event = new $className($this->eventBase, $stream, $className::WRITE | $className::PERSIST, $func);
|
||||
if ($event->add()) {
|
||||
$this->writeEvents[$fdKey] = $event;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offWritable($stream): bool
|
||||
{
|
||||
$fdKey = (int)$stream;
|
||||
if (isset($this->writeEvents[$fdKey])) {
|
||||
$this->writeEvents[$fdKey]->del();
|
||||
unset($this->writeEvents[$fdKey]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onSignal(int $signal, callable $func): void
|
||||
{
|
||||
$className = $this->eventClassName;
|
||||
$fdKey = $signal;
|
||||
$event = $className::signal($this->eventBase, $signal, fn () => $this->safeCall($func, [$signal]));
|
||||
if ($event->add()) {
|
||||
$this->eventSignal[$fdKey] = $event;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offSignal(int $signal): bool
|
||||
{
|
||||
$fdKey = $signal;
|
||||
if (isset($this->eventSignal[$fdKey])) {
|
||||
$this->eventSignal[$fdKey]->del();
|
||||
unset($this->eventSignal[$fdKey]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function deleteAllTimer(): void
|
||||
{
|
||||
foreach ($this->eventTimer as $event) {
|
||||
$event->del();
|
||||
}
|
||||
$this->eventTimer = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->eventBase->loop();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function stop(): void
|
||||
{
|
||||
$this->eventBase->exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTimerCount(): int
|
||||
{
|
||||
return \count($this->eventTimer);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setErrorHandler(callable $errorHandler): void
|
||||
{
|
||||
$this->errorHandler = $errorHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable $func
|
||||
* @param array $args
|
||||
* @return void
|
||||
*/
|
||||
private function safeCall(callable $func, array $args = []): void
|
||||
{
|
||||
try {
|
||||
$func(...$args);
|
||||
} catch (\Throwable $e) {
|
||||
if ($this->errorHandler === null) {
|
||||
echo $e;
|
||||
} else {
|
||||
($this->errorHandler)($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Events;
|
||||
|
||||
interface EventInterface
|
||||
{
|
||||
/**
|
||||
* Delay the execution of a callback.
|
||||
*
|
||||
* @param float $delay
|
||||
* @param callable(mixed...): void $func
|
||||
* @param array $args
|
||||
* @return int
|
||||
*/
|
||||
public function delay(float $delay, callable $func, array $args = []): int;
|
||||
|
||||
/**
|
||||
* Delete a delay timer.
|
||||
*
|
||||
* @param int $timerId
|
||||
* @return bool
|
||||
*/
|
||||
public function offDelay(int $timerId): bool;
|
||||
|
||||
/**
|
||||
* Repeatedly execute a callback.
|
||||
*
|
||||
* @param float $interval
|
||||
* @param callable(mixed...): void $func
|
||||
* @param array $args
|
||||
* @return int
|
||||
*/
|
||||
public function repeat(float $interval, callable $func, array $args = []): int;
|
||||
|
||||
/**
|
||||
* Delete a repeat timer.
|
||||
*
|
||||
* @param int $timerId
|
||||
* @return bool
|
||||
*/
|
||||
public function offRepeat(int $timerId): bool;
|
||||
|
||||
/**
|
||||
* Execute a callback when a stream resource becomes readable or is closed for reading.
|
||||
*
|
||||
* @param resource $stream
|
||||
* @param callable(resource): void $func
|
||||
* @return void
|
||||
*/
|
||||
public function onReadable($stream, callable $func): void;
|
||||
|
||||
/**
|
||||
* Cancel a callback of stream readable.
|
||||
*
|
||||
* @param resource $stream
|
||||
* @return bool
|
||||
*/
|
||||
public function offReadable($stream): bool;
|
||||
|
||||
/**
|
||||
* Execute a callback when a stream resource becomes writable or is closed for writing.
|
||||
*
|
||||
* @param resource $stream
|
||||
* @param callable(resource): void $func
|
||||
* @return void
|
||||
*/
|
||||
public function onWritable($stream, callable $func): void;
|
||||
|
||||
/**
|
||||
* Cancel a callback of stream writable.
|
||||
*
|
||||
* @param resource $stream
|
||||
* @return bool
|
||||
*/
|
||||
public function offWritable($stream): bool;
|
||||
|
||||
/**
|
||||
* Execute a callback when a signal is received.
|
||||
*
|
||||
* @param int $signal
|
||||
* @param callable(int): void $func
|
||||
* @return void
|
||||
*/
|
||||
public function onSignal(int $signal, callable $func): void;
|
||||
|
||||
/**
|
||||
* Cancel a callback of signal.
|
||||
*
|
||||
* @param int $signal
|
||||
* @return bool
|
||||
*/
|
||||
public function offSignal(int $signal): bool;
|
||||
|
||||
/**
|
||||
* Delete all timer.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function deleteAllTimer(): void;
|
||||
|
||||
/**
|
||||
* Run the event loop.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function run(): void;
|
||||
|
||||
/**
|
||||
* Stop event loop.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function stop(): void;
|
||||
|
||||
/**
|
||||
* Get Timer count.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getTimerCount(): int;
|
||||
|
||||
/**
|
||||
* Set error handler.
|
||||
*
|
||||
* @param callable(\Throwable): void $errorHandler
|
||||
* @return void
|
||||
*/
|
||||
public function setErrorHandler(callable $errorHandler): void;
|
||||
}
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Events;
|
||||
|
||||
use Fiber as BaseFiber;
|
||||
use Revolt\EventLoop;
|
||||
use Revolt\EventLoop\Driver;
|
||||
use function count;
|
||||
use function function_exists;
|
||||
use function pcntl_signal;
|
||||
|
||||
/**
|
||||
* Revolt eventloop
|
||||
*/
|
||||
final class Fiber implements EventInterface
|
||||
{
|
||||
/**
|
||||
* @var Driver
|
||||
*/
|
||||
private Driver $driver;
|
||||
|
||||
/**
|
||||
* All listeners for read event.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private array $readEvents = [];
|
||||
|
||||
/**
|
||||
* All listeners for write event.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private array $writeEvents = [];
|
||||
|
||||
/**
|
||||
* Event listeners of signal.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private array $eventSignal = [];
|
||||
|
||||
/**
|
||||
* Event listeners of timer.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private array $eventTimer = [];
|
||||
|
||||
/**
|
||||
* Timer id.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private int $timerId = 1;
|
||||
|
||||
/**
|
||||
* Construct.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->driver = EventLoop::getDriver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get driver.
|
||||
*
|
||||
* @return Driver
|
||||
*/
|
||||
public function driver(): Driver
|
||||
{
|
||||
return $this->driver;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->driver->run();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function stop(): void
|
||||
{
|
||||
foreach ($this->eventSignal as $cbId) {
|
||||
$this->driver->cancel($cbId);
|
||||
}
|
||||
$this->driver->stop();
|
||||
if (function_exists('pcntl_signal')) {
|
||||
pcntl_signal(SIGINT, SIG_IGN);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function delay(float $delay, callable $func, array $args = []): int
|
||||
{
|
||||
$timerId = $this->timerId++;
|
||||
$closure = function () use ($func, $args, $timerId) {
|
||||
unset($this->eventTimer[$timerId]);
|
||||
$this->safeCall($func, ...$args);
|
||||
};
|
||||
$cbId = $this->driver->delay($delay, $closure);
|
||||
$this->eventTimer[$timerId] = $cbId;
|
||||
return $timerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function repeat(float $interval, callable $func, array $args = []): int
|
||||
{
|
||||
$timerId = $this->timerId++;
|
||||
$cbId = $this->driver->repeat($interval, fn() => $this->safeCall($func, ...$args));
|
||||
$this->eventTimer[$timerId] = $cbId;
|
||||
return $timerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onReadable($stream, callable $func): void
|
||||
{
|
||||
$fdKey = (int)$stream;
|
||||
if (isset($this->readEvents[$fdKey])) {
|
||||
$this->driver->cancel($this->readEvents[$fdKey]);
|
||||
}
|
||||
|
||||
$this->readEvents[$fdKey] = $this->driver->onReadable($stream, fn() => $this->safeCall($func, $stream));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offReadable($stream): bool
|
||||
{
|
||||
$fdKey = (int)$stream;
|
||||
if (isset($this->readEvents[$fdKey])) {
|
||||
$this->driver->cancel($this->readEvents[$fdKey]);
|
||||
unset($this->readEvents[$fdKey]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onWritable($stream, callable $func): void
|
||||
{
|
||||
$fdKey = (int)$stream;
|
||||
if (isset($this->writeEvents[$fdKey])) {
|
||||
$this->driver->cancel($this->writeEvents[$fdKey]);
|
||||
unset($this->writeEvents[$fdKey]);
|
||||
}
|
||||
$this->writeEvents[$fdKey] = $this->driver->onWritable($stream, fn() => $this->safeCall($func, $stream));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offWritable($stream): bool
|
||||
{
|
||||
$fdKey = (int)$stream;
|
||||
if (isset($this->writeEvents[$fdKey])) {
|
||||
$this->driver->cancel($this->writeEvents[$fdKey]);
|
||||
unset($this->writeEvents[$fdKey]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onSignal(int $signal, callable $func): void
|
||||
{
|
||||
$fdKey = $signal;
|
||||
if (isset($this->eventSignal[$fdKey])) {
|
||||
$this->driver->cancel($this->eventSignal[$fdKey]);
|
||||
unset($this->eventSignal[$fdKey]);
|
||||
}
|
||||
$this->eventSignal[$fdKey] = $this->driver->onSignal($signal, fn() => $this->safeCall($func, $signal));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offSignal(int $signal): bool
|
||||
{
|
||||
$fdKey = $signal;
|
||||
if (isset($this->eventSignal[$fdKey])) {
|
||||
$this->driver->cancel($this->eventSignal[$fdKey]);
|
||||
unset($this->eventSignal[$fdKey]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offDelay(int $timerId): bool
|
||||
{
|
||||
if (isset($this->eventTimer[$timerId])) {
|
||||
$this->driver->cancel($this->eventTimer[$timerId]);
|
||||
unset($this->eventTimer[$timerId]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offRepeat(int $timerId): bool
|
||||
{
|
||||
return $this->offDelay($timerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function deleteAllTimer(): void
|
||||
{
|
||||
foreach ($this->eventTimer as $cbId) {
|
||||
$this->driver->cancel($cbId);
|
||||
}
|
||||
$this->eventTimer = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTimerCount(): int
|
||||
{
|
||||
return count($this->eventTimer);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setErrorHandler(callable $errorHandler): void
|
||||
{
|
||||
$this->driver->setErrorHandler($errorHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable $func
|
||||
* @param ...$args
|
||||
* @return void
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function safeCall(callable $func, ...$args): void
|
||||
{
|
||||
(new BaseFiber(fn() => $func(...$args)))->start();
|
||||
}
|
||||
}
|
||||
+492
@@ -0,0 +1,492 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Events;
|
||||
|
||||
use SplPriorityQueue;
|
||||
use Throwable;
|
||||
use function count;
|
||||
use function max;
|
||||
use function microtime;
|
||||
use function pcntl_signal;
|
||||
use function pcntl_signal_dispatch;
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
|
||||
/**
|
||||
* select eventloop
|
||||
*/
|
||||
final class Select implements EventInterface
|
||||
{
|
||||
/**
|
||||
* Running.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private bool $running = true;
|
||||
|
||||
/**
|
||||
* All listeners for read/write event.
|
||||
*
|
||||
* @var array<int, callable>
|
||||
*/
|
||||
private array $readEvents = [];
|
||||
|
||||
/**
|
||||
* All listeners for read/write event.
|
||||
*
|
||||
* @var array<int, callable>
|
||||
*/
|
||||
private array $writeEvents = [];
|
||||
|
||||
/**
|
||||
* @var array<int, callable>
|
||||
*/
|
||||
private array $exceptEvents = [];
|
||||
|
||||
/**
|
||||
* Event listeners of signal.
|
||||
*
|
||||
* @var array<int, callable>
|
||||
*/
|
||||
private array $signalEvents = [];
|
||||
|
||||
/**
|
||||
* Fds waiting for read event.
|
||||
*
|
||||
* @var array<int, resource>
|
||||
*/
|
||||
private array $readFds = [];
|
||||
|
||||
/**
|
||||
* Fds waiting for write event.
|
||||
*
|
||||
* @var array<int, resource>
|
||||
*/
|
||||
private array $writeFds = [];
|
||||
|
||||
/**
|
||||
* Fds waiting for except event.
|
||||
*
|
||||
* @var array<int, resource>
|
||||
*/
|
||||
private array $exceptFds = [];
|
||||
|
||||
/**
|
||||
* Timer scheduler.
|
||||
* {['data':timer_id, 'priority':run_timestamp], ..}
|
||||
*
|
||||
* @var SplPriorityQueue
|
||||
*/
|
||||
private SplPriorityQueue $scheduler;
|
||||
|
||||
/**
|
||||
* All timer event listeners.
|
||||
* [[func, args, flag, timer_interval], ..]
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $eventTimer = [];
|
||||
|
||||
/**
|
||||
* Timer id.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private int $timerId = 1;
|
||||
|
||||
/**
|
||||
* Select timeout.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private int $selectTimeout = self::MAX_SELECT_TIMOUT_US;
|
||||
|
||||
/**
|
||||
* Next run time of the timer.
|
||||
*
|
||||
* @var float
|
||||
*/
|
||||
private float $nextTickTime = 0;
|
||||
|
||||
/**
|
||||
* @var ?callable
|
||||
*/
|
||||
private $errorHandler = null;
|
||||
|
||||
/**
|
||||
* Select timeout.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const MAX_SELECT_TIMOUT_US = 800000;
|
||||
|
||||
/**
|
||||
* Construct.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->scheduler = new SplPriorityQueue();
|
||||
$this->scheduler->setExtractFlags(SplPriorityQueue::EXTR_BOTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function delay(float $delay, callable $func, array $args = []): int
|
||||
{
|
||||
$timerId = $this->timerId++;
|
||||
$runTime = microtime(true) + $delay;
|
||||
$this->scheduler->insert($timerId, -$runTime);
|
||||
$this->eventTimer[$timerId] = [$func, $args];
|
||||
if ($this->nextTickTime == 0 || $this->nextTickTime > $runTime) {
|
||||
$this->setNextTickTime($runTime);
|
||||
}
|
||||
return $timerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function repeat(float $interval, callable $func, array $args = []): int
|
||||
{
|
||||
$timerId = $this->timerId++;
|
||||
$runTime = microtime(true) + $interval;
|
||||
$this->scheduler->insert($timerId, -$runTime);
|
||||
$this->eventTimer[$timerId] = [$func, $args, $interval];
|
||||
if ($this->nextTickTime == 0 || $this->nextTickTime > $runTime) {
|
||||
$this->setNextTickTime($runTime);
|
||||
}
|
||||
return $timerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offDelay(int $timerId): bool
|
||||
{
|
||||
if (isset($this->eventTimer[$timerId])) {
|
||||
unset($this->eventTimer[$timerId]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offRepeat(int $timerId): bool
|
||||
{
|
||||
return $this->offDelay($timerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onReadable($stream, callable $func): void
|
||||
{
|
||||
$count = count($this->readFds);
|
||||
if ($count >= 1024) {
|
||||
trigger_error("System call select exceeded the maximum number of connections 1024, please install event extension for more connections.", E_USER_WARNING);
|
||||
} else if (DIRECTORY_SEPARATOR !== '/' && $count >= 256) {
|
||||
trigger_error("System call select exceeded the maximum number of connections 256.", E_USER_WARNING);
|
||||
}
|
||||
$fdKey = (int)$stream;
|
||||
$this->readEvents[$fdKey] = $func;
|
||||
$this->readFds[$fdKey] = $stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offReadable($stream): bool
|
||||
{
|
||||
$fdKey = (int)$stream;
|
||||
if (isset($this->readEvents[$fdKey])) {
|
||||
unset($this->readEvents[$fdKey], $this->readFds[$fdKey]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onWritable($stream, callable $func): void
|
||||
{
|
||||
$count = count($this->writeFds);
|
||||
if ($count >= 1024) {
|
||||
trigger_error("System call select exceeded the maximum number of connections 1024, please install event/libevent extension for more connections.", E_USER_WARNING);
|
||||
} else if (DIRECTORY_SEPARATOR !== '/' && $count >= 256) {
|
||||
trigger_error("System call select exceeded the maximum number of connections 256.", E_USER_WARNING);
|
||||
}
|
||||
$fdKey = (int)$stream;
|
||||
$this->writeEvents[$fdKey] = $func;
|
||||
$this->writeFds[$fdKey] = $stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offWritable($stream): bool
|
||||
{
|
||||
$fdKey = (int)$stream;
|
||||
if (isset($this->writeEvents[$fdKey])) {
|
||||
unset($this->writeEvents[$fdKey], $this->writeFds[$fdKey]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* On except.
|
||||
*
|
||||
* @param resource $stream
|
||||
* @param callable $func
|
||||
*/
|
||||
public function onExcept($stream, callable $func): void
|
||||
{
|
||||
$fdKey = (int)$stream;
|
||||
$this->exceptEvents[$fdKey] = $func;
|
||||
$this->exceptFds[$fdKey] = $stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Off except.
|
||||
*
|
||||
* @param resource $stream
|
||||
* @return bool
|
||||
*/
|
||||
public function offExcept($stream): bool
|
||||
{
|
||||
$fdKey = (int)$stream;
|
||||
if (isset($this->exceptEvents[$fdKey])) {
|
||||
unset($this->exceptEvents[$fdKey], $this->exceptFds[$fdKey]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onSignal(int $signal, callable $func): void
|
||||
{
|
||||
if (!function_exists('pcntl_signal')) {
|
||||
return;
|
||||
}
|
||||
$this->signalEvents[$signal] = $func;
|
||||
pcntl_signal($signal, fn () => $this->safeCall($this->signalEvents[$signal], [$signal]));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offSignal(int $signal): bool
|
||||
{
|
||||
if (!function_exists('pcntl_signal')) {
|
||||
return false;
|
||||
}
|
||||
pcntl_signal($signal, SIG_IGN);
|
||||
if (isset($this->signalEvents[$signal])) {
|
||||
unset($this->signalEvents[$signal]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tick for timer.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function tick(): void
|
||||
{
|
||||
$tasksToInsert = [];
|
||||
while (!$this->scheduler->isEmpty()) {
|
||||
$schedulerData = $this->scheduler->top();
|
||||
$timerId = $schedulerData['data'];
|
||||
$nextRunTime = -$schedulerData['priority'];
|
||||
$timeNow = microtime(true);
|
||||
$this->selectTimeout = (int)(($nextRunTime - $timeNow) * 1000000);
|
||||
|
||||
if ($this->selectTimeout <= 0) {
|
||||
$this->scheduler->extract();
|
||||
|
||||
if (!isset($this->eventTimer[$timerId])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// [func, args, timer_interval]
|
||||
$taskData = $this->eventTimer[$timerId];
|
||||
if (isset($taskData[2])) {
|
||||
$nextRunTime = $timeNow + $taskData[2];
|
||||
$tasksToInsert[] = [$timerId, -$nextRunTime];
|
||||
} else {
|
||||
unset($this->eventTimer[$timerId]);
|
||||
}
|
||||
$this->safeCall($taskData[0], $taskData[1]);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
foreach ($tasksToInsert as $item) {
|
||||
$this->scheduler->insert($item[0], $item[1]);
|
||||
}
|
||||
if (!$this->scheduler->isEmpty()) {
|
||||
$schedulerData = $this->scheduler->top();
|
||||
$nextRunTime = -$schedulerData['priority'];
|
||||
$this->setNextTickTime($nextRunTime);
|
||||
return;
|
||||
}
|
||||
$this->setNextTickTime(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set next tick time.
|
||||
*
|
||||
* @param float $nextTickTime
|
||||
* @return void
|
||||
*/
|
||||
protected function setNextTickTime(float $nextTickTime): void
|
||||
{
|
||||
$this->nextTickTime = $nextTickTime;
|
||||
if ($nextTickTime == 0) {
|
||||
$this->selectTimeout = self::MAX_SELECT_TIMOUT_US;
|
||||
return;
|
||||
}
|
||||
$this->selectTimeout = min(max((int)(($nextTickTime - microtime(true)) * 1000000), 0), self::MAX_SELECT_TIMOUT_US);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function deleteAllTimer(): void
|
||||
{
|
||||
$this->scheduler = new SplPriorityQueue();
|
||||
$this->scheduler->setExtractFlags(SplPriorityQueue::EXTR_BOTH);
|
||||
$this->eventTimer = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
while ($this->running) {
|
||||
$read = $this->readFds;
|
||||
$write = $this->writeFds;
|
||||
$except = $this->exceptFds;
|
||||
if ($read || $write || $except) {
|
||||
// Waiting read/write/signal/timeout events.
|
||||
try {
|
||||
@stream_select($read, $write, $except, 0, $this->selectTimeout);
|
||||
} catch (Throwable) {
|
||||
// do nothing
|
||||
}
|
||||
} else {
|
||||
$this->selectTimeout >= 1 && usleep($this->selectTimeout);
|
||||
}
|
||||
|
||||
foreach ($read as $fd) {
|
||||
$fdKey = (int)$fd;
|
||||
if (isset($this->readEvents[$fdKey])) {
|
||||
$this->readEvents[$fdKey]($fd);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($write as $fd) {
|
||||
$fdKey = (int)$fd;
|
||||
if (isset($this->writeEvents[$fdKey])) {
|
||||
$this->writeEvents[$fdKey]($fd);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($except as $fd) {
|
||||
$fdKey = (int)$fd;
|
||||
if (isset($this->exceptEvents[$fdKey])) {
|
||||
$this->exceptEvents[$fdKey]($fd);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->nextTickTime > 0) {
|
||||
if (microtime(true) >= $this->nextTickTime) {
|
||||
$this->tick();
|
||||
} else {
|
||||
$this->selectTimeout = (int)(($this->nextTickTime - microtime(true)) * 1000000);
|
||||
}
|
||||
}
|
||||
|
||||
// The $this->signalEvents are empty under Windows, make sure not to call pcntl_signal_dispatch.
|
||||
if ($this->signalEvents) {
|
||||
// Calls signal handlers for pending signals
|
||||
pcntl_signal_dispatch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function stop(): void
|
||||
{
|
||||
$this->running = false;
|
||||
$this->deleteAllTimer();
|
||||
foreach ($this->signalEvents as $signal => $item) {
|
||||
$this->offsignal($signal);
|
||||
}
|
||||
$this->readFds = [];
|
||||
$this->writeFds = [];
|
||||
$this->exceptFds = [];
|
||||
$this->readEvents = [];
|
||||
$this->writeEvents = [];
|
||||
$this->exceptEvents = [];
|
||||
$this->signalEvents = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTimerCount(): int
|
||||
{
|
||||
return count($this->eventTimer);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setErrorHandler(callable $errorHandler): void
|
||||
{
|
||||
$this->errorHandler = $errorHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable $func
|
||||
* @param array $args
|
||||
* @return void
|
||||
*/
|
||||
private function safeCall(callable $func, array $args = []): void
|
||||
{
|
||||
try {
|
||||
$func(...$args);
|
||||
} catch (Throwable $e) {
|
||||
if ($this->errorHandler === null) {
|
||||
echo $e;
|
||||
} else {
|
||||
($this->errorHandler)($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Events;
|
||||
|
||||
use Swoole\Coroutine;
|
||||
use Swoole\Event;
|
||||
use Swoole\Process;
|
||||
use Swoole\Timer;
|
||||
use Throwable;
|
||||
|
||||
final class Swoole implements EventInterface
|
||||
{
|
||||
/**
|
||||
* All listeners for read timer
|
||||
*
|
||||
* @var array<int, int>
|
||||
*/
|
||||
private array $eventTimer = [];
|
||||
|
||||
/**
|
||||
* All listeners for read event.
|
||||
*
|
||||
* @var array<int, array>
|
||||
*/
|
||||
private array $readEvents = [];
|
||||
|
||||
/**
|
||||
* All listeners for write event.
|
||||
*
|
||||
* @var array<int, array>
|
||||
*/
|
||||
private array $writeEvents = [];
|
||||
|
||||
/**
|
||||
* @var ?callable
|
||||
*/
|
||||
private $errorHandler = null;
|
||||
|
||||
private bool $stopping = false;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
Coroutine::set(['hook_flags' => SWOOLE_HOOK_ALL]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function delay(float $delay, callable $func, array $args = []): int
|
||||
{
|
||||
$t = (int)($delay * 1000);
|
||||
$t = max($t, 1);
|
||||
$timerId = Timer::after($t, function () use ($func, $args, &$timerId) {
|
||||
unset($this->eventTimer[$timerId]);
|
||||
$this->safeCall($func, $args);
|
||||
});
|
||||
$this->eventTimer[$timerId] = $timerId;
|
||||
return $timerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offDelay(int $timerId): bool
|
||||
{
|
||||
if (isset($this->eventTimer[$timerId])) {
|
||||
Timer::clear($timerId);
|
||||
unset($this->eventTimer[$timerId]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offRepeat(int $timerId): bool
|
||||
{
|
||||
return $this->offDelay($timerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function repeat(float $interval, callable $func, array $args = []): int
|
||||
{
|
||||
$t = (int)($interval * 1000);
|
||||
$t = max($t, 1);
|
||||
$timerId = Timer::tick($t, function () use ($func, $args) {
|
||||
$this->safeCall($func, $args);
|
||||
});
|
||||
$this->eventTimer[$timerId] = $timerId;
|
||||
return $timerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onReadable($stream, callable $func): void
|
||||
{
|
||||
$fd = (int)$stream;
|
||||
if (!isset($this->readEvents[$fd]) && !isset($this->writeEvents[$fd])) {
|
||||
Event::add($stream, fn () => $this->callRead($fd), null, SWOOLE_EVENT_READ);
|
||||
} elseif (isset($this->writeEvents[$fd])) {
|
||||
Event::set($stream, fn () => $this->callRead($fd), null, SWOOLE_EVENT_READ | SWOOLE_EVENT_WRITE);
|
||||
} else {
|
||||
Event::set($stream, fn () => $this->callRead($fd), null, SWOOLE_EVENT_READ);
|
||||
}
|
||||
|
||||
$this->readEvents[$fd] = [$func, [$stream]];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offReadable($stream): bool
|
||||
{
|
||||
$fd = (int)$stream;
|
||||
if (!isset($this->readEvents[$fd])) {
|
||||
return false;
|
||||
}
|
||||
unset($this->readEvents[$fd]);
|
||||
if (!isset($this->writeEvents[$fd])) {
|
||||
Event::del($stream);
|
||||
return true;
|
||||
}
|
||||
Event::set($stream, null, null, SWOOLE_EVENT_WRITE);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onWritable($stream, callable $func): void
|
||||
{
|
||||
$fd = (int)$stream;
|
||||
if (!isset($this->readEvents[$fd]) && !isset($this->writeEvents[$fd])) {
|
||||
Event::add($stream, null, fn () => $this->callWrite($fd), SWOOLE_EVENT_WRITE);
|
||||
} elseif (isset($this->readEvents[$fd])) {
|
||||
Event::set($stream, null, fn () => $this->callWrite($fd), SWOOLE_EVENT_WRITE | SWOOLE_EVENT_READ);
|
||||
} else {
|
||||
Event::set($stream, null, fn () =>$this->callWrite($fd), SWOOLE_EVENT_WRITE);
|
||||
}
|
||||
|
||||
$this->writeEvents[$fd] = [$func, [$stream]];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offWritable($stream): bool
|
||||
{
|
||||
$fd = (int)$stream;
|
||||
if (!isset($this->writeEvents[$fd])) {
|
||||
return false;
|
||||
}
|
||||
unset($this->writeEvents[$fd]);
|
||||
if (!isset($this->readEvents[$fd])) {
|
||||
Event::del($stream);
|
||||
return true;
|
||||
}
|
||||
Event::set($stream, null, null, SWOOLE_EVENT_READ);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onSignal(int $signal, callable $func): void
|
||||
{
|
||||
Process::signal($signal, fn () => $this->safeCall($func, [$signal]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Please see https://wiki.swoole.com/#/process/process?id=signal
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offSignal(int $signal): bool
|
||||
{
|
||||
return Process::signal($signal, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function deleteAllTimer(): void
|
||||
{
|
||||
foreach ($this->eventTimer as $timerId) {
|
||||
Timer::clear($timerId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// Avoid process exit due to no listening
|
||||
Timer::tick(100000000, static fn() => null);
|
||||
Event::wait();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy loop.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function stop(): void
|
||||
{
|
||||
if ($this->stopping) {
|
||||
return;
|
||||
}
|
||||
$this->stopping = true;
|
||||
// Cancel all coroutines before Event::exit
|
||||
foreach (Coroutine::listCoroutines() as $coroutine) {
|
||||
Coroutine::cancel($coroutine);
|
||||
}
|
||||
// Wait for coroutines to exit
|
||||
usleep(200000);
|
||||
Event::exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timer count.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getTimerCount(): int
|
||||
{
|
||||
return count($this->eventTimer);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setErrorHandler(callable $errorHandler): void
|
||||
{
|
||||
$this->errorHandler = $errorHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $fd
|
||||
* @return void
|
||||
*/
|
||||
private function callRead($fd)
|
||||
{
|
||||
if (isset($this->readEvents[$fd])) {
|
||||
$this->safeCall($this->readEvents[$fd][0], $this->readEvents[$fd][1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $fd
|
||||
* @return void
|
||||
*/
|
||||
private function callWrite($fd)
|
||||
{
|
||||
if (isset($this->writeEvents[$fd])) {
|
||||
$this->safeCall($this->writeEvents[$fd][0], $this->writeEvents[$fd][1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable $func
|
||||
* @param array $args
|
||||
* @return void
|
||||
*/
|
||||
private function safeCall(callable $func, array $args = []): void
|
||||
{
|
||||
Coroutine::create(function() use ($func, $args) {
|
||||
try {
|
||||
$func(...$args);
|
||||
} catch (Throwable $e) {
|
||||
if ($this->errorHandler === null) {
|
||||
echo $e;
|
||||
} else {
|
||||
($this->errorHandler)($e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Events;
|
||||
|
||||
use RuntimeException;
|
||||
use Workerman\Coroutine\Coroutine\Swow as Coroutine;
|
||||
use Swow\Signal;
|
||||
use Swow\SignalException;
|
||||
use function Swow\Sync\waitAll;
|
||||
|
||||
final class Swow implements EventInterface
|
||||
{
|
||||
/**
|
||||
* All listeners for read timer.
|
||||
*
|
||||
* @var array<int, int>
|
||||
*/
|
||||
private array $eventTimer = [];
|
||||
|
||||
/**
|
||||
* All listeners for read event.
|
||||
*
|
||||
* @var array<int, Coroutine>
|
||||
*/
|
||||
private array $readEvents = [];
|
||||
|
||||
/**
|
||||
* All listeners for write event.
|
||||
*
|
||||
* @var array<int, Coroutine>
|
||||
*/
|
||||
private array $writeEvents = [];
|
||||
|
||||
/**
|
||||
* All listeners for signal.
|
||||
*
|
||||
* @var array<int, Coroutine>
|
||||
*/
|
||||
private array $signalListener = [];
|
||||
|
||||
/**
|
||||
* @var ?callable
|
||||
*/
|
||||
private $errorHandler = null;
|
||||
|
||||
/**
|
||||
* Get timer count.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getTimerCount(): int
|
||||
{
|
||||
return count($this->eventTimer);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function delay(float $delay, callable $func, array $args = []): int
|
||||
{
|
||||
$t = (int)($delay * 1000);
|
||||
$t = max($t, 1);
|
||||
$coroutine = Coroutine::run(function () use ($t, $func, $args): void {
|
||||
msleep($t);
|
||||
unset($this->eventTimer[Coroutine::getCurrent()->getId()]);
|
||||
$this->safeCall($func, $args);
|
||||
});
|
||||
$timerId = $coroutine->getId();
|
||||
$this->eventTimer[$timerId] = $timerId;
|
||||
return $timerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function repeat(float $interval, callable $func, array $args = []): int
|
||||
{
|
||||
$t = (int)($interval * 1000);
|
||||
$t = max($t, 1);
|
||||
$coroutine = Coroutine::run(function () use ($t, $func, $args): void {
|
||||
// @phpstan-ignore-next-line While loop condition is always true.
|
||||
while (true) {
|
||||
msleep($t);
|
||||
$this->safeCall($func, $args);
|
||||
}
|
||||
});
|
||||
$timerId = $coroutine->getId();
|
||||
$this->eventTimer[$timerId] = $timerId;
|
||||
return $timerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offDelay(int $timerId): bool
|
||||
{
|
||||
if (isset($this->eventTimer[$timerId])) {
|
||||
try {
|
||||
(Coroutine::getAll()[$timerId])->kill();
|
||||
return true;
|
||||
} finally {
|
||||
unset($this->eventTimer[$timerId]);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offRepeat(int $timerId): bool
|
||||
{
|
||||
return $this->offDelay($timerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function deleteAllTimer(): void
|
||||
{
|
||||
foreach ($this->eventTimer as $timerId) {
|
||||
$this->offDelay($timerId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onReadable($stream, callable $func): void
|
||||
{
|
||||
$fd = (int)$stream;
|
||||
if (isset($this->readEvents[$fd])) {
|
||||
$this->offReadable($stream);
|
||||
}
|
||||
Coroutine::run(function () use ($stream, $func, $fd): void {
|
||||
try {
|
||||
$this->readEvents[$fd] = Coroutine::getCurrent();
|
||||
while (true) {
|
||||
if (!is_resource($stream)) {
|
||||
$this->offReadable($stream);
|
||||
break;
|
||||
}
|
||||
// Under Windows, setting a timeout is necessary; otherwise, the accept cannot be listened to.
|
||||
// Setting it to 1000ms will result in a 1-second delay for the first accept under Windows.
|
||||
if (!isset($this->readEvents[$fd]) || $this->readEvents[$fd] !== Coroutine::getCurrent()) {
|
||||
break;
|
||||
}
|
||||
$rEvent = stream_poll_one($stream, STREAM_POLLIN | STREAM_POLLHUP, 1000);
|
||||
if ($rEvent !== STREAM_POLLNONE) {
|
||||
$this->safeCall($func, [$stream]);
|
||||
}
|
||||
if ($rEvent !== STREAM_POLLIN && $rEvent !== STREAM_POLLNONE) {
|
||||
$this->offReadable($stream);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException) {
|
||||
$this->offReadable($stream);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offReadable($stream): bool
|
||||
{
|
||||
// 在当前协程执行 $coroutine->kill() 会导致不可预知问题,所以没有使用$coroutine->kill()
|
||||
$fd = (int)$stream;
|
||||
if (isset($this->readEvents[$fd])) {
|
||||
unset($this->readEvents[$fd]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onWritable($stream, callable $func): void
|
||||
{
|
||||
$fd = (int)$stream;
|
||||
if (isset($this->writeEvents[$fd])) {
|
||||
$this->offWritable($stream);
|
||||
}
|
||||
Coroutine::run(function () use ($stream, $func, $fd): void {
|
||||
try {
|
||||
$this->writeEvents[$fd] = Coroutine::getCurrent();
|
||||
while (true) {
|
||||
if (!is_resource($stream)) {
|
||||
$this->offWritable($stream);
|
||||
break;
|
||||
}
|
||||
if (!isset($this->writeEvents[$fd]) || $this->writeEvents[$fd] !== Coroutine::getCurrent()) {
|
||||
break;
|
||||
}
|
||||
$rEvent = stream_poll_one($stream, STREAM_POLLOUT | STREAM_POLLHUP, 1000);
|
||||
if ($rEvent !== STREAM_POLLNONE) {
|
||||
$this->safeCall($func, [$stream]);
|
||||
}
|
||||
if ($rEvent !== STREAM_POLLOUT && $rEvent !== STREAM_POLLNONE) {
|
||||
$this->offWritable($stream);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException) {
|
||||
$this->offWritable($stream);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offWritable($stream): bool
|
||||
{
|
||||
$fd = (int)$stream;
|
||||
if (isset($this->writeEvents[$fd])) {
|
||||
unset($this->writeEvents[$fd]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onSignal(int $signal, callable $func): void
|
||||
{
|
||||
Coroutine::run(function () use ($signal, $func): void {
|
||||
$this->signalListener[$signal] = Coroutine::getCurrent();
|
||||
while (1) {
|
||||
try {
|
||||
Signal::wait($signal);
|
||||
if (!isset($this->signalListener[$signal]) ||
|
||||
$this->signalListener[$signal] !== Coroutine::getCurrent()) {
|
||||
break;
|
||||
}
|
||||
$this->safeCall($func, [$signal]);
|
||||
} catch (SignalException) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function offSignal(int $signal): bool
|
||||
{
|
||||
if (!isset($this->signalListener[$signal])) {
|
||||
return false;
|
||||
}
|
||||
unset($this->signalListener[$signal]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
waitAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy loop.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function stop(): void
|
||||
{
|
||||
Coroutine::killAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setErrorHandler(callable $errorHandler): void
|
||||
{
|
||||
$this->errorHandler = $errorHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable $func
|
||||
* @param array $args
|
||||
* @return void
|
||||
*/
|
||||
private function safeCall(callable $func, array $args = []): void
|
||||
{
|
||||
Coroutine::run(function () use ($func, $args): void {
|
||||
try {
|
||||
$func(...$args);
|
||||
} catch (\Throwable $e) {
|
||||
if ($this->errorHandler === null) {
|
||||
echo $e;
|
||||
} else {
|
||||
($this->errorHandler)($e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Protocols;
|
||||
|
||||
use function pack;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
use function unpack;
|
||||
|
||||
/**
|
||||
* Frame Protocol.
|
||||
*/
|
||||
class Frame
|
||||
{
|
||||
/**
|
||||
* Check the integrity of the package.
|
||||
*
|
||||
* @param string $buffer
|
||||
* @return int
|
||||
*/
|
||||
public static function input(string $buffer): int
|
||||
{
|
||||
if (strlen($buffer) < 4) {
|
||||
return 0;
|
||||
}
|
||||
$unpackData = unpack('Ntotal_length', $buffer);
|
||||
return $unpackData['total_length'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode.
|
||||
*
|
||||
* @param string $buffer
|
||||
* @return string
|
||||
*/
|
||||
public static function decode(string $buffer): string
|
||||
{
|
||||
return substr($buffer, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode.
|
||||
*
|
||||
* @param string $data
|
||||
* @return string
|
||||
*/
|
||||
public static function encode(string $data): string
|
||||
{
|
||||
$totalLength = 4 + strlen($data);
|
||||
return pack('N', $totalLength) . $data;
|
||||
}
|
||||
}
|
||||
+507
@@ -0,0 +1,507 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Protocols;
|
||||
|
||||
use Workerman\Connection\TcpConnection;
|
||||
use Workerman\Protocols\Http\Request;
|
||||
use Workerman\Protocols\Http\Response;
|
||||
use function clearstatcache;
|
||||
use function count;
|
||||
use function ctype_digit;
|
||||
use function ctype_xdigit;
|
||||
use function explode;
|
||||
use function filesize;
|
||||
use function fopen;
|
||||
use function fread;
|
||||
use function fseek;
|
||||
use function ftell;
|
||||
use function hexdec;
|
||||
use function ini_get;
|
||||
use function is_array;
|
||||
use function is_object;
|
||||
use function ltrim;
|
||||
use function preg_match;
|
||||
use function preg_replace;
|
||||
use function strlen;
|
||||
use function strpos;
|
||||
use function strtolower;
|
||||
use function substr;
|
||||
use function sys_get_temp_dir;
|
||||
use function trim;
|
||||
|
||||
/**
|
||||
* Class Http.
|
||||
* @package Workerman\Protocols
|
||||
*/
|
||||
class Http
|
||||
{
|
||||
/**
|
||||
* Request class name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static string $requestClass = Request::class;
|
||||
|
||||
/**
|
||||
* Upload tmp dir.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static string $uploadTmpDir = '';
|
||||
|
||||
/**
|
||||
* Bad request.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected const HTTP_400 = "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n";
|
||||
|
||||
/**
|
||||
* Payload too large.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected const HTTP_413 = "HTTP/1.1 413 Payload Too Large\r\nConnection: close\r\n\r\n";
|
||||
|
||||
/**
|
||||
* Max bytes buffered while waiting for end of headers, and max offset of "\r\n\r\n" (header block size limit).
|
||||
*/
|
||||
protected const MAX_HEADER_LENGTH = 16384;
|
||||
|
||||
/**
|
||||
* Get or set the request class name.
|
||||
*
|
||||
* @param class-string|null $className
|
||||
* @return string
|
||||
*/
|
||||
public static function requestClass(?string $className = null): string
|
||||
{
|
||||
if ($className !== null) {
|
||||
static::$requestClass = $className;
|
||||
}
|
||||
return static::$requestClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the integrity of the package.
|
||||
*
|
||||
* @param string $buffer
|
||||
* @param TcpConnection $connection
|
||||
* @return int
|
||||
*/
|
||||
public static function input(string $buffer, TcpConnection $connection): int
|
||||
{
|
||||
static $cache = [];
|
||||
|
||||
$crlfPos = strpos($buffer, "\r\n\r\n");
|
||||
if (false === $crlfPos) {
|
||||
if (strlen($buffer) >= static::MAX_HEADER_LENGTH) {
|
||||
$connection->end(static::HTTP_413, true);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
$length = $crlfPos + 4;
|
||||
if ($crlfPos >= static::MAX_HEADER_LENGTH) {
|
||||
$connection->end(static::HTTP_413, true);
|
||||
return 0;
|
||||
}
|
||||
$header = isset($buffer[$length]) ? substr($buffer, 0, $length) : $buffer;
|
||||
|
||||
if ($length <= TcpConnection::MAX_CACHE_STRING_LENGTH && isset($cache[$header])) {
|
||||
return $cache[$header];
|
||||
}
|
||||
|
||||
// Validate request line: METHOD SP origin-form SP HTTP/1.x
|
||||
$firstLineEnd = strpos($header, "\r\n");
|
||||
if (!preg_match(
|
||||
'~^(?-i:GET|POST|OPTIONS|HEAD|DELETE|PUT|PATCH) /[^\x00-\x20\x7f]* (?-i:HTTP)/1\.(?<minor>[01])$~',
|
||||
substr($header, 0, $firstLineEnd),
|
||||
$matches
|
||||
)) {
|
||||
$connection->end(static::HTTP_400, true);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Parse headers
|
||||
$headers = [];
|
||||
$headerBody = substr($header, $firstLineEnd + 2, $crlfPos - $firstLineEnd - 2);
|
||||
foreach (explode("\r\n", $headerBody) as $line) {
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
$parts = explode(':', $line, 2);
|
||||
// field-name must be a token: 1*tchar (RFC 7230 §3.2.6)
|
||||
if (!isset($parts[1]) || !preg_match('/^[a-zA-Z0-9!#$%&\'*+\-.^_`|~]+$/', $parts[0])) {
|
||||
$connection->end(static::HTTP_400, true);
|
||||
return 0;
|
||||
}
|
||||
$headers[strtolower($parts[0])][] = trim($parts[1], " \t");
|
||||
}
|
||||
|
||||
// Host: required for HTTP/1.1, must not be duplicated for any version (RFC 7230 §5.4)
|
||||
$hostCount = count($headers['host'] ?? []);
|
||||
if ($hostCount > 1 || ($matches['minor'] === '1' && $hostCount === 0)) {
|
||||
$connection->end(static::HTTP_400, true);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Transfer-Encoding: must be sole header with value "chunked", no Content-Length
|
||||
if (isset($headers['transfer-encoding'])) {
|
||||
if (isset($headers['content-length'])
|
||||
|| count($headers['transfer-encoding']) !== 1
|
||||
|| strtolower($headers['transfer-encoding'][0]) !== 'chunked') {
|
||||
$connection->end(static::HTTP_400, true);
|
||||
return 0;
|
||||
}
|
||||
return static::inputChunked($buffer, $connection, $length);
|
||||
}
|
||||
|
||||
// Content-Length: must be single header with pure-digit value
|
||||
if (isset($headers['content-length'])) {
|
||||
if (count($headers['content-length']) !== 1 || !ctype_digit($headers['content-length'][0])) {
|
||||
$connection->end(static::HTTP_400, true);
|
||||
return 0;
|
||||
}
|
||||
$length += (int)$headers['content-length'][0];
|
||||
}
|
||||
|
||||
if ($length > $connection->maxPackageSize) {
|
||||
$connection->end(static::HTTP_413, true);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($length <= TcpConnection::MAX_CACHE_STRING_LENGTH) {
|
||||
$cache[$header] = $length;
|
||||
if (count($cache) > TcpConnection::MAX_CACHE_SIZE) {
|
||||
unset($cache[key($cache)]);
|
||||
}
|
||||
}
|
||||
return $length;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check the integrity of a chunked transfer-encoded request body.
|
||||
*
|
||||
* @param string $buffer
|
||||
* @param TcpConnection $connection
|
||||
* @param int $headerLength
|
||||
* @return int
|
||||
*/
|
||||
protected static function inputChunked(string $buffer, TcpConnection $connection, int $headerLength): int
|
||||
{
|
||||
$connection->context ??= new \stdClass();
|
||||
$connection->context->chunked = true;
|
||||
|
||||
$pos = $headerLength;
|
||||
$bufLen = strlen($buffer);
|
||||
$maxSize = $connection->maxPackageSize;
|
||||
|
||||
while (true) {
|
||||
$lineEnd = strpos($buffer, "\r\n", $pos);
|
||||
if ($lineEnd === false) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$semiPos = strpos($buffer, ';', $pos);
|
||||
$hexEnd = ($semiPos !== false && $semiPos < $lineEnd) ? $semiPos : $lineEnd;
|
||||
$hexStr = substr($buffer, $pos, $hexEnd - $pos);
|
||||
|
||||
if ($hexStr === '' || !ctype_xdigit($hexStr) || isset($hexStr[16])) {
|
||||
$connection->end(static::HTTP_400, true);
|
||||
return 0;
|
||||
}
|
||||
|
||||
$chunkSize = hexdec($hexStr);
|
||||
if (is_float($chunkSize)) {
|
||||
$connection->end(static::HTTP_400, true);
|
||||
return 0;
|
||||
}
|
||||
$pos = $lineEnd + 2;
|
||||
|
||||
if ($chunkSize === 0) {
|
||||
while (true) {
|
||||
$lineEnd = strpos($buffer, "\r\n", $pos);
|
||||
if ($lineEnd === false) {
|
||||
return 0;
|
||||
}
|
||||
if ($lineEnd === $pos) {
|
||||
$totalLength = $pos + 2;
|
||||
if ($totalLength > $maxSize) {
|
||||
$connection->end(static::HTTP_413, true);
|
||||
return 0;
|
||||
}
|
||||
return $totalLength;
|
||||
}
|
||||
$pos = $lineEnd + 2;
|
||||
}
|
||||
}
|
||||
|
||||
if ($pos + $chunkSize + 2 > $bufLen) {
|
||||
return 0;
|
||||
}
|
||||
if (substr($buffer, $pos + $chunkSize, 2) !== "\r\n") {
|
||||
$connection->end(static::HTTP_400, true);
|
||||
return 0;
|
||||
}
|
||||
$pos += $chunkSize + 2;
|
||||
|
||||
if ($pos > $maxSize) {
|
||||
$connection->end(static::HTTP_413, true);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Http decode.
|
||||
*
|
||||
* @param string $buffer
|
||||
* @param TcpConnection $connection
|
||||
* @return mixed
|
||||
*/
|
||||
public static function decode(string $buffer, TcpConnection $connection): mixed
|
||||
{
|
||||
$trailers = [];
|
||||
if (isset($connection->context->chunked)) {
|
||||
unset($connection->context->chunked);
|
||||
[$buffer, $trailers] = static::decodeChunked($buffer, strpos($buffer, "\r\n\r\n"));
|
||||
}
|
||||
|
||||
$request = new static::$requestClass($buffer);
|
||||
if ($trailers !== []) {
|
||||
$request->setChunkTrailers($trailers);
|
||||
}
|
||||
$request->connection = $connection;
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a chunked transfer-encoded request into a normalized buffer.
|
||||
*
|
||||
* @param string $buffer
|
||||
* @param int $headerEnd
|
||||
* @return array{string, array}
|
||||
*/
|
||||
protected static function decodeChunked(string $buffer, int $headerEnd): array
|
||||
{
|
||||
$header = preg_replace('~\r\nTransfer-Encoding[ \t]*:[^\r]*~i', '', substr($buffer, 0, $headerEnd), 1);
|
||||
$body = '';
|
||||
$trailers = [];
|
||||
$pos = $headerEnd + 4;
|
||||
$bufLen = strlen($buffer);
|
||||
|
||||
while (true) {
|
||||
$lineEnd = strpos($buffer, "\r\n", $pos);
|
||||
if ($lineEnd === false) {
|
||||
break;
|
||||
}
|
||||
|
||||
$semiPos = strpos($buffer, ';', $pos);
|
||||
$hexEnd = ($semiPos !== false && $semiPos < $lineEnd) ? $semiPos : $lineEnd;
|
||||
$hexStr = substr($buffer, $pos, $hexEnd - $pos);
|
||||
if ($hexStr === '' || !ctype_xdigit($hexStr) || isset($hexStr[16])) {
|
||||
break;
|
||||
}
|
||||
|
||||
$chunkSize = hexdec($hexStr);
|
||||
if (is_float($chunkSize)) {
|
||||
break;
|
||||
}
|
||||
$pos = $lineEnd + 2;
|
||||
|
||||
if ($chunkSize === 0) {
|
||||
while (true) {
|
||||
$lineEnd = strpos($buffer, "\r\n", $pos);
|
||||
if ($lineEnd === false) {
|
||||
break 2;
|
||||
}
|
||||
if ($lineEnd === $pos) {
|
||||
$pos += 2;
|
||||
break;
|
||||
}
|
||||
$colonPos = strpos($buffer, ':', $pos);
|
||||
if ($colonPos !== false && $colonPos < $lineEnd) {
|
||||
$trailers[strtolower(substr($buffer, $pos, $colonPos - $pos))] = ltrim(substr($buffer, $colonPos + 1, $lineEnd - $colonPos - 1));
|
||||
}
|
||||
$pos = $lineEnd + 2;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if ($pos + $chunkSize + 2 > $bufLen) {
|
||||
break;
|
||||
}
|
||||
if (substr($buffer, $pos + $chunkSize, 2) !== "\r\n") {
|
||||
break;
|
||||
}
|
||||
$body .= substr($buffer, $pos, $chunkSize);
|
||||
$pos += $chunkSize + 2;
|
||||
}
|
||||
|
||||
return [$header . "\r\nContent-Length: " . strlen($body) . "\r\n\r\n" . $body, $trailers];
|
||||
}
|
||||
|
||||
/**
|
||||
* Http encode.
|
||||
*
|
||||
* @param string|Response $response
|
||||
* @param TcpConnection $connection
|
||||
* @return string
|
||||
*/
|
||||
public static function encode(mixed $response, TcpConnection $connection): string
|
||||
{
|
||||
if (!is_object($response)) {
|
||||
$extHeader = '';
|
||||
$contentType = 'text/html;charset=utf-8';
|
||||
foreach ($connection->headers as $name => $value) {
|
||||
if ($name === 'Content-Type') {
|
||||
$contentType = $value;
|
||||
continue;
|
||||
}
|
||||
if (is_array($value)) {
|
||||
foreach ($value as $item) {
|
||||
$extHeader .= "$name: $item\r\n";
|
||||
}
|
||||
} else {
|
||||
$extHeader .= "$name: $value\r\n";
|
||||
}
|
||||
}
|
||||
$connection->headers = [];
|
||||
$response = (string)$response;
|
||||
$bodyLen = strlen($response);
|
||||
return "HTTP/1.1 200 OK\r\n{$extHeader}Connection: keep-alive\r\nContent-Type: $contentType\r\nContent-Length: $bodyLen\r\n\r\n$response";
|
||||
}
|
||||
|
||||
if ($connection->headers) {
|
||||
$response->withHeaders($connection->headers);
|
||||
$connection->headers = [];
|
||||
}
|
||||
|
||||
if (isset($response->file)) {
|
||||
$file = $response->file['file'];
|
||||
$offset = $response->file['offset'] ?: 0;
|
||||
$length = $response->file['length'] ?: 0;
|
||||
clearstatcache();
|
||||
$fileSize = (int)filesize($file);
|
||||
$bodyLen = $length > 0 ? $length : $fileSize - $offset;
|
||||
$response->withHeaders([
|
||||
'Content-Length' => $bodyLen,
|
||||
'Accept-Ranges' => 'bytes',
|
||||
]);
|
||||
|
||||
if ($offset || $length) {
|
||||
$offsetEnd = $offset + $bodyLen - 1;
|
||||
$response->header('Content-Range', "bytes $offset-$offsetEnd/$fileSize");
|
||||
$response->withStatus(206);
|
||||
}
|
||||
if ($bodyLen < 2 * 1024 * 1024) {
|
||||
$connection->send($response . file_get_contents($file, false, null, $offset, $bodyLen), true);
|
||||
return '';
|
||||
}
|
||||
$handler = fopen($file, 'r');
|
||||
if (false === $handler) {
|
||||
$connection->close(new Response(403, [], '403 Forbidden'));
|
||||
return '';
|
||||
}
|
||||
$connection->send((string)$response, true);
|
||||
static::sendStream($connection, $handler, $offset, $length);
|
||||
return '';
|
||||
}
|
||||
|
||||
return (string)$response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send remainder of a stream to client.
|
||||
*
|
||||
* @param TcpConnection $connection
|
||||
* @param resource $handler
|
||||
* @param int $offset
|
||||
* @param int $length
|
||||
*/
|
||||
protected static function sendStream(TcpConnection $connection, $handler, int $offset = 0, int $length = 0): void
|
||||
{
|
||||
$connection->context->bufferFull = false;
|
||||
$connection->context->streamSending = true;
|
||||
if ($offset !== 0) {
|
||||
fseek($handler, $offset);
|
||||
}
|
||||
$offsetEnd = $offset + $length;
|
||||
// Read file content from disk piece by piece and send to client.
|
||||
$doWrite = function () use ($connection, $handler, $length, $offsetEnd) {
|
||||
// Send buffer not full.
|
||||
/** @phpstan-ignore-next-line */
|
||||
while ($connection->context->bufferFull === false) {
|
||||
// Read from disk.
|
||||
$size = 1024 * 1024;
|
||||
if ($length !== 0) {
|
||||
$tell = ftell($handler);
|
||||
$remainSize = $offsetEnd - $tell;
|
||||
if ($remainSize <= 0) {
|
||||
fclose($handler);
|
||||
$connection->onBufferDrain = null;
|
||||
return;
|
||||
}
|
||||
$size = min($remainSize, $size);
|
||||
}
|
||||
|
||||
$buffer = fread($handler, $size);
|
||||
// Read eof.
|
||||
if ($buffer === '' || $buffer === false) {
|
||||
fclose($handler);
|
||||
$connection->onBufferDrain = null;
|
||||
$connection->context->streamSending = false;
|
||||
return;
|
||||
}
|
||||
$connection->send($buffer, true);
|
||||
}
|
||||
};
|
||||
// Send buffer full.
|
||||
$connection->onBufferFull = function ($connection) {
|
||||
$connection->context->bufferFull = true;
|
||||
};
|
||||
// Send buffer drain.
|
||||
$connection->onBufferDrain = function ($connection) use ($doWrite) {
|
||||
$connection->context->bufferFull = false;
|
||||
$doWrite();
|
||||
};
|
||||
$doWrite();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or get uploadTmpDir.
|
||||
*
|
||||
* @param string|null $dir
|
||||
* @return string
|
||||
*/
|
||||
public static function uploadTmpDir(string|null $dir = null): string
|
||||
{
|
||||
if (null !== $dir) {
|
||||
static::$uploadTmpDir = $dir;
|
||||
}
|
||||
if (static::$uploadTmpDir === '') {
|
||||
if ($uploadTmpDir = ini_get('upload_tmp_dir')) {
|
||||
static::$uploadTmpDir = $uploadTmpDir;
|
||||
} else if ($uploadTmpDir = sys_get_temp_dir()) {
|
||||
static::$uploadTmpDir = $uploadTmpDir;
|
||||
}
|
||||
}
|
||||
return static::$uploadTmpDir;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Protocols\Http;
|
||||
|
||||
use Stringable;
|
||||
|
||||
use function dechex;
|
||||
use function strlen;
|
||||
|
||||
/**
|
||||
* Class Chunk
|
||||
* @package Workerman\Protocols\Http
|
||||
*/
|
||||
class Chunk implements Stringable
|
||||
{
|
||||
|
||||
public function __construct(protected string $buffer) {}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return dechex(strlen($this->buffer)) . "\r\n$this->buffer\r\n";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,846 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Protocols\Http;
|
||||
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
use Stringable;
|
||||
use Workerman\Connection\TcpConnection;
|
||||
use Workerman\Protocols\Http;
|
||||
use function array_walk_recursive;
|
||||
use function bin2hex;
|
||||
use function clearstatcache;
|
||||
use function count;
|
||||
use function explode;
|
||||
use function file_put_contents;
|
||||
use function is_file;
|
||||
use function json_decode;
|
||||
use function ltrim;
|
||||
use function microtime;
|
||||
use function pack;
|
||||
use function parse_str;
|
||||
use function parse_url;
|
||||
use function preg_match;
|
||||
use function preg_replace;
|
||||
use function strlen;
|
||||
use function strpos;
|
||||
use function strstr;
|
||||
use function strtolower;
|
||||
use function substr;
|
||||
use function tempnam;
|
||||
use function trim;
|
||||
use function unlink;
|
||||
use function urlencode;
|
||||
|
||||
/**
|
||||
* Class Request
|
||||
* @package Workerman\Protocols\Http
|
||||
*/
|
||||
class Request implements Stringable
|
||||
{
|
||||
/**
|
||||
* Connection.
|
||||
*
|
||||
* @var ?TcpConnection
|
||||
*/
|
||||
public ?TcpConnection $connection = null;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public static int $maxFileUploads = 1024;
|
||||
|
||||
/**
|
||||
* Maximum string length for cache
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public const MAX_CACHE_STRING_LENGTH = 4096;
|
||||
|
||||
/**
|
||||
* Maximum cache size.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public const MAX_CACHE_SIZE = 256;
|
||||
|
||||
/**
|
||||
* Properties.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public array $properties = [];
|
||||
|
||||
/**
|
||||
* Request data.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $data = [];
|
||||
|
||||
/**
|
||||
* Is safe.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $isSafe = true;
|
||||
|
||||
/**
|
||||
* Context.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public array $context = [];
|
||||
|
||||
/**
|
||||
* HTTP/1.1 chunked trailers (field names lowercased), set once by the protocol layer.
|
||||
*
|
||||
* @var ?array<string, string>
|
||||
*/
|
||||
protected ?array $chunkTrailers = null;
|
||||
|
||||
/**
|
||||
* Request constructor.
|
||||
*/
|
||||
public function __construct(protected string $buffer) {}
|
||||
|
||||
/**
|
||||
* @internal Set by {@see Http::decode()} for chunked requests; first call wins.
|
||||
*
|
||||
* @param array<string, string> $trailers
|
||||
* @return void
|
||||
*/
|
||||
public function setChunkTrailers(array $trailers): void
|
||||
{
|
||||
if ($this->chunkTrailers !== null) {
|
||||
return;
|
||||
}
|
||||
$this->chunkTrailers = $trailers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query.
|
||||
*
|
||||
* @param string|null $name
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function get(?string $name = null, mixed $default = null): mixed
|
||||
{
|
||||
if (!isset($this->data['get'])) {
|
||||
$this->parseGet();
|
||||
}
|
||||
if (null === $name) {
|
||||
return $this->data['get'];
|
||||
}
|
||||
return $this->data['get'][$name] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get post.
|
||||
*
|
||||
* @param string|null $name
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function post(?string $name = null, mixed $default = null): mixed
|
||||
{
|
||||
if (!isset($this->data['post'])) {
|
||||
$this->parsePost();
|
||||
}
|
||||
if (null === $name) {
|
||||
return $this->data['post'];
|
||||
}
|
||||
return $this->data['post'][$name] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get header item by name.
|
||||
*
|
||||
* @param string|null $name
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function header(?string $name = null, mixed $default = null): mixed
|
||||
{
|
||||
if (!isset($this->data['headers'])) {
|
||||
$this->parseHeaders();
|
||||
}
|
||||
if (null === $name) {
|
||||
return $this->data['headers'];
|
||||
}
|
||||
$name = strtolower($name);
|
||||
return $this->data['headers'][$name] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get trailer item by name.
|
||||
*
|
||||
* @param string|null $name
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function trailer(?string $name = null, mixed $default = null): mixed
|
||||
{
|
||||
$all = $this->chunkTrailers ?? [];
|
||||
if (null === $name) {
|
||||
return $all;
|
||||
}
|
||||
return $all[strtolower($name)] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cookie item by name.
|
||||
*
|
||||
* @param string|null $name
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function cookie(?string $name = null, mixed $default = null): mixed
|
||||
{
|
||||
if (!isset($this->data['cookie'])) {
|
||||
$cookies = explode(';', $this->header('cookie', ''));
|
||||
$mapped = [];
|
||||
|
||||
foreach ($cookies as $cookie) {
|
||||
$cookie = explode('=', $cookie, 2);
|
||||
if (count($cookie) !== 2) {
|
||||
continue;
|
||||
}
|
||||
$mapped[trim($cookie[0])] = $cookie[1];
|
||||
}
|
||||
$this->data['cookie'] = $mapped;
|
||||
}
|
||||
if ($name === null) {
|
||||
return $this->data['cookie'];
|
||||
}
|
||||
return $this->data['cookie'][$name] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upload files.
|
||||
*
|
||||
* @param string|null $name
|
||||
* @return array|null
|
||||
*/
|
||||
public function file(?string $name = null): mixed
|
||||
{
|
||||
clearstatcache();
|
||||
if (!empty($this->data['files'])) {
|
||||
array_walk_recursive($this->data['files'], function ($value, $key) {
|
||||
if ($key === 'tmp_name' && !is_file($value)) {
|
||||
$this->data['files'] = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
if (empty($this->data['files'])) {
|
||||
$this->parsePost();
|
||||
}
|
||||
if (null === $name) {
|
||||
return $this->data['files'];
|
||||
}
|
||||
return $this->data['files'][$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function method(): string
|
||||
{
|
||||
if (!isset($this->data['method'])) {
|
||||
$this->parseHeadFirstLine();
|
||||
}
|
||||
return $this->data['method'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get http protocol version.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function protocolVersion(): string
|
||||
{
|
||||
if (!isset($this->data['protocolVersion'])) {
|
||||
$this->parseProtocolVersion();
|
||||
}
|
||||
return $this->data['protocolVersion'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get host.
|
||||
*
|
||||
* @param bool $withoutPort
|
||||
* @return string|null
|
||||
*/
|
||||
public function host(bool $withoutPort = false): ?string
|
||||
{
|
||||
$host = $this->header('host');
|
||||
if ($host && $withoutPort) {
|
||||
return preg_replace('/:\d{1,5}$/', '', $host);
|
||||
}
|
||||
return $host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get uri.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function uri(): string
|
||||
{
|
||||
if (!isset($this->data['uri'])) {
|
||||
$this->parseHeadFirstLine();
|
||||
}
|
||||
return $this->data['uri'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function path(): string
|
||||
{
|
||||
if (!isset($this->data['path'])) {
|
||||
$this->parseUriComponents();
|
||||
}
|
||||
return $this->data['path'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function queryString(): string
|
||||
{
|
||||
if (!isset($this->data['query_string'])) {
|
||||
$this->parseUriComponents();
|
||||
}
|
||||
return $this->data['query_string'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse URI into path and query string components (single parse_url call).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function parseUriComponents(): void
|
||||
{
|
||||
$uri = $this->uri();
|
||||
$parsed = parse_url($uri);
|
||||
$this->data['path'] = $parsed['path'] ?? '/';
|
||||
$this->data['query_string'] = $parsed['query'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session.
|
||||
*
|
||||
* @return Session
|
||||
* @throws Exception
|
||||
*/
|
||||
public function session(): Session
|
||||
{
|
||||
return $this->context['session'] ??= new Session($this->sessionId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get/Set session id.
|
||||
*
|
||||
* @param string|null $sessionId
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
public function sessionId(?string $sessionId = null): string
|
||||
{
|
||||
if ($sessionId) {
|
||||
unset($this->context['sid'], $this->context['session']);
|
||||
}
|
||||
if (!isset($this->context['sid'])) {
|
||||
$sessionName = Session::$name;
|
||||
$sid = $sessionId ? '' : $this->cookie($sessionName);
|
||||
// Strip surrounding double quotes (RFC 6265 allows DQUOTE-wrapped cookie values).
|
||||
if (is_string($sid) && isset($sid[1]) && $sid[0] === '"' && $sid[-1] === '"') {
|
||||
$sid = substr($sid, 1, -1);
|
||||
}
|
||||
$sid = $this->isValidSessionId($sid) ? $sid : '';
|
||||
if ($sid === '') {
|
||||
if (!$this->connection) {
|
||||
throw new RuntimeException('Request->session() fail, header already send');
|
||||
}
|
||||
$sid = $sessionId ?: static::createSessionId();
|
||||
$cookieParams = Session::getCookieParams();
|
||||
$this->setSidCookie($sessionName, $sid, $cookieParams);
|
||||
}
|
||||
$this->context['sid'] = $sid;
|
||||
}
|
||||
return $this->context['sid'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if session id is valid.
|
||||
*
|
||||
* @param mixed $sessionId
|
||||
* @return bool
|
||||
*/
|
||||
public function isValidSessionId(mixed $sessionId): bool
|
||||
{
|
||||
return is_string($sessionId) && preg_match('/^[a-zA-Z0-9,-]{16,256}$/', $sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Session regenerate id.
|
||||
*
|
||||
* @param bool $deleteOldSession
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
public function sessionRegenerateId(bool $deleteOldSession = false): string
|
||||
{
|
||||
$session = $this->session();
|
||||
$sessionData = $session->all();
|
||||
if ($deleteOldSession) {
|
||||
$session->flush();
|
||||
}
|
||||
$newSid = static::createSessionId();
|
||||
$session = new Session($newSid);
|
||||
$session->put($sessionData);
|
||||
$cookieParams = Session::getCookieParams();
|
||||
$sessionName = Session::$name;
|
||||
$this->setSidCookie($sessionName, $newSid, $cookieParams);
|
||||
$this->context['sid'] = $newSid;
|
||||
$this->context['session'] = $session;
|
||||
return $newSid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get http raw head.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function rawHead(): string
|
||||
{
|
||||
return $this->data['head'] ??= strstr($this->buffer, "\r\n\r\n", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get http raw body.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function rawBody(): string
|
||||
{
|
||||
return substr($this->buffer, strpos($this->buffer, "\r\n\r\n") + 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get raw buffer.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function rawBuffer(): string
|
||||
{
|
||||
return $this->buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse first line of http header buffer.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function parseHeadFirstLine(): void
|
||||
{
|
||||
$firstLine = strstr($this->buffer, "\r\n", true);
|
||||
$tmp = explode(' ', $firstLine, 3);
|
||||
$this->data['method'] = $tmp[0];
|
||||
$this->data['uri'] = $tmp[1] ?? '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse protocol version.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function parseProtocolVersion(): void
|
||||
{
|
||||
$firstLine = strstr($this->buffer, "\r\n", true);
|
||||
$httpStr = strstr($firstLine, 'HTTP/');
|
||||
$protocolVersion = $httpStr ? substr($httpStr, 5) : '1.0';
|
||||
$this->data['protocolVersion'] = $protocolVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse headers.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function parseHeaders(): void
|
||||
{
|
||||
static $cache = [];
|
||||
$this->data['headers'] = [];
|
||||
$rawHead = $this->rawHead();
|
||||
$endLinePosition = strpos($rawHead, "\r\n");
|
||||
if ($endLinePosition === false) {
|
||||
return;
|
||||
}
|
||||
$headBuffer = substr($rawHead, $endLinePosition + 2);
|
||||
$cacheable = !isset($headBuffer[static::MAX_CACHE_STRING_LENGTH]);
|
||||
if ($cacheable && isset($cache[$headBuffer])) {
|
||||
$this->data['headers'] = $cache[$headBuffer];
|
||||
return;
|
||||
}
|
||||
$headData = explode("\r\n", $headBuffer);
|
||||
foreach ($headData as $content) {
|
||||
if ($content === '') {
|
||||
continue;
|
||||
}
|
||||
$parts = explode(':', $content, 2);
|
||||
if (!isset($parts[1])) {
|
||||
continue;
|
||||
}
|
||||
$key = strtolower($parts[0]);
|
||||
$value = trim($parts[1], " \t");
|
||||
if (isset($this->data['headers'][$key])) {
|
||||
$this->data['headers'][$key] = "{$this->data['headers'][$key]},$value";
|
||||
} else {
|
||||
$this->data['headers'][$key] = $value;
|
||||
}
|
||||
}
|
||||
if ($cacheable) {
|
||||
$cache[$headBuffer] = $this->data['headers'];
|
||||
if (count($cache) > static::MAX_CACHE_SIZE) {
|
||||
unset($cache[key($cache)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse head.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function parseGet(): void
|
||||
{
|
||||
static $cache = [];
|
||||
$queryString = $this->queryString();
|
||||
$this->data['get'] = [];
|
||||
if ($queryString === '') {
|
||||
return;
|
||||
}
|
||||
$cacheable = !isset($queryString[static::MAX_CACHE_STRING_LENGTH]);
|
||||
if ($cacheable && isset($cache[$queryString])) {
|
||||
$this->data['get'] = $cache[$queryString];
|
||||
return;
|
||||
}
|
||||
parse_str($queryString, $this->data['get']);
|
||||
if ($cacheable) {
|
||||
$cache[$queryString] = $this->data['get'];
|
||||
if (count($cache) > static::MAX_CACHE_SIZE) {
|
||||
unset($cache[key($cache)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse post.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function parsePost(): void
|
||||
{
|
||||
static $cache = [];
|
||||
$this->data['post'] = $this->data['files'] = [];
|
||||
$contentType = $this->header('content-type', '');
|
||||
if (preg_match('/boundary="?(\S+)"?/', $contentType, $match)) {
|
||||
$httpPostBoundary = '--' . $match[1];
|
||||
$this->parseUploadFiles($httpPostBoundary);
|
||||
return;
|
||||
}
|
||||
$bodyBuffer = $this->rawBody();
|
||||
if ($bodyBuffer === '') {
|
||||
return;
|
||||
}
|
||||
$cacheable = !isset($bodyBuffer[static::MAX_CACHE_STRING_LENGTH]);
|
||||
if ($cacheable && isset($cache[$bodyBuffer])) {
|
||||
$this->data['post'] = $cache[$bodyBuffer];
|
||||
return;
|
||||
}
|
||||
if (str_contains($contentType, 'json')) {
|
||||
$this->data['post'] = (array)json_decode($bodyBuffer, true);
|
||||
} else {
|
||||
parse_str($bodyBuffer, $this->data['post']);
|
||||
}
|
||||
if ($cacheable) {
|
||||
$cache[$bodyBuffer] = $this->data['post'];
|
||||
if (count($cache) > static::MAX_CACHE_SIZE) {
|
||||
unset($cache[key($cache)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse upload files.
|
||||
*
|
||||
* @param string $httpPostBoundary
|
||||
* @return void
|
||||
*/
|
||||
protected function parseUploadFiles(string $httpPostBoundary): void
|
||||
{
|
||||
$httpPostBoundary = trim($httpPostBoundary, '"');
|
||||
$buffer = $this->buffer;
|
||||
$postEncodeString = '';
|
||||
$filesEncodeString = '';
|
||||
$files = [];
|
||||
$bodyPosition = strpos($buffer, "\r\n\r\n") + 4;
|
||||
$offset = $bodyPosition + strlen($httpPostBoundary) + 2;
|
||||
$maxCount = static::$maxFileUploads;
|
||||
while ($maxCount-- > 0 && $offset) {
|
||||
$offset = $this->parseUploadFile($httpPostBoundary, $offset, $postEncodeString, $filesEncodeString, $files);
|
||||
}
|
||||
if ($postEncodeString) {
|
||||
parse_str($postEncodeString, $this->data['post']);
|
||||
}
|
||||
|
||||
if ($filesEncodeString) {
|
||||
parse_str($filesEncodeString, $this->data['files']);
|
||||
array_walk_recursive($this->data['files'], function (&$value) use ($files) {
|
||||
$value = $files[$value];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse upload file.
|
||||
*
|
||||
* @param string $boundary
|
||||
* @param int $sectionStartOffset
|
||||
* @param string $postEncodeString
|
||||
* @param string $filesEncodeStr
|
||||
* @param array $files
|
||||
* @return int
|
||||
*/
|
||||
protected function parseUploadFile(string $boundary, int $sectionStartOffset, string &$postEncodeString, string &$filesEncodeStr, array &$files): int
|
||||
{
|
||||
$file = [];
|
||||
$boundary = "\r\n$boundary";
|
||||
if (strlen($this->buffer) < $sectionStartOffset) {
|
||||
return 0;
|
||||
}
|
||||
$sectionEndOffset = strpos($this->buffer, $boundary, $sectionStartOffset);
|
||||
if (!$sectionEndOffset) {
|
||||
return 0;
|
||||
}
|
||||
$contentLinesEndOffset = strpos($this->buffer, "\r\n\r\n", $sectionStartOffset);
|
||||
if (!$contentLinesEndOffset || $contentLinesEndOffset + 4 > $sectionEndOffset) {
|
||||
return 0;
|
||||
}
|
||||
$contentLinesStr = substr($this->buffer, $sectionStartOffset, $contentLinesEndOffset - $sectionStartOffset);
|
||||
$contentLines = explode("\r\n", trim($contentLinesStr . "\r\n"));
|
||||
$boundaryValue = substr($this->buffer, $contentLinesEndOffset + 4, $sectionEndOffset - $contentLinesEndOffset - 4);
|
||||
$uploadKey = false;
|
||||
foreach ($contentLines as $contentLine) {
|
||||
if (!strpos($contentLine, ': ')) {
|
||||
return 0;
|
||||
}
|
||||
[$key, $value] = explode(': ', $contentLine);
|
||||
switch (strtolower($key)) {
|
||||
|
||||
case "content-disposition":
|
||||
// Is file data.
|
||||
if (preg_match('/name="(.*?)"; filename="(.*?)"/i', $value, $match)) {
|
||||
$error = 0;
|
||||
$tmpFile = '';
|
||||
$fileName = $match[1];
|
||||
$size = strlen($boundaryValue);
|
||||
$tmpUploadDir = HTTP::uploadTmpDir();
|
||||
if (!$tmpUploadDir) {
|
||||
$error = UPLOAD_ERR_NO_TMP_DIR;
|
||||
} else if ($boundaryValue === '' && $fileName === '') {
|
||||
$error = UPLOAD_ERR_NO_FILE;
|
||||
} else {
|
||||
$tmpFile = tempnam($tmpUploadDir, 'workerman.upload.');
|
||||
if ($tmpFile === false || false === file_put_contents($tmpFile, $boundaryValue)) {
|
||||
$error = UPLOAD_ERR_CANT_WRITE;
|
||||
}
|
||||
}
|
||||
$uploadKey = $fileName;
|
||||
// Parse upload files.
|
||||
$file = [...$file, 'name' => $match[2], 'tmp_name' => $tmpFile, 'size' => $size, 'error' => $error, 'full_path' => $match[2]];
|
||||
$file['type'] ??= '';
|
||||
break;
|
||||
}
|
||||
// Is post field.
|
||||
// Parse $POST.
|
||||
if (preg_match('/name="(.*?)"$/', $value, $match)) {
|
||||
$k = $match[1];
|
||||
$postEncodeString .= urlencode($k) . "=" . urlencode($boundaryValue) . '&';
|
||||
}
|
||||
return $sectionEndOffset + strlen($boundary) + 2;
|
||||
|
||||
case "content-type":
|
||||
$file['type'] = trim($value);
|
||||
break;
|
||||
|
||||
case "webkitrelativepath":
|
||||
$file['full_path'] = trim($value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($uploadKey === false) {
|
||||
return 0;
|
||||
}
|
||||
$filesEncodeStr .= urlencode($uploadKey) . '=' . count($files) . '&';
|
||||
$files[] = $file;
|
||||
|
||||
return $sectionEndOffset + strlen($boundary) + 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create session id.
|
||||
*
|
||||
* @return string
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function createSessionId(): string
|
||||
{
|
||||
$sid = session_create_id();
|
||||
if ($sid === false) {
|
||||
throw new RuntimeException('session_create_id() failed');
|
||||
}
|
||||
return $sid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sessionName
|
||||
* @param string $sid
|
||||
* @param array $cookieParams
|
||||
* @return void
|
||||
*/
|
||||
protected function setSidCookie(string $sessionName, string $sid, array $cookieParams): void
|
||||
{
|
||||
if (!$this->connection) {
|
||||
throw new RuntimeException('Request->setSidCookie() fail, header already send');
|
||||
}
|
||||
$this->connection->headers['Set-Cookie'] = [$sessionName . '=' . $sid
|
||||
. (empty($cookieParams['domain']) ? '' : '; Domain=' . $cookieParams['domain'])
|
||||
. (empty($cookieParams['lifetime']) ? '' : '; Max-Age=' . $cookieParams['lifetime'])
|
||||
. (empty($cookieParams['path']) ? '' : '; Path=' . $cookieParams['path'])
|
||||
. (empty($cookieParams['samesite']) ? '' : '; SameSite=' . $cookieParams['samesite'])
|
||||
. (!$cookieParams['secure'] ? '' : '; Secure')
|
||||
. (!$cookieParams['httponly'] ? '' : '; HttpOnly')];
|
||||
}
|
||||
|
||||
/**
|
||||
* __toString.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter.
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
public function __set(string $name, mixed $value): void
|
||||
{
|
||||
$this->properties[$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter.
|
||||
*
|
||||
* @param string $name
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get(string $name): mixed
|
||||
{
|
||||
return $this->properties[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Isset.
|
||||
*
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function __isset(string $name): bool
|
||||
{
|
||||
return isset($this->properties[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset.
|
||||
*
|
||||
* @param string $name
|
||||
* @return void
|
||||
*/
|
||||
public function __unset(string $name): void
|
||||
{
|
||||
unset($this->properties[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* __unserialize.
|
||||
*
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
$this->isSafe = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function destroy(): void
|
||||
{
|
||||
if ($this->context) {
|
||||
$this->context = [];
|
||||
}
|
||||
if ($this->properties) {
|
||||
$this->properties = [];
|
||||
}
|
||||
$this->connection = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if (!empty($this->data['files']) && $this->isSafe) {
|
||||
clearstatcache();
|
||||
array_walk_recursive($this->data['files'], function ($value, $key) {
|
||||
if ($key === 'tmp_name' && is_file($value)) {
|
||||
unlink($value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,591 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Protocols\Http;
|
||||
|
||||
use Stringable;
|
||||
|
||||
use function array_merge_recursive;
|
||||
use function filemtime;
|
||||
use function gmdate;
|
||||
use function is_array;
|
||||
use function is_file;
|
||||
use function pathinfo;
|
||||
use function rawurlencode;
|
||||
use function strlen;
|
||||
|
||||
/**
|
||||
* Class Response
|
||||
* @package Workerman\Protocols\Http
|
||||
*/
|
||||
class Response implements Stringable
|
||||
{
|
||||
|
||||
/**
|
||||
* Http reason.
|
||||
*
|
||||
* @var ?string
|
||||
*/
|
||||
protected ?string $reason = null;
|
||||
|
||||
/**
|
||||
* Http version.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $version = '1.1';
|
||||
|
||||
/**
|
||||
* Send file info
|
||||
*
|
||||
* @var ?array
|
||||
*/
|
||||
public ?array $file = null;
|
||||
|
||||
/**
|
||||
* Mime Type map.
|
||||
* @var array
|
||||
*/
|
||||
protected static array $mimeTypeMap = [
|
||||
// text
|
||||
'html' => 'text/html',
|
||||
'htm' => 'text/html',
|
||||
'shtml' => 'text/html',
|
||||
'css' => 'text/css',
|
||||
'xml' => 'text/xml',
|
||||
'mml' => 'text/mathml',
|
||||
'txt' => 'text/plain',
|
||||
'jad' => 'text/vnd.sun.j2me.app-descriptor',
|
||||
'wml' => 'text/vnd.wap.wml',
|
||||
'htc' => 'text/x-component',
|
||||
|
||||
// image
|
||||
'gif' => 'image/gif',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'jpg' => 'image/jpeg',
|
||||
'png' => 'image/png',
|
||||
'tif' => 'image/tiff',
|
||||
'tiff' => 'image/tiff',
|
||||
'wbmp' => 'image/vnd.wap.wbmp',
|
||||
'ico' => 'image/x-icon',
|
||||
'jng' => 'image/x-jng',
|
||||
'bmp' => 'image/x-ms-bmp',
|
||||
'svg' => 'image/svg+xml',
|
||||
'svgz' => 'image/svg+xml',
|
||||
'webp' => 'image/webp',
|
||||
'avif' => 'image/avif',
|
||||
|
||||
// application
|
||||
'js' => 'application/javascript',
|
||||
'atom' => 'application/atom+xml',
|
||||
'rss' => 'application/rss+xml',
|
||||
'wasm' => 'application/wasm',
|
||||
'jar' => 'application/java-archive',
|
||||
'war' => 'application/java-archive',
|
||||
'ear' => 'application/java-archive',
|
||||
'json' => 'application/json',
|
||||
'hqx' => 'application/mac-binhex40',
|
||||
'doc' => 'application/msword',
|
||||
'pdf' => 'application/pdf',
|
||||
'ps' => 'application/postscript',
|
||||
'eps' => 'application/postscript',
|
||||
'ai' => 'application/postscript',
|
||||
'rtf' => 'application/rtf',
|
||||
'm3u8' => 'application/vnd.apple.mpegurl',
|
||||
'xls' => 'application/vnd.ms-excel',
|
||||
'eot' => 'application/vnd.ms-fontobject',
|
||||
'ppt' => 'application/vnd.ms-powerpoint',
|
||||
'wmlc' => 'application/vnd.wap.wmlc',
|
||||
'kml' => 'application/vnd.google-earth.kml+xml',
|
||||
'kmz' => 'application/vnd.google-earth.kmz',
|
||||
'7z' => 'application/x-7z-compressed',
|
||||
'cco' => 'application/x-cocoa',
|
||||
'jardiff' => 'application/x-java-archive-diff',
|
||||
'jnlp' => 'application/x-java-jnlp-file',
|
||||
'run' => 'application/x-makeself',
|
||||
'pl' => 'application/x-perl',
|
||||
'pm' => 'application/x-perl',
|
||||
'prc' => 'application/x-pilot',
|
||||
'pdb' => 'application/x-pilot',
|
||||
'rar' => 'application/x-rar-compressed',
|
||||
'rpm' => 'application/x-redhat-package-manager',
|
||||
'sea' => 'application/x-sea',
|
||||
'swf' => 'application/x-shockwave-flash',
|
||||
'sit' => 'application/x-stuffit',
|
||||
'tcl' => 'application/x-tcl',
|
||||
'tk' => 'application/x-tcl',
|
||||
'der' => 'application/x-x509-ca-cert',
|
||||
'pem' => 'application/x-x509-ca-cert',
|
||||
'crt' => 'application/x-x509-ca-cert',
|
||||
'xpi' => 'application/x-xpinstall',
|
||||
'xhtml' => 'application/xhtml+xml',
|
||||
'xspf' => 'application/xspf+xml',
|
||||
'zip' => 'application/zip',
|
||||
'bin' => 'application/octet-stream',
|
||||
'exe' => 'application/octet-stream',
|
||||
'dll' => 'application/octet-stream',
|
||||
'deb' => 'application/octet-stream',
|
||||
'dmg' => 'application/octet-stream',
|
||||
'iso' => 'application/octet-stream',
|
||||
'img' => 'application/octet-stream',
|
||||
'msi' => 'application/octet-stream',
|
||||
'msp' => 'application/octet-stream',
|
||||
'msm' => 'application/octet-stream',
|
||||
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
|
||||
// audio
|
||||
'mid' => 'audio/midi',
|
||||
'midi' => 'audio/midi',
|
||||
'kar' => 'audio/midi',
|
||||
'mp3' => 'audio/mpeg',
|
||||
'ogg' => 'audio/ogg',
|
||||
'm4a' => 'audio/x-m4a',
|
||||
'ra' => 'audio/x-realaudio',
|
||||
|
||||
// video
|
||||
'3gpp' => 'video/3gpp',
|
||||
'3gp' => 'video/3gpp',
|
||||
'ts' => 'video/mp2t',
|
||||
'mp4' => 'video/mp4',
|
||||
'mpeg' => 'video/mpeg',
|
||||
'mpg' => 'video/mpeg',
|
||||
'mov' => 'video/quicktime',
|
||||
'webm' => 'video/webm',
|
||||
'flv' => 'video/x-flv',
|
||||
'm4v' => 'video/x-m4v',
|
||||
'mng' => 'video/x-mng',
|
||||
'asx' => 'video/x-ms-asf',
|
||||
'asf' => 'video/x-ms-asf',
|
||||
'wmv' => 'video/x-ms-wmv',
|
||||
'avi' => 'video/x-msvideo',
|
||||
|
||||
// font
|
||||
'ttf' => 'font/ttf',
|
||||
'woff' => 'font/woff',
|
||||
'woff2' => 'font/woff2',
|
||||
];
|
||||
|
||||
/**
|
||||
* Phrases.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*
|
||||
* @link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
|
||||
*/
|
||||
public const PHRASES = [
|
||||
100 => 'Continue',
|
||||
101 => 'Switching Protocols',
|
||||
102 => 'Processing', // WebDAV; RFC 2518
|
||||
103 => 'Early Hints', // RFC 8297
|
||||
|
||||
200 => 'OK',
|
||||
201 => 'Created',
|
||||
202 => 'Accepted',
|
||||
203 => 'Non-Authoritative Information', // since HTTP/1.1
|
||||
204 => 'No Content',
|
||||
205 => 'Reset Content',
|
||||
206 => 'Partial Content', // RFC 7233
|
||||
207 => 'Multi-Status', // WebDAV; RFC 4918
|
||||
208 => 'Already Reported', // WebDAV; RFC 5842
|
||||
226 => 'IM Used', // RFC 3229
|
||||
|
||||
300 => 'Multiple Choices',
|
||||
301 => 'Moved Permanently',
|
||||
302 => 'Found', // Previously "Moved temporarily"
|
||||
303 => 'See Other', // since HTTP/1.1
|
||||
304 => 'Not Modified', // RFC 7232
|
||||
305 => 'Use Proxy', // since HTTP/1.1
|
||||
306 => 'Switch Proxy',
|
||||
307 => 'Temporary Redirect', // since HTTP/1.1
|
||||
308 => 'Permanent Redirect', // RFC 7538
|
||||
|
||||
400 => 'Bad Request',
|
||||
401 => 'Unauthorized', // RFC 7235
|
||||
402 => 'Payment Required',
|
||||
403 => 'Forbidden',
|
||||
404 => 'Not Found',
|
||||
405 => 'Method Not Allowed',
|
||||
406 => 'Not Acceptable',
|
||||
407 => 'Proxy Authentication Required', // RFC 7235
|
||||
408 => 'Request Timeout',
|
||||
409 => 'Conflict',
|
||||
410 => 'Gone',
|
||||
411 => 'Length Required',
|
||||
412 => 'Precondition Failed', // RFC 7232
|
||||
413 => 'Payload Too Large', // RFC 7231
|
||||
414 => 'URI Too Long', // RFC 7231
|
||||
415 => 'Unsupported Media Type', // RFC 7231
|
||||
416 => 'Range Not Satisfiable', // RFC 7233
|
||||
417 => 'Expectation Failed',
|
||||
418 => 'I\'m a teapot', // RFC 2324, RFC 7168
|
||||
421 => 'Misdirected Request', // RFC 7540
|
||||
422 => 'Unprocessable Entity', // WebDAV; RFC 4918
|
||||
423 => 'Locked', // WebDAV; RFC 4918
|
||||
424 => 'Failed Dependency', // WebDAV; RFC 4918
|
||||
425 => 'Too Early', // RFC 8470
|
||||
426 => 'Upgrade Required',
|
||||
428 => 'Precondition Required', // RFC 6585
|
||||
429 => 'Too Many Requests', // RFC 6585
|
||||
431 => 'Request Header Fields Too Large', // RFC 6585
|
||||
451 => 'Unavailable For Legal Reasons', // RFC 7725
|
||||
|
||||
500 => 'Internal Server Error',
|
||||
501 => 'Not Implemented',
|
||||
502 => 'Bad Gateway',
|
||||
503 => 'Service Unavailable',
|
||||
504 => 'Gateway Timeout',
|
||||
505 => 'HTTP Version Not Supported',
|
||||
506 => 'Variant Also Negotiates', // RFC 2295
|
||||
507 => 'Insufficient Storage', // WebDAV; RFC 4918
|
||||
508 => 'Loop Detected', // WebDAV; RFC 5842
|
||||
510 => 'Not Extended', // RFC 2774
|
||||
511 => 'Network Authentication Required', // RFC 6585
|
||||
];
|
||||
|
||||
/**
|
||||
* Response constructor.
|
||||
*
|
||||
* @param int $status
|
||||
* @param array $headers
|
||||
* @param string $body
|
||||
*/
|
||||
public function __construct(
|
||||
protected int $status = 200,
|
||||
protected array $headers = [],
|
||||
protected string $body = ''
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Set header.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $value
|
||||
* @return $this
|
||||
*/
|
||||
public function header(string $name, string $value): static
|
||||
{
|
||||
$this->headers[$name] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set header.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $value
|
||||
* @return $this
|
||||
*/
|
||||
public function withHeader(string $name, string $value): static
|
||||
{
|
||||
return $this->header($name, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set headers.
|
||||
*
|
||||
* @param array $headers
|
||||
* @return $this
|
||||
*/
|
||||
public function withHeaders(array $headers): static
|
||||
{
|
||||
$this->headers = array_merge_recursive($this->headers, $headers);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove header.
|
||||
*
|
||||
* @param string $name
|
||||
* @return $this
|
||||
*/
|
||||
public function withoutHeader(string $name): static
|
||||
{
|
||||
unset($this->headers[$name]);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get header.
|
||||
*
|
||||
* @param string $name
|
||||
* @return null|array|string
|
||||
*/
|
||||
public function getHeader(string $name): array|string|null
|
||||
{
|
||||
return $this->headers[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get headers.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Mime Type from extension.
|
||||
*
|
||||
* @param string $ext
|
||||
* @return string
|
||||
*/
|
||||
public function getMimeType(string $ext): string
|
||||
{
|
||||
return self::$mimeTypeMap[$ext] ?? 'application/octet-stream';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set status.
|
||||
*
|
||||
* @param int $code
|
||||
* @param string|null $reasonPhrase
|
||||
* @return $this
|
||||
*/
|
||||
public function withStatus(int $code, ?string $reasonPhrase = null): static
|
||||
{
|
||||
$this->status = $code;
|
||||
$this->reason = $reasonPhrase !== null ? str_replace(["\r", "\n"], '', $reasonPhrase) : null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status code.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getStatusCode(): int
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reason phrase.
|
||||
*
|
||||
* @return ?string
|
||||
*/
|
||||
public function getReasonPhrase(): ?string
|
||||
{
|
||||
return $this->reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set protocol version.
|
||||
*
|
||||
* @param string $version
|
||||
* @return $this
|
||||
*/
|
||||
public function withProtocolVersion(string $version): static
|
||||
{
|
||||
$this->version = str_replace(["\r", "\n"], '', $version);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set http body.
|
||||
*
|
||||
* @param string $body
|
||||
* @return $this
|
||||
*/
|
||||
public function withBody(string $body): static
|
||||
{
|
||||
$this->body = $body;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get http raw body.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function rawBody(): string
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send file.
|
||||
*
|
||||
* @param string $file
|
||||
* @param int $offset
|
||||
* @param int $length
|
||||
* @return $this
|
||||
*/
|
||||
public function withFile(string $file, int $offset = 0, int $length = 0): static
|
||||
{
|
||||
if (!is_file($file)) {
|
||||
return $this->withStatus(404)->withBody('<h3>404 Not Found</h3>');
|
||||
}
|
||||
$this->file = ['file' => $file, 'offset' => $offset, 'length' => $length];
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cookie.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $value
|
||||
* @param int|null $maxAge
|
||||
* @param string $path
|
||||
* @param string $domain
|
||||
* @param bool $secure
|
||||
* @param bool $httpOnly
|
||||
* @param string $sameSite
|
||||
* @return $this
|
||||
*/
|
||||
public function cookie(string $name, string $value = '', ?int $maxAge = null, string $path = '', string $domain = '', bool $secure = false, bool $httpOnly = false, string $sameSite = ''): static
|
||||
{
|
||||
$this->headers['Set-Cookie'][] = $name . '=' . rawurlencode($value)
|
||||
. (empty($domain) ? '' : '; Domain=' . $domain)
|
||||
. ($maxAge === null ? '' : '; Max-Age=' . $maxAge)
|
||||
. (empty($path) ? '' : '; Path=' . $path)
|
||||
. (!$secure ? '' : '; Secure')
|
||||
. (!$httpOnly ? '' : '; HttpOnly')
|
||||
. (empty($sameSite) ? '' : '; SameSite=' . $sameSite);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create header for file.
|
||||
*
|
||||
* @param array $fileInfo
|
||||
* @return string
|
||||
*/
|
||||
protected function createHeadForFile(array $fileInfo): string
|
||||
{
|
||||
$file = $fileInfo['file'];
|
||||
$reason = $this->reason ?: self::PHRASES[$this->status];
|
||||
$head = "HTTP/$this->version $this->status $reason\r\n";
|
||||
$headers = $this->headers;
|
||||
foreach ($headers as $name => $value) {
|
||||
// Skip unsafe header names
|
||||
if (strpbrk((string)$name, ":\r\n") !== false) {
|
||||
continue;
|
||||
}
|
||||
if (is_array($value)) {
|
||||
foreach ($value as $item) {
|
||||
// Skip unsafe header values
|
||||
if (strpbrk((string)$item, "\r\n") !== false) {
|
||||
continue;
|
||||
}
|
||||
$head .= "$name: $item\r\n";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Skip unsafe header values
|
||||
if (strpbrk((string)$value, "\r\n") !== false) {
|
||||
continue;
|
||||
}
|
||||
$head .= "$name: $value\r\n";
|
||||
}
|
||||
|
||||
if (!isset($headers['Connection'])) {
|
||||
$head .= "Connection: keep-alive\r\n";
|
||||
}
|
||||
|
||||
$fileInfo = pathinfo($file);
|
||||
$extension = $fileInfo['extension'] ?? '';
|
||||
$baseName = $fileInfo['basename'] ?: 'unknown';
|
||||
// Remove ASCII control characters (0x00-0x1F, 0x7F) and unsafe quotes/backslashes to avoid breaking header formatting
|
||||
$baseName = preg_replace('/["\\\\\x00-\x1F\x7F]/', '', $baseName);
|
||||
if ($baseName === '') {
|
||||
$baseName = 'unknown';
|
||||
}
|
||||
$mime = '';
|
||||
if (!isset($headers['Content-Type'])) {
|
||||
$mime = $this->getMimeType($extension);
|
||||
$head .= "Content-Type: " . $mime . "\r\n";
|
||||
}
|
||||
|
||||
if (!isset($headers['Content-Disposition']) && $mime === 'application/octet-stream') {
|
||||
$head .= "Content-Disposition: attachment; filename=\"$baseName\"\r\n";
|
||||
}
|
||||
|
||||
if (!isset($headers['Last-Modified']) && $mtime = filemtime($file)) {
|
||||
$head .= 'Last-Modified: ' . gmdate('D, d M Y H:i:s', $mtime) . ' GMT' . "\r\n";
|
||||
}
|
||||
|
||||
return "$head\r\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* __toString.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
if ($this->file) {
|
||||
return $this->createHeadForFile($this->file);
|
||||
}
|
||||
|
||||
$reason = $this->reason ?: self::PHRASES[$this->status] ?? '';
|
||||
$bodyLen = strlen($this->body);
|
||||
if (empty($this->headers)) {
|
||||
return "HTTP/$this->version $this->status $reason\r\nContent-Type: text/html;charset=utf-8\r\nContent-Length: $bodyLen\r\nConnection: keep-alive\r\n\r\n$this->body";
|
||||
}
|
||||
|
||||
$head = "HTTP/$this->version $this->status $reason\r\n";
|
||||
$headers = $this->headers;
|
||||
foreach ($headers as $name => $value) {
|
||||
// Skip unsafe header names
|
||||
if (strpbrk((string)$name, ":\r\n") !== false) {
|
||||
continue;
|
||||
}
|
||||
if (is_array($value)) {
|
||||
foreach ($value as $item) {
|
||||
// Skip unsafe header values
|
||||
if (strpbrk((string)$item, "\r\n") !== false) {
|
||||
continue;
|
||||
}
|
||||
$head .= "$name: $item\r\n";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Skip unsafe header values
|
||||
if (strpbrk((string)$value, "\r\n") !== false) {
|
||||
continue;
|
||||
}
|
||||
$head .= "$name: $value\r\n";
|
||||
}
|
||||
|
||||
if (!isset($headers['Connection'])) {
|
||||
$head .= "Connection: keep-alive\r\n";
|
||||
}
|
||||
|
||||
if (!isset($headers['Content-Type'])) {
|
||||
$head .= "Content-Type: text/html;charset=utf-8\r\n";
|
||||
} else if ($headers['Content-Type'] === 'text/event-stream') {
|
||||
// For Server-Sent Events, send headers once and keep the connection open.
|
||||
// Headers must be terminated by an empty line; ignore any preset body to avoid
|
||||
// polluting the event stream with extra bytes or OS-specific newlines.
|
||||
return $head . "\r\n";
|
||||
}
|
||||
|
||||
if (!isset($headers['Transfer-Encoding'])) {
|
||||
$head .= "Content-Length: $bodyLen\r\n\r\n";
|
||||
} else {
|
||||
return $bodyLen ? "$head\r\n" . dechex($bodyLen) . "\r\n$this->body\r\n" : "$head\r\n";
|
||||
}
|
||||
|
||||
// The whole http package
|
||||
return $head . $this->body;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Protocols\Http;
|
||||
|
||||
use Stringable;
|
||||
|
||||
use function str_replace;
|
||||
|
||||
/**
|
||||
* Class ServerSentEvents
|
||||
* @package Workerman\Protocols\Http
|
||||
*/
|
||||
class ServerSentEvents implements Stringable
|
||||
{
|
||||
/**
|
||||
* ServerSentEvents constructor.
|
||||
* $data for example ['event'=>'ping', 'data' => 'some thing', 'id' => 1000, 'retry' => 5000]
|
||||
*/
|
||||
public function __construct(protected array $data) {}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
$buffer = '';
|
||||
$data = $this->data;
|
||||
if (isset($data[''])) {
|
||||
$buffer = ": {$data['']}\n";
|
||||
}
|
||||
if (isset($data['event'])) {
|
||||
$buffer .= "event: {$data['event']}\n";
|
||||
}
|
||||
if (isset($data['id'])) {
|
||||
$buffer .= "id: {$data['id']}\n";
|
||||
}
|
||||
if (isset($data['retry'])) {
|
||||
$buffer .= "retry: {$data['retry']}\n";
|
||||
}
|
||||
if (isset($data['data'])) {
|
||||
$buffer .= 'data: ' . str_replace("\n", "\ndata: ", $data['data']) . "\n";
|
||||
}
|
||||
return "$buffer\n";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Protocols\Http;
|
||||
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
use Workerman\Protocols\Http\Session\FileSessionHandler;
|
||||
use Workerman\Protocols\Http\Session\SessionHandlerInterface;
|
||||
use function array_key_exists;
|
||||
use function ini_get;
|
||||
use function is_array;
|
||||
use function is_scalar;
|
||||
use function random_int;
|
||||
use function session_get_cookie_params;
|
||||
|
||||
/**
|
||||
* Class Session
|
||||
* @package Workerman\Protocols\Http
|
||||
*/
|
||||
class Session
|
||||
{
|
||||
/**
|
||||
* Session andler class which implements SessionHandlerInterface.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static string $handlerClass = FileSessionHandler::class;
|
||||
|
||||
/**
|
||||
* Parameters of __constructor for session handler class.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
protected static mixed $handlerConfig = null;
|
||||
|
||||
/**
|
||||
* Session name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static string $name = 'PHPSID';
|
||||
|
||||
/**
|
||||
* Auto update timestamp.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public static bool $autoUpdateTimestamp = false;
|
||||
|
||||
/**
|
||||
* Session lifetime.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public static int $lifetime = 1440;
|
||||
|
||||
/**
|
||||
* Cookie lifetime.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public static int $cookieLifetime = 1440;
|
||||
|
||||
/**
|
||||
* Session cookie path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static string $cookiePath = '/';
|
||||
|
||||
/**
|
||||
* Session cookie domain.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static string $domain = '';
|
||||
|
||||
/**
|
||||
* HTTPS only cookies.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public static bool $secure = false;
|
||||
|
||||
/**
|
||||
* HTTP access only.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public static bool $httpOnly = true;
|
||||
|
||||
/**
|
||||
* Same-site cookies.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static string $sameSite = '';
|
||||
|
||||
/**
|
||||
* Gc probability.
|
||||
*
|
||||
* @var int[]
|
||||
*/
|
||||
public static array $gcProbability = [1, 20000];
|
||||
|
||||
/**
|
||||
* Session handler instance.
|
||||
*
|
||||
* @var ?SessionHandlerInterface
|
||||
*/
|
||||
protected static ?SessionHandlerInterface $handler = null;
|
||||
|
||||
/**
|
||||
* Session data.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected mixed $data = [];
|
||||
|
||||
/**
|
||||
* Session changed and need to save.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $needSave = false;
|
||||
|
||||
/**
|
||||
* Session id.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $sessionId;
|
||||
|
||||
/**
|
||||
* Is safe.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $isSafe = true;
|
||||
|
||||
/**
|
||||
* Session serialize_handler
|
||||
* @var array|string[]
|
||||
*/
|
||||
protected array $serializer = ['serialize', 'unserialize'];
|
||||
|
||||
/**
|
||||
* Session constructor.
|
||||
*
|
||||
* @param string $sessionId
|
||||
*/
|
||||
public function __construct(string $sessionId)
|
||||
{
|
||||
if (extension_loaded('igbinary') && ini_get('session.serialize_handler') == 'igbinary') {
|
||||
$this->serializer = ['igbinary_serialize', 'igbinary_unserialize'];
|
||||
}
|
||||
if (static::$handler === null) {
|
||||
static::initHandler();
|
||||
}
|
||||
$this->sessionId = $sessionId;
|
||||
if ($data = static::$handler->read($sessionId)) {
|
||||
$this->data = $this->safeDeserialize($data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session id.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session.
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function get(string $name, mixed $default = null): mixed
|
||||
{
|
||||
return $this->data[$name] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store data in the session.
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function set(string $name, mixed $value): void
|
||||
{
|
||||
$this->data[$name] = $value;
|
||||
$this->needSave = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an item from the session.
|
||||
*
|
||||
* @param string $name
|
||||
*/
|
||||
public function delete(string $name): void
|
||||
{
|
||||
unset($this->data[$name]);
|
||||
$this->needSave = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve and delete an item from the session.
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function pull(string $name, mixed $default = null): mixed
|
||||
{
|
||||
$value = $this->get($name, $default);
|
||||
$this->delete($name);
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store data in the session.
|
||||
*
|
||||
* @param array|string $key
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function put(array|string $key, mixed $value = null): void
|
||||
{
|
||||
if (!is_array($key)) {
|
||||
$this->set($key, $value);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($key as $k => $v) {
|
||||
$this->data[$k] = $v;
|
||||
}
|
||||
$this->needSave = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a piece of data from the session.
|
||||
*
|
||||
* @param array|string $name
|
||||
*/
|
||||
public function forget(array|string $name): void
|
||||
{
|
||||
if (is_scalar($name)) {
|
||||
$this->delete($name);
|
||||
return;
|
||||
}
|
||||
foreach ($name as $key) {
|
||||
unset($this->data[$key]);
|
||||
}
|
||||
$this->needSave = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all the data in the session.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all data from the session.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function flush(): void
|
||||
{
|
||||
$this->needSave = true;
|
||||
$this->data = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determining If An Item Exists In The Session.
|
||||
*
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function has(string $name): bool
|
||||
{
|
||||
return isset($this->data[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* To determine if an item is present in the session, even if its value is null.
|
||||
*
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function exists(string $name): bool
|
||||
{
|
||||
return array_key_exists($name, $this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save session to store.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function save(): void
|
||||
{
|
||||
if ($this->needSave) {
|
||||
if (empty($this->data)) {
|
||||
static::$handler->destroy($this->sessionId);
|
||||
} else {
|
||||
static::$handler->write($this->sessionId, $this->serializer[0]($this->data));
|
||||
}
|
||||
} elseif (static::$autoUpdateTimestamp) {
|
||||
$this->refresh();
|
||||
}
|
||||
$this->needSave = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh session expire time.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function refresh(): bool
|
||||
{
|
||||
return static::$handler->updateTimestamp($this->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Init.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init(): void
|
||||
{
|
||||
if (($gcProbability = (int)ini_get('session.gc_probability')) && ($gcDivisor = (int)ini_get('session.gc_divisor'))) {
|
||||
static::$gcProbability = [$gcProbability, $gcDivisor];
|
||||
}
|
||||
|
||||
if ($gcMaxLifeTime = ini_get('session.gc_maxlifetime')) {
|
||||
self::$lifetime = (int)$gcMaxLifeTime;
|
||||
}
|
||||
|
||||
$sessionCookieParams = session_get_cookie_params();
|
||||
static::$cookieLifetime = $sessionCookieParams['lifetime'];
|
||||
static::$cookiePath = $sessionCookieParams['path'];
|
||||
static::$domain = $sessionCookieParams['domain'];
|
||||
static::$secure = $sessionCookieParams['secure'];
|
||||
static::$httpOnly = $sessionCookieParams['httponly'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set session handler class.
|
||||
*
|
||||
* @param mixed $className
|
||||
* @param mixed $config
|
||||
* @return string
|
||||
*/
|
||||
public static function handlerClass(mixed $className = null, mixed $config = null): string
|
||||
{
|
||||
if ($className) {
|
||||
static::$handlerClass = $className;
|
||||
}
|
||||
if ($config) {
|
||||
static::$handlerConfig = $config;
|
||||
}
|
||||
return static::$handlerClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cookie params.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getCookieParams(): array
|
||||
{
|
||||
return [
|
||||
'lifetime' => static::$cookieLifetime,
|
||||
'path' => static::$cookiePath,
|
||||
'domain' => static::$domain,
|
||||
'secure' => static::$secure,
|
||||
'httponly' => static::$httpOnly,
|
||||
'samesite' => static::$sameSite,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Init handler.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected static function initHandler(): void
|
||||
{
|
||||
if (static::$handlerConfig === null) {
|
||||
static::$handler = new static::$handlerClass();
|
||||
} else {
|
||||
static::$handler = new static::$handlerClass(static::$handlerConfig);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely deserialize session data, preventing object instantiation.
|
||||
*
|
||||
* @param string $data
|
||||
* @return array
|
||||
*/
|
||||
protected function safeDeserialize(string $data): array
|
||||
{
|
||||
if ($this->serializer[1] === 'unserialize') {
|
||||
$result = unserialize($data, ['allowed_classes' => false]);
|
||||
} else {
|
||||
$result = ($this->serializer[1])($data);
|
||||
}
|
||||
return is_array($result) ? $result : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* GC sessions.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function gc(): void
|
||||
{
|
||||
static::$handler->gc(static::$lifetime);
|
||||
}
|
||||
|
||||
/**
|
||||
* __unserialize.
|
||||
*
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
$this->isSafe = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* __destruct.
|
||||
*
|
||||
* @return void
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if (!$this->isSafe) {
|
||||
return;
|
||||
}
|
||||
$this->save();
|
||||
if (random_int(1, static::$gcProbability[1]) <= static::$gcProbability[0]) {
|
||||
$this->gc();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Init session.
|
||||
Session::init();
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Protocols\Http\Session;
|
||||
|
||||
use Exception;
|
||||
use Workerman\Protocols\Http\Session;
|
||||
use function clearstatcache;
|
||||
use function file_get_contents;
|
||||
use function file_put_contents;
|
||||
use function filemtime;
|
||||
use function glob;
|
||||
use function is_dir;
|
||||
use function is_file;
|
||||
use function mkdir;
|
||||
use function rename;
|
||||
use function session_save_path;
|
||||
use function strlen;
|
||||
use function sys_get_temp_dir;
|
||||
use function time;
|
||||
use function touch;
|
||||
use function unlink;
|
||||
|
||||
/**
|
||||
* Class FileSessionHandler
|
||||
* @package Workerman\Protocols\Http\Session
|
||||
*/
|
||||
class FileSessionHandler implements SessionHandlerInterface
|
||||
{
|
||||
/**
|
||||
* Session save path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static string $sessionSavePath;
|
||||
|
||||
/**
|
||||
* Session file prefix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static string $sessionFilePrefix = 'session_';
|
||||
|
||||
/**
|
||||
* Init.
|
||||
*/
|
||||
public static function init()
|
||||
{
|
||||
$savePath = @session_save_path();
|
||||
if (!$savePath || str_starts_with($savePath, 'tcp://')) {
|
||||
$savePath = sys_get_temp_dir();
|
||||
}
|
||||
static::sessionSavePath($savePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* FileSessionHandler constructor.
|
||||
* @param array $config
|
||||
*/
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
if (isset($config['save_path'])) {
|
||||
static::sessionSavePath($config['save_path']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function open(string $savePath, string $name): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function read(string $sessionId): string|false
|
||||
{
|
||||
$sessionFile = static::sessionFile($sessionId);
|
||||
clearstatcache();
|
||||
if (is_file($sessionFile)) {
|
||||
if (time() - filemtime($sessionFile) > Session::$lifetime) {
|
||||
unlink($sessionFile);
|
||||
return false;
|
||||
}
|
||||
$data = file_get_contents($sessionFile);
|
||||
return $data ?: false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @throws Exception
|
||||
*/
|
||||
public function write(string $sessionId, string $sessionData): bool
|
||||
{
|
||||
$tempFile = static::$sessionSavePath . uniqid(bin2hex(random_bytes(8)), true);
|
||||
if (!file_put_contents($tempFile, $sessionData)) {
|
||||
return false;
|
||||
}
|
||||
return rename($tempFile, static::sessionFile($sessionId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update session modify time.
|
||||
*
|
||||
* @see https://www.php.net/manual/en/class.sessionupdatetimestamphandlerinterface.php
|
||||
* @see https://www.php.net/manual/zh/function.touch.php
|
||||
*
|
||||
* @param string $sessionId Session id.
|
||||
* @param string $data Session Data.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function updateTimestamp(string $sessionId, string $data = ""): bool
|
||||
{
|
||||
$sessionFile = static::sessionFile($sessionId);
|
||||
if (!file_exists($sessionFile)) {
|
||||
return false;
|
||||
}
|
||||
// set file modify time to current time
|
||||
$setModifyTime = touch($sessionFile);
|
||||
// clear file stat cache
|
||||
clearstatcache();
|
||||
return $setModifyTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function close(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function destroy(string $sessionId): bool
|
||||
{
|
||||
$sessionFile = static::sessionFile($sessionId);
|
||||
if (is_file($sessionFile)) {
|
||||
unlink($sessionFile);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function gc(int $maxLifetime): bool
|
||||
{
|
||||
$timeNow = time();
|
||||
foreach (glob(static::$sessionSavePath . static::$sessionFilePrefix . '*') as $file) {
|
||||
if (is_file($file) && $timeNow - filemtime($file) > $maxLifetime) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session file path.
|
||||
*
|
||||
* @param string $sessionId
|
||||
* @return string
|
||||
*/
|
||||
protected static function sessionFile(string $sessionId): string
|
||||
{
|
||||
return static::$sessionSavePath . static::$sessionFilePrefix . $sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or set session file path.
|
||||
*
|
||||
* @param string $path
|
||||
* @return string
|
||||
*/
|
||||
public static function sessionSavePath(string $path): string
|
||||
{
|
||||
if ($path) {
|
||||
if ($path[strlen($path) - 1] !== DIRECTORY_SEPARATOR) {
|
||||
$path .= DIRECTORY_SEPARATOR;
|
||||
}
|
||||
static::$sessionSavePath = $path;
|
||||
if (!is_dir($path)) {
|
||||
mkdir($path, 0777, true);
|
||||
}
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
FileSessionHandler::init();
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Protocols\Http\Session;
|
||||
|
||||
use Redis;
|
||||
use RedisCluster;
|
||||
use RedisClusterException;
|
||||
use RedisException;
|
||||
|
||||
class RedisClusterSessionHandler extends RedisSessionHandler
|
||||
{
|
||||
/**
|
||||
* @param array $config
|
||||
* @throws RedisClusterException
|
||||
* @throws RedisException
|
||||
*/
|
||||
public function __construct(array $config)
|
||||
{
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create redis connection.
|
||||
* @param array $config
|
||||
* @return Redis|RedisCluster
|
||||
* @throws RedisClusterException
|
||||
*/
|
||||
protected function createRedisConnection(array $config): Redis|RedisCluster
|
||||
{
|
||||
$timeout = $config['timeout'] ?? 2;
|
||||
$readTimeout = $config['read_timeout'] ?? $timeout;
|
||||
$persistent = $config['persistent'] ?? false;
|
||||
$auth = $config['auth'] ?? '';
|
||||
$args = [null, $config['host'], $timeout, $readTimeout, $persistent];
|
||||
if ($auth) {
|
||||
$args[] = $auth;
|
||||
}
|
||||
$redis = new RedisCluster(...$args);
|
||||
if (empty($config['prefix'])) {
|
||||
$config['prefix'] = 'redis_session_';
|
||||
}
|
||||
$redis->setOption(Redis::OPT_PREFIX, $config['prefix']);
|
||||
return $redis;
|
||||
}
|
||||
}
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Protocols\Http\Session;
|
||||
|
||||
use Redis;
|
||||
use RedisCluster;
|
||||
use RedisException;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
use Workerman\Coroutine\Utils\DestructionWatcher;
|
||||
use Workerman\Events\Fiber;
|
||||
use Workerman\Protocols\Http\Session;
|
||||
use Workerman\Timer;
|
||||
use Workerman\Coroutine\Pool;
|
||||
use Workerman\Coroutine\Context;
|
||||
use Workerman\Worker;
|
||||
|
||||
/**
|
||||
* Class RedisSessionHandler
|
||||
* @package Workerman\Protocols\Http\Session
|
||||
*/
|
||||
class RedisSessionHandler implements SessionHandlerInterface
|
||||
{
|
||||
/**
|
||||
* @var Redis|RedisCluster
|
||||
*/
|
||||
protected Redis|RedisCluster|null $connection = null;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $config;
|
||||
|
||||
/**
|
||||
* @var Pool|null
|
||||
*/
|
||||
protected static ?Pool $pool = null;
|
||||
|
||||
/**
|
||||
* RedisSessionHandler constructor.
|
||||
* @param array $config = [
|
||||
* 'host' => '127.0.0.1',
|
||||
* 'port' => 6379,
|
||||
* 'timeout' => 2,
|
||||
* 'auth' => '******',
|
||||
* 'database' => 2,
|
||||
* 'prefix' => 'redis_session_',
|
||||
* 'ping' => 55,
|
||||
* ]
|
||||
* @throws RedisException
|
||||
*/
|
||||
public function __construct(array $config)
|
||||
{
|
||||
if (false === extension_loaded('redis')) {
|
||||
throw new RuntimeException('Please install redis extension.');
|
||||
}
|
||||
|
||||
$config['timeout'] ??= 2;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection.
|
||||
* @return Redis
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function connection(): Redis|RedisCluster
|
||||
{
|
||||
// Cannot switch fibers in current execution context when PHP < 8.4
|
||||
if (Worker::$eventLoopClass === Fiber::class && PHP_VERSION_ID < 80400) {
|
||||
if (!$this->connection) {
|
||||
$this->connection = $this->createRedisConnection($this->config);
|
||||
Timer::delay($this->config['pool']['heartbeat_interval'] ?? 55, function () {
|
||||
$this->connection->ping();
|
||||
});
|
||||
}
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
$key = 'session.redis.connection';
|
||||
/** @var Redis|null $connection */
|
||||
$connection = Context::get($key);
|
||||
if (!$connection) {
|
||||
if (!static::$pool) {
|
||||
$poolConfig = $this->config['pool'] ?? [];
|
||||
static::$pool = new Pool($poolConfig['max_connections'] ?? 10, $poolConfig);
|
||||
static::$pool->setConnectionCreator(function () {
|
||||
return $this->createRedisConnection($this->config);
|
||||
});
|
||||
static::$pool->setConnectionCloser(function (Redis|RedisCluster $connection) {
|
||||
$connection->close();
|
||||
});
|
||||
static::$pool->setHeartbeatChecker(function (Redis|RedisCluster $connection) {
|
||||
$connection->ping();
|
||||
});
|
||||
}
|
||||
try {
|
||||
$connection = static::$pool->get();
|
||||
Context::set($key, $connection);
|
||||
} finally {
|
||||
$closure = function () use ($connection) {
|
||||
try {
|
||||
$connection && static::$pool && static::$pool->put($connection);
|
||||
} catch (Throwable) {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
$obj = Context::get('context.onDestroy');
|
||||
if (!$obj) {
|
||||
$obj = new \stdClass();
|
||||
Context::set('context.onDestroy', $obj);
|
||||
}
|
||||
DestructionWatcher::watch($obj, $closure);
|
||||
}
|
||||
}
|
||||
return $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create redis connection.
|
||||
* @param array $config
|
||||
* @return Redis
|
||||
*/
|
||||
protected function createRedisConnection(array $config): Redis|RedisCluster
|
||||
{
|
||||
$redis = new Redis();
|
||||
if (false === $redis->connect($config['host'], $config['port'], $config['timeout'])) {
|
||||
throw new RuntimeException("Redis connect {$config['host']}:{$config['port']} fail.");
|
||||
}
|
||||
if (!empty($config['auth'])) {
|
||||
$redis->auth($config['auth']);
|
||||
}
|
||||
if (!empty($config['database'])) {
|
||||
$redis->select((int)$config['database']);
|
||||
}
|
||||
if (empty($config['prefix'])) {
|
||||
$config['prefix'] = 'redis_session_';
|
||||
}
|
||||
$redis->setOption(Redis::OPT_PREFIX, $config['prefix']);
|
||||
return $redis;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function open(string $savePath, string $name): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @param string $sessionId
|
||||
* @return string|false
|
||||
* @throws RedisException
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function read(string $sessionId): string|false
|
||||
{
|
||||
return $this->connection()->get($sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @throws RedisException
|
||||
*/
|
||||
public function write(string $sessionId, string $sessionData): bool
|
||||
{
|
||||
return true === $this->connection()->setex($sessionId, Session::$lifetime, $sessionData);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @throws RedisException
|
||||
*/
|
||||
public function updateTimestamp(string $sessionId, string $data = ""): bool
|
||||
{
|
||||
return true === $this->connection()->expire($sessionId, Session::$lifetime);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @throws RedisException
|
||||
*/
|
||||
public function destroy(string $sessionId): bool
|
||||
{
|
||||
$this->connection()->del($sessionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function close(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function gc(int $maxLifetime): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Protocols\Http\Session;
|
||||
|
||||
interface SessionHandlerInterface
|
||||
{
|
||||
/**
|
||||
* Close the session
|
||||
* @link http://php.net/manual/en/sessionhandlerinterface.close.php
|
||||
* @return bool <p>
|
||||
* The return value (usually TRUE on success, FALSE on failure).
|
||||
* Note this value is returned internally to PHP for processing.
|
||||
* </p>
|
||||
* @since 5.4.0
|
||||
*/
|
||||
public function close(): bool;
|
||||
|
||||
/**
|
||||
* Destroy a session
|
||||
* @link http://php.net/manual/en/sessionhandlerinterface.destroy.php
|
||||
* @param string $sessionId The session ID being destroyed.
|
||||
* @return bool <p>
|
||||
* The return value (usually TRUE on success, FALSE on failure).
|
||||
* Note this value is returned internally to PHP for processing.
|
||||
* </p>
|
||||
* @since 5.4.0
|
||||
*/
|
||||
public function destroy(string $sessionId): bool;
|
||||
|
||||
/**
|
||||
* Cleanup old sessions
|
||||
* @link http://php.net/manual/en/sessionhandlerinterface.gc.php
|
||||
* @param int $maxLifetime <p>
|
||||
* Sessions that have not updated for
|
||||
* the last maxlifetime seconds will be removed.
|
||||
* </p>
|
||||
* @return bool <p>
|
||||
* The return value (usually TRUE on success, FALSE on failure).
|
||||
* Note this value is returned internally to PHP for processing.
|
||||
* </p>
|
||||
* @since 5.4.0
|
||||
*/
|
||||
public function gc(int $maxLifetime): bool;
|
||||
|
||||
/**
|
||||
* Initialize session
|
||||
* @link http://php.net/manual/en/sessionhandlerinterface.open.php
|
||||
* @param string $savePath The path where to store/retrieve the session.
|
||||
* @param string $name The session name.
|
||||
* @return bool <p>
|
||||
* The return value (usually TRUE on success, FALSE on failure).
|
||||
* Note this value is returned internally to PHP for processing.
|
||||
* </p>
|
||||
* @since 5.4.0
|
||||
*/
|
||||
public function open(string $savePath, string $name): bool;
|
||||
|
||||
|
||||
/**
|
||||
* Read session data
|
||||
* @link http://php.net/manual/en/sessionhandlerinterface.read.php
|
||||
* @param string $sessionId The session id to read data for.
|
||||
* @return string|false <p>
|
||||
* Returns an encoded string of the read data.
|
||||
* If nothing was read, it must return false.
|
||||
* Note this value is returned internally to PHP for processing.
|
||||
* </p>
|
||||
* @since 5.4.0
|
||||
*/
|
||||
public function read(string $sessionId): string|false;
|
||||
|
||||
/**
|
||||
* Write session data
|
||||
* @link http://php.net/manual/en/sessionhandlerinterface.write.php
|
||||
* @param string $sessionId The session id.
|
||||
* @param string $sessionData <p>
|
||||
* The encoded session data. This data is the
|
||||
* result of the PHP internally encoding
|
||||
* the $SESSION superglobal to a serialized
|
||||
* string and passing it as this parameter.
|
||||
* Please note sessions use an alternative serialization method.
|
||||
* </p>
|
||||
* @return bool <p>
|
||||
* The return value (usually TRUE on success, FALSE on failure).
|
||||
* Note this value is returned internally to PHP for processing.
|
||||
* </p>
|
||||
* @since 5.4.0
|
||||
*/
|
||||
public function write(string $sessionId, string $sessionData): bool;
|
||||
|
||||
/**
|
||||
* Update session modify time.
|
||||
*
|
||||
* @see https://www.php.net/manual/en/class.sessionupdatetimestamphandlerinterface.php
|
||||
*
|
||||
* @param string $sessionId
|
||||
* @param string $data Session Data.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function updateTimestamp(string $sessionId, string $data = ""): bool;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Protocols;
|
||||
|
||||
use Workerman\Connection\ConnectionInterface;
|
||||
|
||||
/**
|
||||
* Protocol interface
|
||||
*/
|
||||
interface ProtocolInterface
|
||||
{
|
||||
/**
|
||||
* Check the integrity of the package.
|
||||
* Please return the length of package.
|
||||
* If length is unknown please return 0 that means waiting for more data.
|
||||
* If the package has something wrong please return -1 the connection will be closed.
|
||||
*
|
||||
* @param string $buffer
|
||||
* @param ConnectionInterface $connection
|
||||
* @return int
|
||||
*/
|
||||
public static function input(string $buffer, ConnectionInterface $connection): int;
|
||||
|
||||
/**
|
||||
* Decode package and emit onMessage($message) callback, $message is the result that decode returned.
|
||||
*
|
||||
* @param string $buffer
|
||||
* @param ConnectionInterface $connection
|
||||
* @return mixed
|
||||
*/
|
||||
public static function decode(string $buffer, ConnectionInterface $connection): mixed;
|
||||
|
||||
/**
|
||||
* Encode package before sending to client.
|
||||
*
|
||||
* @param mixed $data
|
||||
* @param ConnectionInterface $connection
|
||||
* @return string
|
||||
*/
|
||||
public static function encode(mixed $data, ConnectionInterface $connection): string;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Protocols;
|
||||
|
||||
use Workerman\Connection\ConnectionInterface;
|
||||
use function rtrim;
|
||||
use function strlen;
|
||||
use function strpos;
|
||||
|
||||
/**
|
||||
* Text Protocol.
|
||||
*/
|
||||
class Text
|
||||
{
|
||||
/**
|
||||
* Check the integrity of the package.
|
||||
*
|
||||
* @param string $buffer
|
||||
* @param ConnectionInterface $connection
|
||||
* @return int
|
||||
*/
|
||||
public static function input(string $buffer, ConnectionInterface $connection): int
|
||||
{
|
||||
// Judge whether the package length exceeds the limit.
|
||||
if (isset($connection->maxPackageSize) && strlen($buffer) >= $connection->maxPackageSize) {
|
||||
$connection->close();
|
||||
return 0;
|
||||
}
|
||||
// Find the position of "\n".
|
||||
$pos = strpos($buffer, "\n");
|
||||
// No "\n", packet length is unknown, continue to wait for the data so return 0.
|
||||
if ($pos === false) {
|
||||
return 0;
|
||||
}
|
||||
// Return the current package length.
|
||||
return $pos + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode.
|
||||
*
|
||||
* @param string $buffer
|
||||
* @return string
|
||||
*/
|
||||
public static function encode(string $buffer): string
|
||||
{
|
||||
// Add "\n"
|
||||
return $buffer . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode.
|
||||
*
|
||||
* @param string $buffer
|
||||
* @return string
|
||||
*/
|
||||
public static function decode(string $buffer): string
|
||||
{
|
||||
// Remove "\n"
|
||||
return rtrim($buffer, "\r\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Protocols;
|
||||
|
||||
use Throwable;
|
||||
use Workerman\Connection\ConnectionInterface;
|
||||
use Workerman\Connection\TcpConnection;
|
||||
use Workerman\Protocols\Http\Request;
|
||||
use Workerman\Worker;
|
||||
use function base64_encode;
|
||||
use function chr;
|
||||
use function deflate_add;
|
||||
use function deflate_init;
|
||||
use function floor;
|
||||
use function inflate_add;
|
||||
use function inflate_init;
|
||||
use function is_scalar;
|
||||
use function ord;
|
||||
use function pack;
|
||||
use function preg_match;
|
||||
use function sha1;
|
||||
use function str_repeat;
|
||||
use function stripos;
|
||||
use function strlen;
|
||||
use function strpos;
|
||||
use function substr;
|
||||
use function unpack;
|
||||
use const ZLIB_DEFAULT_STRATEGY;
|
||||
use const ZLIB_ENCODING_RAW;
|
||||
|
||||
/**
|
||||
* WebSocket protocol.
|
||||
*/
|
||||
class Websocket
|
||||
{
|
||||
/**
|
||||
* Websocket blob type.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const BINARY_TYPE_BLOB = "\x81";
|
||||
|
||||
/**
|
||||
* Websocket blob type.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const BINARY_TYPE_BLOB_DEFLATE = "\xc1";
|
||||
|
||||
/**
|
||||
* Websocket arraybuffer type.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const BINARY_TYPE_ARRAYBUFFER = "\x82";
|
||||
|
||||
/**
|
||||
* Websocket arraybuffer type.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const BINARY_TYPE_ARRAYBUFFER_DEFLATE = "\xc2";
|
||||
|
||||
private const ZLIB_INIT_OPTIONS = [
|
||||
ZLIB_ENCODING_RAW,
|
||||
[
|
||||
'level' => -1,
|
||||
'memory' => 8,
|
||||
'window' => 15,
|
||||
'strategy' => ZLIB_DEFAULT_STRATEGY
|
||||
]
|
||||
];
|
||||
|
||||
public static function input(string $buffer, TcpConnection $connection): int
|
||||
{
|
||||
// Receive length.
|
||||
$recvLen = strlen($buffer);
|
||||
// We need more data.
|
||||
if ($recvLen < 6) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Has not yet completed the handshake.
|
||||
if (empty($connection->context->websocketHandshake)) {
|
||||
return static::dealHandshake($buffer, $connection);
|
||||
}
|
||||
|
||||
// Buffer websocket frame data.
|
||||
if ($connection->context->websocketCurrentFrameLength) {
|
||||
// We need more frame data.
|
||||
if ($connection->context->websocketCurrentFrameLength > $recvLen) {
|
||||
// Return 0, because it is not clear the full packet length, waiting for the frame of fin=1.
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
$firstByte = ord($buffer[0]);
|
||||
$secondByte = ord($buffer[1]);
|
||||
$dataLen = $secondByte & 127;
|
||||
$isFinFrame = $firstByte >> 7;
|
||||
$masked = $secondByte >> 7;
|
||||
|
||||
if (!$masked) {
|
||||
Worker::safeEcho("frame not masked so close the connection\n");
|
||||
$connection->close();
|
||||
return 0;
|
||||
}
|
||||
|
||||
$opcode = $firstByte & 0xf;
|
||||
switch ($opcode) {
|
||||
case 0x0:
|
||||
// Blob type.
|
||||
case 0x1:
|
||||
// Arraybuffer type.
|
||||
case 0x2:
|
||||
// Ping package.
|
||||
case 0x9:
|
||||
// Pong package.
|
||||
case 0xa:
|
||||
break;
|
||||
// Close package.
|
||||
case 0x8:
|
||||
// Try to emit onWebSocketClose callback.
|
||||
$closeCb = $connection->onWebSocketClose ?? $connection->worker->onWebSocketClose ?? false;
|
||||
if ($closeCb) {
|
||||
try {
|
||||
$closeCb($connection);
|
||||
} catch (Throwable $e) {
|
||||
Worker::stopAll(250, $e);
|
||||
}
|
||||
} // Close connection.
|
||||
else {
|
||||
$connection->close("\x88\x02\x03\xe8", true);
|
||||
}
|
||||
return 0;
|
||||
// Wrong opcode.
|
||||
default :
|
||||
Worker::safeEcho("error opcode $opcode and close websocket connection. Buffer:" . bin2hex($buffer) . "\n");
|
||||
$connection->close();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Calculate packet length.
|
||||
$headLen = 6;
|
||||
if ($dataLen === 126) {
|
||||
$headLen = 8;
|
||||
if ($headLen > $recvLen) {
|
||||
return 0;
|
||||
}
|
||||
$dataLen = unpack('nn/ntotal_len', $buffer)['total_len'];
|
||||
} else {
|
||||
if ($dataLen === 127) {
|
||||
$headLen = 14;
|
||||
if ($headLen > $recvLen) {
|
||||
return 0;
|
||||
}
|
||||
$arr = unpack('n/N2c', $buffer);
|
||||
$dataLen = $arr['c1'] * 4294967296 + $arr['c2'];
|
||||
}
|
||||
}
|
||||
$currentFrameLength = $headLen + $dataLen;
|
||||
|
||||
$totalPackageSize = strlen($connection->context->websocketDataBuffer) + $currentFrameLength;
|
||||
if ($totalPackageSize > $connection->maxPackageSize) {
|
||||
Worker::safeEcho("error package. package_length=$totalPackageSize\n");
|
||||
$connection->close();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($isFinFrame) {
|
||||
if ($opcode === 0x9) {
|
||||
if ($recvLen >= $currentFrameLength) {
|
||||
$pingData = static::decode(substr($buffer, 0, $currentFrameLength), $connection);
|
||||
$connection->consumeRecvBuffer($currentFrameLength);
|
||||
$tmpConnectionType = $connection->websocketType ?? static::BINARY_TYPE_BLOB;
|
||||
$connection->websocketType = "\x8a";
|
||||
$pingCb = $connection->onWebSocketPing ?? $connection->worker->onWebSocketPing ?? false;
|
||||
if ($pingCb) {
|
||||
try {
|
||||
$pingCb($connection, $pingData);
|
||||
} catch (Throwable $e) {
|
||||
Worker::stopAll(250, $e);
|
||||
}
|
||||
} else {
|
||||
$connection->send($pingData);
|
||||
}
|
||||
$connection->websocketType = $tmpConnectionType;
|
||||
if ($recvLen > $currentFrameLength) {
|
||||
return static::input(substr($buffer, $currentFrameLength), $connection);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($opcode === 0xa) {
|
||||
if ($recvLen >= $currentFrameLength) {
|
||||
$pongData = static::decode(substr($buffer, 0, $currentFrameLength), $connection);
|
||||
$connection->consumeRecvBuffer($currentFrameLength);
|
||||
$tmpConnectionType = $connection->websocketType ?? static::BINARY_TYPE_BLOB;
|
||||
$connection->websocketType = "\x8a";
|
||||
// Try to emit onWebSocketPong callback.
|
||||
$pongCb = $connection->onWebSocketPong ?? $connection->worker->onWebSocketPong ?? false;
|
||||
if ($pongCb) {
|
||||
try {
|
||||
$pongCb($connection, $pongData);
|
||||
} catch (Throwable $e) {
|
||||
Worker::stopAll(250, $e);
|
||||
}
|
||||
}
|
||||
$connection->websocketType = $tmpConnectionType;
|
||||
if ($recvLen > $currentFrameLength) {
|
||||
return static::input(substr($buffer, $currentFrameLength), $connection);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return $currentFrameLength;
|
||||
}
|
||||
|
||||
$connection->context->websocketCurrentFrameLength = $currentFrameLength;
|
||||
}
|
||||
|
||||
// Received just a frame length data.
|
||||
if ($connection->context->websocketCurrentFrameLength === $recvLen) {
|
||||
static::decode($buffer, $connection);
|
||||
$connection->consumeRecvBuffer($connection->context->websocketCurrentFrameLength);
|
||||
$connection->context->websocketCurrentFrameLength = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The length of the received data is greater than the length of a frame.
|
||||
if ($connection->context->websocketCurrentFrameLength < $recvLen) {
|
||||
static::decode(substr($buffer, 0, $connection->context->websocketCurrentFrameLength), $connection);
|
||||
$connection->consumeRecvBuffer($connection->context->websocketCurrentFrameLength);
|
||||
$currentFrameLength = $connection->context->websocketCurrentFrameLength;
|
||||
$connection->context->websocketCurrentFrameLength = 0;
|
||||
// Continue to read next frame.
|
||||
return static::input(substr($buffer, $currentFrameLength), $connection);
|
||||
}
|
||||
|
||||
// The length of the received data is less than the length of a frame.
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static function encode(mixed $buffer, TcpConnection $connection): string
|
||||
{
|
||||
if (!is_scalar($buffer)) {
|
||||
$buffer = json_encode($buffer, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
$connection->websocketType ??= static::BINARY_TYPE_BLOB;
|
||||
|
||||
if (ord($connection->websocketType) & 64) {
|
||||
$buffer = static::deflate($connection, $buffer);
|
||||
}
|
||||
|
||||
$firstByte = $connection->websocketType;
|
||||
$len = strlen($buffer);
|
||||
|
||||
$encodeBuffer = match(true) {
|
||||
$len <= 125 => $firstByte . chr($len) . $buffer,
|
||||
$len <= 65535 => $firstByte . chr(126) . pack("n", $len) . $buffer,
|
||||
default => $firstByte . chr(127) . pack("xxxxN", $len) . $buffer,
|
||||
};
|
||||
|
||||
// Handshake not completed so temporary buffer websocket data waiting for send.
|
||||
if (empty($connection->context->websocketHandshake)) {
|
||||
if (empty($connection->context->tmpWebsocketData)) {
|
||||
$connection->context->tmpWebsocketData = '';
|
||||
}
|
||||
// If buffer has already full then discard the current package.
|
||||
if (strlen($connection->context->tmpWebsocketData) > $connection->maxSendBufferSize) {
|
||||
if ($connection->onError) {
|
||||
try {
|
||||
($connection->onError)($connection, ConnectionInterface::SEND_FAIL, 'send buffer full and drop package');
|
||||
} catch (Throwable $e) {
|
||||
Worker::stopAll(250, $e);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
$connection->context->tmpWebsocketData .= $encodeBuffer;
|
||||
// Check buffer is full.
|
||||
if ($connection->onBufferFull && $connection->maxSendBufferSize <= strlen($connection->context->tmpWebsocketData)) {
|
||||
try {
|
||||
($connection->onBufferFull)($connection);
|
||||
} catch (Throwable $e) {
|
||||
Worker::stopAll(250, $e);
|
||||
}
|
||||
}
|
||||
// Return empty string.
|
||||
return '';
|
||||
}
|
||||
|
||||
return $encodeBuffer;
|
||||
}
|
||||
|
||||
public static function decode(string $buffer, TcpConnection $connection): string
|
||||
{
|
||||
$firstByte = ord($buffer[0]);
|
||||
$secondByte = ord($buffer[1]);
|
||||
$len = $secondByte & 127;
|
||||
$isFinFrame = (bool)($firstByte >> 7);
|
||||
$rsv1 = 64 === ($firstByte & 64);
|
||||
|
||||
[$masks, $data] = match(true) {
|
||||
$len === 126 => [substr($buffer, 4, 4), substr($buffer, 8)],
|
||||
$len === 127 => [substr($buffer, 10, 4), substr($buffer, 14)],
|
||||
default => [substr($buffer, 2, 4), substr($buffer, 6)],
|
||||
};
|
||||
|
||||
$dataLength = strlen($data);
|
||||
$masks = str_repeat($masks, (int)floor($dataLength / 4)) . substr($masks, 0, $dataLength % 4);
|
||||
$decoded = $data ^ $masks;
|
||||
if ($connection->context->websocketCurrentFrameLength) {
|
||||
$connection->context->websocketDataBuffer .= $decoded;
|
||||
if ($rsv1) {
|
||||
return static::inflate($connection, $connection->context->websocketDataBuffer, $isFinFrame);
|
||||
}
|
||||
return $connection->context->websocketDataBuffer;
|
||||
}
|
||||
if ($connection->context->websocketDataBuffer !== '') {
|
||||
$decoded = $connection->context->websocketDataBuffer . $decoded;
|
||||
$connection->context->websocketDataBuffer = '';
|
||||
}
|
||||
if ($rsv1) {
|
||||
return static::inflate($connection, $decoded, $isFinFrame);
|
||||
}
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
protected static function inflate(TcpConnection $connection, string $buffer, bool $isFinFrame): false|string
|
||||
{
|
||||
$connection->context->inflator ??= inflate_init(...self::ZLIB_INIT_OPTIONS);
|
||||
|
||||
if ($isFinFrame) {
|
||||
$buffer .= "\x00\x00\xff\xff";
|
||||
}
|
||||
$result = inflate_add($connection->context->inflator, $buffer);
|
||||
// Guard against decompression bomb: check inflated size against maxPackageSize.
|
||||
if ($result !== false && strlen($result) > $connection->maxPackageSize) {
|
||||
Worker::safeEcho("WebSocket inflate data exceeds maxPackageSize limit\n");
|
||||
$connection->close();
|
||||
return false;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected static function deflate(TcpConnection $connection, string $buffer): false|string
|
||||
{
|
||||
|
||||
$connection->context->deflator ??= deflate_init(...self::ZLIB_INIT_OPTIONS);
|
||||
|
||||
return substr(deflate_add($connection->context->deflator, $buffer), 0, -4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Websocket handshake.
|
||||
*
|
||||
*/
|
||||
public static function dealHandshake(string $buffer, TcpConnection $connection): int
|
||||
{
|
||||
$HTTP_400 = "HTTP/1.1 400 Bad Request\r\n\r\n<div style=\"text-align:center\"><h1>400 Bad Request</h1><hr>workerman</div>";
|
||||
|
||||
// HTTP protocol.
|
||||
if (!str_starts_with($buffer, 'GET')) {
|
||||
// Bad websocket handshake request.
|
||||
$connection->close($HTTP_400, true);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Find \r\n\r\n.
|
||||
$headerEndPos = strpos($buffer, "\r\n\r\n");
|
||||
if (!$headerEndPos) {
|
||||
return 0;
|
||||
}
|
||||
$headerLength = $headerEndPos + 4;
|
||||
|
||||
// Check WebSocket version - RFC 6455 Section 4.4
|
||||
if (preg_match("/Sec-WebSocket-Version: *(.*?)\r\n/i", $buffer, $match)) {
|
||||
if($match[1] !== '13') {
|
||||
$_426 = "HTTP/1.1 426 Upgrade Required\r\n"
|
||||
. "Connection: Upgrade\r\n"
|
||||
. "Upgrade: WebSocket\r\n"
|
||||
. "Sec-WebSocket-Version: 13\r\n\r\n";
|
||||
|
||||
$connection->close($_426, true);
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
$connection->close(
|
||||
"HTTP/1.1 400 Bad Request\r\nSec-WebSocket-Version: 13\r\n\r\n", true);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Get Sec-WebSocket-Key.
|
||||
if (preg_match("/Sec-WebSocket-Key: *(.*?)\r\n/i", $buffer, $match)) {
|
||||
$SecWebSocketKey = $match[1];
|
||||
} else {
|
||||
$connection->close($HTTP_400, true);
|
||||
return 0;
|
||||
}
|
||||
// Calculation websocket key.
|
||||
$newKey = base64_encode(sha1($SecWebSocketKey . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true));
|
||||
// Handshake response data.
|
||||
$handshakeMessage = "HTTP/1.1 101 Switching Protocol\r\n"
|
||||
. "Upgrade: websocket\r\n"
|
||||
. "Sec-WebSocket-Version: 13\r\n"
|
||||
. "Connection: Upgrade\r\n"
|
||||
. "Sec-WebSocket-Accept: $newKey\r\n";
|
||||
|
||||
// Websocket data buffer.
|
||||
$connection->context->websocketDataBuffer = '';
|
||||
// Current websocket frame length.
|
||||
$connection->context->websocketCurrentFrameLength = 0;
|
||||
// Current websocket frame data.
|
||||
$connection->context->websocketCurrentFrameBuffer = '';
|
||||
// Consume handshake data.
|
||||
$connection->consumeRecvBuffer($headerLength);
|
||||
// Request from buffer
|
||||
$request = new Request($buffer);
|
||||
|
||||
// Try to emit onWebSocketConnect callback.
|
||||
$onWebsocketConnect = $connection->onWebSocketConnect ?? $connection->worker->onWebSocketConnect ?? false;
|
||||
if ($onWebsocketConnect) {
|
||||
try {
|
||||
$onWebsocketConnect($connection, $request);
|
||||
} catch (Throwable $e) {
|
||||
Worker::stopAll(250, $e);
|
||||
}
|
||||
}
|
||||
|
||||
// blob or arraybuffer
|
||||
$connection->websocketType ??= static::BINARY_TYPE_BLOB;
|
||||
|
||||
if ($connection->headers) {
|
||||
foreach ($connection->headers as $header) {
|
||||
if (strpbrk($header, "\r\n") !== false) {
|
||||
continue;
|
||||
}
|
||||
$handshakeMessage .= "$header\r\n";
|
||||
}
|
||||
}
|
||||
$handshakeMessage .= "\r\n";
|
||||
// Send handshake response.
|
||||
$connection->send($handshakeMessage, true);
|
||||
// Mark handshake complete.
|
||||
$connection->context->websocketHandshake = true;
|
||||
|
||||
// Try to emit onWebSocketConnected callback.
|
||||
$onWebsocketConnected = $connection->onWebSocketConnected ?? $connection->worker->onWebSocketConnected ?? false;
|
||||
if ($onWebsocketConnected) {
|
||||
try {
|
||||
$onWebsocketConnected($connection, $request);
|
||||
} catch (Throwable $e) {
|
||||
Worker::stopAll(250, $e);
|
||||
}
|
||||
}
|
||||
|
||||
// There are data waiting to be sent.
|
||||
if (!empty($connection->context->tmpWebsocketData)) {
|
||||
$connection->send($connection->context->tmpWebsocketData, true);
|
||||
$connection->context->tmpWebsocketData = '';
|
||||
}
|
||||
if (strlen($buffer) > $headerLength) {
|
||||
return static::input(substr($buffer, $headerLength), $connection);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+475
@@ -0,0 +1,475 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman\Protocols;
|
||||
|
||||
use Throwable;
|
||||
use Workerman\Connection\AsyncTcpConnection;
|
||||
use Workerman\Connection\ConnectionInterface;
|
||||
use Workerman\Protocols\Http\Response;
|
||||
use Workerman\Timer;
|
||||
use Workerman\Worker;
|
||||
use function base64_encode;
|
||||
use function bin2hex;
|
||||
use function explode;
|
||||
use function floor;
|
||||
use function ord;
|
||||
use function pack;
|
||||
use function preg_match;
|
||||
use function sha1;
|
||||
use function str_repeat;
|
||||
use function strlen;
|
||||
use function strpos;
|
||||
use function substr;
|
||||
use function trim;
|
||||
use function unpack;
|
||||
|
||||
/**
|
||||
* Websocket protocol for client.
|
||||
*/
|
||||
class Ws
|
||||
{
|
||||
/**
|
||||
* Websocket blob type.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const BINARY_TYPE_BLOB = "\x81";
|
||||
|
||||
/**
|
||||
* Websocket arraybuffer type.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const BINARY_TYPE_ARRAYBUFFER = "\x82";
|
||||
|
||||
public static function input(string $buffer, AsyncTcpConnection $connection): int
|
||||
{
|
||||
if (!isset($connection->context->handshakeStep)) {
|
||||
Worker::safeEcho("recv data before handshake. Buffer:" . bin2hex($buffer) . "\n");
|
||||
return -1;
|
||||
}
|
||||
// Recv handshake response
|
||||
if ($connection->context->handshakeStep === 1) {
|
||||
return self::dealHandshake($buffer, $connection);
|
||||
}
|
||||
$recvLen = strlen($buffer);
|
||||
if ($recvLen < 2) {
|
||||
return 0;
|
||||
}
|
||||
// Buffer websocket frame data.
|
||||
if ($connection->context->websocketCurrentFrameLength) {
|
||||
// We need more frame data.
|
||||
if ($connection->context->websocketCurrentFrameLength > $recvLen) {
|
||||
// Return 0, because it is not clear the full packet length, waiting for the frame of fin=1.
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
|
||||
$firstByte = ord($buffer[0]);
|
||||
$secondByte = ord($buffer[1]);
|
||||
$dataLen = $secondByte & 127;
|
||||
$isFinFrame = $firstByte >> 7;
|
||||
$masked = $secondByte >> 7;
|
||||
|
||||
if ($masked) {
|
||||
Worker::safeEcho("frame masked so close the connection\n");
|
||||
$connection->close();
|
||||
return 0;
|
||||
}
|
||||
|
||||
$opcode = $firstByte & 0xf;
|
||||
|
||||
switch ($opcode) {
|
||||
case 0x0:
|
||||
// Blob type.
|
||||
case 0x1:
|
||||
// Arraybuffer type.
|
||||
case 0x2:
|
||||
// Ping package.
|
||||
case 0x9:
|
||||
// Pong package.
|
||||
case 0xa:
|
||||
break;
|
||||
// Close package.
|
||||
case 0x8:
|
||||
// Try to emit onWebSocketClose callback.
|
||||
if (isset($connection->onWebSocketClose)) {
|
||||
try {
|
||||
($connection->onWebSocketClose)($connection, self::decode($buffer, $connection));
|
||||
} catch (Throwable $e) {
|
||||
Worker::stopAll(250, $e);
|
||||
}
|
||||
} else { // Close connection.
|
||||
$connection->close();
|
||||
}
|
||||
|
||||
return 0;
|
||||
// Wrong opcode.
|
||||
default :
|
||||
Worker::safeEcho("error opcode $opcode and close websocket connection. Buffer:" . $buffer . "\n");
|
||||
$connection->close();
|
||||
return 0;
|
||||
}
|
||||
// Calculate packet length.
|
||||
if ($dataLen === 126) {
|
||||
if (strlen($buffer) < 4) {
|
||||
return 0;
|
||||
}
|
||||
$currentFrameLength = unpack('nn/ntotal_len', $buffer)['total_len'] + 4;
|
||||
} else if ($dataLen === 127) {
|
||||
if (strlen($buffer) < 10) {
|
||||
return 0;
|
||||
}
|
||||
$arr = unpack('n/N2c', $buffer);
|
||||
$currentFrameLength = $arr['c1'] * 4294967296 + $arr['c2'] + 10;
|
||||
} else {
|
||||
$currentFrameLength = $dataLen + 2;
|
||||
}
|
||||
|
||||
$totalPackageSize = strlen($connection->context->websocketDataBuffer) + $currentFrameLength;
|
||||
if ($totalPackageSize > $connection->maxPackageSize) {
|
||||
Worker::safeEcho("error package. package_length=$totalPackageSize\n");
|
||||
$connection->close();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($isFinFrame) {
|
||||
if ($opcode === 0x9) {
|
||||
if ($recvLen >= $currentFrameLength) {
|
||||
$pingData = static::decode(substr($buffer, 0, $currentFrameLength), $connection);
|
||||
$connection->consumeRecvBuffer($currentFrameLength);
|
||||
$tmpConnectionType = $connection->websocketType ?? static::BINARY_TYPE_BLOB;
|
||||
$connection->websocketType = "\x8a";
|
||||
if (isset($connection->onWebSocketPing)) {
|
||||
try {
|
||||
($connection->onWebSocketPing)($connection, $pingData);
|
||||
} catch (Throwable $e) {
|
||||
Worker::stopAll(250, $e);
|
||||
}
|
||||
} else {
|
||||
$connection->send($pingData);
|
||||
}
|
||||
$connection->websocketType = $tmpConnectionType;
|
||||
if ($recvLen > $currentFrameLength) {
|
||||
return static::input(substr($buffer, $currentFrameLength), $connection);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
|
||||
}
|
||||
|
||||
if ($opcode === 0xa) {
|
||||
if ($recvLen >= $currentFrameLength) {
|
||||
$pongData = static::decode(substr($buffer, 0, $currentFrameLength), $connection);
|
||||
$connection->consumeRecvBuffer($currentFrameLength);
|
||||
$tmpConnectionType = $connection->websocketType ?? static::BINARY_TYPE_BLOB;
|
||||
$connection->websocketType = "\x8a";
|
||||
// Try to emit onWebSocketPong callback.
|
||||
if (isset($connection->onWebSocketPong)) {
|
||||
try {
|
||||
($connection->onWebSocketPong)($connection, $pongData);
|
||||
} catch (Throwable $e) {
|
||||
Worker::stopAll(250, $e);
|
||||
}
|
||||
}
|
||||
$connection->websocketType = $tmpConnectionType;
|
||||
if ($recvLen > $currentFrameLength) {
|
||||
return static::input(substr($buffer, $currentFrameLength), $connection);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return $currentFrameLength;
|
||||
}
|
||||
|
||||
$connection->context->websocketCurrentFrameLength = $currentFrameLength;
|
||||
}
|
||||
// Received just a frame length data.
|
||||
if ($connection->context->websocketCurrentFrameLength === $recvLen) {
|
||||
self::decode($buffer, $connection);
|
||||
$connection->consumeRecvBuffer($connection->context->websocketCurrentFrameLength);
|
||||
$connection->context->websocketCurrentFrameLength = 0;
|
||||
return 0;
|
||||
} // The length of the received data is greater than the length of a frame.
|
||||
else if ($connection->context->websocketCurrentFrameLength < $recvLen) {
|
||||
self::decode(substr($buffer, 0, $connection->context->websocketCurrentFrameLength), $connection);
|
||||
$connection->consumeRecvBuffer($connection->context->websocketCurrentFrameLength);
|
||||
$currentFrameLength = $connection->context->websocketCurrentFrameLength;
|
||||
$connection->context->websocketCurrentFrameLength = 0;
|
||||
// Continue to read next frame.
|
||||
return self::input(substr($buffer, $currentFrameLength), $connection);
|
||||
} // The length of the received data is less than the length of a frame.
|
||||
|
||||
return 0;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Websocket encode.
|
||||
*
|
||||
* @param string $payload
|
||||
* @param AsyncTcpConnection $connection
|
||||
* @return string
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function encode(string $payload, AsyncTcpConnection $connection): string
|
||||
{
|
||||
$connection->websocketType ??= self::BINARY_TYPE_BLOB;
|
||||
|
||||
$connection->websocketOrigin ??= null;
|
||||
$connection->websocketClientProtocol ??= null;
|
||||
if (!isset($connection->context->handshakeStep)) {
|
||||
static::sendHandshake($connection);
|
||||
}
|
||||
|
||||
$maskKey = "\x00\x00\x00\x00";
|
||||
$length = strlen($payload);
|
||||
|
||||
$head = match(true) {
|
||||
$length < 126 => chr(0x80 | $length),
|
||||
$length < 0xFFFF => chr(0x80 | 126) . pack("n", $length),
|
||||
default => chr(0x80 | 127) . pack("N", 0) . pack("N", $length),
|
||||
};
|
||||
|
||||
$frame = $connection->websocketType . $head . $maskKey;
|
||||
// append payload to frame:
|
||||
$maskKey = str_repeat($maskKey, (int)floor($length / 4)) . substr($maskKey, 0, $length % 4);
|
||||
$frame .= $payload ^ $maskKey;
|
||||
if ($connection->context->handshakeStep === 1) {
|
||||
// If buffer has already full then discard the current package.
|
||||
if (strlen($connection->context->tmpWebsocketData) > $connection->maxSendBufferSize) {
|
||||
if ($connection->onError) {
|
||||
try {
|
||||
($connection->onError)($connection, ConnectionInterface::SEND_FAIL, 'send buffer full and drop package');
|
||||
} catch (Throwable $e) {
|
||||
Worker::stopAll(250, $e);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
$connection->context->tmpWebsocketData .= $frame;
|
||||
// Check buffer is full.
|
||||
if ($connection->onBufferFull && $connection->maxSendBufferSize <= strlen($connection->context->tmpWebsocketData)) {
|
||||
try {
|
||||
($connection->onBufferFull)($connection);
|
||||
} catch (Throwable $e) {
|
||||
Worker::stopAll(250, $e);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
return $frame;
|
||||
}
|
||||
|
||||
/**
|
||||
* Websocket decode.
|
||||
*
|
||||
* @param string $bytes
|
||||
* @param AsyncTcpConnection $connection
|
||||
* @return string
|
||||
*/
|
||||
public static function decode(string $bytes, AsyncTcpConnection $connection): string
|
||||
{
|
||||
$decodedData = match(ord($bytes[1])) { // data length
|
||||
126 => substr($bytes, 4),
|
||||
127 => substr($bytes, 10),
|
||||
default => substr($bytes, 2),
|
||||
};
|
||||
|
||||
if ($connection->context->websocketCurrentFrameLength) {
|
||||
return $connection->context->websocketDataBuffer .= $decodedData;
|
||||
}
|
||||
|
||||
if ($connection->context->websocketDataBuffer !== '') {
|
||||
$decodedData = $connection->context->websocketDataBuffer . $decodedData;
|
||||
$connection->context->websocketDataBuffer = '';
|
||||
}
|
||||
return $decodedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send websocket handshake data.
|
||||
*
|
||||
* @param AsyncTcpConnection $connection
|
||||
* @return void
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function onConnect(AsyncTcpConnection $connection): void
|
||||
{
|
||||
$connection->websocketOrigin = $connection->websocketOrigin ?? null;
|
||||
$connection->websocketClientProtocol = $connection->websocketClientProtocol ?? null;
|
||||
static::sendHandshake($connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean
|
||||
*
|
||||
* @param AsyncTcpConnection $connection
|
||||
*/
|
||||
public static function onClose(AsyncTcpConnection $connection): void
|
||||
{
|
||||
unset($connection->context->handshakeStep);
|
||||
$connection->context->websocketCurrentFrameLength = 0;
|
||||
$connection->context->tmpWebsocketData = '';
|
||||
$connection->context->websocketDataBuffer = '';
|
||||
if (!empty($connection->context->websocketPingTimer)) {
|
||||
Timer::del($connection->context->websocketPingTimer);
|
||||
$connection->context->websocketPingTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send websocket handshake.
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function sendHandshake(AsyncTcpConnection $connection): void
|
||||
{
|
||||
if (!empty($connection->context->handshakeStep)) {
|
||||
return;
|
||||
}
|
||||
// Get Host.
|
||||
$port = $connection->getRemotePort();
|
||||
$host = match($port) {
|
||||
80, 443 => $connection->getRemoteHost(),
|
||||
default => $connection->getRemoteHost() . ":$port",
|
||||
};
|
||||
// Handshake header.
|
||||
$connection->context->websocketSecKey = base64_encode(random_bytes(16));
|
||||
$userHeader = $connection->headers ?? null;
|
||||
$userHeaderStr = '';
|
||||
if (!empty($userHeader)) {
|
||||
foreach ($userHeader as $k => $v) {
|
||||
// Skip unsafe header names or values containing CR/LF
|
||||
if (strpbrk((string)$k, ":\r\n") !== false) {
|
||||
continue;
|
||||
}
|
||||
if (strpbrk((string)$v, "\r\n") !== false) {
|
||||
continue;
|
||||
}
|
||||
$userHeaderStr .= "$k: $v\r\n";
|
||||
}
|
||||
$userHeaderStr = $userHeaderStr !== '' ? "\r\n" . trim($userHeaderStr) : '';
|
||||
}
|
||||
$requestUri = str_replace(["\r", "\n"], '', $connection->getRemoteURI());
|
||||
// Sanitize Origin and Sec-WebSocket-Protocol
|
||||
$origin = $connection->websocketOrigin ?? null;
|
||||
$origin = $origin !== null ? str_replace(["\r", "\n"], '', $origin) : null;
|
||||
$clientProtocol = $connection->websocketClientProtocol ?? null;
|
||||
$clientProtocol = $clientProtocol !== null ? str_replace(["\r", "\n"], '', $clientProtocol) : null;
|
||||
$header = 'GET ' . $requestUri . " HTTP/1.1\r\n" .
|
||||
(!preg_match("/\nHost:/i", $userHeaderStr) ? "Host: $host\r\n" : '') .
|
||||
"Connection: Upgrade\r\n" .
|
||||
"Upgrade: websocket\r\n" .
|
||||
($origin ? "Origin: " . $origin . "\r\n" : '') .
|
||||
($clientProtocol ? "Sec-WebSocket-Protocol: $clientProtocol\r\n" : '') .
|
||||
"Sec-WebSocket-Version: 13\r\n" .
|
||||
"Sec-WebSocket-Key: " . $connection->context->websocketSecKey . "$userHeaderStr\r\n\r\n";
|
||||
$connection->send($header, true);
|
||||
$connection->context->handshakeStep = 1;
|
||||
$connection->context->websocketCurrentFrameLength = 0;
|
||||
$connection->context->websocketDataBuffer = '';
|
||||
$connection->context->tmpWebsocketData = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Websocket handshake.
|
||||
*
|
||||
* @param string $buffer
|
||||
* @param AsyncTcpConnection $connection
|
||||
* @return bool|int
|
||||
*/
|
||||
public static function dealHandshake(string $buffer, AsyncTcpConnection $connection): bool|int
|
||||
{
|
||||
$pos = strpos($buffer, "\r\n\r\n");
|
||||
if (!$pos) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
//checking Sec-WebSocket-Accept
|
||||
if (preg_match("/Sec-WebSocket-Accept: *(.*?)\r\n/i", $buffer, $match)) {
|
||||
if ($match[1] !== base64_encode(sha1($connection->context->websocketSecKey . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true))) {
|
||||
Worker::safeEcho("Sec-WebSocket-Accept not match. Header:\n" . substr($buffer, 0, $pos) . "\n");
|
||||
$connection->close();
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
Worker::safeEcho("Sec-WebSocket-Accept not found. Header:\n" . substr($buffer, 0, $pos) . "\n");
|
||||
$connection->close();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// handshake complete
|
||||
$connection->context->handshakeStep = 2;
|
||||
$handshakeResponseLength = $pos + 4;
|
||||
$buffer = substr($buffer, 0, $handshakeResponseLength);
|
||||
$response = static::parseResponse($buffer);
|
||||
// Try to emit onWebSocketConnect callback.
|
||||
if (isset($connection->onWebSocketConnect)) {
|
||||
try {
|
||||
($connection->onWebSocketConnect)($connection, $response);
|
||||
} catch (Throwable $e) {
|
||||
Worker::stopAll(250, $e);
|
||||
}
|
||||
}
|
||||
// Headbeat.
|
||||
if (!empty($connection->websocketPingInterval)) {
|
||||
$connection->context->websocketPingTimer = Timer::add($connection->websocketPingInterval, function () use ($connection) {
|
||||
if (false === $connection->send(pack('H*', '898000000000'), true)) {
|
||||
Timer::del($connection->context->websocketPingTimer);
|
||||
$connection->context->websocketPingTimer = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$connection->consumeRecvBuffer($handshakeResponseLength);
|
||||
if (!empty($connection->context->tmpWebsocketData)) {
|
||||
$connection->send($connection->context->tmpWebsocketData, true);
|
||||
$connection->context->tmpWebsocketData = '';
|
||||
}
|
||||
if (strlen($buffer) > $handshakeResponseLength) {
|
||||
return self::input(substr($buffer, $handshakeResponseLength), $connection);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse response.
|
||||
*
|
||||
* @param string $buffer
|
||||
* @return Response
|
||||
*/
|
||||
protected static function parseResponse(string $buffer): Response
|
||||
{
|
||||
[$http_header, ] = explode("\r\n\r\n", $buffer, 2);
|
||||
$header_data = explode("\r\n", $http_header);
|
||||
[$protocol, $status, $phrase] = explode(' ', $header_data[0], 3);
|
||||
$protocolVersion = substr($protocol, 5);
|
||||
unset($header_data[0]);
|
||||
$headers = [];
|
||||
foreach ($header_data as $content) {
|
||||
// \r\n\r\n
|
||||
if (empty($content)) {
|
||||
continue;
|
||||
}
|
||||
[$key, $value] = explode(':', $content, 2);
|
||||
$headers[$key] = trim($value);
|
||||
}
|
||||
return (new Response())->withStatus((int)$status, $phrase)->withHeaders($headers)->withProtocolVersion($protocolVersion);
|
||||
}
|
||||
}
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Workerman;
|
||||
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
use Workerman\Events\EventInterface;
|
||||
use Workerman\Events\Fiber;
|
||||
use Workerman\Events\Swoole;
|
||||
use Revolt\EventLoop;
|
||||
use Swoole\Coroutine\System;
|
||||
use function function_exists;
|
||||
use function pcntl_alarm;
|
||||
use function pcntl_signal;
|
||||
use function time;
|
||||
use const PHP_INT_MAX;
|
||||
use const SIGALRM;
|
||||
|
||||
/**
|
||||
* Timer.
|
||||
*/
|
||||
class Timer
|
||||
{
|
||||
/**
|
||||
* Tasks that based on ALARM signal.
|
||||
* [
|
||||
* run_time => [[$func, $args, $persistent, time_interval],[$func, $args, $persistent, time_interval],..]],
|
||||
* run_time => [[$func, $args, $persistent, time_interval],[$func, $args, $persistent, time_interval],..]],
|
||||
* ..
|
||||
* ]
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static array $tasks = [];
|
||||
|
||||
/**
|
||||
* Event
|
||||
*
|
||||
* @var ?EventInterface
|
||||
*/
|
||||
protected static ?EventInterface $event = null;
|
||||
|
||||
/**
|
||||
* Timer id
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected static int $timerId = 0;
|
||||
|
||||
/**
|
||||
* Timer status
|
||||
* [
|
||||
* timer_id1 => bool,
|
||||
* timer_id2 => bool,
|
||||
* ....................,
|
||||
* ]
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static array $status = [];
|
||||
|
||||
/**
|
||||
* Init.
|
||||
*
|
||||
* @param EventInterface|null $event
|
||||
* @return void
|
||||
*/
|
||||
public static function init(?EventInterface $event = null): void
|
||||
{
|
||||
if ($event) {
|
||||
self::$event = $event;
|
||||
return;
|
||||
}
|
||||
if (function_exists('pcntl_signal')) {
|
||||
pcntl_signal(SIGALRM, self::signalHandle(...), false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Repeat.
|
||||
*
|
||||
* @param float $timeInterval
|
||||
* @param callable $func
|
||||
* @param array $args
|
||||
* @return int
|
||||
*/
|
||||
public static function repeat(float $timeInterval, callable $func, array $args = []): int
|
||||
{
|
||||
return self::$event->repeat($timeInterval, $func, $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delay.
|
||||
*
|
||||
* @param float $timeInterval
|
||||
* @param callable $func
|
||||
* @param array $args
|
||||
* @return int
|
||||
*/
|
||||
public static function delay(float $timeInterval, callable $func, array $args = []): int
|
||||
{
|
||||
return self::$event->delay($timeInterval, $func, $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* ALARM signal handler.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function signalHandle(): void
|
||||
{
|
||||
if (!self::$event) {
|
||||
pcntl_alarm(1);
|
||||
self::tick();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a timer.
|
||||
*
|
||||
* @param float $timeInterval
|
||||
* @param callable $func
|
||||
* @param null|array $args
|
||||
* @param bool $persistent
|
||||
* @return int
|
||||
*/
|
||||
public static function add(float $timeInterval, callable $func, ?array $args = [], bool $persistent = true): int
|
||||
{
|
||||
if ($timeInterval < 0) {
|
||||
throw new RuntimeException('$timeInterval can not less than 0');
|
||||
}
|
||||
|
||||
if ($args === null) {
|
||||
$args = [];
|
||||
}
|
||||
|
||||
if (self::$event) {
|
||||
return $persistent ? self::$event->repeat($timeInterval, $func, $args) : self::$event->delay($timeInterval, $func, $args);
|
||||
}
|
||||
|
||||
// If not workerman runtime just return.
|
||||
if (!Worker::getAllWorkers()) {
|
||||
throw new RuntimeException('Timer can only be used in workerman running environment');
|
||||
}
|
||||
|
||||
if (empty(self::$tasks)) {
|
||||
pcntl_alarm(1);
|
||||
}
|
||||
|
||||
$runTime = (int)floor(time() + $timeInterval);
|
||||
if (!isset(self::$tasks[$runTime])) {
|
||||
self::$tasks[$runTime] = [];
|
||||
}
|
||||
|
||||
self::$timerId = self::$timerId == PHP_INT_MAX ? 1 : ++self::$timerId;
|
||||
self::$status[self::$timerId] = true;
|
||||
self::$tasks[$runTime][self::$timerId] = [$func, (array)$args, $persistent, $timeInterval];
|
||||
|
||||
return self::$timerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Coroutine sleep.
|
||||
*
|
||||
* @param float $delay
|
||||
* @return void
|
||||
*/
|
||||
public static function sleep(float $delay): void
|
||||
{
|
||||
switch (Worker::$eventLoopClass) {
|
||||
// Fiber
|
||||
case Fiber::class:
|
||||
$suspension = EventLoop::getSuspension();
|
||||
static::add($delay, function () use ($suspension) {
|
||||
$suspension->resume();
|
||||
}, null, false);
|
||||
$suspension->suspend();
|
||||
return;
|
||||
// Swoole
|
||||
case Swoole::class:
|
||||
System::sleep($delay);
|
||||
return;
|
||||
}
|
||||
usleep((int)($delay * 1000 * 1000));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tick.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected static function tick(): void
|
||||
{
|
||||
if (empty(self::$tasks)) {
|
||||
pcntl_alarm(0);
|
||||
return;
|
||||
}
|
||||
$timeNow = time();
|
||||
foreach (self::$tasks as $runTime => $taskData) {
|
||||
if ($timeNow >= $runTime) {
|
||||
foreach ($taskData as $index => $oneTask) {
|
||||
$taskFunc = $oneTask[0];
|
||||
$taskArgs = $oneTask[1];
|
||||
$persistent = $oneTask[2];
|
||||
$timeInterval = $oneTask[3];
|
||||
try {
|
||||
$taskFunc(...$taskArgs);
|
||||
} catch (Throwable $e) {
|
||||
Worker::safeEcho((string)$e);
|
||||
}
|
||||
if ($persistent && !empty(self::$status[$index])) {
|
||||
$newRunTime = (int)floor(time() + $timeInterval);
|
||||
if (!isset(self::$tasks[$newRunTime])) {
|
||||
self::$tasks[$newRunTime] = [];
|
||||
}
|
||||
self::$tasks[$newRunTime][$index] = [$taskFunc, (array)$taskArgs, $persistent, $timeInterval];
|
||||
}
|
||||
}
|
||||
unset(self::$tasks[$runTime]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a timer.
|
||||
*
|
||||
* @param int $timerId
|
||||
* @return bool
|
||||
*/
|
||||
public static function del(int $timerId): bool
|
||||
{
|
||||
if (self::$event) {
|
||||
return self::$event->offDelay($timerId);
|
||||
}
|
||||
foreach (self::$tasks as $runTime => $taskData) {
|
||||
if (array_key_exists($timerId, $taskData)) {
|
||||
unset(self::$tasks[$runTime][$timerId]);
|
||||
}
|
||||
}
|
||||
if (array_key_exists($timerId, self::$status)) {
|
||||
unset(self::$status[$timerId]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all timers.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function delAll(): void
|
||||
{
|
||||
self::$tasks = self::$status = [];
|
||||
if (function_exists('pcntl_alarm')) {
|
||||
pcntl_alarm(0);
|
||||
}
|
||||
self::$event?->deleteAllTimer();
|
||||
}
|
||||
}
|
||||
+2831
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user