Catching terminal terminations/exits with Symfony Console (CTRL+C) - php

I have built a command which triggers file downloads from over the internet, however since these files need to be processed by another component, we need to make sure that every file that has been downloaded and has not been modified in the last 10 seconds, is a proper video and not corrupted/partially downloaded.
For this reason, we need to find a way to catch CTRL+C or command terminations and cleanup any applicable file that has not been successfully downloaded.
This is what I tried so far by using symfony/console and symfony/event-dispatcher:
#!/usr/bin/env php
<?php
require_once(__DIR__ . '/../vendor/autoload.php');
use Symfony\Component\Console\Application;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\EventDispatcher\EventDispatcher;
use ImportExport\Console\ImportCommand;
use Monolog\Logger;
$dotenv = new Dotenv\Dotenv(__DIR__ . '/../');
$dotenv->load();
$logger = new Logger('console');
$dispatcher = new EventDispatcher();
$dispatcher->addListener(ConsoleEvents::TERMINATE, function (ConsoleTerminateEvent $event) {
// gets the command that has been executed
$command = $event->getCommand();
var_dump($command);
});
$application = new Application("Import-Export System", 'v0.1.0-ALPHA');
$application->add(new ImportCommand($logger));
$application->setDispatcher($dispatcher);
$application->run();
However the var_dump() is never shown in the console, if I do CTRL+C.
Suggestions?

When you do CTRL+C it is actually SIGINT that is being sent, not SIGTERM. The best way I can think of is to register handler with http://php.net/manual/en/function.pcntl-signal.php and dispatch the signal with pcntl_signal_dispatch, example:
pcntl_signal(SIGINT,'sigIntHandler');
function sigIntHandler() {
// Do some stuff
}
Of course you need to adjust it to your needs. Note that you can also defer to class methods inside your commands, so you could for example create an AbstractCommand with generic sigIntHandler() and register it in the constructor:
pcntl_signal(SIGINT, [$this, 'sigIntHandler']);
Then use pcntl_signal_dispatch() for example in the loop of your command (it needs to be called in each iteration).

Since Symfony 5.2, it's a native feature: https://symfony.com/blog/new-in-symfony-5-2-console-signals
Implement an interface (SignalableCommandInterface), subscribe to signals, and handle them:
public function handleSignal(int $signal)
{
if (SIGINT === $signal) {
// ...
}
// ...
}

Related

Monolog implementation with common class

I succeeded in implementing a Monolog logger for test purposes. And now I'm trying to use it in a project. This project doesn't use any MVC framework.
I'm trying to write a common class file to wrap access to the Monolog instance.
Common class file: File: app_log.php
require 'autoload.php';
use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Processor\UidProcessor;
use Monolog\Processor\WebProcessor;
use Monolog\Processor\MemoryUsageProcessor;
use Monolog\Processor\ProcessIdProcessor;
use Monolog\Formatter\LineFormatter;
class app_log {
public function info(){
$logger = new Logger('applog');
$handler = new RotatingFileHandler('useractivity.log', 0, Logger::INFO);
$handler->setFormatter(new LineFormatter("[%datetime%] %extra.process_id% %channel%.%level_name%: %message% %extra% %context% \n"));
$logger->pushHandler($handler);
$logger->pushProcessor(new WebProcessor);
}
}
Other File: users.php
include_once 'app_log.php';
class users extends dbconnector{
function login(){
// Some project code.
$logger = new app_log();
$logger->info('User logged successfully');
}
}
Up to this works well and i want to include filename, method name, request parameters. But i am getting app_log.php file name instead users.php and Method name is 'info' instead 'login' in the logs.
Example:
[2018-06-07 20:55:50] 4410 applog.INFO: User logged successfully {"file":"/var/www/portal/lib/app_log.php","line":59,"class":"app_log","function":"info"} []
Could you guys help on this part?
I'm afraid your whole design is simply wrong.
Instead of instantiating a new Logger every time you need to log, you should be creating a single $logger to use as a service around your application.
Without knowing more about your application (and having to rewrite your application would make the question too broad anyway), it's hard to guess how to implement dependency injection here.
But a simplistic and naive approach would be:
class users extends dbconnector {
protected $logger;
public function _construct(Logger $logger) {
$this->logger = $logger;
}
function login() {
$this->logger->info('User logged successfully');
}
}
And then:
$logger = new Logger('applog');
$handler = new RotatingFileHandler('useractivity.log', 0, Logger::INFO);
$handler->setFormatter(new LineFormatter("[%datetime%] %extra.process_id% %channel%.%level_name%: %message% %extra% %context% \n"));
$logger->pushHandler($handler);
$logger->pushProcessor(new WebProcessor);
$logger->pushProcessor(new IntrospectionProcessor);
$users = new users($logger);
If you inject that Logger instance in the objects that need it, you can use it directly without having to create your own "wrapper" (which in your example was poorly designed), and the output in the log files will match your expectations.
Notice that you are not using the IntrospectionProcessor, which you'd need to capture the filename and file line number. In the above example I'm also pushing it in $logger, the Logger instance.
(And also note that simply adding this processor to your code won't solve your issue, since the call to Logger::info() would always happen in app_log::info()).
Remember that you'll need to add the corresponding use statement: use Monolog\Processor\IntrospectionProcessor;
I do not know all the details of your system, and can't build it all for you anyway, but if dependency injection is still too much for you, you could just cheat temporarily with a global-state approach.
E.g.:
function get_logger() {
static $logger;
if ($logger !== null) {
return $logger;
}
$logger = new Logger('applog');
$handler = new RotatingFileHandler('useractivity.log', 0, Logger::INFO);
$handler->setFormatter(new LineFormatter("[%datetime%] %extra.process_id% %channel%.%level_name%: %message% %extra% %context% \n"));
$logger->pushHandler($handler);
$logger->pushProcessor(new WebProcessor);
$logger->pushProcessor(new IntrospectionProcessor);
return $logger;
}
You could put this in a file that you require_once, and in each of these places where you need access to the logger you can simply do:
get_logger()->info('write to the log!');
This is not something I'd endorse, but a crummy workaround for you to get moving until you advance somewhat in your understanding of other OOP topics. This is nothing else than a poor's man singleton, which more often than not is a pattern to avoid anyway; but that could help you right now..
WebProcessor will not add data you need, I mean file and line.
IntrospectionProcessor does what you need, try
$logger->pushProcessor(new IntrospectionProcessor());

testing error_log with PHPUnit

I have this function I want to test looking like this:
class Logger {
function error($msg){
if (is_string($msg)){
error_log($msg);
die($msg);
} elseif (is_object($msg)){
error_log($msg.' '.$msg->getTraceAsString());
die('exception');
} else {
var_dump($msg);
die('error');
}
}
I want to test this function without logging the $msg. Is there a way to determine if error_log works without logging? I tried using setExpectedException but I wasn't able to catch the error and it kept logging.
The obvious answer is a simple alias/proxy-function that itself called error_log in the Logger class (which can be easily mocked, and checked to see what is set to it),
To actually test the native error_log function however (without a proxy in the original class), can be done with namespaces. The test would end up defined to be the same namespace as the original code, and then after the test class, add a function - in this case error_log() - but that function is also defined in the namespace - and so would be run in preference to the root-namespace-equivalent from the native functions.
Unfortunately, you can't do the same overriding with die (or its alias, exit). They are 'language constructs', and cannot be overridden like error_log can.
<?php
namespace abc;
use abc\Logger;
class ThreeTest extends \PHPUnit_Framework_TestCase
{
public function setUp() { $this->l = new Logger(); }
// test code to exercise 'abc\Logger'
}
// Now define a function, still inside the namespace '\abc'.
public function error_log($msg)
{
// this will be called from abc\Logger::error
// instead of the native error_log() function
echo "ERR: $msg, ";
}
you can use a function-mocking framework like php-mock (there are others as well) to mock the call to error_log (and check whether it is called with your expected parameters).
Unfortunately you will not be able to use that for the die-construct as that is not a normal function but anlanguage construct.
I'd replace the die() with a 'throw new \Exception()' (or any other appropriate exception) as you can then
test for the thrown exception and
can decide in your programming whether execution shall be stopped on calling the logger or whether you want to go on by wrapping the call into a try/catch
But I'd also ask myself whether the execution has to stop when calling a logger
Capturing error_log() output in a variable
If you want to redirect the error_log() output in a way that lets you inspect it with PHPUnit assertions, the following code works for me:
$errorLogTmpfile = tmpfile();
$errorLogLocationBackup = ini_set('error_log', stream_get_meta_data($errorLogTmpfile)['uri']);
error_log("Test for this message");
ini_set('error_log', $errorLogLocationBackup);
$result = stream_get_contents($errorLogTmpfile);
// Result: [11-May-2022 22:27:08 UTC] Test for this message
As you can see, it uses a temporary file to collect the output, then grabs the content into a variable and resets the error_log config.
Re-usable methods
Personally, I've organized this into a pair of methods that I inject into the PHPUnit object with a trait so I can re-use them.
Of course the code below won't work out of the box, but it serves to demonstrate how you can make this system re-usable:
trait WithWPTestCaseGeneralTools {
var $gvErrorLogLocationBackup = "";
var $gvErrorLogTmpfile = "";
public function gvErrorLogStartListening() {
$this->gvErrorLogTmpfile = tmpfile();
$streamUri = stream_get_meta_data($this->gvErrorLogTmpfile)['uri'];
$this->gvErrorLogLocationBackup = ini_set('error_log', $streamUri);
}
public function gvErrorLogGetContents() {
ini_set('error_log', $this->gvErrorLogLocationBackup);
return stream_get_contents($this->gvErrorLogTmpfile);
}
}
You could of course achieve the same things with a couple of functions that use globals, I'll leave that to you if it's what you need!

Aborting and resuming a Symfony Console command

I have a Symfony Console command that iterates over a potentially big collection of items and does a task with each of them. Since the collection can be big, the command can take a long time to run (hours). Once the command finishes, it displays some statistics.
I'd like to make it possible to abort the command in a nice way. Right now if I abort it (ie with ctrl+c in the CLI), there is no statistics summary and no way to output the parameters needed to resume the command. Another issue is that the command might be terminated in the middle of handling an item - it'd be better if it could only terminate in between handling items.
So is there a way to tell a command to "abort nicely as soon as possible", or have the ctrl+c command be interpreted as such?
I tried using the ConsoleEvents::TERMINATE event, though the handlers for this only get fired on command completion, not when I ctrl+c the thing. And I've not been able to find further info on making such resumable commands.
This is what worked for me. You need to call pcntl_signal_dispatch before the signal handlers are actually executed. Without it, all tasks will finish first.
<?php
use Symfony\Component\Console\Command\Command;
class YourCommand extends Command
{
protected function execute(InputInterface $input, OutputInterface $output)
{
pcntl_signal(SIGTERM, [$this, 'stopCommand']);
pcntl_signal(SIGINT, [$this, 'stopCommand']);
$this->shouldStop = false;
foreach ( $this->tasks as $task )
{
pcntl_signal_dispatch();
if ( $this->shouldStop ) break;
$task->execute();
}
$this->showSomeStats($output);
}
public function stopCommand()
{
$this->shouldStop = true;
}
}
You should take a look at RabbitMqBundle's signal handling. Its execute method just links some callbacks via the pcntl_signal() function call. A common case should look pretty much like this:
<?php
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand as Command;
class YourCommand extends Command
{
protected function execute(InputInterface $input, OutputInterface $output)
{
pcntl_signal(SIGTERM, array(&$this, 'stopCommand', $output));
pcntl_signal(SIGINT, array(&$this, 'stopCommand', $output));
pcntl_signal(SIGHUP, array(&$this, 'restartCommand', $output));
// The real execute method body
}
public function stopCommand(OutputInterface $output)
{
$output->writeln('Stopping');
// Do what you need to stop your process
}
public function restartCommand(OutputInterface $output)
{
$output->writeln('Restarting');
// Do what you need to restart your process
}
}
The answers are more complex than they need to be. Sure, you can register POSIX signal handlers, but if the only signals that need to be handled are basic interrupts and the like, you should just define a destructor on the Command.
class YourCommand extends Command
{
// Other code goes here.
__destruct()
{
$this->shouldStop = true;
}
}
A case where you would want to register a POSIX signal is for the SIGCONT signal, which can handle the resumption of a process that was stopped (SIGSTOP).
Another case would be where you want every signal to behave differently; for the most part, though, SIGINT and SIGTERM and a handful of others would be registered with the same "OMG THE PROCESS HAS BEEN KILLED" operation.
Aside from these examples, registering signal events is unnecessary. This is why destructors exist.
You can even extend Symfony's base Command class with a __destruct method, which would automatically provide cleanup for every command; should a particular command require additional operations, just overwrite it.

Execute a Function in Silverstripe via Cronjob

Hi I want to execute a function via cronjob to start an csv import. At the moment the import is triggered by accessing a controller in the browser tld.de/Update
The controller has this code http://pastie.org/8351266
How can I execute the function init() via Cronjob?
Thx!
In SilverStripe you can access any route that is accessible via HTTP also by running cli-script.php in the command line
There also is sake which is just a bash wrapper around cli-script.php (but sake needs to be installed)
so, from your project directory, you can run both commands which will perform the same action (in this case, run a dev/build):
php framework/cli-script.php dev/build
sake dev/build
see the docs for commandline ussage of silverstripe: http://doc.silverstripe.org/framework/en/topics/commandline
the 2nd part of your question (how to call a method from a controller) is actually more a question of routing in silverstripe and has nothing to do with how it is called (cronjob)
I assume that your controller is a Page_Controller or a subclass of that (so bound to a SiteTree Model), then routing is done for you (it takes the URLs you set in the CMS).
so lets see some example code and lets assume you have a page with the URLSegment about:
class Page_Controller extends ContentController {
private static $allowed_actions = array('something');
public function init() {
// the init method will run before every action
// this means this code will run, no matter if you visit /about or /about/something
}
public function index() {
// this is the default action (this code is optional and can be removed),
// and will be called if you visit website.com/about
return $this;
}
public function something() {
// this is the somethingaction,
// and will be called if you visit website.com/about/something
// do something here
return $this;
}
}
you can then call run to get the result of the index():
php framework/cli-script.php about
and this to get the result of something():
php framework/cli-script.php about/something
NOTE: the init method itself is not accessable via URL, it is the "setup" that runs before an action
NOTE: all actions other than index() have to be allowed by adding them to $allowed_actions (also note that you need to ?flush=1 after adding to $allowed_actions to reload the config cache)
EDIT: this was actually the response to your first question, after seeing your code example, this addition:
for standalone controllers it works the same way, just that you have to define the routes, and make sure that you have $Action in the route so that something() can be called
You could do this without Silverstripe sake. Install curl and call the URL via a cronjob, ie:
0 0 * * * curl --silent http://tld.de/Update
The proper way to do this would be to write a Silverstripe task, and invoke your controller from within the task. I haven't tested this code but it would go something like this:
class YourTask extends BuildTask {
public $description = "...";
//...
public function run($request) {
YourController::init();
}
}
You can invoke it over sake using:
0 0 * * * /path/to/framework/sake dev/tasks/YourTask
why not create a build task ? which is specially designed for such requirements (at-least that's how I consider build tasks)
<?php
class ArticleCsvUpdateTask extends BuildTask {
protected $title = 'Article Csv Update';
protected $description = 'Build task for article Csv update';
public function run($request) {
$loader = new ArticleCsvBulkLoader('Color');
if($loader->load('import-new.csv')) {
$loader->load('import-new.csv');
}
}
}
Which can be assess both from browser using "yoursite/dev/tasks/ArticleCsvUpdateTask" and from command line using "php framework/cli-script.php dev/tasks/ArticleCsvUpdateTask" OR using "sake dev/tasks/ArticleCsvUpdateTask" (if you have sake installed).
May be I am not getting your exact requirement but I believe this is much cleaner and nicer way of running a cron job with silverstripe.
See Zauberfisch's answer for a complete solution
I'm not familiar with Silverstripe, but if I understand correctly, this controller init function can be called with a HTTP request.
As the silverstripe docs say, you can call any url from the command line:
php framework/cli-script.php Update/init
More information is available here, and consider using sake for this task.
I think the right way do this is create a php file console like:
#!/usr/bin/env php
<?php
require_once "/path/to/your/class/Update.php";
$class = new Update();
$class->init();
Add right perms to this file
chmod 755 consolefile
And finally run this script with cronjob

How do I execute a php-cli from php-cgi with nohup

I am trying to execute a php-cli script from php-cgi
What I got below does not work from cgi.
It does however work when I execute it directly from a shell.
Is there a way to accomplish this?
<?php
if (PHP_SAPI === 'cli')
{
require '../boot.php';
Logger::mailit();
exit;
}
class Logger {
private static $instance = NULL;
private function __construct(){}
public function __destruct()
{
if (PHP_SAPI != 'cli')
{
exec('nohup php '. __FILE__ .' &');
}
}
public static function mailit(){
// Database stuff ...
mail( $row->email, $row->subject, $row->message, $row->headers);
}
}
?>
You have to place the call (Logger::mailit()) of your class after the definition (class Logger) of your class.
Got it actually working, and it was all just a little bug
changed this:
require '../boot.php';
to this:
require __dir__.'/../boot.php';
You can either remove the CLI check if (PHP_SAPI === 'cli'), or include the file and call require '../boot.php'; Logger::mailit(); yourself. Or you can look into exec() (or proc_open(), …) to execute the script as its own process. This might be necessary if you need the sandboxing.
have you tried something like exec('your/script.php &')? (NOHUP)
with proc_open() you can setup the environment for your script (including funky file descriptor stuff). with proc_get_status() you can get the PID of the process you started with proc_open(). With posix_kill() you can send signals to a process.
If I'm not mistaking, your parent process (in your case the php-cgi) keeps running until the child process (in your case the php-cli) finished - even if you setup non-blocking execution.

Categories