#!/usr/bin/env php
<?php
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();