2018-02-01 11:49:40 +00:00
|
|
|
<?php
|
2018-08-04 22:05:14 +00:00
|
|
|
namespace App\Radio;
|
2018-02-01 11:49:40 +00:00
|
|
|
|
|
|
|
use Doctrine\ORM\EntityManager;
|
2018-08-04 22:05:14 +00:00
|
|
|
use App\Entity\Station;
|
2018-02-01 11:49:40 +00:00
|
|
|
use Supervisor\Supervisor;
|
|
|
|
|
|
|
|
class Configuration
|
|
|
|
{
|
|
|
|
/** @var EntityManager */
|
|
|
|
protected $em;
|
|
|
|
|
|
|
|
/** @var Adapters */
|
|
|
|
protected $adapters;
|
|
|
|
|
|
|
|
/** @var Supervisor */
|
|
|
|
protected $supervisor;
|
|
|
|
|
2018-09-06 06:33:16 +00:00
|
|
|
/** @var array */
|
|
|
|
protected $used_ports;
|
|
|
|
|
2018-02-01 11:49:40 +00:00
|
|
|
/**
|
|
|
|
* Configuration constructor.
|
|
|
|
* @param EntityManager $em
|
|
|
|
* @param Adapters $adapters
|
|
|
|
* @param Supervisor $supervisor
|
|
|
|
*/
|
|
|
|
public function __construct(EntityManager $em, Adapters $adapters, Supervisor $supervisor)
|
|
|
|
{
|
|
|
|
$this->em = $em;
|
|
|
|
$this->adapters = $adapters;
|
|
|
|
$this->supervisor = $supervisor;
|
2018-09-06 06:33:16 +00:00
|
|
|
|
|
|
|
$this->used_ports = [];
|
2018-02-01 11:49:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Write all configuration changes to the filesystem and reload supervisord.
|
|
|
|
*
|
|
|
|
* @param Station $station
|
|
|
|
* @param bool $regen_auth_key Regenerate the API authorization key (will trigger a full reset of processes).
|
|
|
|
* @throws \Exception
|
|
|
|
*/
|
|
|
|
public function writeConfiguration(Station $station, $regen_auth_key = false)
|
|
|
|
{
|
|
|
|
if (APP_TESTING_MODE) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-03-24 07:38:27 +00:00
|
|
|
// Initialize adapters.
|
|
|
|
$config_path = $station->getRadioConfigDir();
|
|
|
|
$supervisor_config = [];
|
|
|
|
$supervisor_config_path = $config_path . '/supervisord.conf';
|
|
|
|
|
|
|
|
if (!$station->isEnabled()) {
|
|
|
|
@unlink($supervisor_config_path);
|
|
|
|
$this->_reloadSupervisor();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-02-13 16:31:10 +00:00
|
|
|
// Ensure port configuration exists
|
|
|
|
$this->assignRadioPorts($station, false);
|
|
|
|
|
2018-02-01 11:49:40 +00:00
|
|
|
if ($regen_auth_key || empty($station->getAdapterApiKey())) {
|
|
|
|
$station->generateAdapterApiKey();
|
|
|
|
$this->em->persist($station);
|
|
|
|
$this->em->flush();
|
|
|
|
}
|
|
|
|
|
|
|
|
$frontend = $this->adapters->getFrontendAdapter($station);
|
|
|
|
$backend = $this->adapters->getBackendAdapter($station);
|
|
|
|
|
|
|
|
// If no processes need to be managed, remove any existing config.
|
|
|
|
if (!$frontend->hasCommand() && !$backend->hasCommand()) {
|
|
|
|
@unlink($supervisor_config_path);
|
|
|
|
$this->_reloadSupervisor();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write config files for both backend and frontend.
|
|
|
|
$frontend->write();
|
|
|
|
$backend->write();
|
|
|
|
|
|
|
|
// Get group information
|
|
|
|
$backend_name = $backend->getProgramName();
|
|
|
|
list($backend_group, $backend_program) = explode(':', $backend_name);
|
|
|
|
|
|
|
|
$frontend_name = $frontend->getProgramName();
|
|
|
|
list(,$frontend_program) = explode(':', $frontend_name);
|
|
|
|
|
|
|
|
$frontend_watch_name = $frontend->getWatchProgramName();
|
|
|
|
list(,$frontend_watch_program) = explode(':', $frontend_watch_name);
|
|
|
|
|
|
|
|
// Write group section of config
|
|
|
|
$programs = [];
|
|
|
|
if ($backend->hasCommand()) {
|
|
|
|
$programs[] = $backend_program;
|
|
|
|
}
|
|
|
|
if ($frontend->hasCommand()) {
|
|
|
|
$programs[] = $frontend_program;
|
|
|
|
}
|
|
|
|
if ($frontend->hasWatchCommand()) {
|
|
|
|
$programs[] = $frontend_watch_program;
|
|
|
|
}
|
|
|
|
|
|
|
|
$supervisor_config[] = '[group:' . $backend_group . ']';
|
|
|
|
$supervisor_config[] = 'programs=' . implode(',', $programs);
|
|
|
|
$supervisor_config[] = '';
|
|
|
|
|
|
|
|
// Write frontend
|
|
|
|
if ($frontend->hasCommand()) {
|
2018-06-09 16:17:21 +00:00
|
|
|
$this->_writeConfigurationSection($supervisor_config, $frontend_program, [
|
|
|
|
'directory' => $config_path,
|
|
|
|
'command' => $frontend->getCommand(),
|
|
|
|
'priority' => 90,
|
|
|
|
]);
|
2018-02-01 11:49:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Write frontend watcher program
|
|
|
|
if ($frontend->hasWatchCommand()) {
|
2018-06-09 16:17:21 +00:00
|
|
|
$this->_writeConfigurationSection($supervisor_config, $frontend_watch_program, [
|
|
|
|
'directory' => '/var/azuracast/servers/station-watcher',
|
|
|
|
'command' => $frontend->getWatchCommand(),
|
|
|
|
'priority' => 95,
|
|
|
|
]);
|
2018-02-01 11:49:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Write backend
|
|
|
|
if ($backend->hasCommand()) {
|
2018-06-09 16:17:21 +00:00
|
|
|
$this->_writeConfigurationSection($supervisor_config, $backend_program, [
|
|
|
|
'directory' => $config_path,
|
|
|
|
'command' => $backend->getCommand(),
|
|
|
|
'priority' => 100,
|
|
|
|
]);
|
2018-02-01 11:49:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Write config contents
|
|
|
|
$supervisor_config_data = implode("\n", $supervisor_config);
|
|
|
|
file_put_contents($supervisor_config_path, $supervisor_config_data);
|
|
|
|
|
|
|
|
$this->_reloadSupervisor();
|
|
|
|
}
|
|
|
|
|
2018-06-09 16:17:21 +00:00
|
|
|
protected function _writeConfigurationSection(&$supervisor_config, $program_name, $config_lines)
|
|
|
|
{
|
|
|
|
$defaults = [
|
|
|
|
'user' => 'azuracast',
|
|
|
|
'priority' => 100,
|
|
|
|
];
|
|
|
|
|
|
|
|
if (APP_INSIDE_DOCKER) {
|
|
|
|
$defaults['stdout_logfile'] = '/dev/stdout';
|
|
|
|
$defaults['stdout_logfile_maxbytes'] = 0;
|
|
|
|
$defaults['stderr_logfile'] = '/dev/stderr';
|
|
|
|
$defaults['stderr_logfile_maxbytes'] = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
$supervisor_config[] = '[program:' . $program_name . ']';
|
|
|
|
$config_lines = array_merge($defaults, $config_lines);
|
|
|
|
|
|
|
|
foreach($config_lines as $config_key => $config_value) {
|
|
|
|
$supervisor_config[] = $config_key . '=' . $config_value;
|
|
|
|
}
|
|
|
|
|
|
|
|
$supervisor_config[] = '';
|
|
|
|
}
|
|
|
|
|
2018-02-01 11:49:40 +00:00
|
|
|
/**
|
|
|
|
* Remove configuration (i.e. prior to station removal) and trigger a Supervisor refresh.
|
|
|
|
*
|
|
|
|
* @param Station $station
|
|
|
|
*/
|
|
|
|
public function removeConfiguration(Station $station)
|
|
|
|
{
|
|
|
|
if (APP_TESTING_MODE) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$config_path = $station->getRadioConfigDir();
|
|
|
|
$supervisor_config_path = $config_path . '/supervisord.conf';
|
|
|
|
|
|
|
|
@unlink($supervisor_config_path);
|
|
|
|
|
|
|
|
$this->_reloadSupervisor();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Trigger a supervisord reload and restart all relevant services.
|
|
|
|
*/
|
|
|
|
protected function _reloadSupervisor()
|
|
|
|
{
|
|
|
|
$reload_result = $this->supervisor->reloadConfig();
|
|
|
|
|
|
|
|
$reload_added = $reload_result[0][0];
|
|
|
|
$reload_changed = $reload_result[0][1];
|
|
|
|
$reload_removed = $reload_result[0][2];
|
|
|
|
|
|
|
|
foreach ($reload_removed as $group) {
|
|
|
|
$this->supervisor->stopProcessGroup($group);
|
|
|
|
$this->supervisor->removeProcessGroup($group);
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($reload_changed as $group) {
|
|
|
|
$this->supervisor->stopProcessGroup($group);
|
|
|
|
$this->supervisor->removeProcessGroup($group);
|
|
|
|
$this->supervisor->addProcessGroup($group);
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($reload_added as $group) {
|
|
|
|
$this->supervisor->addProcessGroup($group);
|
|
|
|
}
|
|
|
|
}
|
2018-02-13 16:31:10 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Assign the first available port range to this station, or ensure it already is configured properly.
|
|
|
|
*
|
|
|
|
* @param Station $station
|
|
|
|
* @param bool $force
|
|
|
|
*/
|
|
|
|
public function assignRadioPorts(Station $station, $force = false)
|
|
|
|
{
|
|
|
|
if ($station->getFrontendType() !== 'remote' && $station->getBackendType() !== 'none') {
|
|
|
|
$frontend_config = (array)$station->getFrontendConfig();
|
2018-02-17 01:58:58 +00:00
|
|
|
$backend_config = (array)$station->getBackendConfig();
|
2018-02-13 16:31:10 +00:00
|
|
|
|
|
|
|
if (empty($frontend_config['port']) || $force) {
|
|
|
|
$base_port = $this->getFirstAvailableRadioPort($station);
|
|
|
|
|
|
|
|
$station->setFrontendConfig([
|
|
|
|
'port' => $base_port,
|
|
|
|
]);
|
2018-02-17 01:58:58 +00:00
|
|
|
} else {
|
|
|
|
$base_port = (int)$frontend_config['port'];
|
|
|
|
}
|
2018-02-13 16:31:10 +00:00
|
|
|
|
2018-02-17 01:58:58 +00:00
|
|
|
if (empty($backend_config['dj_port'])) {
|
2018-02-13 16:31:10 +00:00
|
|
|
$station->setBackendConfig([
|
|
|
|
'dj_port' => $base_port + 5,
|
|
|
|
]);
|
2018-02-17 01:58:58 +00:00
|
|
|
}
|
2018-02-13 16:31:10 +00:00
|
|
|
|
2018-02-17 01:58:58 +00:00
|
|
|
if (empty($backend_config['telnet_port'])) {
|
|
|
|
$station->setBackendConfig([
|
|
|
|
'telnet_port' => $base_port + 4,
|
|
|
|
]);
|
2018-02-13 16:31:10 +00:00
|
|
|
}
|
2018-02-17 01:58:58 +00:00
|
|
|
|
|
|
|
$this->em->persist($station);
|
|
|
|
$this->em->flush();
|
2018-02-13 16:31:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determine the first available 10-port block that has no stations occupying it.
|
|
|
|
*
|
|
|
|
* @param Station|null $station A station to exclude, or null to include all stations.
|
|
|
|
* @return int The first available radio port to use.
|
|
|
|
*/
|
|
|
|
public function getFirstAvailableRadioPort(Station $station = null): int
|
|
|
|
{
|
2018-09-06 06:33:16 +00:00
|
|
|
$used_ports = $this->getUsedPorts($station);
|
2018-02-13 16:31:10 +00:00
|
|
|
|
|
|
|
// Iterate from port 8000 to 9000, in increments of 10
|
2018-08-16 10:49:15 +00:00
|
|
|
$protected_ports = [8080];
|
|
|
|
|
2018-02-13 16:31:10 +00:00
|
|
|
for($port = 8000; $port < 9000; $port += 10) {
|
2018-08-16 10:49:15 +00:00
|
|
|
if (in_array($port, $protected_ports)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-02-13 16:31:10 +00:00
|
|
|
$range_in_use = false;
|
|
|
|
for($i = $port; $i < $port+10; $i++) {
|
|
|
|
if (isset($used_ports[$i])) {
|
|
|
|
$range_in_use = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$range_in_use) {
|
|
|
|
return $port;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new \App\Exception('This installation has no available ports for new radio stations.');
|
|
|
|
}
|
2018-09-06 06:33:16 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get an array of all used ports across the system, except the ones used by the station specified (if specified).
|
|
|
|
*
|
|
|
|
* @param Station|null $except_station
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function getUsedPorts(Station $except_station = null): array
|
|
|
|
{
|
|
|
|
if (!$this->used_ports) {
|
|
|
|
$this->used_ports = [];
|
|
|
|
|
|
|
|
// Get all station used ports.
|
|
|
|
$station_configs = $this->em->createQuery('SELECT s.id, s.name, s.frontend_type, s.frontend_config, s.backend_type, s.backend_config FROM '.Station::class.' s')
|
|
|
|
->getArrayResult();
|
|
|
|
|
|
|
|
foreach($station_configs as $row) {
|
|
|
|
$station_reference = ['id' => $row['id'], 'name' => $row['name']];
|
|
|
|
|
|
|
|
if ($row['frontend_type'] !== 'remote' && $row['backend_type'] !== 'none') {
|
|
|
|
$frontend_config = (array)$row['frontend_config'];
|
|
|
|
|
|
|
|
if (!empty($frontend_config['port'])) {
|
|
|
|
$port = (int)$frontend_config['port'];
|
|
|
|
$this->used_ports[$port] = $station_reference;
|
|
|
|
}
|
|
|
|
|
|
|
|
$backend_config = (array)$row['backend_config'];
|
|
|
|
|
|
|
|
if (!empty($backend_config['dj_port'])) {
|
|
|
|
$port = (int)$frontend_config['dj_port'];
|
|
|
|
$this->used_ports[$port] = $station_reference;
|
|
|
|
}
|
|
|
|
if (!empty($backend_config['telnet_port'])) {
|
|
|
|
$port = (int)$frontend_config['telnet_port'];
|
|
|
|
$this->used_ports[$port] = $station_reference;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($except_station !== null) {
|
|
|
|
return array_filter($this->used_ports, function($station_reference) use ($except_station) {
|
|
|
|
return ($station_reference['id'] !== $except_station->getId());
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->used_ports;
|
|
|
|
}
|
2018-08-04 22:05:14 +00:00
|
|
|
}
|