Laravel hangs when running command via exec without sending it to background - php

I have a weird issue that I've been stuck with for a couple of days now. I'm trying to generate a pdf in a Laravel app using chrome headless with this command
google-chrome --headless --disable-gpu --print-to-pdf=outputfile.pdf http://localurl/pdf-html
The command basically opens chrome in headless mode, navigates to the given url and prints it as pdf saving the file in the specified location. This command is working perfectly when run in my system's shell (I'm using Ubuntu 18.04). Now, my issue arises when trying to run the same command from a Laravel controller, I've tried exec, shell_exec, system and passthru and all give me the same problem. If I run the command without redirecting output and running the process on the backgroung, by adding >> tmpfile 2>&1 & to the end of the command then the request hangs. Running the command in the background would not be a problem normally, except that I need for the command to finish in order to send the file back to the client as a download. By running it on the background this basically executes it asynchrounously and I have no way of knowing when the process ends (or to wait until it ends) to then send the file as a dowload on the response.
I've tried other alternatives to no avail. I've tried using Symfony's Process which comes bundled with Laravel and it also fails. I've tried using puppeteer and instead of running the google-chrome command use a node.js script with code from the puppeteer documentation (which by the way also works when run directly in my system shell), but when run from Laravel throws a Navigation Timeout Error exception.
Finally I created a simple php file with the following code:
<?php
$chromeBinary = 'google-chrome';
$pdfRenderUrl = "http://localhost:8000/pdf-html";
$fileName = 'invoice.pdf';
$outputDirectory = "/path/to/my/file/" . $fileName;
$command = sprintf(
'%s --headless --disable-gpu --print-to-pdf=%s %s',
escapeshellarg($chromeBinary),
escapeshellarg($outputDirectory),
escapeshellarg($pdfRenderUrl)
);
exec( $command );
echo ( file_exists("/path/to/my/file/" . $fileName) ? 'TRUE' : 'FALSE');
?>
And the code runs just fine when run from shell like php thefile.php printing TRUE, meaning the command in exec was launched and after it ended then the file exists; and THAT is the exact code I'm using on Laravel except it only works, as mentioned above, when I send the process to the background.
Can anybody throw me a line here, please? Thanks
EDIT: #namoshek thanks for the quick reply and sorry if I did not made myself clear. The problem is not long waiting times, perhaps I could live with that. The problem is that exec never finishes and I eventually have to forcefully terminate the process (nor exec, nor any other alternative, they all freeze the request completely forever, with the exception of Process which fails by throwing a TimeoutException). I'm using postman to query the endpoint. The frontend is an Angular app, meaning the request for the invoice download will be made asynchronously eventually. Furthermore the task itself is not a long running task, as a matter of facts it finishes pretty quick. Using a polling strategy or a notification system, to me, does not seem like a viable solution. Imagine an app with a download button to download a simple document and you have to click the button and then wait for the app to notify you via email (or some other way) that the document is ready. I could understand it if it were a more complicated process, but a document download seems like something trivial. But what has me at a loss is why is it that running the task from a php script works as I want it to (synchonously) and I can't replicate the behaviour on the laravel controller
EDIT: I've also tried using BrowserShot, which, BTW also fails. Browsershot provides a way to interact, behind the scenes with puppeteer by using Process, and generate a pdf file. And even though it's an external program, it still seems to me that the behaviour I'm getting is not normal, I should be able to obtain the download even if the request took 10secs to finish because it executed the external program synchronously. But in my case it's failing due to a timeout error
EDIT: So after a while I came upon the apparent reason of the server hang up. The problem is that I was using artisan's development server. This, initially, did not seem like a problem to me but it seems that artisan can't handle that load. In the feature I'm implementing I'm performing a request to a particular endpoint, let's call it endpoint 1, to generate the pdf, the code on this endpoint triggers the external command, and when executed synchronously it means the code in endpoint 1 is waiting for the external command to finish. The external command in turn needs to browse to endpoint 2 on the same server, endpoint 2 contains an html view with the content to be put on the pdf, since the server is still waiting on endpoint 1 for the return of the external command then endpoint 2 is unresponsive, which apparently creates a loop which artisan's development server can't handle. Problem is I did a quick search and I found nothing that indicated that defficiency on artisan's development server. I moved the environment to Apache just to test my theory and it worked, though it should be noted that the request takes a very long time to finish (around 10-20 secs). This, so far, seems like the only reasonable explanation as to why that issue was happenning. If anyone knows how I can improve performance on this request, or anyone can provide a better explanation to the original issue I'd appreciate it.

#hrivera I'm a bit late to the game here, but regarding your last edit I believe you're almost correct, but my thoughts on this is that PHP's built-in server, which Laravel uses for development, is single threaded. The issue I had is that any assets within the page that was being passed to Chrome couldn't be loaded (CSS, js, etc) as the thread was already in use, and so it hung. Removing any assets from the HTML fixed the issue.
Production servers are multi-threaded, so we should have no issues. Not entirely sure I'm right, but wanted to comment anyway.

I don't really get what you are asking for, because it seems you already understood that executing a long running task like creating a snapshot will block the request if being run synchronously. Using other software such as Puppeteer will not change that. If your requests needs to wait for the result of this process to return, then the only way to have your request return faster is by speeding up the task itself. Which is most likely not possible.
So, there are basically only two options left: Live with the long wait times (if you want to perform the task synchronously) or execute the request/task asynchronously. The latter can be achieved in two ways:
Make the actual HTTP request be run in the background (using ajax) and use a loading indicator to keep your users patient. This way you could still run the process synchronously, but I would not recommend doing so as you would have to use high timeout times for the ajax request and in some situations the requests would probably still timeout (depending on the workload of your server).
Use the power of Laravel and make use of queue workers to perform the calculation in background. When the snapshot generation is finished, you can then use one of the following three options to return the result:
Use polling on the client side to see if the result is available.
Send the result or a link to the result per mail or something similar to the user.
Use a notification system to notify the user about the finished process and return the result in some way (i.e. fetch it or send it as part of the notification - there are plenty of options available). A built-in notification system that does exactly what I described is Laravel Echo. On receiving the notification that tells you the process finished, you could then fetch the result from the server.
In current times, the standard for web apps and user experience is option 2 with the notification system (3rd point).

Related

Asynchronous Process doesn't seem to work on my prod environment

I'm currently working on a rather complex PHP5/Symfony 2.8 project. In this project, I have a command that can be called by a Crontab, manually in console, or using a button on the website, that is used to call a webservice on an external site, which gives me an xml file, that I manage to import into my database using SimpleXML.
The command works like a charm on my local dev environment, no matter how it is called.
But for unknown reasons (which is why I'm posting here), on my int or prod environments, which are located on external servers, calling the command from the button on the site isn't working.
My button is triggering this action :
$process = new Process('php '.$kernelRootDir.'/console my:super:command');
$process->start();
As the command is kinda heavy, I can not afford to wait for the command to be completed, which is why I'm using process->start rather than run. I'm not in need of logs for it, so that's fine that it just starts the command and let it run while the user can go on another page.
And again, this works great on my local environment (debian 7 VM), but not on the distant server (not sure what it is using). However, if I manually launch the command via console or let the crontab call it, it runs perfectly until the end. That's just triggering it from my controller via Process that doesn't work.
This is pulling my hair off my head since yesterday, but I can't figure out why the command isn't even starting on the prod environment.. Any tips ?
Update : Tried to make my command only dumping a small "it worked" message, and using wait() and getOutput() methods to get the result in my controller. On my local environment, I instantly got my message as a result from the controller using dump/die combo. On the distant server, my command trigger a ProcessTimedOutException : the process exceeded the 60 seconds timeout.
So there is really a problem with Process being unable to launch a custom command, though that command works when called manually on console.
Okay, after struggling since two days, I found that the distant server might be responsible of this, it seems that if I don't wait for the process answer, that process is killed instantly. I don't know why, I don't know how, but it seems like it is.
So I started searching for an alternative solution that was not using Process. And I found something. I can use the php exec() method to launch an asynchronous call by doing so :
exec('php '.$kernelRootDir.'/console my:super:command > /dev/null &');
//Apparently, using "&" launch the command asynchronously.
All my tests have been working on my production server so far, so I'll keep that.

start /B doesn't start the task

I'm currently launching an asynchronous job with PHP to perform some tests.
To make it work, I found on SO some tips, like the use of popen and start:
$commande = "testu.bat";
$pid = popen('start /B ' . $commande, 'r');
$status = pclose($pid);
The testu.bat's folder is in my user PATH.
This script performs some task, and to control it's execution, it should generates a log file, but I never get it.
Whereas if I just remove the /B option, it works fine and I get my log file.
Did I miss something about background execution? How can I catch the error informations when it is running in the background?
It appears you are operating under the assumption that the /B switch to the start command means "background". It does not. From the start usage:
B Start application without creating a new window. The
application has ^C handling ignored. Unless the application
enables ^C processing, ^Break is the only way to interrupt
the application.
Processes launched by start are asynchronous by default. Since that appears to be what you want, just run the command without the /B switch.
Interesting one... Ok, here's what I think is going on:
Because you run the task in the background, the PHP script will just carry on, it is not waiting for testu.bat to return anything...
Or put another way, popen does what it was instructed to do, which is starting the task in the background (which it does) but then control is handed immediately back to PHP, whilst the log file is still being created in the background and the php script carries on at the same time...
What I would do in this case is let testu.bat call the php script (or another PHP script) in a callback type fashion once it has done its processing, in a similar way as in Javascript you would use callbacks for asynchromous Ajax calls...
Maybe provide the callback script command as a parameter to testu.bat..?
Hope this is of any help...
I'm not quite sure about your goal here, but here are some info you might use:
for figuring out background errors, you may find these functions useful:
set_exception_handler();
set_error_handler();
register_shutdown_function();
Of course write out the errors they catch into some file.
If you do not need any data back from your requests, you can simply use:
fsockopen()
curl
and give them a short timeout (10 milisec). The scripts will run in the backround.
Alternatively if you do need the data back, you can either put it into a database and set up a loop that checks if the data has already been inserted, or simply output it into a file and check for its existence.
In my opinion start launches the specified command by creating a new prcoess in the "background". Therefore the execution of start itself "just" starts the second process and exists immediately.
However, using the /B switch, the command to be executed will be excuted in the context of the start process. Therefore the execution of the start process takes longer. Now what I suspect is that executing pclose terminates the start process and as a result of this you don't get your log file.
Maybe one solution (not testet though) could be executing something like
start /B cmd "/C testu.bat" where start just tries to execute cmd and cmd gets /C testu.bat as parameter which is the "command" it shall execute.
Another thought:
What happens if you don't call $status = pclose($pid);?
Just for some people seeking this trick to works, in my case it just needs to activate the PHP directive ignore_user_abort in php.ini or by the PHP platform function.
Without this activated, the process is killed by pclose() without finishing the job.
Your problem is most likely properly solved by using a queue system. You insert a job into a queue that a background process picks up and works on. In this way the background task is completely independent of the HTTP request that initiated the task - but you can still monitor its progress.
The two most popular software packages that can help you in your scenario:
Gearman
Check out this gist and this totorial for installation on Windows.
RabbitMQ
Check out this tutorial for installation on Windows.
Note that implementing a queuing solution is not a 5 minute prospect, but it is technically the right approach for this sort of situation. For a company product this is the only viable approach; for a personal project where you just want something to work, there is a level of commitment required to see it through for the first time. If you're looking to expand your development horizons, I strongly suggest you give the queue a shot.

Run PHP script like an application without Browser

Hi I am new to PHP and have no idea if what I am about to ask is even possible or does it even make sense but here goes.
I want to execute a PHP script as if I am executing a standalone application on the WebServer, what I am trying to implement is that when the Customer purchases something on the website and the once he sees the payment confirmation notice on the website, he should be allowed to close the browser window or logoff without affecting the big order generation process that get's started once the user is taken to the page that displays that the payment that he made was successful.
Right now I am making use of AJAX to call my after payment processing PHP script and have kept that script to ignore any user abort call.
This is the page that tells the user that the payment was received successfully.
thankyou.php
This is the page that performs the processing that needs to be done only after successful receipt of payment
FinishCheckoutProcess.inc.php
Now thankyou.php makes use of AJAX to execute FinishCheckoutProcess.inc.php asynchronously and FinishCheckoutProcess.inc.php has a PHP.ini setting in it that goes like this:
ignore_user_abort(true);
Now the combination of AJAX and ignore_user_abort(true) allows the after payment process to run without any errors even if the user closes his browser window, but since this script has nothing to do with the user or the browser I just wanted to know if it is possible to run this script in the background like a standalone application independent of the browser.
Also my WebServer is Apache and OS is Linux(Ubuntu OS).
My work is getting done but I just want to know if there is a better/safer way to do it.
Anyway thanks in advance to everyone, this site has helped me more than any book could have. So all you experts out there who donate their times to newbies like me you guys are awesome. Please keep up the good work.
Again thanks a lot.
Based on suggestions received
If I use the "exec" method to execute the FinishCheckoutProcess.inc.php, will this execute database related commands and will it be able to run further PHP scripts.
FinishCheckoutProcess.inc.php in turn executes a series of other PHP scripts which in turn executes other PHP scripts, so will using "exec" command to run FinishCheckoutProcess.inc.php create any difficulties.
FinishCheckoutProcess.inc.php process also does interaction with the MySQL database, so will I be able to do this if I execute this script using "exec" command. I am passing the necessary MySQLi connection object to this PHP script right now. So can I pass it the same way to it using "exec"
Also the process is quite heavy as it generates a set of 4 image files using IMagick and ImageMagick.
It generates a set of 4 image files for every product ordered, so if the quantity of 1 product is 10 then the total files generated will be 1x10x4 = 40
If there are two products with one having quantity as 2 and the other having quantity as 4 then the total files generated will be
1x2x4 = 8 +
1x4x4 = 16 = 24
So this script might need to run for a long time and cannot be allowed to be stopped due to time out reasons, it needs to finish what it started.
Basiclly the FinishCheckoutProcess.inc.php logic and process is quite complex so just want to confirm if the "exec" can handle it or not.
Also I am not sure but some of them also make use of $_SESSION variables, but if this a problem I can modify it, $_SESSION variables only get's used in one place and yes the $_SESSION get's set in the browser before the FinishCheckoutProcess.inc.php script is executed. By some previous PHP script.
I just want to execute the FinishCheckoutProcess.inc.php script independent of the parent/calling script i.e. thankyou.php, so that if the user closes the browser then the FinishCheckoutProcess.inc.php will not stop or abort becuse the parent/calling script i.e. thankyou.php is now no longer running.
FYI you can run php scripts like php my/script.php.
A safer way to do it would be have a master/worker process workflow. The master process runs on the server and checks a queue of work and the spawns worker processes to handle items on the queue as the arrive.
In your scenario you add stuff to the queue when the user pays. Once it is added to the queue you can send back thankyou.php to the user and they can continue or leave or whatever. Once the work is on the queue your master process spawns a worker process to handle the stuff (basically does everything in FinishCheckoutProcess.inc.php).
You can implement this in php with: php master.php
master.php
while( true ){
//check queue
//if found queue item
//shell_exec( 'php worker.php' );
}
From what i understand, you are looking for something like Laravel offers with it's illuminate/queue package:
Queues allow you to defer the processing of a time consuming task, such as sending an e-mail, until a later time which drastically speeds up web requests to your application.
This isn't something that only Laravel offers, though it does simplify/ease the implementation of such mechanism.
In the background you have supervisord executing a "worker" php script that executes tasks you put in a common place (db tabel, filesystem, anything), those tasks are usually references to a certain class/method with some variables to send to it.
The following links might give you a better understanding:
http://supervisord.org/index.html
https://laravel.com/docs/5.1/queues
https://laravel.com/docs/5.1/queues#supervisor-configuration
There are many ways you could implement a queue system, also without the use of supervisord. But i recently implemented this method myself because it guarantees my tasks are being processed, even after server restart (if configured properly).

PHP : How to run process separate from main process

I have a function to import data from excel to database, I make this function to run on server so this function doesn't need to interact with client anymore, the client web browser just need to upload the excel file to server, after that, the task will be run just on server so if the browser closed by client, the function still run on server, i've got this, the problem is, when the browser is leave open by client, the browser will loading as long as the function still active.How can i made the browser not wait respond from server so the browser will not loading while the process is run on server.Please help me.
Use a message queue to offload the task of processing the file from the web server to another daemon running separately.
You can take the cheap and easy route of execing a process with & in the command line, causing it to be backgrounded. However, that gives you little control / status.
The right way to go about it IMO is to queue up these long-running tasks in a database, with some status info associated with them. Then have a dedicated process which runs separate from your webserver, checking the database for tasks, and performs them, updating the database with success/failure status.
Look into using a queue such as Mseven's Queue Plugin:
Msevens Queue Plugin
Or, if you want a more daemon based job, look into Beanstalkd. The queue plugin by mseven is pretty self explanatry though. Stay away from forking processes using &, it can get out of control.

php script that runs on the server without a client request

I am working on a site that require a php script running on a server without any request,
it is a bot script that keeps (not full time but at least once a day) checking client accounts and send alert messages to clients when something happens.
any ideas are appreciated.
Assuming you need to do this on linux, you may run any php script from the browser and from the CLI as well.
You may run a simple php script:
<? echo "Ana are mere"; ?>
like this:
php -f ./index.php
Be careful about file-permissions, and any bug that may creep inside your code, memory leaks or unallocated variables will become VERY visible now, as the process will run continuously.
If you dont want it running in the background all the time, take a look at crontab (http://unixgeeks.org/security/newbie/unix/cron-1.html) to be able to start jobs regularly.
-- edit--
take a look at php execute a background process and PHP: How to return information to a waiting script and continue processing
Basically you want to start a background process, and you may do this by either using exec() or fsockopen() or a file_get_contents() on your own script probably in this order, if don't have access to exec, or socket functions.
Also take a look at http://us2.php.net/manual/en/function.session-write-close.php so the "background script" won't "block" the request and http://us2.php.net/manual/en/function.ignore-user-abort.php
Use a cron job to do it http://www.cronjobs.org/
You can automatically call a script at any interval you like indefinitely. Your hosting provider should support them if they are good.
You should also consider putting a unique key on the end of the page
ie. www.yoursite.com/cronjob.php?key=randomstring
and then only run the script if the key is correct, to prevent bots and other users from running the script when you don't want it run.
If you can't create a cron job, then create a page that does what you want and create a scheduled task on another machine (maybe your PC?) that just goes out and hits that page at a certain time every day.
It's really a hack, but if you absolutely can't set up a cron job, it would be an option.
As Evernoob and Quamis said, you want to have a cron job (UNIX/Linux/Mac OS) or a scheduled task (MS Windows). Furthermore, you can either have the PHP script run using the PHP command line interface (CLI), in which case you can invoke the PHP executable and then your script name. As an alternate, you can use a tool like wget (availble on all platforms) to invoke the PHP script as if someone had typed the URL in the location bar of a web browser.
A php script could not be used like you imagine here. Because it's executed through apache after a request from somewhere.
Even if you do while(1) in your script, apache/php will automaticly stop your script.
Responding to your comment, yes you'll need ssh access to do this, except if your web interface allow you to add cronjob.
Maybe you can write a service which can be executed with a program on another server and do the job.
If you have no access to the server the easiest way would probably be to hit it through the browser, but that would require you or an external script hitting the URL at the same interval each day when you wanted it to one. You may also be able to setup a Selenium test suite that runs locally on a schedule and hits the page. I'm not 100% if that's possible with Selenium though, you may need some 3rd-party apps to make it happen.
Something else you could try would be to see about using PHP's Process Control Functions (link). These will let you create a script that is a deamon and runs in the background. You may be able to do this to keep the script running on the server and firing off commands at programmed intervals. You will still need some way to get it running the first time (browser request or via command line) though.

Categories