I have a script that is designed to be run both as a web page, and via the console.
Detecting which method was used to invoke the script seems pretty straight forward, but when the script is being run from the console, I need to know if the script is being run interactively or not (user typing commands, or input redirected from a file).
php script.php
versus
php script.php < input_file
Is this possible?
I also needed a slightly more flexible solution than posix_isatty that could detect:
Is the script being run from the terminal
Is the script receiving data via a pipe or from a file
Is the output being redirected to a file
After a bit of experimenting and digging around through libc headers, I came up with a very simple class that can do all of the above and more.
class IOMode
{
public $stdin;
public $stdout;
public $stderr;
private function getMode(&$dev, $fp)
{
$stat = fstat($fp);
$mode = $stat['mode'] & 0170000; // S_IFMT
$dev = new StdClass;
$dev->isFifo = $mode == 0010000; // S_IFIFO
$dev->isChr = $mode == 0020000; // S_IFCHR
$dev->isDir = $mode == 0040000; // S_IFDIR
$dev->isBlk = $mode == 0060000; // S_IFBLK
$dev->isReg = $mode == 0100000; // S_IFREG
$dev->isLnk = $mode == 0120000; // S_IFLNK
$dev->isSock = $mode == 0140000; // S_IFSOCK
}
public function __construct()
{
$this->getMode($this->stdin, STDIN);
$this->getMode($this->stdout, STDOUT);
$this->getMode($this->stderr, STDERR);
}
}
$io = new IOMode;
Some example usage, to show what it can detect.
Input:
$ php io.php
// Character device as input
// $io->stdin->isChr == true
$ echo | php io.php
// Input piped from another command
// $io->stdin->isFifo == true
$ php io.php < infile
// Input from a regular file (name taken verbatim from C headers)
// $io->stdin->isReg == true
$ mkdir test
$ php io.php < test
// Directory used as input
// $io->stdin->isDir == true
Output:
$ php io.php
// $io->stdout->isChr == true
$ php io.php | cat
// $io->stdout->isFifo == true
$ php io.php > outfile
// $io->stdout->isReg == true
Error:
$ php io.php
// $io->stderr->isChr == true
$ php io.php 2>&1 | cat
// stderr redirected to stdout AND piped to another command
// $io->stderr->isFifo == true
$ php io.php 2>error
// $io->stderr->isReg == true
I've not included examples for links, sockets, or block devices, but there's no reason they shouldn't work, as the device mode masks for them are in the class.
(Not tested on Windows - mileage may vary)
posix_isatty()
if (posix_isatty(0)) {
// STDIN is a TTY
} else {
// STDIN is a pipe or has no associated TTY
}
Obviously this only works on POSIX compliant operating systems, where PHP has the posix extension installed. I am not aware of a Windoze equivalent.
For Windows, you can use this:
$b = stream_isatty(STDIN);
if ($b) {
echo "php script.php\n";
} else {
echo "php script.php < input_file\n";
}
https://php.net/function.stream-isatty
Related
I made a Symfony console command which uses pngquant to process and compress a long list of images. The images are read from a CSV file.
The batch is working fine until the end in local environment but in the stage environment it works for about 5 minutes and then it starts returning empty result from the shell_exec command. I even made a retry system but it's always returning empty result:
// escapeshellarg() makes this safe to use with any path
// errors are redirected to standard output
$command = sprintf(
'%s --quality %d-%d --output %s --force %s 2>&1',
$this->pngquantBinary,
$minQuality,
$maxQuality,
escapeshellarg($tempPath),
$path
);
// tries a few times
$data = null;
$attempt = 0;
do {
// command result
$data = shell_exec($command);
// error
if (null !== $data) {
$this->logger->warning('An error occurred while compressing the image with pngquant', [
'command' => $command,
'output' => $data,
'cpu' => sys_getloadavg(),
'attempt' => $attempt + 1,
'sleep' => self::SLEEP_BETWEEN_ATTEMPTS,
]);
sleep(self::SLEEP_BETWEEN_ATTEMPTS);
}
++$attempt;
} while ($attempt < self::MAX_NUMBER_OF_ATTEMPTS && null !== $data);
// verifies that the command has finished successfully
if (null !== $data) {
throw new \Exception(sprintf('There was an error compressing the file with command "%s": %s.', $command, $data));
}
The problem is that the same command executed in another shell in the same environment works fine! I mean, when I log the error, if I put the exactly same command in another instance on the same server, works fine.
Even from the Symfony logs I can't read any error, where should I look for a more detailed error?
What can be causing this? Memory and processor are fine during execution!
After many attempts I read this question:
Symfony2 Process component - unable to create pipe and launch a new process
The solution was to add a call to gc_collect_cycles after flush during the loop!
if ($flush || 0 === ($index % self::BATCH_SIZE)) {
$this->em->flush();
$this->em->clear();
// clears the temp directory after flushing
foreach ($this->tempImages as $tempImage) {
unlink($tempImage);
}
$this->tempImages = [];
// forces collection of any existing garbage cycles
gc_collect_cycles();
}
IMPORTANT: also keep an eye on the number of disk inodes.
df -i
Filesystem Inodes IUsed IFree IUse% Mounted on
udev 125193 392 124801 1% /dev
tmpfs 127004 964 126040 1% /run
/dev/vda2 5886720 5831604 55116 100% /
Am trying to run php Interactive shell from a php script. To be more specific, I want to be able to call my classes from interactive shell.
I manage to find this
# custom_interactive_shell.php
function proc($command, &$return_var = null, array &$stderr_output = null)
{
$return_var = null;
$stderr_output = [];
$descriptorspec = [
// Must use php://stdin(out) in order to allow display of command output
// and the user to interact with the process.
0 => ['file', 'php://stdin', 'r'],
1 => ['file', 'php://stdout', 'w'],
2 => ['pipe', 'w'],
];
$pipes = [];
$process = #proc_open($command, $descriptorspec, $pipes);
if (is_resource($process)) {
// Loop on process until it exits normally.
do {
$status = proc_get_status($process);
// If our stderr pipe has data, grab it for use later.
if (!feof($pipes[2])) {
// We're acting like passthru would and displaying errors as they come in.
$error_line = fgets($pipes[2]);
echo $error_line;
$stderr_output[] = $error_line;
}
} while ($status['running']);
// According to documentation, the exit code is only valid the first call
// after a process is finished. We can't rely on the return value of
// proc_close because proc_get_status will read the exit code first.
$return_var = $status['exitcode'];
proc_close($process);
}
}
proc('php -a -d auto_prepend_file=./vendor/autoload.php');
But its just not working, it tries to be interactive but freezes up a lot, and even after the lag it doesn't really execute commands properly.
Example:
> php custom_interactive_shell.php
Interactive shell
php > echo 1;
Warning: Use of undefined constant eo - assumed 'eo' (this will throw an Error in a future version of PHP) in php shell code on line 1
If you want to be able to run your PHP classes from an interactive shell then you can use the default one that ships with Terminal. From the terminal just type:
php -a
Then, in the below example I had created a file called Agency.php that had class Agency in it. I was able to require_once() it into the active shell and then call the class and its methods:
Interactive shell
php > require_once('Agency.php');
php > $a = new Agency();
php > $a->setname("some random name");
php > echo $a->getname();
some random name
You can also use the following in the interactive shell to autoload the files / classes in the current directory:
spl_autoload_register(function ($class_name) {
include $class_name . '.php';
});
I'm trying to write a bash script that adds a line into a php function like this:
public function register()
{
$this->app->bind('App\Repositories\Corporate\CorporateContract',
'App\Repositories\Corporate\EloquentCorporateRepository');
}
Here's my code:
function bindContractToRepository {
sed -i -e '/register()\n/a \ \n\t\t'${repoBinding}' \n\t\t\t\t ' ./app/Providers/${repoName}${provider}.php 2> /dev/null
}
bindContractToRepository
I actually want my code to come inside the function itself like the example at the top.
NB. I can't specify a particular line because the line number varies with different version
In general it is very difficult to detect PHP function boundaries without a parser.
We can do the parser's job in Bash: iterate the code line by line, solve the opening and closing braces etc. until we get the range of lines covered by this function and, finally, replace it with new content. We can do the same using the sed commands (in this case will likely look less readable than an assembly code).
But we already have a PHP parser! Why not use it?
Example
The following PHP CLI script accepts:
PHP input file (where we are going to replace a method/function);
PHP Method/function name
PHP code string for the replacement as a string, or dash(the standard input)
replace-method.php
<?php
function usage($error = false) {
global $argv;
$str = <<<EOS
USAGE: php {$argv[0]} OPTIONS
OPTIONS:
-h, --help Print help message
-i, --input-file Input PHP file
-m, --method PHP function/method name, e.g.:
my_func, MyClass::myMethod
-c, --code The new PHP code including the function/method declaration.
String of code, or dash ('-') meaning the standard input.
EXAMPLE:
php {$argv[0]} -i source/file.php -m 'MyClass::register' -c - <<ENDOFPHP
public function register()
{
echo time();
}
ENDOFPHP
Replaces the code of 'MyClass::register' method with new code from the standard input
in source/file.php.
EOS;
fprintf($error ? STDERR : STDOUT, $str);
exit((int)!$error);
}
if (false === ($opt = getopt('hi:m:c:', ['help', 'input-file:', 'method:', 'code:']))) {
fprintf(STDERR, "Failed to parse options\n");
usage(true);
}
if (isset($opt['h']) || isset($opt['help']))
usage();
// Using PHP7 Null coalescing operator
$file = $opt['i'] ?? $opt['input-file'] ?? null;
if (!file_exists($file)) {
fprintf(STDERR, "File '$file' does not exist\n");
usage(true);
}
$new_code = $opt['c'] ?? $opt['code'] ?? null;
if (!$new_code) {
fprintf(STDERR, "Code option expected\n");
usage(true);
}
if ($new_code == '-')
$new_code = file_get_contents('php://stdin');
$method = $opt['m'] ?? $opt['method'] ?? null;
if (!$method) {
fprintf(STDERR, "Method option expected\n");
usage(true);
}
// You most likely want to include project's autoloading file instead.
// (You might accept it as a CLI argument, too.)
require_once $file;
$rf = strpos($method, '::') ?
new ReflectionMethod($method) :
new ReflectionFunction($method);
$start_line = $rf->getStartLine();
$end_line = $rf->getEndLine();
$lines = file($file);
$code = implode(array_slice($lines, 0, $start_line - 1)) .
$new_code . implode(array_slice($lines, $end_line));
file_put_contents($file, $code);
Suppose we have path/to/file.php with A class and its register method:
<?php
class A
{
public function register()
{
echo 'aaaa';
}
}
We can replace the code of A::register method in a Bash as follows:
php replace-method.php -i path/to/file.php -m 'A::register' -c - <<ENDOFPHP
public function register()
{
echo 'something new';
}
ENDOFPHP
I've used Bash here document for input. You can use any kind of the shell redirection, for instance:
generate_php_code | php 1.php -i file.php -m 'A::register' -c -
My intention is this.
My client.html calls a php script check.php via ajax. I want check.php to check if another script task.php is already being run. If it is, I do nothing. If it is not, I need to run it in the background.
I have an idea what I want to do, but am unsure how to do it.
Part A. I know how to call check.php via ajax.
Part B. In check.php I might need to run task.php. I think I need something like:
$PID = shell_exec("php task.php > /dev/null & echo $!");
I think the "> /dev/null &" bit tells it to run in the background, but am unsure what the "$!" does.
Part C. The $PID I need as a tag of the process. I need to write this number (or whatever) to a file in the same directory, and need to read it every call to check.php. I can't work out how to do that. Could someone give me a link of how to read/write a file with a single number in to the same directory?
Part D. Then to check if the last launched task.php is still running I am going to use the function:
function is_process_running($PID)
{
exec("ps $PID", $ProcessState);
return(count($ProcessState) >= 2);
}
I think that is all the bits I need, but as you can see I am unsure on how to do a few of them.
I would use an flock() based mechanism to make sure that task.php runs only once.
Use a code like this:
<?php
$fd = fopen('lock.file', 'w+');
// try to get an exclusive lock. LOCK_NB let the operation not blocking
// if a process instance is already running. In this case, the else
// block will being entered.
if(flock($fd, LOCK_EX | LOCK_NB )) {
// run your code
sleep(10);
// ...
flock($fd, LOCK_UN);
} else {
echo 'already running';
}
fclose($fd);
Also note that flock() is, as the PHP documentation points out, portable across all supported operating systems.
!$
gives you the pid of the last executed program in bash. Like this:
command &
pid=$!
echo pid
Note that you will have to make sure your php code runs on a system with bash support. (Not windows)
Update (after comment of opener).
flock() will work on all operating systems (As I mentioned). The problem I see in your code when working with windows is the !$ (As I mentioned ;) ..
To obtain the pid of the task.php you should use proc_open() to start task.php. I've prepared two example scripts:
task.php
$fd = fopen('lock.file', 'w+');
// try to get an exclusive lock. LOCK_NB let the operation not blocking
// if a process instance is already running. In this case, the else
// block will being entered.
if(flock($fd, LOCK_EX | LOCK_NB )) {
// your task's code comes here
sleep(10);
// ...
flock($fd, LOCK_UN);
echo 'success';
$exitcode = 0;
} else {
echo 'already running';
// return 2 to let check.php know about that
// task.php is already running
$exitcode = 2;
}
fclose($fd);
exit($exitcode);
check.php
$cmd = 'php task.php';
$descriptorspec = array(
0 => array('pipe', 'r'), // STDIN
1 => array('pipe', 'w'), // STDOUT
2 => array('pipe', 'w') // STDERR
);
$pipes = array(); // will be set by proc_open()
// start task.php
$process = proc_open($cmd, $descriptorspec, $pipes);
if(!is_resource($process)) {
die('failed to start task.php');
}
// get output (stdout and stderr)
$output = stream_get_contents($pipes[1]);
$errors = stream_get_contents($pipes[2]);
do {
// get the pid of the child process and it's exit code
$status = proc_get_status($process);
} while($status['running'] !== FALSE);
// close the process
proc_close($process);
// get pid and exitcode
$pid = $status['pid'];
$exitcode = $status['exitcode'];
// handle exit code
switch($exitcode) {
case 0:
echo 'Task.php has been executed with PID: ' . $pid
. '. The output was: ' . $output;
break;
case 1:
echo 'Task.php has been executed with errors: ' . $output;
break;
case 2:
echo 'Cannot execute task.php. Another instance is running';
break;
default:
echo 'Unknown error: ' . $stdout;
}
You asked me why my flock() solution is the best. It's just because the other answer will not reliably make sure that task.php runs once. This is because the race condition I've mentioned in the comments below that answer.
You can realize it, using lock file:
if(is_file(__DIR__.'/work.lock'))
{
die('Script already run.');
}
else
{
file_put_contents(__DIR__.'/work.lock', '');
// YOUR CODE
unlink(__DIR__.'/work.lock');
}
Too bad I didn't see this before it was accepted..
I have written a class to do just this. ( using file locking ) and PID, process ID checking, on both windows and Linux.
https://github.com/ArtisticPhoenix/MISC/blob/master/ProcLock.php
I think your are really overdoing it with all the processes and background checks. If you run a PHP script without a session, then you are already essentially running it in the background. Because it will not block any other request from the user. So make sure you don't call session_start();
Then the next step would be to run it even when the user cancels the request, which is a basic function in PHP. ignore_user_abort
Last check is to make sure it's only runs once, which can be easily done with creating a file, since PHP doesnt have an easy application scope.
Combined:
<?php
// Ignore user aborts and allow the script
// to run forever
ignore_user_abort(true);
set_time_limit(0);
$checkfile = "./runningtask.tmp";
//LOCK_EX basicaly uses flock() to prevents racecondition in regards to a regular file open.
if(file_put_contents($checkfile, "running", LOCK_EX)===false) {
exit();
}
function Cleanup() {
global $checkfile;
unlink($checkfile);
}
/*
actual code for task.php
*/
//run cleanup when your done, make sure you also call it if you exit the code anywhere else
Cleanup();
?>
In your javascript you can now call the task.php directly and cancel the request when the connection to the server has been established.
<script>
function Request(url){
if (window.XMLHttpRequest) { // Mozilla, Safari, ...
httpRequest = new XMLHttpRequest();
} else if (window.ActiveXObject) { // IE
httpRequest = new ActiveXObject("Microsoft.XMLHTTP");
} else{
return false;
}
httpRequest.onreadystatechange = function(){
if (httpRequest.readyState == 1) {
//task started, exit
httpRequest.abort();
}
};
httpRequest.open('GET', url, true);
httpRequest.send(null);
}
//call Request("task.php"); whenever you want.
</script>
Bonus points: You can have the actual code for task.php write occasional updates to $checkfile to have a sense of what is going on. Then you can have another ajax file read the content and show the status to the user.
Lets make the whole process from B to D simple
Step B-D:
$rslt =array(); // output from first exec
$output = array(); // output of task.php execution
//Check if any process by the name 'task.php' is running
exec("ps -auxf | grep 'task.php' | grep -v 'grep'",$rslt);
if(count($rslt)==0) // if none,
exec('php task.php',$output); // run the task,
Explanation:
ps -auxf --> gets all running processes with details
grep 'task.php' --> filter the process by 'task.php' keyword
grep -v 'grep' --> filters the grep process out
NB:
Its also advisable to put the same check in task.php file.
If task.php is executed directly via httpd (webserver), it will only be displayed as a httpd process and cannot be identified by 'ps' command
It wouldn't work under load-balanced environment. [Edited: 17Jul17]
You can get an exclusive lock on the script itself for the duration of the script running
Any other attempts to run it will end as soon as the lock() function is invoked.
//try to set a global exclusive lock on the file invoking this function and die if not successful
function lock(){
$file = isset($_SERVER['SCRIPT_FILENAME'])?
realpath($_SERVER['SCRIPT_FILENAME']):
(isset($_SERVER['PHP_SELF'])?realpath($_SERVER['PHP_SELF']):false);
if($file && file_exists($file)){
//global handle stays alive for the duration if this script running
global $$file;
if(!isset($$file)){$$file = fopen($file,'r');}
if(!flock($$file, LOCK_EX|LOCK_NB)){
echo 'This script is already running.'."\n";
die;
}
}
}
Test
Run this in one shell and try to run it in another while it is waiting for input.
lock();
//this will pause execution until an you press enter
echo '...continue? [enter]';
$handle = fopen("php://stdin","r");
$line = fgets($handle);
fclose($handle);
Is there a way to check in a php script if exec() is enabled or disabled on a server?
This will check if the function actually works (permissions, rights, etc):
if(#exec('echo EXEC') == 'EXEC'){
echo 'exec works';
}
if(function_exists('exec')) {
echo "exec is enabled";
}
ini_get('disable_functions')
What you actually want to do is use ini_get('disable_functions') to find out if it is available to you:
<?php
function exec_enabled() {
$disabled = explode(',', ini_get('disable_functions'));
return !in_array('exec', $disabled);
}
?>
Answered on stackoverflow here: Check if "exec" is disabled, Which actually seems to come from the PHP Man page: http://php.net/manual/en/function.exec.php#97187
Path
If the above returns true (you can use exec()), but PHP can still not trigger the script there is a good chance that you have a path issue for that script, test this by doing:
print exec('which bash');
and then try
print exec('which ogr2ogr');
This will check that exec is available and enabled BEFORE trying to run it. If you run exec() and the function does not exist or is disabled a warning will be generated. Depending on the server settings that may render to the browser and will almost-always write a line to a log file = performance hit.
// Exec function exists.
// Exec is not disabled.
// Safe Mode is not on.
$exec_enabled =
function_exists('exec') &&
!in_array('exec', array_map('trim', explode(', ', ini_get('disable_functions')))) &&
strtolower(ini_get('safe_mode')) != 1
;
if($exec_enabled) { exec('blah'); }
It's a bit tricky to find exec function available until unless checking all possibilities
1.function_exist('exec')
2.Scanning through ini_get('disabled_functions)
3.Checking safe_mode enabled
function is_shell_exec_available() {
if (in_array(strtolower(ini_get('safe_mode')), array('on', '1'), true) || (!function_exists('exec'))) {
return false;
}
$disabled_functions = explode(',', ini_get('disable_functions'));
$exec_enabled = !in_array('exec', $disabled_functions);
return ($exec_enabled) ? true : false;
}
This function never throws warnings unless ini_get function not disabled.
I am assuming that you are running this on a linux server.
You could test the exec function by running the following php script:
exec("whoami", $ret);
echo $ret[0];
This will return the command whoami.
If it errors out, it is because the exec function could not run.
Example:
if(strpos(ini_get('disable_functions'),'ini_set')===false)
#ini_set('display_errors',0);
This is some ugly code I made to detect if a function is enabled or not.
function is_enabled($f)
{
if($f=='ini_get')return#ini_get('a')===false;
return(($l=#ini_get('disable_functions'))===null||!is_callable($f)||!function_exists($f)||!in_array($f,array_map('trim',explode(',',$l)));
}
//Usage example:
print_r(is_enabled('str_split'));//true or null if ini_get() is disabled
(Based on other responses)
To check if exec exists and services are running:
function isRunning($serviceName)
{
return exec('pgrep '.$serviceName);
}
if (#exec('echo EXEC') == 'EXEC') {
$services = [
'mysql',
'nginx',
'redis',
'supervisord',
];
foreach ($services as $service) {
if (isRunning($service)) {
echo $service.' service is running.<br>';
} else {
echo $service.' service is down.<br>';
}
}
}
// mysql service is running.
// nginx service is running.
// redis service is running.
// supervisord service is down.
I would use this:
if (in_array('exec', preg_split('/\s*,\s*/', ini_get('disable_functions')) {
echo "exec is disabled";
}