Skip to content
Snippets Groups Projects
Verified Commit d1003206 authored by Thomas Flori's avatar Thomas Flori
Browse files

initial commit

parents
No related branches found
No related tags found
No related merge requests found
Showing
with 1008 additions and 0 deletions
/vendor
<?php
namespace App;
use App\Service\Exception\ConsoleHandler;
use Hugga\Console;
use Monolog\Logger;
use Whoops;
/**
* @property-read Config $config
* @property-read Console $console
* @property-read Logger $logger
* @property-read FanConfig $fanConfig
*/
class Application extends \Riki\Application
{
/** @var Whoops\Run */
protected $whoops;
protected $fallbackStatus;
public function __construct(string $basePath, Whoops\Run $whoops = null)
{
$this->whoops = $whoops;
parent::__construct($basePath);
define('HWMON_PATH', '/sys/class/hwmon');
$this->saveFallbackStatus();
register_shutdown_function([$this, 'resetStatus']);
pcntl_async_signals(true);
pcntl_signal(SIGTERM, fn() => $this->handleSignals(SIGTERM));
pcntl_signal(SIGINT, fn() => $this->handleSignals(SIGINT));
// bootstrap the application
$this->initWhoops();
}
protected function initDependencies()
{
parent::initDependencies();
// Register a namespace for factories
$this->registerNamespace('App\Factory', 'Factory');
}
protected function initWhoops()
{
$this->whoops || $this->whoops = $this->make(Whoops\Run::class);
$this->whoops->register();
$this->whoops->appendHandler(new ConsoleHandler($this));
}
protected function saveFallbackStatus()
{
foreach (glob('/sys/class/hwmon/hwmon*/pwm*') as $pwm) {
if (!preg_match('/pwm\d+$/', $pwm)) {
continue;
}
$this->fallbackStatus[$pwm] = [
'' => file_get_contents($pwm),
'_enable' => file_get_contents($pwm . '_enable'),
'_mode' => file_get_contents($pwm . '_mode'),
];
}
}
protected function resetStatus()
{
if (posix_getuid() !== 0) {
return;
}
foreach ($this->fallbackStatus as $pwm => $values) {
foreach ($values as $name => $value) {
file_put_contents($pwm . $name, $value);
}
}
}
protected function handleSignals(int $sigNo)
{
switch ($sigNo) {
case SIGINT:
case SIGTERM:
$this->resetStatus();
exit(0);
case SIGUSR1:
// @todo restart? reread config?
}
}
}
<?php
namespace App\Command;
use App\Application;
use App\Concerns\WritesToConsole;
use GetOpt\Command;
use GetOpt\GetOpt;
use GetOpt\Option;
use Hugga\Console;
abstract class AbstractCommand extends Command
{
use WritesToConsole;
/** @var string */
protected $name = 'unnamed';
/** @var string */
protected $description = '';
/** @var Application */
protected $app;
public function __construct(Application $app, Console $console)
{
$this->app = $app;
$this->console = $console;
parent::__construct($this->name, [$this, 'handle']);
if (!empty($this->description)) {
$this->setDescription($this->description);
}
}
abstract public function handle(GetOpt $getOpt): int;
}
<?php
namespace App\Command;
use App\Application;
use GetOpt\GetOpt;
use Hugga\Console;
use Hugga\Input\Question\Choice;
use Hugga\Input\Question\Confirmation;
class Config extends AbstractCommand
{
protected $name = 'config';
protected $description = 'Generate or update fan configuration';
public function __construct(Application $app, Console $console)
{
parent::__construct($app, $console);
}
public function handle(GetOpt $getOpt): int
{
if (posix_getuid() !== 0) {
$this->warn('This programm has to run as root');
return 1;
}
if ($this->getOption('quiet')) {
$this->warn('This command requires user interaction');
return 1;
}
$fanConfig = $this->app->fanConfig;
if (!$fanConfig->hasFans()) {
if (!$this->initializeConfig()) {
return 1;
}
}
$actions = new Choice([
'edit a fan',
'add a sensor',
'edit a sensor',
'add a rule',
'edit a rule',
'save and exit',
'exit without saving',
], 'What next?', 'save and exit');
do {
$answer = $this->console->ask($actions);
switch ($answer) {
case 'exit without saving':
break 2;
case 'save and exit':
// $fanConfig->save();
break 2;
default:
$this->info('not implemented');
}
} while (true);
// - edit a fan
// - add a sensor
// - edit a sensor
// - add a rule
// - edit a rule
// - save and leave
// - leave without saving
return 0;
}
protected function initializeConfig()
{
$fanConfig = $this->app->fanConfig;
$this->info('No fans configured. Detecting fans...');
$fans = $this->detectControllableFans();
if (count($fans) > 0) {
// for each fan
// ask if it should be added
// ask for a name
// test min speed
// ask for a pwm limit
// add the fan to the config
$fanConfig->setFans($fans);
$fanConfig->save();
return true;
}
$this->error('We could not find any fan to control.');
return false;
}
protected function detectControllableFans(): array
{
$fans = [];
$inputs = glob(HWMON_PATH . '/hwmon*/fan*_input');
$inputs = array_reduce($inputs, function ($inputs, $input) {
if (!preg_match('~(hwmon(\d+)/(fan\d+))_input$~', $input, $match)) {
return $inputs;
}
$hwmon = trim(file_get_contents(HWMON_PATH . '/hwmon' . $match[2] . '/name'));
$fan = $match[3];
$pwm = preg_replace('~fan(\d+)~', 'pwm$1', $fan);
$name = $hwmon . '/' . $fan;
$inputs[$name] = [
'input' => $input,
'pwm' => preg_replace('~fan(\d+)_input~', 'pwm$1', $input),
'hwmon' => 'hwmon' . $match[2],
'name' => $name,
'type' => 'hwmon',
'options' => [
'hwmon' => $hwmon,
'fan' => $fan,
'pwm' => $pwm,
],
];
return $inputs;
}, []);
$unknownInputs = [];
foreach ($inputs as $name => $input) {
[$min, $max] = $this->testPwm($input['pwm'], $input['input']);
if ($max === 0 || $min > $max*0.4) {
$unknownInputs[$name] = $input;
continue;
}
$this->info(sprintf('%s at full speed: %d; stopped: %d', $name, $max, $min));
$fans[] = $input;
}
if (count($unknownInputs) > 0) {
$this->info('Some fans could not be assigned to a pwm. Trying remaining pwms...');
$assignedPwms = array_map(fn($fan) => $fan['pwm'], $fans);
$pwms = array_reduce(glob(HWMON_PATH . '/hwmon*/pwm*'), function ($pwms, $pwm) use ($assignedPwms) {
if (!preg_match('~(hwmon(\d+)/pwm(\d+))$~', $pwm, $match) || in_array($pwm, $assignedPwms)) {
return $pwms;
}
$hwmon = $match[2];
if (!isset($pwms[$hwmon])) {
$pwms[$hwmon] = [];
}
$pwms[$hwmon][] = $pwm;
return $pwms;
}, []);
foreach ($unknownInputs as $id => $input) {
if (!isset($pwms[$input['hwmon']])) {
continue;
}
foreach ($pwms[$input['hwmon']] as $pwm) {
if ($input['pwm'] !== $pwm) {
continue;
}
[$min, $max] = $this->testPwm($pwm, $input['input']);
if ($max === 0 || $min > $max*0.4) {
continue;
}
$input['pwm'] = $pwm;
$input['options']['pwm'] = basename($pwm);
$fans[] = $input;
continue 2;
}
$this->info($id . ' not connected or not controllable');
}
}
return array_map(function ($fan) {
unset($fan['input'], $fan['pwm']);
return $fan;
}, $fans);
}
protected function testPwm($pwm, $input): array
{
$currentEnable = file_get_contents($pwm . '_enable');
file_put_contents($pwm . '_enable', '1');
file_put_contents($pwm, '255');
sleep(4);
$fullSpeed = (int)file_get_contents($input);
if ($fullSpeed === 0) {
file_put_contents($pwm . '_enable', $currentEnable);
return [0, 0];
}
file_put_contents($pwm, '0');
sleep(10);
$stopSpeed = (int)file_get_contents($input);
file_put_contents($pwm . '_enable', $currentEnable);
return [$stopSpeed, $fullSpeed];
}
}
<?php
namespace App\Concerns;
use Hugga\Console;
use Hugga\Output\Drawing\Table;
/** @codeCoverageIgnore wrapper for console methods */
trait WritesToConsole
{
/** @var Console */
protected $console;
public function setConsole(Console $console)
{
$this->console = $console;
}
/**
* Write $message to stdout
*
* @param string $message
* @param int $weight
*/
protected function write(string $message, int $weight = Console::WEIGHT_NORMAL): void
{
$this->console->write($message, $weight);
}
/**
* Write $message to stderr
*
* @param string $message
* @param int $weight
*/
protected function writeError(string $message, int $weight = Console::WEIGHT_HIGH): void
{
$this->console->writeError($message, $weight);
}
/**
* Shortcut to ->write('Your message' . PHP_EOL);
*
* @param string $message
* @param int $weight
*/
protected function line(string $message, int $weight = Console::WEIGHT_NORMAL): void
{
$this->console->line($message, $weight);
}
/**
* Shortcut to ->write('${green;bold}Your message' . PHP_EOL)
*
* @param string $message
* @param int $weight
*/
protected function info(string $message, int $weight = Console::WEIGHT_NORMAL)
{
$this->console->info($message, $weight);
}
/**
* Shortcut to ->write('${red;bold}Your message' . PHP_EOL, WEIGHT_HIGHER);
*
* @param string $message
* @param int $weight
*/
protected function warn(string $message, int $weight = Console::WEIGHT_HIGHER)
{
$this->console->warn($message, $weight);
}
/**
* Write a highlighted error message (red bg; spacing) to stderr
*
* @param string $message
* @param int $weight
*/
public function error(string $message, int $weight = Console::WEIGHT_HIGH): void
{
$this->console->error($message, $weight);
}
public function table(iterable $rows, array $columns = null)
{
$table = new Table($this->console, $rows, $columns);
$table->draw();
}
}
<?php
namespace App;
use Riki\Environment;
class Config extends \Riki\Config
{
/** @var string */
public $defaultConfigPath;
/** @var string */
public $defaultLogPath;
/** @var string */
public $hwmonPath = '/sys/class/hwmon';
public function __construct(Environment $environment)
{
parent::__construct($environment);
$this->defaultConfigPath = $this->env('CONFIG_PATH', '/etc/fan-ctrl.php');
$this->defaultLogPath = $this->env('LOG_PATH', '/var/log/fan-ctrl.log');
}
}
<?php
namespace App;
class Environment extends \Riki\Environment
{
public function canCacheConfig(): bool
{
return false;
}
}
<?php
namespace App\Factory;
use App\Application;
use DependencyInjector\Factory\AbstractFactory as BaseAbstractFactory;
/**
* @property Application $container
*/
abstract class AbstractFactory extends BaseAbstractFactory
{
}
<?php
namespace App\Factory;
use Hugga\Console;
class ConsoleFactory extends AbstractFactory
{
protected $shared = true;
/**
* @return Console
*/
protected function build()
{
$console = new Console($this->container->get('logger'));
$console->logMessages(false);
return $console;
}
}
<?php
namespace App\Factory;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
class LoggerFactory extends AbstractFactory
{
protected $shared = true;
/**
* This method builds the instance.
*
* @return Logger
* @throws \Exception
*/
protected function build()
{
$logPath = $this->container->config->defaultLogPath;
$handler = new StreamHandler($logPath, Logger::INFO);
$handler->setFormatter(new LineFormatter(null, null, true));
return new Logger('app', [ $handler ]);
}
}
<?php
namespace App;
class FanConfig
{
protected $path = '/dev/null';
protected $fans = [];
public function hasFans(): bool
{
return count($this->fans) > 0;
}
public function setFans(array $fans): self
{
// do we validate that? or was it validated already?
$this->fans = $fans;
return $this;
}
public static function readConfig(string $path): self
{
$self = new FanConfig();
$self->path = $path;
if (!file_exists($path)) {
return $self;
}
$cfg = include($path);
// validate config
if (!is_array($cfg)) {
throw new \InvalidArgumentException('given configuration is not a fan-ctrl config');
}
$self = new FanConfig();
$self->path = $path;
$self->fans = $cfg['fans'] ?? [];
return $self;
}
public function save()
{
// ob_start();
// $fans = $this->fans;
// include __DIR__ . '/../resources/config-template.php';
// $code = ob_get_clean();
$code = '<?php' . PHP_EOL . PHP_EOL .
'return ' . $this->exportArray([
'fans' => $this->fans,
]) . ';' . PHP_EOL;
file_put_contents($this->path, $code);
// exec(__DIR__ . '/../vendor/bin/php-cs-fixer fix ' . $this->path . ' --rules');
}
protected function exportArray(array $array, $indentation = 0)
{
$indexed = array_keys($array) === array_keys(array_keys($array));
$indentation += 4;
$var = '[' . PHP_EOL;
foreach ($array as $key => $value) {
$var .= str_repeat(' ', $indentation);
if (!$indexed) {
$var .= var_export($key, true) . ' => ';
}
$var .= is_array($value) ? $this->exportArray($value, $indentation) : var_export($value, true);
$var .= ',' . PHP_EOL;
}
$indentation -= 4;
$var .= str_repeat(' ', $indentation) . ']';
return $var;
}
}
<?php
namespace App;
use GetOpt\ArgumentException;
use GetOpt\ArgumentException\Missing;
use GetOpt\Arguments;
use GetOpt\GetOpt;
use GetOpt\Option;
use Hugga\Console;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
class Kernel extends \Riki\Kernel
{
/** @var string[] */
protected static $commands = [
\App\Command\Config::class,
];
/** @var \Riki\Application|Application */
protected $app;
/** @var GetOpt */
protected $getOpt;
public function __construct(Application $app)
{
parent::__construct($app);
// bootstrap the kernel
}
/**
* @param array|string|Arguments $arguments
* @return int
*/
public function handle($arguments = null): int
{
$getOpt = $this->getGetOpt();
$console = $this->app->console;
// process arguments and catch user errors
try {
try {
$getOpt->process($arguments);
} catch (Missing $exception) {
// catch missing exceptions if help is requested
if (!$getOpt->getOption('help')) {
throw $exception;
}
}
} catch (ArgumentException $exception) {
$console->error($exception->getMessage());
$console->write($getOpt->getHelpText());
return 128;
}
if ($logPath = $getOpt->getOption('log')) {
/** @var StreamHandler $logHandler */
$logHandler = $this->app->logger->popHandler();
$formatter = $logHandler->getFormatter();
$logHandler = new StreamHandler($logPath, Logger::INFO);
$logHandler->setFormatter($formatter);
$this->app->logger->pushHandler($logHandler);
}
$configPath = $getOpt->getOption('config') ?: $this->app->config->defaultConfigPath;
$this->app->instance('fanConfig', FanConfig::readConfig($configPath));
$command = $getOpt->getCommand();
if (!$command || $getOpt->getOption('help')) {
if ($cmdName = $getOpt->getOperand(0)) {
$console->error(sprintf('Command %s not found', $cmdName));
} elseif (!$getOpt->getOption('help')) {
$console->error('No command given');
}
$console->write($getOpt->getHelpText());
return $getOpt->getOption('help') ? 0 : 1;
}
if ($verbose = $getOpt->getOption('verbose')) {
while ($verbose--) {
$console->increaseVerbosity();
}
} elseif ($getOpt->getOption('quiet')) {
$console->setVerbosity(Console::WEIGHT_HIGH);
}
return call_user_func($command->getHandler(), $getOpt);
}
/**
* Create a getOpt instance for this kernel, add default options and load registered commands.
*
* @return GetOpt
*/
public function getGetOpt(): GetOpt
{
if (!$this->getOpt) {
/** @var GetOpt $getOpt */
$this->getOpt = $getOpt = $this->app->make(GetOpt::class);
$getOpt->addOptions([
Option::create('h', 'help')
->setDescription('Show this help message'),
Option::create('v', 'verbose')
->setDescription('Be verbose (can be stacked: -vv very verbose -vvv debug)'),
Option::create('q', 'quiet')
->setDescription('Disable questions and show only warnings'),
Option::create('c', 'config', GetOpt::REQUIRED_ARGUMENT)
->setDescription('Use a different configuration file'),
Option::create('l', 'log', GetOpt::REQUIRED_ARGUMENT)
->setDescription('Log to a different file'),
]);
foreach (static::$commands as $class) {
if (!class_exists($class)) {
// @codeCoverageIgnoreStart
continue; // avoid errors for deleted commands
// @codeCoverageIgnoreEnd
}
$getOpt->addCommand($this->app->make($class, $this->app, $this->app->console));
}
}
return $this->getOpt;
}
}
<?php
namespace App\Model;
class Pwm
{
const ENABLE_FULL_SPEED = 0;
const ENABLE_MANUAL = 1;
const ENABLE_MIN_AUTOMATIC = 2;
}
<?php
namespace App\Service\Exception;
use App\Application;
use App\Service\Exception\Formatter\Console as ConsoleFormatter;
use Hugga\Console;
use Whoops\Handler\Handler;
class ConsoleHandler extends Handler
{
/**
* @return int|null A handler may return nothing, or a Handler::HANDLE_* constant
*/
public function handle()
{
$console = Application::console();
/** @var ConsoleFormatter $formatter */
$formatter = Application::app()->make(ConsoleFormatter::class, $this->getInspector());
$console->logMessages(false);
$console->writeError($formatter->formatMessage() . PHP_EOL);
$console->writeError(
PHP_EOL . '${b}Stack Trace:${r}' . PHP_EOL . $formatter->formatTrace() . PHP_EOL,
Console::WEIGHT_NORMAL
);
return self::QUIT;
}
}
<?php
namespace App\Service\Exception;
use App\Application as app;
use Whoops\Exception\Inspector;
abstract class Formatter
{
/** @var Inspector */
protected $inspector;
/** @var array */
protected $options = [];
/**
* Formatter constructor.
*
* @param Inspector $inspector
* @param array $options
*/
public function __construct(Inspector $inspector, array $options = [])
{
$this->inspector = $inspector;
$this->options = array_merge_recursive($this->options, $options);
}
abstract public function formatMessage(): string;
abstract public function formatTrace(): string;
protected function replacePath(string $path): string
{
try {
$projectPath = app::config()->env('PROJECT_PATH');
if ($projectPath) {
$path = preg_replace(
'~^' . app::app()->getBasePath() . '~',
$projectPath,
$path
);
}
} catch (\Throwable $e) {
// ignore this error because we are already in an error handler
}
return $path;
}
protected function generateArgs(array $args): string
{
$result = [];
foreach ($args as $arg) {
switch (gettype($arg)) {
case 'object':
$result[] = get_class($arg);
break;
case 'string':
if (!class_exists($arg)) {
$arg = strlen($arg) > 20 ? substr($arg, 0, 20) . '…' : $arg;
}
$result[] = sprintf('"%s"', $arg);
break;
case 'integer':
case 'double':
$result[] = (string)$arg;
break;
case 'boolean':
$result[] = $arg ? 'true' : 'false';
break;
default:
$result[] = gettype($arg);
break;
}
}
return implode(', ', $result);
}
}
<?php
namespace App\Service\Exception\Formatter;
use App\Service\Exception\Formatter;
class Console extends Formatter
{
public function formatMessage(): string
{
return $this->generateMessage($this->inspector->getException());
}
public function formatTrace(): string
{
static $template = PHP_EOL . '${b}%3d.${r} ${light-magenta}%s->%s${r}(%s)' . PHP_EOL .
' ${grey}in ${blue}%s ${grey}on line ${yellow}%d${r}';
$frames = $this->inspector->getFrames();
$response = '';
$line = 1;
foreach ($frames as $frame) {
$response .= str_replace('{none}->', '', sprintf(
$template,
$line,
$frame->getClass() ?: '{none}',
$frame->getFunction(),
$this->generateArgs($frame->getArgs()),
$this->replacePath($frame->getFile()),
$frame->getLine()
));
$line++;
}
return substr($response, strlen(PHP_EOL));
}
protected function generateMessage(\Throwable $exception): string
{
$message = sprintf(
'${b}%s${yellow}%s${r}: ${red;b}%s ${grey}in ${blue}%s ${grey}on line ${yellow}%d${r}',
get_class($exception),
$exception->getCode() ? '(' . $exception->getCode() . ')' : '',
$exception->getMessage(),
$this->replacePath($exception->getFile()),
$exception->getLine()
);
if ($exception->getPrevious()) {
$message .= PHP_EOL . PHP_EOL . 'Caused by ' . $this->generateMessage($exception->getPrevious());
}
return $message;
}
}
<?php
namespace App\Service\Exception\Formatter;
use App\Service\Exception\Formatter;
use Throwable;
class Log extends Formatter
{
public function formatMessage(): string
{
return $this->generateMessage($this->inspector->getException());
}
public function formatTrace(): string
{
static $template = ' #%d %s->%s(%s) in %s on line %d';
$frames = $this->inspector->getFrames();
$response = '';
$line = 1;
foreach ($frames as $frame) {
$response .= str_replace('{none}->', '', sprintf(
$template,
$line,
$frame->getClass() ?: '{none}',
$frame->getFunction(),
$this->generateArgs($frame->getArgs()),
$this->replacePath($frame->getFile()),
$frame->getLine()
));
$line++;
}
return substr($response, 1); // remove the first space
}
protected function generateMessage(Throwable $exception)
{
$message = sprintf(
'%s%s: %s in %s on line %d',
get_class($exception),
$exception->getCode() ? '(' . $exception->getCode() . ')' : '',
$exception->getMessage(),
$this->replacePath($exception->getFile()),
$exception->getLine()
);
if ($exception->getPrevious()) {
$message .= ' [Caused by ' . $this->generateMessage($exception->getPrevious()) . ']';
}
return $message;
}
}
<?php
namespace App\Service\Exception;
use App\Application;
use App\Service\Exception\Formatter\Log as LogFormatter;
use Whoops\Handler\Handler;
class LogHandler extends Handler
{
public function handle()
{
$logger = Application::logger();
/** @var LogFormatter $formatter */
$formatter = Application::app()->make(LogFormatter::class, $this->getInspector());
$logger->error($formatter->formatMessage());
$logger->debug('Stack Trace: ' . $formatter->formatTrace());
return Handler::DONE;
}
}
#!/usr/bin/env php
<?php
use App\Application;
use App\Kernel;
require_once __DIR__ . '/../vendor/autoload.php';
$app = new Application(realPath(__DIR__ . '/..'));
$kernel = new Kernel($app);
$returnVar = $app->run($kernel);
exit($returnVar);
{
"minimum-stability": "RC",
"bin": ["bin/fan-ctrl"],
"require": {
"tflori/riki-framework": "dev-master",
"tflori/hugga": "^1.1",
"ulrichsg/getopt-php": "^4.0",
"filp/whoops": "^2.14",
"monolog/monolog": "^1.9",
"ext-pcntl": "*",
"php": ">=7.4"
},
"autoload": {
"psr-4": {
"App\\": "app"
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment