首页 > 代码库 > PHP CLI shell 多进程入口

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();

PHP CLI shell 多进程入口