PHP CLI shell 多进程入口
#!/usr/bin/env php
<?php
/*
* PHP CLI shell 多进程入口
*
* 运行 ./bat.php [--help] 查看帮助
* 运行 ./bat.php bat-test.php 执行示例
*
* bat-test.php 脚本内容如下:
* <?php
#防误确认
if(!bat::confirm()){
bat::message("用户取消");
exit;
}
#全局变量
global $x;
$x = 12345;
#添加任务
bat::run('a');
bat::run('b', __LINE__);
bat::run('c');
bat::run('b', __LINE__);
bat::run('a');
#启动任务
bat::start();
#任务函数
function a(){
global $x;
do{
bat::notify("我是通知主进程显示的提示文字,测试变量 \$x = " . $x++);
usleep(500000);
}while(mt_rand(100, 999) > 159);
}
function b($line){
do{
bat::notify("我是显示传递的参数 \$line = $line");
usleep(500000);
}while(mt_rand(100, 999) > 359);
}
function c(){
global $x;
bat::notify("多个任务之间的初始变量值不受影响, \$x = $x");
bat::notify("我是暂停 9 秒时间测试");
sleep(9);
bat::notify("我是出错代码 5 测试");
exit(5);
}
* ?>
*/
/** 确保这个脚本只能运行在 SHELL 中 */
if(substr(php_sapi_name(), 0, 3) !== 'cli'){
die("This Programe can only be run in CLI mode.\n");
}
if(!is_callable('pcntl_fork') || !is_callable('msg_send')){
bat::message("本程序需要 pcntl, sysvmsg 扩展,但您的系统没有安装!", 2);
exit(5);
}
class bat{
static private
$max = 3,
$total = 0,
$running = 0,
$failure = 0,
$finished = 0,
$tasks = array(),
$msg, $msgs = array(),
$logfile = "/tmp/bat.php.log",
$childs, $get, $parent,
$start, $split;
static function main(){
$i = 1;
$files = array();
if($_SERVER["argc"] > 1){
while($i < $_SERVER["argc"]){
switch($_SERVER["argv"][$i++]){
case "?":
case "/?":
case "-?":
case "-h":
case "--help":
self::usage();
case "-f":
case "--file":
$file = $_SERVER["argv"][$i++];
if(is_readable($file)){
$files[] = $file;
continue;
}
if(is_null($file)){
self::message("缺少脚本参数", 1);
help(1);
}else{
self::message("脚本 $file 不在在或不可访问", 2);
exit(4);
}
case "-m":
case "--max":
if(self::$max = $_SERVER["argv"][$i++]){
self::$max = intval(self::$max);
if(self::$max >= 1){
continue;
}
self::message("进程数量应为正整数", 2);
exit(8);
}
self::message("未指定进程数量", 2);
exit(7);
case "-l":
case "--log":
case "--logfile":
if(self::$logfile = $_SERVER["argv"][$i++]){
if(is_dir(self::$logfile)){
self::$logfile .= "/bat.php.log";
}
if(is_file(self::$logfile)){
if(is_writable(self::$logfile)){
continue;
}
}else{
if(is_writable(dirname(self::$logfile))){
continue;
}
}
self::message("日志目录不可写", 2);
exit(9);
}
case "-v":
case "--version":
exit(self::version());
default :
$file = $_SERVER["argv"][$i - 1];
if(is_readable($file)){
$files[] = $file;
continue;
}
self::message("脚本 $file 不在在或不可访问", 2);
exit(4);
}
}
set_time_limit(0);
error_reporting(8106 & E_ALL);
ini_set('display_errors', 'Off');
set_error_handler(array(__CLASS__, 'error'), E_ALL);
set_exception_handler(array(__CLASS__, 'exception'));
register_shutdown_function(array(__CLASS__, 'shutdown'));
self::$start = time();
self::$split = str_repeat('=', 512);
self::$parent = msg_get_queue(getmypid());
foreach($files as $file){
self::inc($file);
}
self::end();
exit;
}
self::usage();
}
static function run($fun, $arg = null){
if(is_callable($fun)){
self::$tasks[] = array($fun, $arg);
}else{
throw new Exception("不是函数或不可调用", 9);
}
}
static function start(){
self::$total = count(self::$tasks);
foreach(self::$tasks as $fun_arg){
if(self::$max < ++self::$running){
self::run_wait();
}elseif(self::$running == 1){
# 清屏并设置光标到第一行
$x = intval(`tput lines`);
echo str_repeat("\n", $x -1);
self::flush('程序开始执行...', 1);
}
if($cid = pcntl_fork()){
if($cid < 0){
throw new Exception("创建进程失败", 3);
}
self::$childs[$cid] = msg_get_queue($cid);
}else{
ob_start();
self::$tasks = array();
self::$get = msg_get_queue(getmypid());
self::$msg = sprintf("%-6d", getmypid());
msg_send(self::$parent, 1, getmypid(), false);
call_user_func($fun_arg[0], $fun_arg[1]);
exit;
}
}
while(self::$running) self::run_wait();
self::$tasks = array();
}
static private function run_wait(){
$nomsg_interval = time();
label_wait:
if(msg_receive(self::$parent, 0, $typ, 8192, $msg, false, MSG_NOERROR | MSG_IPC_NOWAIT)){
if($typ != 3){
if($typ == 1){
$msg = sprintf("%-6d%s %s", $msg, date("H:i:s"), '进程启动');
}elseif($typ == 4){
label_child_exit:
unset(self::$childs[pcntl_waitpid($msg, &$status)]);
if(!pcntl_wifexited($status) || pcntl_wexitstatus($status)){
$msg = sprintf("%-6d%s %s", $msg, date("H:i:s"), '进程异常退出');
if(pcntl_wifexited($status)){
$msg .= ',错误代码:' . pcntl_wexitstatus($status);
}
self::$failure++;
}else{
$msg = sprintf("%-6d%s %s", $msg, date("H:i:s"), '进程执行完毕');
self::$finished++;
}
self::flush($msg, $nomsg_interval);
self::$running--;
return;
}else{
goto label_wait;
}
}
$nomsg_interval = time();
self::flush($msg, $nomsg_interval);
}else{
if($nomsg_interval != time()){
foreach(self::$childs as $msg => $t){
if(!msg_queue_exists($msg)){
goto label_child_exit;
}
}
echo "\33[0;0H"; $lines = intval(`tput lines`);
echo "\33[K运行时长:", self::run_time(), ' ', date("Y-m-d H:i:s", self::$start), ' - ', date("Y-m-d H:i:s"), "\33[$lines;0H";
}
usleep(100000);
}
goto label_wait;
}
static function notify($msg){
msg_send(self::$parent, 3, self::$msg . date("H:i:s ") . $msg, false);
}
static function message($msg, $code = 0){
switch($code){
case 0:
echo "\33[37m提示:\33[0m", $msg, "\n";
break;
case 1:
echo "\33[33m警告:\33[0m", $msg, "\n";
break;
case 2:
echo "\33[31m错误:\33[0m", $msg, "\n";
break;
}
}
static function confirm($msg = "确定要继续执行"){
echo $msg, "(yes/no)?: "; # 暂这样
return "yes\n" == fgets(STDIN);
}
static function help($code = 0){
echo "\n请使用 $_ENV[_] --help 查看帮助!\n";
$code && exit($code);
}
static function usage(){
$bat = __CLASS__;
echo ""
, "Usage:\n"
, " $_ENV[_] [options] [-f | --file] <file>\n"
, "Options:\n"
, " -h | --help 显示本帮助信息\n"
, " -v | --version 查看程序版本信息\n"
, "\n"
, " -m | --max <num> 同时执行进程数量,默认 ", self::$max, " 个\n"
, " -l | --log <file> 错误记录日志文件,默认 ", self::$logfile, "\n"
, "Information:\n"
, " 脚本中调用 $bat::run(fun[, arg]) 来添加任务\n"
, " fun 为要执行的函数名;arg 为传递给这个函数的参数,可省\n"
, "\n"
, " 脚本中调用 $bat::start() 来启动子进程执行上面添加的任务\n"
, " 在子进程中,通过调用 $bat::notify(msg) 发送要显示的信息给父进程\n"
, " 在子进程中,程序执行发生错误,要让主进程统计为失败需用 exit(num) 非零返回\n"
;
exit;
}
static function version(){
return "Version: 0.1 by huye\n";
}
static private function inc($file){
include $file;
}
static private function end(){
$cols = intval(`tput cols`);
$lines = intval(`tput lines`);
if(self::$total){
self::flush("执行完毕.", 1);
echo "\33[$lines;{$cols}H\33[1C\n\n";
}
if(is_file(self::$logfile))echo "\33[K发生错误:", self::$logfile, "\n";
echo "\33[K运行时长:", self::run_time(), ' ', date("Y-m-d H:i:s", self::$start), ' - ', date("Y-m-d H:i:s"), "\n";
echo "\33[K执行完毕:已完成任务 ", self::$finished, " 个", self::$failure ? ",失败 " . self::$failure . " 个" : "", self::$total ? "(共 " . self::$total . " 个)" : "", "。\n";
}
static private function flush($msg, $time){
$cols = intval(`tput cols`);
$lines = intval(`tput lines`);
if($msg){
$_max = $cols;
foreach(explode("\n", $msg) as $msg){
if($cols < strlen($msg)){# ascii utf8 ascii utf8 ascii ...
$tmp = preg_split("#((?:[\xe0-\xef][\x80-\xbf]{2})+)#", $msg, 0, PREG_SPLIT_DELIM_CAPTURE);
for($i = 0, $l = count($tmp); $i < $l;){
$x = strlen($z = $tmp[$i]);
if($_max > $x){
$_max -= $x;
if(++$i >= $l)break;
$x = strlen($z = $tmp[$i]) / 3 * 2;
if($_max > $x){
$_max -= $x;
$i++; continue;
}elseif($_max < $x){
$_max = floor($_max / 2) * 3;
$msg = array_slice($tmp, $i -1);
$msg[0] = '';
$msg[1] = substr($z, $_max);
$tmp[$i] = substr($z, 0, $_max);
}else{
$msg = array_slice($tmp, $i + 1);
}
}elseif($_max < $x){
$msg = array_slice($tmp, $i);
$msg[0] = substr($z, $_max);
$tmp[$i] = substr($z, 0, $_max);
}else{
$msg = array_slice($tmp, $i);
$msg[0] = '';
}
if(++$i < $l){
array_splice($tmp, $i);
}
if(isset($msg[1])){
self::$msgs[] = implode("", $tmp);
$msg[0] = " " . $msg[0];
$i = 0; $l = count($msg); $tmp = $msg; $_max = $cols;
}elseif(isset($msg[0]) && strlen($msg[0])){
self::$msgs[] = implode("", $tmp);
if($cols - 15 < strlen($msg[0])){
foreach(str_split($msg[0], $cols - 15) as $tmp){
$tmp = " " . $tmp;
if($cols == strlen($tmp)){
self::$msgs[] = $tmp;
}else{
$tmp = array($tmp);
break;
}
}
}else{
$tmp = array(" " . $msg[0]);
}
break;
}else{
break;
}
}
self::$msgs[] = implode("", $tmp);
}else{
self::$msgs[] = $msg;
}
}
}else{
self::$msgs[] = $msg;
}
static $last_time = 0;
if($last_time == $time)return true;
$last_time = $time; # 防止在远程 ssh 的时候刷屏死掉
echo "\33[0;0H";
# echo "\33[K程序信息:", self::version();
echo "\33[K运行时长:", self::run_time(), ' ', date("Y-m-d H:i:s", self::$start), ' - ', date("Y-m-d H:i:s"), "\n";
if($lines < 5){
if($lines < 3) return;
}else{
echo $split = substr(self::$split, 0, $cols), "\n";
if(($_max = count(self::$msgs) + 4) > $lines){
array_splice(self::$msgs, 0, $_max - $lines);
}elseif($lines > $_max){
$split = str_repeat("\n\33[K", $lines - $_max) . $split;
}
echo "\33[K", implode("\n\33[K", self::$msgs), "\n", $split, "\n";
}
$msg = "已完成任务 " . self::$finished . " 个";
if(self::$failure) $msg .= ",失败 " . self::$failure . " 个";
if(self::$total) $msg .= "(共 " . self::$total . " 个)";
echo str_repeat(' ', $cols - strlen(preg_replace("#[\xe0-\xef][\x80-\xbf]{2}#", "**", $msg))), $msg, "\33[$lines;0H";
}
static function run_time(){
$consume = time()
- self::$start;
$str = "";
if($consume >= 86400){
$str = floor($consume / 86400) . "天";
$consume = $consume % 86400;
$zero = true;
}
if($consume >= 3600){
$str .= floor($consume / 3600) . "时";
$consume = $consume % 3600;
$zero = true;
}elseif($consume > 0 && isset($zero)){
unset($zero);
$str .= "零";
}
if($consume >= 60){
$str .= floor($consume / 60) . "分";
$consume = $consume % 60;
$zero = true;
}elseif($consume > 0 && isset($zero)){
unset($zero);
$str .= "零";
}
if($consume > 0){
$str .= $consume . "秒";
}elseif($str == ""){
$str = "0秒";
}
return $str;
}
static function error($no, $err, $file, $line){
if(error_reporting()){
$log = $no & 1032 ? 'M' : ($no & 514 ? 'W' : ($no & 2048 ? 'M' : 'E'));
$log = "[" . date("m-d H:i:s") . "] $log $line $file $err\n";
file_put_contents(self::$logfile, $log, FILE_APPEND);
}
}
static function shutdown(){
if($last = error_get_last() and 85 & $last['type']){
self::error($last['type'], $last['message'], $last['file'], $last['line']);
self::$get || self::end();
}
if(self::$get){
msg_send(self::$parent, 4, getmypid(), false); # 通知父进程结束
self::$parent = self::$get; # 同时也防止上一行通知失败
ob_end_clean();
}
msg_remove_queue(self::$parent);
}
static function exception($e){
self::error($e->getCode(), $e->getMessage(), $e->getFile(), $e->getLine());
exit($e->getCode());
}
}
bat::main();