cli, the other sapi
DESCRIPTION
PHP isn't only used as a web-based scripting language, it can also be used on the command line. This talks explains the benefits of command line PHP. Additionally, process control using CLI PHP is explained.TRANSCRIPT
CLI, the other SAPIUsing PHP on the command line in a Linux
environmentThijs Feryn - [email protected]
• Thijs Feryn (http://blog.feryn.eu)• Support manager at COMBELL• Zend Certified PHP 5 developer• I love the LAMP stack• Working on my MySQL certification• Open-source contributor• Nerd/geek of some sort
Who am I?
Who’s COMBELL?
• Largest independent hoster in Belgium• Founded in 1999 (a decade of COMBELL)• Focus on premium/quality hosting• More than 25 000 customers• More than 100 000 domains• More than 20 000 websites• More than 1000 servers• More than 800 resellers
Summarized: authorative partner for all hosted activity
Who’s COMBELL?
• Who’s a developer?
• Who’s a PHP developer?
• Who has ever used the PHP CLI?
• Who is used to working on Linux?
• Who has experience with process forking?
Who are you?
1. SAPI ... schmapi? (definition)2. Common PHP SAPI’s (overview)3. The CLI SAPI (how to use, possibilities, when to use)4. Apache vs CLI (a comparison)5. PHP binary options6. CLI in action (some examples)7. PCNTL == $fooDoo (the magic of process control)8. Wake the daemons (putting it all together)9. Q & A (the obligatory epilogue)
What’s up?
Definition
SAPI ... schmapi?
Wikipedia says: “The Server Application Programming Interface
(SAPI) is the generic term used to designate direct module interfaces to
web server applications”
In concreto: the SAPI is the way your server interacts with PHP
Overview
• Apache/Apache 2
• CGI
• FastCGI
• ISAPI
• CLI
• GTK
Common SAPI’s
Enabling the SAPI you need
• CLI is included by default• Add the right parameter to enable the Apache
Common SAPI’s
./configure --with-apxs=/location/of/your/apache/module
SAPI when compiling from source
Definition
The CLI SAPI
Php.net says: As of version 4.3.0, PHP supports a new SAPI type (Server Application Programming Interface) named CLI which means Command Line Interface. As the name implies, this SAPI type main focus is on developing shell (or desktop as well) applications with PHP.
In Concreto: PHP scripts are no longer exclusively called via the web browser. Using CLI, PHP scripts can be executed from the command line of the server which offers a vast array of benefits
When to use
• In cron’s
• For batches
• Process control
• Linux interactivity (e.g.: pipes)
The CLI SAPI
CLI 101
• Each CLI script is called via the PHP binary
• Arguments can be passed
• Arguments can be interpreted via special variables
• Input can be passed via STDIN
• Output can be returned via STDOUT
• Errors can be returned via STDERR
• I/O via pipes is possible
The CLI SAPI
$ php cli.php
CLI 101: arguments
• Passing arguments
• Interpreting arguments via $_SERVER
• $_SERVER[‘argc’] = counts the number of arguments
• $_SERVER[‘argv’][0] = scriptname (cli.php)
• $_SERVER[‘argv’][1] = first argument (arg1)
• $_SERVER[‘argv’][2] = second argument (arg2)
The CLI SAPI
$ php cli.php arg1 arg2
CLI 101: arguments
If ‘register_argc_argv’ is enabled 2 extra local variables are registered:
• $argc: counts the number of arguments
• $argv: array which stores the arguments
• $argv[0] = scriptname (cli.php)
• $argv[1] = first argument (arg1)
• $argv[2] = second argument (arg2)
The CLI SAPI
CLI 101: arguments & getopt()
• Gets command line arguments & values based on an input variable containing all the options:
• Individual characters (= flags, do not accept values)
• Characters followed by colon (input value required)
• Characters followed by 2 colons (input value optional)
• Long options only work well in PHP 5.3
• Check out Zend_Console_Getopt, it has some nice features similar to getopt()
The CLI SAPI
$options = getopt(‘ab:c::’);
CLI 101: input
• Reading from STDIN by opening a stream
• Reading a single line from STDIN
The CLI SAPI
$handle = fopen('php://stdin', 'r');
$stdin = trim(fgets(STDIN));
CLI 101: output
• Writing to STDOUT by opening a stream
• Writing a single line to STDERR
The CLI SAPI
$handle = fopen('php://stdout', 'w');
fwrite(STDOUT,‘output’);
CLI 101: errors
• Writing to STDERR by opening a stream
• Writing a single line to STDERR
The CLI SAPI
$handle = fopen('php://stderr', 'w');
fwrite(STDERR,‘error’);
CLI 101: piping
Using STDIN, STDOUT & STDERR streams you can easily benefit from the piping mechanisms in Linux to interact with other binaries
The CLI SAPI
$ cat input.ext | php myCliScript.php > output.ext
$ php myCliScript.php | grep filterText > output.ext
CLI 101: piping
You can also combine it with arguments
The CLI SAPI
$ php myCliScript.php arg1 arg2 > output.ext
$ cat input.ext | php myCliScript.php arg1 arg2 arg3 | grep filterText > output.ext
State means everything
Apache VS CLI
Apache• HTTP is stateless a protocol
• Limited interactivity
• Sessions & cookies as workaround for these drawbacks
• Execution timeouts
• Limited packet size
State means everything
Apache VS CLI
CLI• Controllable state
• More interactivity
• No (need for) sessions
• No execution timeouts
Ins & outs: input
Apache VS CLI
Apache$_GET
$_POST
$_COOKIE
$_SESSION
$_SERVER
$_ENV
CLI$_SERVER[‘argv’]
$argv
$_ENV
getopt()
STDIN
Ins & outs: output
Apache VS CLI
ApacheHTTP output only
Limited packet size
CLISTDOUT
STDERR
CLI usage, Apache mentality
Apache VS CLI
• Don’t use sessions or cookies, just use a local variables
• Don’t “get” your input, “read” it
• No execution timeouts
• Bundled output (HTTP): in response message
• Distributed output (CLI): via STDOUT. Available when needes
CLI usage, Apache mentality
Apache VS CLI
• OVERHEAD ALERT: don’t use HTTP via crons (e.g. via lynx or wget), use the php binary.
• CLI mode doesn’t change the CWD (current working directory). Be careful with path reference
• Use “dirname(__FILE__)”
• Use “chdir()”
CLI options reference
PHP binary options
Usage: php [options] [-f] <file> [--] [args...] php [options] -r <code> [--] [args...] php [options] [-B <begin_code>] -R <code> [-E <end_code>] [--] [args...] php [options] [-B <begin_code>] -F <file> [-E <end_code>] [--] [args...] php [options] -- [args...] php [options] -a
-a Run interactively -c <path>|<file> Look for php.ini file in this directory -n No php.ini file will be used -d foo[=bar] Define INI entry foo with value 'bar' -e Generate extended information for debugger/profiler -f <file> Parse and execute <file>. -h This help -i PHP information -l Syntax check only (lint) -m Show compiled in modules -r <code> Run PHP <code> without using script tags <?..?> -B <begin_code> Run PHP <begin_code> before processing input lines -R <code> Run PHP <code> for every input line -F <file> Parse and execute <file> for every input line -E <end_code> Run PHP <end_code> after processing all input lines -H Hide any passed arguments from external tools. -s Display colour syntax highlighted source. -v Version number -w Display source with stripped comments and whitespace. -z <file> Load Zend extension <file>.
args... Arguments passed to script. Use -- args when first argument starts with - or script is read from stdin
--ini Show configuration file names
--rf <name> Show information about function <name>. --rc <name> Show information about class <name>. --re <name> Show information about extension <name>. --ri <name> Show configuration for extension <name>.
Run code
PHP binary options
$ php -r “echo \"Give me some output\";”
Give me some output
$
• Binary option: -r
• Meaning: parse & run PHP code that is passed as an argument
Example
Interactive mode
PHP binary options
$ php -a
$ php > echo "Give me some interactive output";
Give me some interactive output
$ php>
• Binary option: -a
• Meaning: running PHP interactively in a true command line environment
• Requirements: PHP compiled with readline support
Example
Define configuration
PHP binary options
$ php -d max_execution_time=20 -r “echo ini_get("max_execution_time");”20$
• Binary option: -d
• Meaning: define INI configuration settings at runtime
Example
Lint check
PHP binary options
• Binary option: -l
• Meaning: perform a “lint” check on a PHP file. Validates the syntax of a script. Errors are printed via STDOUT. Shell return codes are “0” or “1”
• Notice:
• Doesn’t work combined with the “-r” option.
• Only checks for parsing errors & cannot retrieve fatal errors (e.g. undefined functions)
List modules
PHP binary options
$ php -m
[PHP Modules]
xmltokenizerstandardsessionposixpcreoverloadmysqlmbstringctype
[Zend Modules]
• Binary option: -m
• Meaning: prints all loaded PHP & Zend modules
Example
Syntax highlighting
PHP binary options
$ echo "<?php var_dump($_SERVER);" | php -s
<code><span style="color: #000000">
<span style="color: #0000BB"><?php var_dump</span><span style="color: #007700">();<br /></span>
</span>
$
• Binary option: -s
• Meaning: format PHP code into highlighted HTML code
• Notice: doesn’t work with the “-r” option.
Example
Version information
PHP binary options
$ php -v
PHP 5.2.4-2ubuntu5.3 with Suhosin-Patch 0.9.6.2 (cli) (built: Jul 23 2008 06:44:49)
Copyright (c) 1997-2007 The PHP Group
Zend Engine v2.2.0, Copyright (c) 1998-2007 Zend Technologies
with Xdebug v2.0.3, Copyright (c) 2002-2007, by Derick Rethans
$
• Binary option: -v
• Meaning: displays version information
Example
Function reflection
PHP binary options
$ php --rf var_dumpFunction [ <internal:standard> function var_dump ] { - Parameters [2] { Parameter #0 [ <required> $var ] Parameter #1 [ <optional> $... ] }}$
• Binary option: --rf
• Meaning: displays function API information
• Requirements: PHP must be compiled with “Reflection” support
Example
Class reflection
PHP binary options
• Binary option: --rc
• Meaning: displays class API information
• Requirements: PHP must be compiled with “Reflection” support
Class reflection
PHP binary options
$ php --rc stdclass
Class [ <internal> class stdClass ] { - Constants [0] { } - Static properties [0] { } - Static methods [0] { } - Properties [0] { } - Methods [0] { }}
$
Example
Read from STDIN + distributed output
CLI in action
<?php/** * Read input from STDIN and print input with line numbers. * Quit when blank rule entered. */$line = 0;do{ $line++; $input = trim(fgets(STDIN)); if(strlen($input) > 0){ echo "$line# $input".PHP_EOL; }else{ break; }}while (true); Code
Read from STDIN + distributed output
CLI in action
$ php cli.php
this1# thisis2# ismy3# myoutput4# output
$
Output
Read from STDIN + bundled output
CLI in action
<?php/** * Read input from STDIN and print input with line numbers. * Quit when blank rule entered. * Bundle output and print at the end of the script. */$line = 0;$output = '';do{ $line++; $input = trim(fgets(STDIN)); if(strlen($input) > 0){ $output.= "$line# $input".PHP_EOL; }else{ break; }}while (true);echo $output; Code
Read from STDIN + bundled output
CLI in action
$ php cli.php
thisismyoutput
1# this2# is3# my4# output
$ Output
Working with arguments
CLI in action
<?php/** * Parse all arguments starting at index 1. * If format is "--key" or "-key", parse key and assign "true" as value * If format is "--key=value" or "-key=value", parse key and extract value * Else, value is argument, key is argument index */for($i=1;$i<$argc;$i++) { if(preg_match('/^(\-+)([a-zA-Z0-9_\-]+)(=([a-zA-Z0-9_\-])+)?$/',$argv[$i],$matches)){ $key = $matches[2]; if(array_key_exists(4,$matches)){ $value = $matches[4]; } else { $value = true; } } else { $key = $i; $value = $argv[$i]; } echo "Key: $key - Value: $value".PHP_EOL;}
Code
Working with arguments
CLI in action
$ php cli.php --flag1 -flag2 --option1=value1 -option2=value2 unNamedArgument
Key: flag1 - Value: 1Key: flag2 - Value: 1Key: option1 - Value: 1Key: option2 - Value: 2Key: 5 - Value: unNamedArgument
$Output
Shell binary
CLI in action
#!/usr/bin/php<?php/** * Print the current time. * This binary directly uses the PHP binary. * When the script has execute permissions it can be called as "./cli" instead of "php cli.php" */echo "The current time is ".date('H:i:s').PHP_EOL;
Code
$ chmod +x ./cli
$
$ ./cli
The current time is 17:43:08
$
Permissions
Call script + output
Linux interaction & piping
CLI in action
$ php -r 'for($i=0;$i<10;$i++) echo $i.PHP_EOL;' | wc -l
10
$
• Pass PHP code to binary using the “run” flag
• Print 10 lines
• Pipe output from PHP script as input for the “word count” binary
• Add the “-l” flag to count the number of lines
• Output 10
Using getopt()
CLI in action
<?php$shortopts = "";$shortopts .= "f:"; // Required value$shortopts .= "v::"; // Optional value$shortopts .= "abc"; // These options do not accept values
$options = getopt($shortopts);var_dump($options);
$ php cli.php -a -v -f value
array(3) { ["a"]=> bool(false) ["v"]=> bool(false) ["f"]=>string(5) "value"}
Code
Output
The magic of process control
PCNTL == $fooDoo
Php.net says: Process Control support in PHP implements the Unix style of process creation, program execution, signal handling and process termination. Process Control should not be enabled within a web server environment and unexpected results may happen if any Process Control functions are used within a web server environment.
InstallationAdd “--enable-pcntl” configuration option when compiling PHP
Forking is not a culinary term
PCNTL == $fooDoo
• pcntl_fork() copies the program execution into a child process
• Workload can be distributed between parent & child(s) via PID checking
• Allow “multithreadish’ parallel execution by forking multiple child processes
• Use a shared resource to define the workload (e.g.: file, array, DB, IPC, ...)
Wikipedia says: In computing, when a process forks, it creates a copy of itself, which is called a "child process." The original process is then called the "parent process". More generally, a fork in a multithreading environment means that a thread of execution is duplicated, creating a child thread from the parent thread.
PCNTL voodoo/$fooDoo
PCNTL == $fooDoo
• After forking, your logic is distributed in different PHP processes
• Forking too many childs can cause performance issues
• When using infinite loops, build in sleeps to avoid performance issues
• Only use PCNTL in CLI mode
• Be absolutely sure you master & control your child processes
• Avoid zombie processes
• Be able to kill your child processes at any time
• Only kil your own processes
Be very careful when forking child processes, because there are risks involved (hence the term $fooDoo)
Forking example
PCNTL == $fooDoo
<?php$pid = pcntl_fork();if ($pid == -1) { die('could not fork');} else if ($pid) { echo "[parent] I am the parent process and my child process has PID $pid".PHP_EOL; echo "[parent] Waiting for child termination".PHP_EOL; pcntl_wait($status); echo "[parent] Exiting".PHP_EOL;} else { for($i=0;$i<10;$i++){ echo "[child] Loop $i, sleeping for a second ...".PHP_EOL; sleep(1); } echo "[child] Exiting".PHP_EOL; exit;}
<?phpfor($i=0;$i<10;$i++){ $pid = pcntl_fork(); if ($pid == -1) { die('could not fork'); } else if ($pid == 0) { echo "[child] Child $i doing stuff".PHP_EOL; sleep(1); echo "[child] Child $i exiting".PHP_EOL; exit; } else { echo "[parent] Starting parent $i".PHP_EOL; pcntl_wait($status); echo "[parent] Ending parent $i".PHP_EOL; }}
Multiple forking
PCNTL == $fooDoo
<?phpecho "[parent] Starting parent".PHP_EOL;for($i=0;$i<10;$i++){ $pid = pcntl_fork(); if ($pid == -1) { die('could not fork'); } else if ($pid == 0) { echo "[child] Child $i doing stuff".PHP_EOL; sleep($i); echo "[child] Child $i exiting".PHP_EOL; exit; } }echo "[parent] Ending parent".PHP_EOL;exit;
Multiple forking without waiting
PCNTL == $fooDoo
Signals
PCNTL == $fooDoo
Wikipedia says: A signal is a limited form of inter-process communication used in Unix, Unix-like, and other POSIX-compliant operating systems. Essentially it is an asynchronous notification sent to a process in order to notify it of an event that occurred. When a signal is sent to a process, the operating system interrupts the process' normal flow of execution. Execution can be interrupted during any non-atomic instruction. If the process has previously registered a signal handler, that routine is executed. Otherwise the default signal handler is executed.
Signals
PCNTL == $fooDoo
• Communication between 2 processes
• Process sends asynchronous notification of an event
• Signal handlers can be used to execute custom logic
• Otherwise default signal handlers
Signals: SIGTERM
PCNTL == $fooDoo
• SIGTERM is used to terminate a process in a ‘nice’ way
• Much more gentle than SIGKILL
• Allows cleanup and closure of process
• Issued via ‘kill’ (no kill -9)
Signals: SIGINT
PCNTL == $fooDoo
• SIGINT is used to terminate a process via interruption
• Stops program execution just like SIGTERM
• Issued by a terminal (via CTRL+C)
Signals: SIGCHLD
PCNTL == $fooDoo
• SIGCHLD is sent to the parent when a child process is terminated
• Common when using process forking
• SIGCHLD is by default ignored and a zombie process is created
• Only sent when the parent issues a ‘wait’ call (avoids zombies)
<?phpdeclare(ticks = 1);function sig_handler($signo){
switch ($signo) { case SIGTERM: echo "SIGTERM".PHP_EOL; exit(); break; case SIGINT: echo "SIGINT".PHP_EOL; exit(); break; }
}pcntl_signal(SIGTERM, "sig_handler");pcntl_signal(SIGINT, "sig_handler");sleep(100);
Signals: example of SIGTERM & SIGINT
PCNTL == $fooDoo
<?phpdeclare(ticks = 1);$max=5;$simultaneous=0;$childId=0;
function sig_handler($signo) { global $simultaneous,$childId; switch ($signo) { case SIGCHLD: $simultaneous--; echo "SIGCHLD received for child #$childId".PHP_EOL; echo "Decrementing to $simultaneous".PHP_EOL; }}
pcntl_signal(SIGCHLD, "sig_handler");
Signals: example of SIGCHLD
PCNTL == $fooDoo
for ($childId=0;$childId<10;$childId++){ $pid=pcntl_fork(); if ($pid == -1) { die("could not fork"); } else if ($pid) { if ( $simultaneous >= $max ){ pcntl_wait($status); } else { $simultaneous++; echo "Increment to $simultaneous".PHP_EOL; } } else { echo "Starting new child #$childId ".PHP_EOL; echo "Now we de have $simultaneous simultaneous child processes".PHP_EOL; sleep(rand(3,5)); exit; }}
Signals: example of SIGCHLD
PCNTL == $fooDoo
POSIX functions
PCNTL == $fooDoo
Wikipedia says: POSIX or "Portable Operating System Interface for Unix" is the collective name of a family of related standards specified by the IEEE to define the application programming interface (API), along with shell and utilities interfaces for software compatible with variants of the Unix operating system, although the standard can apply to any operating system.
POSIX functions
PCNTL == $fooDoo
In PHP: POSIX functions in PHP allow you to interact with the POSIX interface of the operating system. For process control functions we mainly use POSIX to retrieve PID information and terminate/kill processes
POSIX functions
PCNTL == $fooDoo
• Determine the PID of the current process
• pcntl_fork() return value for the parent process is the same as posix_getpid() for the child process
posix_getpid()
• Determine the PID of the parent of the process
• posix_getpid() value for the parent process is the same as posix_getppid() for the child process
• posix_getppid() value for the parent process is the PID of the bash session
posix_getppid()
<?phpecho "[prefork] PID POSIX: ".posix_getpid().", parent PID: ".posix_getppid().PHP_EOL;for($i=0;$i<3;$i++){ $pid = pcntl_fork(); if ($pid == -1) { die('could not fork'); } else if ($pid == 0) { echo "[child] ID: $i, PID: ".posix_getpid().", parent PID: ".posix_getppid().PHP_EOL; exit; } else { echo "[parent] PID : ".posix_getpid().", parent PID: ".posix_getppid().PHP_EOL; pcntl_wait($status); }}
POSIX functions: example of posix_getpid() & posix_getppid()
PCNTL == $fooDoo
POSIX functions
PCNTL == $fooDoo
• Send a signal to a certain process
• posix_kill($pid,0): gets PID status
• true: pid exists
• false: pid no longer exists
• posix_kill($pid,9) or posix_kill($pid,SIGKILL): kill a process
• posix_kill($pid,15) or posix_kill($pid,SIGTERM): terminate a process
posix_kill()
<?php$pid=pcntl_fork(); if ($pid == -1) { die("could not fork");} else if ($pid) { $exists = posix_kill($pid,0)?'still':'no longer'; echo "[parent] Child process $pid $exists exists".PHP_EOL; echo "[parent] Killing child process $pid".PHP_EOL; posix_kill($pid,SIGKILL); echo "[parent] Child process $pid killed".PHP_EOL; pcntl_wait($status); $exists = posix_kill($pid,0)?'still':'no longer'; echo "[parent] Child process $pid $exists exists".PHP_EOL; } else { while(true){ sleep(100); } exit;}
POSIX functions: example of posix_kill()
PCNTL == $fooDoo
#!/usr/bin/php<?phpdate_default_timezone_set('Europe/Brussels');$options = getopt('p:d:');/** * Determine pidfile */if(!array_key_exists('p',$options)){ die('No pidfile specified'.PHP_EOL);}define('PIDFILE',$options['p']);/** * Determine datadir */if(!array_key_exists('d',$options)){ die('No data directory specified'.PHP_EOL);}else{ if(!is_dir($options['d'])){ die('Data directory does not exist'.PHP_EOL); }}define('DATADIR',$options['d']);
The daemon (1)
Wake the daemons
/** * Delete pidfile on startup */if(file_exists(PIDFILE)){ unlink(PIDFILE);}/** * Delete pidfile after SIGINT or SIGTERM */function signal_handler($sig){ if(file_exists(PIDFILE)){ unlink(PIDFILE); }}/** * Set signal handler */pcntl_signal(SIGINT,'signal_handler');pcntl_signal(SIGTERM,'signal_handler');
The daemon (2)
Wake the daemons
/* * Setup daemon */$pid=pcntl_fork(); if ($pid == -1) { die("could not fork".PHP_EOL);} else if ($pid) { exit; }/** * System settings */posix_setsid();chdir("/");/** * Store PID */$handle = fopen(PIDFILE,'w+');fwrite($handle,posix_getpid());fclose($handle);
The daemon (3)
Wake the daemons
/** * Perform daemon logic */while(true){ $handle = fopen(DATADIR.'/thijs_'.date('YmdHis'),'w+'); fwrite($handle,'abc123'); fclose($handle); sleep(10);}
The daemon (4)
Wake the daemons
#!/usr/bin/php<?php/** * Count args */if($argc < 2){ die("\tUsage: php ".basename(__FILE__)." start|stop|status|restart".PHP_EOL);}/** * Init params */define('PIDFILE','/tmp/daemon.pid');define('DAEMONBIN','/home/thijs/test/pcntl/daemon.php');define('DATADIR','/tmp');function startDaemon(){ echo "Starting daemon".PHP_EOL; $start = DAEMONBIN.' -p'.PIDFILE.' -d '.DATADIR; passthru($start);}
The startup script (1)
Wake the daemons
function stopDaemon(){ if(file_exists(PIDFILE)){ $pid = (int)trim(file_get_contents(PIDFILE)); if(posix_kill($pid,0)){ if(posix_kill($pid,SIGKILL)){ echo 'Daemon stopped'.PHP_EOL; } else { echo 'Error stopping daemon'.PHP_EOL; } } else { echo 'Daemon no longer active, deleting pidfile'.PHP_EOL; } unlink(PIDFILE); } else { echo 'Daemon stopped'.PHP_EOL; }}
The startup script (2)
Wake the daemons
function statusDaemon(){ if(file_exists(PIDFILE)){ $pid = (int)trim(file_get_contents(PIDFILE)); if(posix_kill($pid,0)){ echo'Daemon is running'.PHP_EOL; } else { echo 'Daemon no longer active, but pidfile still exists'.PHP_EOL; } } else { echo'Daemon is not running'.PHP_EOL; }}
The startup script (3)
Wake the daemons
switch ($argv[1]){ case 'start': startDaemon(); break; case 'stop': stopDaemon(); break; case 'status': statusDaemon(); break; case 'restart': stopDaemon(); sleep(2); startDaemon(); break; default: die("\tUsage: ".basename(__FILE__)." start | stop | status | restart".PHP_EOL);}
The startup script (4)
Wake the daemons
Q & A
THANKS !