How to capture output from a non-command scheduled task in Laravel? - php

Given the following snippet:
$schedule->call(function () {
// echo "HELLO 123"; // nope
// return "HELLO 123"; // also nope
})
->everyMinute()
->sendOutputTo(storage_path('logs/cron/hello.cron.log'))
->emailOutputTo('me#example.org');
The scheduled task is running, the emails are being generated, but with no content. I can capture output via the $schedule->exec and $schedule->command.
Ideal end state here is, run a few internal, non-command processes within the call method and output the results into a file/email.

I just googled the documentation of the class you are using with the words laravel scheduled output and the documentation (Just above the anchor in a red box) states:
Note: The emailOutputTo and sendOutputTo methods are exclusive to the
command method and are not supported for call.
Hope that helps.

The accepted answer got me to the solution, but for future reference:
$schedule->call(function () {
// Do stuff
// Report to file
\Storage::append('logs/cron/hello.cron.log', "HELLO 123");
})->everyMinute();

I find it quiet unsatisfying that Laravel does not support running multiple Artisan commands consecutively, including capturing their outputs and stopping in case of exceptions.
\Storage::append() from #Chris' answer does only write into the storage/app/ directory, but I wanted the logs in storage/logs/. So I replaced it with \File::append().
However, my log file storage/logs/schedule.log is a symbolic link in production environment and file_put_contents(), on which both methods rely on, is not able to write to symbolic links.
This is the solution I came up with:
use Illuminate\Support\Facades\Artisan;
use Symfony\Component\Console\Output\OutputInterface;
// ...
$schedule->callWithOutput(function (OutputInterface $output) {
Artisan::call(FooCommand::class, outputBuffer: $output);
Artisan::call(BarCommand::class, outputBuffer: $output);
}, storage_path('logs/schedule.log'))>everyMinute();
Schedule::callWithOutput() is a macro that takes our callback and captures all output in a BufferedOutput(). It is eventually redirected to the log file (second argument) with shell_exec(), so it works with regular files and symlinks:
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Support\Facades\App;
use Illuminate\Support\ServiceProvider;
use Symfony\Component\Console\Output\BufferedOutput;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Schedule::macro('callWithOutput', function (callable $callback, string $logFile, array $parameters = []) {
/** #var Schedule $this */
return $this->call(function () use ($callback, $logFile, $parameters) {
$output = new BufferedOutput();
try {
App::call($callback, compact('output') + $parameters);
} finally {
shell_exec("echo -n '{$output->fetch()}' >> $logFile");
}
});
});
}
}
You can also pass $parameters (second parameter of Schedule::call()) to Schedule::callWithOutput() as third parameter.
If you're not executing Artisan commands, you can write output with $output->write() or $output->writeln().

Related

PHP - send command line output to dynamically named files

I have an application which is built in CakePHP 3.
It uses Console Commands to execute several intensive processes in the background using cron.
The application consists of 5 individual commands:
src/Command/Stage1Command.php
src/Command/Stage2Command.php
src/Command/Stage3Command.php
src/Command/Stage4Command.php
src/Command/Stage5Command.php
These can be executed manually by running each one individually, e.g. to execute Stage1Command.php:
$ php bin/cake.php stage1
To make them run via Cron, I created a 6th command (src/Command/RunAllCommand.php) which goes through these in order.
// src/Command/RunAllCommand.php
class RunAllCommand extends Command
{
public function execute(Arguments $args, ConsoleIo $io)
{
$stage1 = new Step1Command();
$this->executeCommand($stage1);
// ...
$stage5 = new Stage5Command();
$this->executeCommand($stage5);
}
}
This works fine so I can now execute everything with 1 command, php bin/cake.php run_all, which will be added as a cron task to automate running the 5 processes.
The problem I'm having is that each of the 5 commands (Stage1Command ... Stage5Command) produces output which appears on standard output in the console.
I need to be able to write the output produced by each of the 5 commands individually into dynamically named files.
So I can't do something like this
$ php bin/cake.php run_all > output.log
Because
output.log would contain everything, i.e. the output from all 5 commands.
output.log isn't a dynamic filename, it has been entered manually on the command line (or as the output destination of the cron task).
I looked at Redirecting PHP output to a text file and tried the following.
Added ob_start(); to RunAllCommand.php:
namespace App\Command;
ob_start();
class RunAllCommand extends Command { ... }
After executing the first task (Stage1Command) capturing ob_get_clean() to a variable called $content:
$stage1 = new Step1Command();
$this->executeCommand($stage1);
$content = ob_get_clean();
When I var_dump($content); it comes out as an empty string:
string(0) ""
But the output is still produced on the command line when executing php bin/cake.php run_all (RunAllCommand.php).
My plan for the dynamic filename was to generate it with PHP inside RunAllCommand.php, e.g.
// $id is a dynamic ID generated from a database call.
// This $id is being generated inside a foreach() loop so is different on each iteration (hence the dynamic nature of the filename).
$id = 234343;
$filename_stage1 = 'logs/stage1_' . $id . '.txt'; // e.g. "logs/stage1_234343.txt"
Then write $content to the above file, e.g.
file_put_contents($filename_stage1, $content);
So I have 2 problems:
The output is being echoed to the console, and unavailable in $content.
Assuming (1) is fixed, how to "reset" the output buffering such that I can use file_put_contents with 5 different filenames to capture the output for the relevant stage.
On each command file you could use the LogTrait then output what file is outputting before any commands to seperate what command is logging or setup the log config with different scopes to output to different files. example of outputting to the cli-debug.log file.
use Cake\Log\LogTrait;
class Stage1Command extends Command
{
use LogTrait;
public function execute(Arguments $args, ConsoleIo $io)
{
$this->log('Stage 1 Output: ', 'debug');
//do stuff
$this->log('output stage 1 stuff', 'debug');
}
}
I have two suggestions for solving your issue.
Option 1 - Using shell_exec
shell_exec returns a string of the output, so you can write it to a log file directly.
public function execute(Arguments $args, ConsoleIo $io)
{
$stage1_log = shell_exec('bin/cake stage1 arguments');
file_put_contents('stage1_dynamic_log_file.txt', $stage1_log);
$stage2_log = shell_exec('bin/cake stage2 arguments');
file_put_contents('stage2_dynamic_log_file.txt', $stage2_log);
}
Option 2 - Overwrite the ConsoleOut stream
Alternatively a more CakePHP style would be to call the command slightly differently. If you look at the contents of executeCommand() it does a few checks and then calls command->run($args, $io)
Also if you look at how the ConsoleIo is constructed, we can override the output method so instead of using php://stdout we could use a file instead, if you look at the code for ConsoleOutput it's just using normal fopen and fwrite.
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOutput;
public function execute(Arguments $args, ConsoleIo $io)
{
// File names
$id = 234343;
$filename_stage1 = 'logs/stage1_' . $id . '.txt';
// Create command object
$stage1 = new Stage1Command();
// Define output as this filename
$output = new ConsoleOutput($filename_stage1);
// Create a new ConsoleIo using this new output method
$stage1_io = new ConsoleIo($output);
// Execute passing in the ConsoleIo with text file for output
$this->executeCommand($stage1, ['arguments'], $stage1_io);
}

Cakephp 3: execute custom command from command

In CakPHP 3.6.0 Console Commands have been added to replace Shells and Tasks long term.
I'm currently designing a cronjob command to execute other commands in different time intervals. So I want to run a command from a Command class like this:
namespace App\Command;
// ...
class CronjobCommand extends Command
{
public function execute(Arguments $args, ConsoleIo $io)
{
// Run other command
}
}
For Shells / Tasks it is possible to use Cake\Console\ShellDispatcher:
$shell = new ShellDispatcher();
$output = $shell->run(['cake', $task]);
but this does not work for Commands. As I did not find any information in the docs, any ideas how to solve this problem?
You can simply instantiate the command and then run it, like this:
try {
$otherCommand = new \App\Command\OtherCommand();
$result = $otherCommand->run(['--foo', 'bar'], $io);
} catch (\Cake\Console\Exception\StopException $e) {
$result = $e->getCode();
}
CakePHP 3.8 will introduce a convenience method that helps with this. Quote from the upcoming docs:
You may need to call other commands from your command. You can use
executeCommand to do that::
// You can pass an array of CLI options and arguments.
$this->executeCommand(OtherCommand::class, ['--verbose', 'deploy']);
// Can pass an instance of the command if it has constructor args
$command = new OtherCommand($otherArgs);
$this->executeCommand($command, ['--verbose', 'deploy']);
See also https://github.com/cakephp/cakephp/pull/13163.

Calling console command security:check from controller action produces Lock file not found response

(Symfony3)
I'm toying with the idea of setting up some simple cron tasks to generate security reports for our project managers so that they can schedule upgrade time for developers (vs. me forgetting to run them manually).
As a very basic check, I'll simply run...
php bin/console security:check
...to see what composer has to say about vulnerabilities. Ultimately I'd like to roll this output into an email or post it to a slack channel or basecamp job when the cron is run.
Problem
When I run the command from via terminal it works great. Running the command inside a controller always returns the response Lock file does not exist. I'm assuming this in reference to the composer.lock file at the root of the project. I can confirm that this file does in fact exist.
Following is the controller I'm currently using, which is adapted from this:
http://symfony.com/doc/current/console/command_in_controller.html
<?php
namespace Treetop1500\SecurityReportBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
class DefaultController extends Controller
{
public function indexAction($key)
{
if ($key != $this->getParameter('easy_cron_key')) {
throw new UnauthorizedHttpException("You are not authorized to access this page.");
}
$kernel = $this->get('kernel');
$application = new Application($kernel);
$application->setAutoExit(false);
$input = new ArrayInput(array(
'command' => 'security:check'
));
// You can use NullOutput() if you don't need the output
$output = new BufferedOutput();
$application->run($input, $output);
// return the output, don't use if you used NullOutput()
$content = $output->fetch();
// return new Response(""), if you used NullOutput()
return new Response($content);
}
}
$content always has the value "Lock file does not exist."
I realize there are probably better tools and ways to do this, however I would really like to understand why this is the generated response from in this controller action. Thank you for taking a look!
Pass absolute path to composer.lock file just like that:
php bin/console security:check /path/to/another/composer.lock
So in your example, that's would be:
$input = new ArrayInput([
'command' => 'security:check',
'lockfile' => '/path/to/another/composer.lock'
]);
Read more: SecurityCheckerCommand from SensioLabs. Optional argument is lockfile, which is checked by SecurityChecker. On line 46, they are looking for composer.lock file (default argument) and throw an exception, when they not found.
P.S. Earlier, I type the wrong parameters to array. I checked in Symfony documentation (How to Call Other Commands) and fixed the answer.
The solution to this is to pass the lockfile argument to the ArrayInput object like this:
$lockfile = $this->get('kernel')->getRootDir()."/../composer.lock";
$input = new ArrayInput(array('command'=>'security:check','lockfile'=>$lockfile));

Get output of a Symfony command and save it to a file

I'm using Symfony 2.0.
I have created a command in Symfony and I want to take its output and write it to a file.
All I want is to take everything that is written on the standard output (on the console) and to have it in a variable. By all I mean things echoed in the command, exceptions catched in other files, called by the command and so on. I want the output both on the screen and in a variable (in order to write the content of the variable in a file). I will do the writing in the file in the end of the execute() method of the command.
Something like this:
protected function execute(InputInterface $input, OutputInterface $output)
{
// some logic and calls to services and functions
echo 'The operation was successful.';
$this->writeLogToFile($file, $output???);
}
And in the file I want to have:
[Output from the calls to other services, if any]
The operation was successful.
Can you please help me?
I tried something like this:
$stream = $output->getStream();
$content = stream_get_contents($stream, 5);
but the command doesn't finish in that way. :(
You could just forward the command output using standard shell methods with php app/console your:command > output.log. Or, if this is not an option, you could introduce a wrapper for the OutputInterface that would write to a stream and then forward calls to the wrapped output.
I needed the same thing, in my case, I wanted to email the console output for debug and audit to email, so I've made anon PHP class wrapper, which stores the line data and then passes to the original output instance, this will work only for PHP 7+.
protected function execute(InputInterface $input, OutputInterface $output) {
$loggableOutput = new class {
private $linesData;
public $output;
public function write($data) {
$this->linesData .= $data;
$this->output->write($data);
}
public function writeln($data) {
$this->linesData .= $data . "\n";
$this->output->writeln($data);
}
public function getLinesData() {
return $this->linesData;
}
};
$loggableOutput->output = $output;
//do some work with output
var_dump($loggableOutput->getLinesData());
}
Note this will only store the data written using write and writeln OutputInterface methods, this will no store any PHP warnings etc.
Sorry for bringing this up again.
I'm in a similar situation and if you browse the code for Symfony versions (2.7 onwards), there already is a solution.
You can easily adapt this to your specific problem:
// use Symfony\Component\Console\Output\BufferedOutput;
// You can use NullOutput() if you don't need the output
$output = new BufferedOutput();
$application->run($input, $output);
// return the output, don't use if you used NullOutput()
$content = $output->fetch();
This should neatly solve the problem.

Laravel / Symfony - How can I test confirmations in console commands?

I know I can unit test console commands by passing arguments and options like so:
$command->run(new ArrayInput($data), new NullOutput);
But what if I want to add a confirmation dialog to my command by using the confirm() method in Laravel?
Have you read the example on Symfony's site?
http://symfony.com/doc/current/components/console/helpers/dialoghelper.html#testing-a-command-which-expects-input
If you have but are still having trouble making it work, let us know.
I forgot to mention I was using PHPSpec for my unit tests.
Finally I figured out how to test confirmations with it.
These are the use statements:
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Helper\HelperSet;
And this is a sample test:
public function it_fires_the_command(QuestionHelper $question, HelperSet $helpers)
{
// $data is an array containing arguments and options for the console command
$input = new ArrayInput($data);
$output = new NullOutput;
// $query must be an instance of ConfirmationQuestion
$query = Argument::type('Symfony\Component\Console\Question\ConfirmationQuestion');
// with "willReturn" we can decide whether (TRUE) or not (FALSE) the user confirms
$question->ask($input, $output, $query)->willReturn(true);
// we expect the HelperSet to be invoked returning the mocked QuestionHelper
$helpers->get('question')->willReturn($question);
// finally we set the mocked HelperSet in our console command...
$this->setHelperSet($helpers);
// ...and run it
$this->run($input, $output);
}

Categories