summaryrefslogtreecommitdiffstats
path: root/vendor/gipfl/icinga-cli-daemon/src
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/gipfl/icinga-cli-daemon/src')
-rw-r--r--vendor/gipfl/icinga-cli-daemon/src/DbResourceConfigWatch.php163
-rw-r--r--vendor/gipfl/icinga-cli-daemon/src/FinishedProcessState.php66
-rw-r--r--vendor/gipfl/icinga-cli-daemon/src/IcingaCli.php144
-rw-r--r--vendor/gipfl/icinga-cli-daemon/src/IcingaCliRpc.php45
-rw-r--r--vendor/gipfl/icinga-cli-daemon/src/IcingaCliRunner.php88
-rw-r--r--vendor/gipfl/icinga-cli-daemon/src/RetryUnless.php244
-rw-r--r--vendor/gipfl/icinga-cli-daemon/src/StateMachine.php113
7 files changed, 863 insertions, 0 deletions
diff --git a/vendor/gipfl/icinga-cli-daemon/src/DbResourceConfigWatch.php b/vendor/gipfl/icinga-cli-daemon/src/DbResourceConfigWatch.php
new file mode 100644
index 0000000..08b188f
--- /dev/null
+++ b/vendor/gipfl/icinga-cli-daemon/src/DbResourceConfigWatch.php
@@ -0,0 +1,163 @@
+<?php
+
+namespace gipfl\IcingaCliDaemon;
+
+use Icinga\Application\Config;
+use InvalidArgumentException;
+use React\EventLoop\LoopInterface;
+
+/**
+ * DbResourceConfigWatch
+ *
+ * Checks every $interval = 3 seconds for changed DB resource configuration.
+ * Notifies registered callbacksin case this happens.
+ */
+class DbResourceConfigWatch
+{
+ /** @var string */
+ protected $configFile;
+
+ /** @var string */
+ protected $resourceConfigFile;
+
+ /** @var string|null */
+ protected $dbResourceName;
+
+ /** @var array|null|false It's false on initialization to trigger */
+ protected $resourceConfig = false;
+
+ /** @var int|float */
+ protected $interval = 3;
+
+ /** @var callable[] */
+ protected $callbacks = [];
+
+ /**
+ * @param string $dbResourceName
+ * @return DbResourceConfigWatch
+ */
+ public static function name($dbResourceName)
+ {
+ $self = new static();
+ $self->dbResourceName = $dbResourceName;
+
+ return $self;
+ }
+
+ /**
+ * @param string $moduleName
+ * @return DbResourceConfigWatch
+ */
+ public static function module($moduleName)
+ {
+ $self = new static();
+ $self->configFile = Config::module($moduleName)->getConfigFile();
+ $self->resourceConfigFile = Config::app('resources')->getConfigFile();
+
+ return $self;
+ }
+
+ /**
+ * @param int|float $interval
+ * @return $this
+ */
+ public function setInterval($interval)
+ {
+ if (! \is_int($interval) && ! \is_float($interval)) {
+ throw new InvalidArgumentException(
+ '$interval needs to be either int or float'
+ );
+ }
+ $this->interval = $interval;
+
+ return $this;
+ }
+
+ /**
+ * @param callable $callable
+ * @return $this
+ */
+ public function notify($callable)
+ {
+ if (! \is_callable($callable)) {
+ throw new InvalidArgumentException('$callable needs to be callable');
+ }
+ $this->callbacks[] = $callable;
+
+ return $this;
+ }
+
+ /**
+ * @param LoopInterface $loop
+ */
+ public function run(LoopInterface $loop)
+ {
+ $check = function () {
+ $this->checkForFreshConfig();
+ };
+ $loop->addPeriodicTimer($this->interval, $check);
+ $loop->futureTick($check);
+ }
+
+ protected function checkForFreshConfig()
+ {
+ if ($this->configHasBeenChanged()) {
+ $this->emitNewConfig($this->resourceConfig);
+ }
+ }
+
+ protected function emitNewConfig($config)
+ {
+ foreach ($this->callbacks as $callback) {
+ $callback($config);
+ }
+ }
+
+ protected function getResourceName()
+ {
+ if ($this->dbResourceName) {
+ return $this->dbResourceName;
+ } else {
+ return $this->loadDbResourceName();
+ }
+ }
+
+ protected function loadDbResourceName()
+ {
+ $parsed = @\parse_ini_file($this->configFile, true);
+ if (isset($parsed['db']['resource'])) {
+ return $parsed['db']['resource'];
+ } else {
+ return null;
+ }
+ }
+
+ protected function loadDbConfigFromDisk($name)
+ {
+ if ($name === null) {
+ return null;
+ }
+
+ $parsed = @\parse_ini_file($this->resourceConfigFile, true);
+ if (isset($parsed[$name])) {
+ $section = $parsed[$name];
+ \ksort($section);
+
+ return $section;
+ } else {
+ return null;
+ }
+ }
+
+ protected function configHasBeenChanged()
+ {
+ $resource = $this->loadDbConfigFromDisk($this->loadDbResourceName());
+ if ($resource !== $this->resourceConfig) {
+ $this->resourceConfig = $resource;
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/vendor/gipfl/icinga-cli-daemon/src/FinishedProcessState.php b/vendor/gipfl/icinga-cli-daemon/src/FinishedProcessState.php
new file mode 100644
index 0000000..9ca5e5f
--- /dev/null
+++ b/vendor/gipfl/icinga-cli-daemon/src/FinishedProcessState.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace gipfl\IcingaCliDaemon;
+
+class FinishedProcessState
+{
+ /** @var int|null */
+ protected $exitCode;
+
+ /** @var int|null */
+ protected $termSignal;
+
+ public function __construct($exitCode, $termSignal)
+ {
+ $this->exitCode = $exitCode;
+ $this->termSignal = $termSignal;
+ }
+
+ public function succeeded()
+ {
+ return $this->exitCode === 0;
+ }
+
+ /**
+ * @return int|null
+ */
+ public function getExitCode()
+ {
+ return $this->exitCode;
+ }
+
+ public function getTermSignal()
+ {
+ return $this->termSignal;
+ }
+
+ public function getCombinedExitCode()
+ {
+ if ($this->exitCode === null) {
+ if ($this->termSignal === null) {
+ return 255;
+ } else {
+ return 255 + $this->termSignal;
+ }
+ } else {
+ return $this->exitCode;
+ }
+ }
+
+ public function getReason()
+ {
+ if ($this->exitCode === null) {
+ if ($this->termSignal === null) {
+ return 'Process died';
+ } else {
+ return 'Process got terminated with SIGNAL ' . $this->termSignal;
+ }
+ } else {
+ if ($this->exitCode === 0) {
+ return 'Process finished successfully';
+ } else {
+ return 'Process exited with exit code ' . $this->exitCode;
+ }
+ }
+ }
+}
diff --git a/vendor/gipfl/icinga-cli-daemon/src/IcingaCli.php b/vendor/gipfl/icinga-cli-daemon/src/IcingaCli.php
new file mode 100644
index 0000000..8aad4c6
--- /dev/null
+++ b/vendor/gipfl/icinga-cli-daemon/src/IcingaCli.php
@@ -0,0 +1,144 @@
+<?php
+
+namespace gipfl\IcingaCliDaemon;
+
+use Exception;
+use Evenement\EventEmitterTrait;
+use gipfl\Protocol\JsonRpc\Connection;
+use React\EventLoop\LoopInterface;
+use React\Promise\Deferred;
+use React\Promise\Stream;
+
+class IcingaCli
+{
+ use EventEmitterTrait;
+
+ /** @var IcingaCliRunner */
+ protected $runner;
+
+ /** @var Connection|null */
+ protected $rpc;
+
+ protected $arguments = [];
+
+ /** @var \React\Stream\WritableStreamInterface|null */
+ protected $stdin;
+
+ /** @var Deferred|null */
+ protected $deferredStdin;
+
+ /** @var \React\Stream\ReadableStreamInterface|null */
+ protected $stdout;
+
+ /** @var Deferred|null */
+ protected $deferredStdout;
+
+ /** @var \React\Stream\ReadableStreamInterface|null */
+ protected $stderr;
+
+ /** @var Deferred|null */
+ protected $deferredStderr;
+
+ public function __construct(IcingaCliRunner $runner = null)
+ {
+ if ($runner === null) {
+ $runner = new IcingaCliRunner();
+ }
+ $this->runner = $runner;
+ $this->init();
+ }
+
+ protected function init()
+ {
+ // Override this if you want.
+ }
+
+ public function setArguments($arguments)
+ {
+ $this->arguments = $arguments;
+
+ return $this;
+ }
+
+ public function getArguments()
+ {
+ return $this->arguments;
+ }
+
+ public function run(LoopInterface $loop)
+ {
+ $process = $this->runner->command($this->getArguments());
+ $canceller = function () use ($process) {
+ // TODO: first soft, then hard
+ $process->terminate();
+ };
+ $deferred = new Deferred($canceller);
+ $process->on('exit', function ($exitCode, $termSignal) use ($deferred) {
+ $state = new FinishedProcessState($exitCode, $termSignal);
+ if ($state->succeeded()) {
+ $deferred->resolve();
+ } else {
+ $deferred->reject(new Exception($state->getReason()));
+ }
+ });
+
+ $process->start($loop);
+ if ($this->deferredStdin instanceof Deferred) {
+ $this->deferredStdin->resolve($process->stdin);
+ } else {
+ $this->stdin = $process->stdin;
+ }
+ if ($this->deferredStdout instanceof Deferred) {
+ $this->deferredStdout->resolve($process->stdout);
+ } else {
+ $this->stdout = $process->stdout;
+ }
+ if ($this->deferredStderr instanceof Deferred) {
+ $this->deferredStderr->resolve($process->stderr);
+ } else {
+ $this->stderr = $process->stderr;
+ }
+ $this->emit('start', [$process]);
+
+ return $deferred->promise();
+ }
+
+ /**
+ * @return \React\Stream\WritableStreamInterface
+ */
+ public function stdin()
+ {
+ if ($this->stdin === null) {
+ $this->deferredStdin = new Deferred();
+ $this->stdin = Stream\unwrapWritable($this->deferredStdin->promise());
+ }
+
+ return $this->stdin;
+ }
+
+ /**
+ * @return \React\Stream\ReadableStreamInterface
+ */
+ public function stdout()
+ {
+ if ($this->stdout === null) {
+ $this->deferredStdout = new Deferred();
+ $this->stdout = Stream\unwrapReadable($this->deferredStdout->promise());
+ }
+
+ return $this->stdout;
+ }
+
+ /**
+ * @return \React\Stream\ReadableStreamInterface
+ */
+ public function stderr()
+ {
+ if ($this->stderr === null) {
+ $this->deferredStderr = new Deferred();
+ $this->stderr = Stream\unwrapReadable($this->deferredStderr->promise());
+ }
+
+ return $this->stderr;
+ }
+}
diff --git a/vendor/gipfl/icinga-cli-daemon/src/IcingaCliRpc.php b/vendor/gipfl/icinga-cli-daemon/src/IcingaCliRpc.php
new file mode 100644
index 0000000..473d4ed
--- /dev/null
+++ b/vendor/gipfl/icinga-cli-daemon/src/IcingaCliRpc.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace gipfl\IcingaCliDaemon;
+
+use Exception;
+use gipfl\Protocol\JsonRpc\Connection;
+use gipfl\Protocol\NetString\StreamWrapper;
+use React\ChildProcess\Process;
+
+class IcingaCliRpc extends IcingaCli
+{
+ /** @var IcingaCliRunner */
+ protected $runner;
+
+ /** @var Connection|null */
+ protected $rpc;
+
+ protected $arguments = [];
+
+ protected function init()
+ {
+ $this->on('start', function (Process $process) {
+ $netString = new StreamWrapper(
+ $process->stdout,
+ $process->stdin
+ );
+ $netString->on('error', function (Exception $e) {
+ $this->emit('error', [$e]);
+ });
+ $this->rpc()->handle($netString);
+ });
+ }
+
+ /**
+ * @return Connection
+ */
+ public function rpc()
+ {
+ if ($this->rpc === null) {
+ $this->rpc = new Connection();
+ }
+
+ return $this->rpc;
+ }
+}
diff --git a/vendor/gipfl/icinga-cli-daemon/src/IcingaCliRunner.php b/vendor/gipfl/icinga-cli-daemon/src/IcingaCliRunner.php
new file mode 100644
index 0000000..60a25cc
--- /dev/null
+++ b/vendor/gipfl/icinga-cli-daemon/src/IcingaCliRunner.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace gipfl\IcingaCliDaemon;
+
+use React\ChildProcess\Process;
+use gipfl\Cli\Process as CliProcess;
+
+class IcingaCliRunner
+{
+ /** @var string */
+ protected $binary;
+
+ /** @var string|null */
+ protected $cwd;
+
+ /** @var array|null */
+ protected $env;
+
+ public function __construct($binary = null)
+ {
+ if ($binary === null) {
+ $this->binary = CliProcess::getBinaryPath();
+ $this->cwd = CliProcess::getInitialCwd();
+ } else {
+ $this->binary = $binary;
+ }
+ }
+
+ /**
+ * @param mixed array|...$arguments
+ * @return Process
+ */
+ public function command($arguments = null)
+ {
+ if (! \is_array($arguments)) {
+ $arguments = \func_get_args();
+ }
+
+ return new Process(
+ $this->escapedCommand($arguments),
+ $this->cwd,
+ $this->env
+ );
+ }
+
+ /**
+ * @param string|null $cwd
+ */
+ public function setCwd($cwd)
+ {
+ if ($cwd === null) {
+ $this->cwd = $cwd;
+ } else {
+ $this->cwd = (string) $cwd;
+ }
+ }
+
+ /**
+ * @param array|null $env
+ */
+ public function setEnv($env)
+ {
+ if ($env === null) {
+ $this->env = $env;
+ } else {
+ $this->env = (array) $env;
+ }
+ }
+
+ /**
+ * @param $arguments
+ * @return string
+ */
+ protected function escapedCommand($arguments)
+ {
+ $command = ['exec', \escapeshellcmd($this->binary)];
+
+ foreach ($arguments as $argument) {
+ if (\ctype_alnum(preg_replace('/^-{1,2}/', '', $argument))) {
+ $command[] = $argument;
+ } else {
+ $command[] = \escapeshellarg($argument);
+ }
+ }
+
+ return \implode(' ', $command);
+ }
+}
diff --git a/vendor/gipfl/icinga-cli-daemon/src/RetryUnless.php b/vendor/gipfl/icinga-cli-daemon/src/RetryUnless.php
new file mode 100644
index 0000000..0303b2c
--- /dev/null
+++ b/vendor/gipfl/icinga-cli-daemon/src/RetryUnless.php
@@ -0,0 +1,244 @@
+<?php
+
+namespace gipfl\IcingaCliDaemon;
+
+use Exception;
+use Icinga\Application\Logger;
+use React\EventLoop\LoopInterface;
+use React\EventLoop\TimerInterface;
+use React\Promise\Deferred;
+use React\Promise\PromiseInterface;
+use RuntimeException;
+
+class RetryUnless
+{
+ /** @var LoopInterface */
+ protected $loop;
+
+ /** @var Deferred */
+ protected $deferred;
+
+ /** @var TimerInterface */
+ protected $timer;
+
+ /** @var callable */
+ protected $callback;
+
+ /** @var bool */
+ protected $expectsSuccess;
+
+ /** @var int Regular interval */
+ protected $interval = 1;
+
+ /** @var int|null Optional, interval will be changed after $burst attempts */
+ protected $burst = null;
+
+ /** @var int|null Interval after $burst attempts */
+ protected $finalInterval = null;
+
+ /** @var int Current attempt count */
+ protected $attempts = 0;
+
+ /** @var bool No attempts will be made while paused */
+ protected $paused = false;
+
+ protected $lastError;
+
+ protected function __construct($callback, $expectsSuccess = true)
+ {
+ $this->callback = $callback;
+ $this->expectsSuccess = $expectsSuccess;
+ }
+
+ public static function succeeding($callback)
+ {
+ return new static($callback);
+ }
+
+ public static function failing($callback)
+ {
+ return new static($callback, false);
+ }
+
+ public function run(LoopInterface $loop)
+ {
+ $this->assertNotRunning();
+ $this->deferred = $deferred = new Deferred();
+ $this->loop = $loop;
+ $loop->futureTick(function () {
+ $this->nextAttempt();
+ });
+
+ return $deferred->promise();
+ }
+
+ public function getLastError()
+ {
+ return $this->lastError;
+ }
+
+ public function setInterval($interval)
+ {
+ $this->interval = $interval;
+
+ return $this;
+ }
+
+ public function slowDownAfter($burst, $interval)
+ {
+ $this->burst = $burst;
+ $this->finalInterval = $interval;
+
+ return $this;
+ }
+
+ public function pause()
+ {
+ $this->removeEventualTimer();
+ $this->paused = true;
+
+ return $this;
+ }
+
+ public function resume()
+ {
+ if ($this->paused) {
+ $this->paused = false;
+ if ($this->timer === null) {
+ $this->nextAttempt();
+ }
+ }
+ }
+
+ public function reset()
+ {
+ $this->attempts = 0;
+ $this->paused = false;
+ $this->removeEventualTimer();
+ $this->rejectEventualDeferred('RetryUnless has been reset');
+
+ return $this;
+ }
+
+ public function getAttempts()
+ {
+ return $this->attempts;
+ }
+
+ protected function nextAttempt()
+ {
+ if ($this->paused) {
+ return;
+ }
+
+ $this->removeEventualTimer();
+ $this->attempts++;
+ try {
+ $callback = $this->callback;
+ $this->handleResult($callback());
+ } catch (Exception $e) {
+ $this->handleResult($e);
+ }
+ }
+
+ protected function logError(Exception $e)
+ {
+ if ($this->lastError !== $e->getMessage()) {
+ $this->lastError = $e->getMessage();
+ Logger::error($e);
+ }
+ }
+
+ protected function handleResult($result)
+ {
+ if ($this->expectsSuccess) {
+ if ($result instanceof Exception) {
+ $this->logError($result);
+ $this->scheduleNextAttempt();
+ } elseif ($result instanceof PromiseInterface) {
+ $later = function ($result) {
+ $this->handleResult($result);
+ };
+ $result->then($later, $later);
+ } else {
+ $this->succeed($result);
+ }
+ } else {
+ if ($result instanceof Exception) {
+ $this->succeed($result);
+ } else {
+ $this->scheduleNextAttempt();
+ }
+ }
+ }
+
+ protected function scheduleNextAttempt()
+ {
+ if ($this->timer !== null) {
+ throw new RuntimeException(
+ 'RetryUnless schedules next attempt while already scheduled'
+ );
+ }
+ $this->timer = $this->loop->addTimer($this->getNextInterval(), function () {
+ $this->nextAttempt();
+ });
+ }
+
+ protected function succeed($result)
+ {
+ $this->removeEventualTimer();
+ if ($this->deferred === null) {
+ Logger::warning('RetryUnless tries to resolve twice');
+
+ return;
+ }
+ $this->deferred->resolve($result);
+ $this->deferred = null;
+ $this->reset();
+ }
+
+ protected function getNextInterval()
+ {
+ if ($this->burst === null) {
+ return $this->interval;
+ }
+
+ return $this->attempts >= $this->burst
+ ? $this->finalInterval
+ : $this->interval;
+ }
+
+ protected function assertNotRunning()
+ {
+ if ($this->deferred) {
+ throw new RuntimeException(
+ 'Cannot re-run RetryUnless while already running'
+ );
+ }
+ }
+
+ protected function removeEventualTimer()
+ {
+ if ($this->timer) {
+ $this->loop->cancelTimer($this->timer);
+ $this->timer = null;
+ }
+ }
+
+ protected function rejectEventualDeferred($reason)
+ {
+ if ($this->deferred !== null) {
+ $deferred = $this->deferred;
+ $this->deferred = null;
+ $deferred->reject($reason);
+ }
+ }
+
+ public function __destruct()
+ {
+ $this->removeEventualTimer();
+ $this->rejectEventualDeferred('RetryUnless has been destructed');
+
+ $this->loop = null;
+ }
+}
diff --git a/vendor/gipfl/icinga-cli-daemon/src/StateMachine.php b/vendor/gipfl/icinga-cli-daemon/src/StateMachine.php
new file mode 100644
index 0000000..d8ec470
--- /dev/null
+++ b/vendor/gipfl/icinga-cli-daemon/src/StateMachine.php
@@ -0,0 +1,113 @@
+<?php
+
+namespace gipfl\IcingaCliDaemon;
+
+use RuntimeException;
+
+trait StateMachine
+{
+ /** @var string */
+ private $currentState;
+
+ /** @var array [fromState][toState] = [callback, ...] */
+ private $allowedTransitions = [];
+
+ /** @var array [state] = [callback, ...] */
+ private $onState = [];
+
+ public function initializeStateMachine($initialState)
+ {
+ if ($this->currentState !== null) {
+ throw new RuntimeException('StateMachine has already been initialized');
+ }
+ $this->currentState = $initialState;
+ }
+
+ /**
+ * @param string|array $fromState
+ * @param string $toState
+ * @param callable $callback
+ * @return $this
+ */
+ public function onTransition($fromState, $toState, $callback)
+ {
+ if (is_array($fromState)) {
+ foreach ($fromState as $state) {
+ $this->onTransition($state, $toState, $callback);
+ }
+ } else {
+ $this->allowTransition($fromState, $toState);
+ $this->allowedTransitions[$fromState][$toState][] = $callback;
+ }
+
+ return $this;
+ }
+
+ public function allowTransition($fromState, $toState)
+ {
+ if (! isset($this->allowedTransitions[$fromState][$toState])) {
+ $this->allowedTransitions[$fromState][$toState] = [];
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param $state
+ * @param $callback
+ * @return $this
+ */
+ public function onState($state, $callback)
+ {
+ if (! isset($this->onState[$state])) {
+ $this->onState[$state] = [];
+ }
+
+ $this->onState[$state][] = $callback;
+
+ return $this;
+ }
+
+ public function getState()
+ {
+ if ($this->currentState === null) {
+ throw new RuntimeException('StateMachine has not been initialized');
+ }
+
+ return $this->currentState;
+ }
+
+ public function setState($state)
+ {
+ $fromState = $this->getState();
+ if ($this->canTransit($fromState, $state)) {
+ $this->currentState = $state;
+ $this->runStateTransition($fromState, $state);
+ } else {
+ throw new RuntimeException(sprintf(
+ 'A transition from %s to %s is not allowed',
+ $fromState,
+ $state
+ ));
+ }
+ }
+
+ private function runStateTransition($fromState, $toState)
+ {
+ if (isset($this->allowedTransitions[$fromState][$toState])) {
+ foreach ($this->allowedTransitions[$fromState][$toState] as $callback) {
+ $callback();
+ }
+ }
+ if (isset($this->onState[$toState])) {
+ foreach ($this->onState[$toState] as $callback) {
+ $callback();
+ }
+ }
+ }
+
+ public function canTransit($fromState, $toState)
+ {
+ return isset($this->allowedTransitions[$fromState][$toState]);
+ }
+}