Added file configuration parser and LunchCondition objects

This commit is contained in:
Zi Xing 2021-12-09 16:58:50 -05:00
parent 7d47bc7564
commit 1e16b28dad
17 changed files with 1378 additions and 0 deletions

60
README.md Normal file
View File

@ -0,0 +1,60 @@
# Software Service Manager
Software Service Manager, also known as `ssm` or `ssmctl` is an alternative
to systemd and supervisord, allowing the configuration & management of services
with the use of `.service` files located under `/etc/ssm` This software works
great for containerized software and or systems that simply don't like systemd.
---------------------------------------------------------------------------------
# Configuration Documentation
This part of the README will contain basic documentation on how to write and
configure your own service to run.
## Service Files
Service files also files that ends with `.service` are going to have to be located
under `/etc/ssm` for ssm to identify and register services. This means to manually
or automatically register a service simply requires a `.service` file to be created
in `/etc/ssm` with the file name being the service name, for example
`/etc/ssm/htop.service` will allow you to run commands such as `ssmctl start htop`
If the main `ssm` process is running, it will automatically detect changes and apply
it accordingly. If the service is already running, changes will not be applied until
the service restarts.
The format of a `.service` file is simply a INI file format and this part of the
documentation will explain what sections can be configured, and examples. The options
provided by `ssm` are similar to what `systemd` provides.
### Lunch Condition
A lunch condition is applied to many sections of the file that you can optionally add,
this includes the main `start` condition that represents the main execution point of
your service configuration, there can be many other lunch conditions that can be
executed for example before and after your `start` condition runs, or before and after
restart and stop conditions. Allowing you to fully customize and handle the service
execution flow. Below is a table of options that a Lunch Condition section can consist
of but note that some options may only be applicable to `start` and other lunch
conditions. Not all options are required.
| Parameter | Example Value | Default Value | Description |
|--------------------------|------------------------------------------------|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Exec | `/usr/bin/htop` | `null` | The main execution point of the lunch condition |
| Args | `GIT_API_KEY=Hello!:TEST_PARTY=netkas\:netkas` | `null` | Command-line Arguments to pass on to the execution point, separated by `:` to escape the separator use `\:`, in this example the following arguments are passed: `GIT_API_KEY=Hello!` & `TEST_PARTY=netkas:netkas` |
| Env | `GIT_API_KEY=Hello!:TEST_PARTY=netkas\:netkas` | `null` | Environment variables to apply to the execution point, separated by `:`, to escape the separator use `\:`, in this example the following arguments are passed: `GIT_API_KEY=Hello!` & `TEST_PARTY=netkas:netkas` |
| Cwd | `/usr/bin` | `null` | The working directory that the process should be running in if configured |
| Restart | `always` | `on-failure` | Configures whether the executed process shall be restarted when the process exits, is killed, or a timeout reached. Takes one of the `no`, `on-success`, `on-failure`, `unless-stopped` or `always`. `no` Means the process will not automatically restart for any reason whatsoever. `on-failure` will only restart if the process's lunch condition indicates failure. `unless-stopped` will always restart the process unless stopped manually this is only applicable to the `start` lunch condition. `always` will always restart the process, this is only applicable to the `start` lunch condition. |
| MaxFailureRestarts | `3` | `infinity` | If `Restart` is configured to `always` or `on-failure`, this indicates how many times can the process restart due to failure conditions before it is considered failed and stops the service. |
| MaxAutomaticRestarts | `3` | `infinity` | If `Restart` is configured to `alyways` or `on-success` or and if `MaxFailureRestarts` is set to `infinity` this indicates how many times the process can restart due to failure and or success conditions before it is considered failed and stops the service. |
| TimeoutStartSec | `60` | `infinity` | ONLY APPLICABLE TO `start`'s lunch condition. Indicates the time to wait for a start-up, this is conditioned by other lunch parameters such as `start_pre` where if the pre conditions are not completed within the configured time (seconds) then the service will be considered failed and shutdown again. |
| TimeoutStopSec | `60` | `infinity` | ONLY APPLICABLE TO `start`'s lunch condition. Indicates the time to wait for the shutdown, this is conditioned by other lunch parameters such as `stop_pre` & `stop_post` and including the main shutdown process of `start`, where if all condtions fails to exit or finish executing during the configured time (seconds) then the service will be considered failed and forced to shutdown. |
| RuntimeMaxSec | `60` | `infinity` | Configured the maximum time for a service to run, if this is used and the process has been active for longer than the specified time then it will be terminated and put into a failure state, this value's unit is in seconds. |
| SuccessExitStatus | `0 75 250` | `0 75 250` | Takes a list of exit status codes that, when returned by the executed process, will be considered a successful termination, in addition to the normal successful exit status 0. These exit codes are separated by spaces and only accepts integers, anything else will be ignored. |
| ErrorExitStatus | `1` | `1` | Takes a list of exit status codes that, when returned by the executed process will be considered an error, in addition to the the exit code 1. These exit codes are separated by spaces and only accepts integers, anything else will be ignored. |
| RestartPreventExitStatus | `1` | `null` | Takes a list of exit status codes that, when returned by the executed process will prevent automatic service restarts, regardless of the restart setting configured with `Restart`, these exit codes are separated by spaces and only accepts integers, anything else will be ignored. |
| RestartForceExitStatus | `1` | `null` | Takes a list of exit status definitions that, when returned by the executed process will force automatic service restarts regardless of the restart setting configured with `Restart`, these exit codes are separated by spaces and only accepts integers, anything else will be ignored. |
| LogStdout | `true` | `true` | Indicates if the standard output from this executed process should be logged or ignored, this only accepts `true` or `false` as a value. Logging is configured globally with in `logging` section of the service's configuration file. |
| LogStderr | `true` | `true` | Indicates if the standard error output from this executed process should be logged or ignored, this only accepts `true` or `false` as a value. Logging is configured globally with in `logging` section of the service's configuration file. |

File diff suppressed because one or more lines are too long

3
scratch/example.service Normal file
View File

@ -0,0 +1,3 @@
[start]
Exec=/usr/bin/htop
Restart=always

3
scratch/proc_test.php Normal file
View File

@ -0,0 +1,3 @@
<?php
$a = new \ProcLib\Process('', '')

View File

@ -0,0 +1,31 @@
<?php
namespace ssm\Abstracts;
abstract class RestartCondition
{
/**
* the process will not automatically restart for any reason whatsoever
*/
const No = 'no';
/**
* will only restart if the process's lunch condition indicates success.
*/
const OnSuccess = 'on-success';
/**
* will only restart if the process's lunch condition indicates failure.
*/
const OnFailure = 'on-failure';
/**
* will always restart the process unless stopped manually this is only applicable to the start lunch condition
*/
const UnlessStopped = 'unless-stopped';
/**
* will always restart the process, this is only applicable to the start lunch condition.
*/
const Always = 'always';
}

View File

@ -0,0 +1,8 @@
<?php
namespace ssm\Abstracts;
abstract class Time
{
const Infinity = 'infinity';
}

View File

@ -0,0 +1,231 @@
<?php
namespace ssm\Classes;
use ssm\Exceptions\InvalidDataException;
use ssm\Objects\IniSection;
class IniParser
{
const SECTION_INHERITANCE_OPERATOR = ':';
/**
* @var IniParser
*/
protected static $instance;
/**
* @var string
*/
protected $invalidCharsForItemNames = '?{}|&~!()^"';
/**
* @var array
*/
protected $invalidItemNames = ['null', 'yes', 'no', 'true', 'false', 'on', 'off', 'none'];
/**
* @return IniParser
*/
public static function i()
{
return static::getInstance();
}
/**
* @return IniParser
*/
public static function getInstance(): IniParser
{
if (is_null(static::$instance))
{
static::$instance = new static();
}
return static::$instance;
}
/**
* @param string $iniString
* @param string $localIniString
* @return IniSection[]
* @throws InvalidDataException
*/
public function parseIniString(string $iniString, string $localIniString = ''): array
{
$rawContents = [];
$iniStrings = ['ini' => $iniString, 'local' => $localIniString];
foreach ($iniStrings as $key => $string)
{
if (strlen($string) > 0)
{
$rawContents[$key] = @parse_ini_string($string, true, INI_SCANNER_RAW);
if (false === $rawContents[$key])
{
throw new InvalidDataException('Error parsing ini string!');
}
}
else
{
$rawContents[$key] = [];
}
}
$rawContents = array_replace_recursive($rawContents['ini'], $rawContents['local']);
return $this->parseArray($rawContents);
}
/**
* Cast item string value to proper type
*
* @param string $value
* @return bool|float|int|string|null
*/
public function castItemValueToProperType(string $value)
{
$normalized = $value;
if (in_array($value, ['true', 'on', 'yes']))
{
$normalized = true;
}
elseif (in_array($value, ['false', 'off', 'no', 'none']))
{
$normalized = false;
}
elseif ('null' == $value)
{
$normalized = null;
}
elseif (is_numeric($value))
{
$number = $value + 0;
if (intval($number) == $number)
{
$normalized = (int)$number;
}
elseif (floatval($number) == $number)
{
$normalized = (float)$number;
}
}
elseif (is_array($value))
{
foreach ($value as $itemKey => $itemValue)
{
$normalized[$itemKey] = $this->castItemValueToProperType($itemValue);
}
}
return $normalized;
}
/**
* Get an string (or an array of strings) representation of $value
*
* @param bool|int|float|null|array $value
*
* @return array|string
* @throws InvalidDataException
*/
public function itemValuetoStringRepresentation($value)
{
if (is_bool($value))
{
$castedValue = (true === $value) ? 'true' : 'false';
}
elseif (is_null($value))
{
$castedValue = 'null';
}
elseif (is_array($value))
{
$castedValue = [];
foreach ($value as $k => $v)
{
$castedValue[$k] = $this->itemValuetoStringRepresentation($v);
}
}
elseif (is_numeric($value))
{
$castedValue = (string)$value;
}
elseif (is_string($value))
{
$castedValue = $value;
}
else
{
throw new InvalidDataException('Invalid item value type!');
}
return $castedValue;
}
/**
* @param string $name
* @return array [validationResult, message]
*/
public function validateItemName(string $name): array
{
$valid = true;
$message = 'Valid item name.';
if (!is_string($name))
{
$valid = false;
$message = 'Only string values are allowed for item names!';
}
if (in_array($name, $this->invalidItemNames))
{
$valid = false;
$message = sprintf('Item name is not allowed! Not allowed item names: %s',
implode(', ', $this->invalidItemNames));
}
if (preg_match(sprintf('/[%s]/', $this->invalidCharsForItemNames), $name) > 0)
{
$valid = false;
$message = sprintf('Invalid name for ini item! Provided item name contains not ' .
'allowed chars (%s).', $this->invalidCharsForItemNames);
}
return [$valid, $message];
}
/**
* @param array $rawContents
* @return IniSection[]
* @throws InvalidDataException
*/
public function parseArray(array $rawContents): array
{
$parsedContents = [];
foreach ($rawContents as $sectionFullName => $sectionContents)
{
$pieces = explode(self::SECTION_INHERITANCE_OPERATOR, $sectionFullName, 2);
$sectionName = trim($pieces[0]);
if (!is_array($sectionContents))
{
throw new InvalidDataException(sprintf('Orphan fields are not allowed! ' .
'Please define a section for field "%s".',
$sectionName));
}
$parsedContents[$sectionName] = new IniSection($sectionName);
$parsedContents[$sectionName]->setContents($sectionContents);
$parentName = isset($pieces[1]) ? trim($pieces[1]) : null;
if (!is_null($parentName))
{
if (!isset($parsedContents[$parentName]))
{
throw new InvalidDataException(sprintf('Parent section not found! ' .
'Define "%s" section before "%s" section.',
$parentName, $sectionName));
}
$parsedContents[$sectionName]->setParent($parsedContents[$parentName]);
}
}
return $parsedContents;
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace ssm\Exceptions;
use Exception;
use Throwable;
class FileException extends Exception
{
/**
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
public function __construct($message = "", $code = 0, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->message = $message;
$this->code = $code;
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace ssm\Exceptions;
use Exception;
use Throwable;
class InvalidDataException extends Exception
{
/**
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
public function __construct($message = "", $code = 0, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->message = $message;
$this->code = $code;
}
}

224
src/ssm/Objects/IniFile.php Normal file
View File

@ -0,0 +1,224 @@
<?php
namespace ssm\Objects;
use ssm\Classes\IniParser;
use ssm\Exceptions\FileException;
use ssm\Exceptions\InvalidDataException;
class IniFile
{
/**
* @var string
*/
protected $file;
/**
* @var IniParser;
*/
protected $parser;
/**
* @var IniSection[]
*/
protected $sections = [];
/**
* IniFile constructor.
*
* @param string|null $file
* @throws InvalidDataException
*/
public function __construct(?string $file=null)
{
$localRawContents = '';
$this->parser = IniParser::i();
if(is_null($file) == false)
{
$rawContents = file_get_contents($file);
$this->file = $file;
$this->sections = $this->parser->parseIniString($rawContents, $localRawContents);
}
}
/**
* @param string $file
* @return IniFile
*/
public static function load(string $file): IniFile
{
return new self($file);
}
/**
* @param array $data
* @return IniFile
* @throws InvalidDataException
*/
public static function fromArray(array $data): IniFile
{
$iniSections = IniParser::i()->parseArray($data);
return self::fromIniSections($iniSections);
}
/**
* @param IniSection[] $iniSections
* @return IniFile
* @throws InvalidDataException
*/
public static function fromIniSections(array $iniSections): IniFile
{
$iniFile = new IniFile();
foreach ($iniSections as $iniSection)
{
if (false === $iniSection instanceof IniSection)
{
throw new InvalidDataException('The service file contains invalid data');
}
$iniFile->addSection($iniSection);
}
return $iniFile;
}
/**
* @param string|null $outputFile
* @throws FileException
*/
public function save(string $outputFile = null)
{
if (is_null($outputFile))
{
if (is_null($this->file))
{
throw new FileException('No output file set! Please, set an output file.');
}
$outputFile = $this->file;
}
if (is_file($outputFile) && !is_writable($outputFile))
{
throw new FileException(sprintf('Output file "%s" is not writable!', $outputFile));
}
$result = file_put_contents($outputFile, $this->toString());
if (false === $result)
{
throw new FileException(sprintf('Error writing file "%s"!', $outputFile));
}
}
/**
* @param IniSection $section
* @return $this
* @throws InvalidDataException
* @noinspection PhpUnused
*/
public function addSection(IniSection $section): IniFile
{
if ($this->hasSection($section->getName()))
{
throw new InvalidDataException(sprintf('Section "%s" already exists!',
$section->getName()));
}
if ($section->hasParent())
{
if (!isset($this->sections[$section->getParent()->getName()]))
{
throw new InvalidDataException(sprintf('Parent section "%s" does not exists!',
$section->getParent()->getName()));
}
}
$this->sections[$section->getName()] = $section;
return $this;
}
/**
* @param string $sectionName
* @return bool
*/
public function hasSection(string $sectionName): bool
{
return isset($this->sections[$sectionName]);
}
/**
* @param string $sectionName
*
* @return IniSection
* @throws InvalidDataException
* @noinspection PhpMissingParamTypeInspection
*/
public function getSection($sectionName): IniSection
{
if (!$this->hasSection($sectionName))
{
throw new InvalidDataException(sprintf('Section "%s" does not exists!', $sectionName));
}
return $this->sections[$sectionName];
}
/**
* Get normalized item value
*
* @param string $sectionName
* @param string $itemName
* @param mixed $defaultValue
* @return array|bool|float|int|string|null
* @throws InvalidDataException
*/
public function get(string $sectionName, string $itemName, $defaultValue = null)
{
$section = $this->getSection($sectionName);
return $section->get($itemName, $defaultValue);
}
/**
* @param string $sectionName
* @param string $itemName
* @param string $itemValue
* @return $this
* @throws InvalidDataException
*/
public function set(string $sectionName, string $itemName, string $itemValue): IniFile
{
$section = $this->getSection($sectionName);
$section->set($itemName, $itemValue);
return $this;
}
/**
* @return array
*/
public function toArray(): array
{
$data = [];
foreach ($this->sections as $sectionName => $section)
{
$data[$sectionName] = $section->toArray();
}
return $data;
}
/**
* @return string
*/
public function toString(): string
{
$contents = [];
foreach ($this->sections as $section)
{
$contents[] = $section->toString();
}
return implode(PHP_EOL, $contents);
}
}

View File

@ -0,0 +1,273 @@
<?php
namespace ssm\Objects;
use ssm\Classes\IniParser;
use ssm\Exceptions\InvalidDataException;
class IniSection
{
/**
* @var string
*/
protected $name;
/**
* @var IniSection|null
*/
protected $parent;
/**
* @var array
*/
protected $contents = [];
/**
* Section constructor.
*
* @param string $name
* @param IniSection|null $parent
* @noinspection PhpMissingParamTypeInspection
*/
public function __construct($name, IniSection $parent = null)
{
$this->name = $name;
$this->parent = $parent;
}
/**
* @param IniSection $parent
*
* @return $this
*/
public function setParent(IniSection $parent): IniSection
{
$this->parent = $parent;
return $this;
}
/**
* @param array $data
* @return $this
* @throws InvalidDataException
* @noinspection PhpMissingParamTypeInspection
*/
public function setContents($data): IniSection
{
if (!is_array($data))
{
throw new InvalidDataException('Invalid section contents! ' .
'Section contents must be an array.');
}
$this->contents = IniParser::i()->itemValuetoStringRepresentation($data);
return $this;
}
/**
* @return array
*/
protected function getContents(): array
{
return $this->contents;
}
/**
* @return bool
*/
public function hasParent(): bool
{
return ($this->parent instanceof IniSection);
}
/**
* @return IniSection|null
*/
public function getParent(): ?IniSection
{
return $this->parent;
}
/**
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* @return IniSection[]
*/
protected function getParents(): array
{
$parents = [];
$currentSection = $this;
while ($currentSection->hasParent())
{
$parent = $currentSection->getParent();
$parents[] = $parent;
$currentSection = $parent;
}
rsort($parents);
return $parents;
}
/**
* @return array
*/
protected function composeContents(): array
{
$contents = [];
$parents = $this->getParents();
foreach ($parents as $section)
{
$contents = array_merge($contents, $section->getContents());
}
return array_merge($contents, $this->getContents());
}
/**
* Get normalized item value
*
* @param string $itemName
* @param mixed $defaultValue
* @return array|bool|float|int|string|null
*/
public function get(string $itemName, $defaultValue = null)
{
$contents = $this->composeContents();
/** @noinspection PhpIssetCanBeReplacedWithCoalesceInspection */
$value = isset($contents[$itemName]) ? $contents[$itemName] : $defaultValue;
return IniParser::i()->castItemValueToProperType($value);
}
/**
* @param string $itemName
* @param string|array|bool|null $itemValue
*
* @return $this
* @throws InvalidDataException
*/
public function set(string $itemName, $itemValue): IniSection
{
list($validationResult, $message) = IniParser::i()->validateItemName($itemName);
if (false === $validationResult)
{
throw new InvalidDataException($message);
}
$this->contents[$itemName] = IniParser::i()->itemValuetoStringRepresentation($itemValue);
return $this;
}
/**
* @param string $itemName
* @return bool
* @noinspection PhpMissingParamTypeInspection
* @noinspection PhpRedundantOptionalArgumentInspection
* @noinspection PhpUnused
*/
public function hasItem($itemName): bool
{
$value = $this->get($itemName, null);
return !is_null($value);
}
/**
* @return array
*/
public function toArray(): array
{
return $this->composeContents();
}
/**
* @return string
*/
public function toString(): string
{
$lines = $this->renderName();
foreach ($this->contents as $itemName => $itemValue)
{
$lines = array_merge($lines, $this->renderItem($itemName, $itemValue));
}
return implode(PHP_EOL, $lines) . PHP_EOL;
}
/**
* @return string[]
*/
protected function renderName(): array
{
if ($this->hasParent())
{
$line = [sprintf('[%s : %s]', $this->getName(), $this->getParent()->getName())];
}
else
{
$line = [sprintf('[%s]', $this->getName())];
}
return $line;
}
/**
* @param string $name
* @param string|array $value
* @return array
* @noinspection PhpMissingParamTypeInspection
*/
protected function renderItem($name, $value): array
{
if (is_array($value))
{
$lines = $this->renderArrayItem($name, $value);
}
else
{
$lines = $this->renderStringItem($name, $value);
}
return $lines;
}
/**
* @param string $name
* @param string $value
* @return string[]
* @noinspection PhpMissingParamTypeInspection
*/
protected function renderStringItem($name, $value): array
{
return [sprintf('%s = "%s"', $name, $value)];
}
/**
* @param string $name
* @param array $values
* @return array
* @noinspection PhpMissingParamTypeInspection
*/
protected function renderArrayItem($name, array $values): array
{
$lines = [];
$isAssocArray = (array_values($values) !== $values);
foreach ($values as $key => $value)
{
$stringKey = $isAssocArray ? $key : '';
$lines[] = sprintf('%s[%s] = "%s"', $name, $stringKey, $value);
}
return $lines;
}
}

View File

@ -0,0 +1,292 @@
<?php
namespace ssm\Objects;
use ssm\Abstracts\RestartCondition;
use ssm\Abstracts\Time;
use ssm\Utilities\Parser;
class LunchCondition
{
/**
* The main execution point of the lunch condition
*
* @var string|null
*/
public $Exec = null;
/**
* Command-line Arguments to pass on to the execution point, separated by `:`
* to escape the separator use `\:`, in this example the following arguments
* are passed: `GIT_API_KEY=Hello!` & `TEST_PARTY=netkas:netkas`
*
* @var array|null
*/
public $Arguments = null;
/**
* Environment variables to apply to the execution point, separated by :, to escape
* the separator use \:, in this example the following arguments are passed:
* GIT_API_KEY=Hello! & TEST_PARTY=netkas:netkas
*
* @var array|null
*/
public $EnvironmentVariables = null;
/**
* The working directory that the process should be running in if configured
*
* @var string|null
*/
public $CurrentWorkingDirectory = null;
/**
* Configures whether the executed process shall be restarted when the process exits, is killed,
* or a timeout reached. Takes one of the no, on-success, on-failure, unless-stopped or always.
* no Means the process will not automatically restart for any reason whatsoever. on-failure will
* only restart if the process's lunch condition indicates failure. unless-stopped will always
* restart the process unless stopped manually this is only applicable to the start lunch condition.
* always will always restart the process, this is only applicable to the start lunch condition.
*
* @var string
*/
public $Restart = RestartCondition::OnFailure;
/**
* If Restart is configured to always or on-failure, this indicates how many times can the process
* restart due to failure conditions before it is considered failed and stops the service.
*
* @var string|int
*/
public $MaxFailureRestarts = Time::Infinity;
/**
* If Restart is configured to always or on-success or and if MaxFailureRestarts is set to infinity
* this indicates how many times the process can restart due to failure and or success conditions
* before it is considered failed and stops the service.
*
* @var string|int
*/
public $MaxAutomaticRestarts = Time::Infinity;
/**
* ONLY APPLICABLE TO start's lunch condition. Indicates the time to wait for a start-up, this is
* conditioned by other lunch parameters such as start_pre where if the preconditions are not
* completed within the configured time (seconds) then the service will be considered failed
* and shutdown again.
*
* @var string|int
*/
public $TimeoutStartSeconds = Time::Infinity;
/**
* ONLY APPLICABLE TO start's lunch condition. Indicates the time to wait for the shutdown, this
* is conditioned by other lunch parameters such as stop_pre & stop_post and including the main
* shutdown process of start, where if all conditions fails to exit or finish executing during the
* configured time (seconds) then the service will be considered failed and forced to shut down.
*
* @var string|int
*/
public $TimeoutStopSeconds = Time::Infinity;
/**
* Configured the maximum time for a service to run, if this is used and the process has been
* active for longer than the specified time then it will be terminated and put into a failure
* state, this value's unit is in seconds.
*
* @var string|int
*/
public $RuntimeMaxSeconds = Time::Infinity;
/**
* Takes a list of exit status codes that, when returned by the executed process, will be
* considered a successful termination, in addition to the normal successful exit status 0.
* These exit codes are separated by spaces and only accepts integers, anything else will be ignored.
*
* @var int[]
*/
public $SuccessExitStatus = [0, 75, 250];
/**
* Takes a list of exit status codes that, when returned by the executed process will be considered an
* error, in addition to the exit code 1. These exit codes are separated by spaces and only
* accepts integers, anything else will be ignored.
*
* @var int[]
*/
public $ErrorExitStatus = [1];
/**
* Takes a list of exit status codes that, when returned by the executed process will prevent automatic
* service restarts, regardless of the restart setting configured with Restart, these exit codes are
* separated by spaces and only accepts integers, anything else will be ignored.
*
* @var int[]|null
*/
public $RestartPreventExitStatus = null;
/**
* Takes a list of exit status definitions that, when returned by the executed process will force automatic
* service restarts regardless of the restart setting configured with Restart, these exit codes are
* separated by spaces and only accepts integers, anything else will be ignored.
*
* @var int[]|null
*/
public $RestartForceExitStatus = null;
/**
* Indicates if the standard output from this executed process should be logged or ignored, this only accepts
* true or false as a value. Logging is configured globally with in logging section of the service's
* configuration file.
*
* @var bool
*/
public $LogStdout = true;
/**
* Indicates if the standard error output from this executed process should be logged or ignored, this only
* accepts true or false as a value. Logging is configured globally with in logging section of the service's
* configuration file.
*
* @var bool
*/
public $LogStderr = true;
/**
* Returns an array representation of the object
*
* @return array
*/
public function toArray(): array
{
return [
'Exec' => $this->Exec,
'Args' => implode(':', $this->Arguments),
'Env' => implode(':', $this->EnvironmentVariables),
'Cwd' => $this->CurrentWorkingDirectory,
'Restart' => $this->Restart,
'MaxFailureRestarts' => $this->MaxFailureRestarts,
'MaxAutomaticRestarts' => $this->MaxAutomaticRestarts,
'TimeoutStartSec' => $this->TimeoutStartSeconds,
'TimeoutStopSec' => $this->TimeoutStopSeconds,
'RuntimeMaxSec' => $this->RuntimeMaxSeconds,
'SuccessExitStatus' => implode(' ', $this->SuccessExitStatus),
'ErrorExitStatus' => implode(' ', $this->ErrorExitStatus),
'RestartPreventExitStatus' => implode(' ', $this->RestartPreventExitStatus),
'RestartForceExitStatus' => implode(' ', $this->RestartForceExitStatus),
'LogStdout' => $this->LogStdout,
'LogStderr' => $this->LogStderr
];
}
/**
* Constructs object from an array representation
*
* @param array $data
* @return LunchCondition
*/
public static function fromArray(array $data): LunchCondition
{
$LunchConditionObject = new LunchCondition();
if(isset($data['Exec']))
$LunchConditionObject->Exec = $data['Exec'];
if(isset($data['Args']))
$LunchConditionObject->Arguments = Parser::parseArgumentsString($data['Args']);
if(isset($data['Env']))
$LunchConditionObject->EnvironmentVariables = Parser::parseArgumentsString($data['Env']);
if(isset($data['Cwd']))
$LunchConditionObject->CurrentWorkingDirectory = $data['Cwd'];
if(isset($data['Restart']))
{
switch(strtolower($data['Restart']))
{
case RestartCondition::OnFailure:
case RestartCondition::No:
case RestartCondition::Always:
case RestartCondition::UnlessStopped:
case RestartCondition::OnSuccess:
$LunchConditionObject->Restart = strtolower($data['Restart']);
break;
default:
$LunchConditionObject->Restart = RestartCondition::No;
}
}
if(isset($data['MaxFailureRestarts']))
$LunchConditionObject->MaxFailureRestarts = (int)$data['MaxFailureRestarts'];
if(isset($data['MaxAutomaticRestarts']))
$LunchConditionObject->MaxAutomaticRestarts = (int)$data['MaxAutomaticRestarts'];
if(isset($data['TimeoutStartSec']))
$LunchConditionObject->TimeoutStartSeconds = (int)$data['TimeoutStartSec'];
if(isset($data['TimeoutStopSec']))
$LunchConditionObject->TimeoutStopSeconds = (int)$data['TimeoutStopSec'];
if(isset($data['RuntimeMaxSec']))
$LunchConditionObject->RuntimeMaxSeconds = (int)$data['RuntimeMaxSec'];
if(isset($data['SuccessExitStatus']))
{
foreach(explode(' ', $data['SuccessExitStatus']) as $item)
{
if(in_array((int)$item, $LunchConditionObject->SuccessExitStatus) == false)
{
$LunchConditionObject->SuccessExitStatus[] = (int)$item;
}
}
}
if(isset($data['ErrorExitStatus']))
{
foreach(explode(' ', $data['ErrorExitStatus']) as $item)
{
if(in_array((int)$item, $LunchConditionObject->ErrorExitStatus) == false)
{
$LunchConditionObject->ErrorExitStatus[] = (int)$item;
}
}
}
if(isset($data['RestartPreventExitStatus']))
{
$LunchConditionObject->RestartPreventExitStatus = [];
foreach(explode(' ', $data['RestartPreventExitStatus']) as $item)
{
if(in_array((int)$item, $LunchConditionObject->RestartPreventExitStatus) == false)
{
$LunchConditionObject->RestartPreventExitStatus[] = (int)$item;
}
}
}
if(isset($data['RestartForceExitStatus']))
{
$LunchConditionObject->RestartForceExitStatus = [];
foreach(explode(' ', $data['RestartForceExitStatus']) as $item)
{
if(in_array((int)$item, $LunchConditionObject->RestartForceExitStatus) == false)
{
$LunchConditionObject->RestartForceExitStatus[] = (int)$item;
}
}
}
if(isset($data['LogStdout']))
$LunchConditionObject->LogStdout = (bool)$data['LogStdout'];
if(isset($data['LogStderr']))
$LunchConditionObject->LogStderr = (bool)$data['LogStderr'];
return $LunchConditionObject;
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace ssm\Objects;
class ServiceConfiguration
{
/**
* The main execution point for the service
*
* @var LunchCondition|null
*/
public $Start;
/**
* The lunch condition before starting the main execution point
*
* @var LunchCondition|null
*/
public $StartPre;
/**
* The lunch condition after starting the main execution point
*
* @var LunchCondition|null
*/
public $StartPost;
/**
* Constructs object from a INI file
*
* @param string $file
* @return ServiceConfiguration
*/
public static function fromFile(string $file): ServiceConfiguration
{
$ini_file = IniFile::load($file);
return ServiceConfiguration::fromArray($ini_file->toArray());
}
/**
* Returns an array representation of the object
*
* @return array
*/
public function toArray(): array
{
$return_results = [];
if($this->Start !== null)
$return_results['start'] = $this->Start->toArray();
if($this->StartPre !== null)
$return_results['start_pre'] = $this->StartPre->toArray();
if($this->StartPost !== null)
$return_results['start_post'] = $this->StartPost->toArray();
return $return_results;
}
/**
* Constructs object from an array representation
*
* @param array $data
* @return ServiceConfiguration
*/
public static function fromArray(array $data): ServiceConfiguration
{
$ServiceConfigurationObject = new ServiceConfiguration();
if(isset($data['start']))
$ServiceConfigurationObject->Start = LunchCondition::fromArray($data['start']);
if(isset($data['start_pre']))
$ServiceConfigurationObject->StartPre = LunchCondition::fromArray($data['start_pre']);
if(isset($data['start_post']))
$ServiceConfigurationObject->StartPost = LunchCondition::fromArray($data['start_post']);
return $ServiceConfigurationObject;
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace ssm\Utilities;
class Parser
{
/**
* Parses an argument string
*
* @param string $input
* @return array
*/
public static function parseArgumentsString(string $input): array
{
return self::separate($input, ':');
}
/**
* Custom seperator with escape handler
*
* @param $string
* @param string $separator
* @param string $escape
* @return array
*/
public static function separate($string, $separator = '|', $escape = '\\'): array
{
$segments = [];
$string = (string) $string;
do
{
$segment = '';
do
{
$segment_length = strcspn($string, "$separator$escape");
if ($segment_length)
{
$segment .= substr($string, 0, $segment_length);
}
if (strlen($string) <= $segment_length)
{
$string = null;
break;
}
if ($escaped = $string[$segment_length] == $escape)
{
$segment .= (string)substr($string, ++$segment_length, 1);
}
$string = (string) substr($string, ++$segment_length);
} while ($escaped);
$segments[] = $segment;
} while ($string !== null);
return $segments;
}
}

65
src/ssm/package.json Normal file
View File

@ -0,0 +1,65 @@
{
"package": {
"package_name": "net.intellivoid.ssm",
"name": "Software Service Manager",
"version": "1.0.0.0",
"author": "Zi Xing Narrakas",
"organization": "Intellivoid Technologies",
"description": "Manages software services like systemd or supervisord",
"url": "https://github.com/intellivoid/ssm",
"dependencies": [],
"configuration": {
"autoload_method": "generated_spl",
"main": null,
"post_installation": [],
"pre_installation": []
}
},
"components": [
{
"required": true,
"file": "Abstracts/RestartCondition.php"
},
{
"required": true,
"file": "Abstracts/Time.php"
},
{
"required": true,
"file": "Classes/IniParser.php"
},
{
"required": true,
"file": "Exceptions/FileException.php"
},
{
"required": true,
"file": "Exceptions/InvalidDataException.php"
},
{
"required": true,
"file": "Utilities/Parser.php"
},
{
"required": true,
"file": "ssm.php"
},
{
"required": true,
"file": "Objects/LunchCondition.php"
},
{
"required": true,
"file": "Objects/ServiceConfiguration.php"
},
{
"required": true,
"file": "Objects/IniSection.php"
},
{
"required": true,
"file": "Objects/IniFile.php"
}
],
"files": []
}

2
tests/config_parser.php Normal file
View File

@ -0,0 +1,2 @@
<?php

3
tests/example.service Normal file
View File

@ -0,0 +1,3 @@
[start]
Exec=/usr/bin/htop
Restart=always