一个简单的php守护进程代码示例

工作中同事需要做一个简单的工作进程,需要在进程结束时不能被硬生生的掐断当前正在执行的工作流程,需要等一个处理流程跑完了再结束,所以这时候就需要用到pcntl的信号量来工作了,主要的设计思路:

  1. 捕获系统发给进程的中断的信号量
  2. 在handler代码中设置标志位变量
  3. 在一个业务循环处理完成之后判断标志位变量,如果接收到过终止请求,则跳出整个工作循环。 主要的处理逻辑:
  • 注册绑定函数:
1
2
3
4
5
6
7
    protected function regist_sig_handler()
    {
        declare(ticks = 1);
        pcntl_signal(SIGTERM, [$this, 'sig_handler']);
        pcntl_signal(SIGHUP, [$this, 'sig_handler']);
        pcntl_signal(SIGINT, [$this, 'sig_handler']);
    }
  • 在函数中设置标志位:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
protected function sig_handler($signo)
    {
        switch ($signo) {
            case SIGTERM:
            case SIGHUP:
            case SIGINT:
                self::$_MYDAEMON_SHOULD_STOP = true;
                // ...
                break;
        }
    }
  • 构建退出处理函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 protected function shouldStop()
    {
        if (self::$_MYDAEMON_SHOULD_STOP) {
            // before exit staff code
            // ...
            return true;
        }

        return false;
    }
  • 在主循环中一次处理完成后判断是否要终止退出
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
        while (true) {
            if ($this->shouldStop()) {
                break;
            }
            try {
                $jobStatusInfo = $this->execute([]);
            } catch (Exception $ex) {
                $this->log($ex->getMessage());
            }
            usleep(10);
        }

附上完整代码(loop是示例需要长时间处理的业务逻辑,memoryusage超限也会退出,外部可以由supervisor/daemontools等进程监控程序控制)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
#!/usr/bin/env php
<?php
/**
 * Description
 *
 * @project mydevkit
 * @package mydevkit
 * @author nickfan <[email protected]>
 * @link http://www.axiong.me
 * @version $Id$
 * @lastmodified: 2015-07-08 09:37
 *
 */
@set_time_limit(0);

class myDaemon
{
    const DEBUG = true;
    protected static $_MYDAEMON_SHOULD_STOP = false;
    public static $memLimit = 5242880;
    private $loop = 10;
    protected $logpath = '';

    public function __construct($option = [])
    {
        $option += [
            'loop' => 10,
            'memLimit' => 5242880,
            'logpath' => '/tmp/daemon_kill.log',
        ];
        $this->loop = $option['loop'];
        $this->logpath = $option['logpath'];
        self::$memLimit = $option['memLimit'];
        $this->regist_sig_handler();
    }

    public function __destruct()
    {
        $this->log('worker progress ending...');
    }

    protected function regist_sig_handler()
    {
        declare(ticks = 1);
        pcntl_signal(SIGTERM, [$this, 'sig_handler']);
        pcntl_signal(SIGHUP, [$this, 'sig_handler']);
        pcntl_signal(SIGINT, [$this, 'sig_handler']);
    }

    protected function sig_handler($signo)
    {
        switch ($signo) {
            case SIGTERM:
            case SIGHUP:
            case SIGINT:
                self::$_MYDAEMON_SHOULD_STOP = true;
                $signoStr = '';
                if ($signo == SIGTERM) {
                    $signoStr = 'SIGTERM';
                } elseif ($signo == SIGHUP) {
                    $signoStr = 'SIGHUP';
                } elseif ($signo == SIGINT) {
                    $signoStr = 'SIGINT';
                }
                $content = date('Y-m-d H:i:s') . ' ' . $signoStr . PHP_EOL;
                file_put_contents($this->logpath, $content, FILE_APPEND);
                break;
        }
    }

    protected function shouldStop()
    {
        if (self::$_MYDAEMON_SHOULD_STOP) {
            // before exit staff code
            // ...
            return true;
        }
        return false;
    }

    protected function log()
    {
        $args = func_get_args();
        $retstr = date('[Y-m-d H:i:s] ') . implode(' ', $args) . PHP_EOL;
        if (self::DEBUG == true) {
            echo $retstr;
        } else {
            return $retstr;
        }
    }

    public function execute($data = [])
    {
        $this->log('[EXECUTE] process lot of data.');
        for ($i = 0; $i < $this->loop; $i++) {
            $this->log('step:' . ($i + 1));
            sleep(1);
        }
        return rand(1, $this->loop);
    }

    public function run()
    {
        $this->log('[RUN] worker starting to run ...');
        while (true) {
            if ($this->shouldStop()) {
                break;
            }
            $this->log('processJobData Start ...');
            try {
                $jobStatusInfo = $this->execute([]);
                $this->log('processJobData end with: ' . var_export($jobStatusInfo, true));
                unset($jobStatusInfo);
            } catch (Exception $ex) {
                $this->log($ex->getMessage());
            }
            $memory = memory_get_usage();
            $this->log('memory usage:' . sprintf('%.2fMB', round($memory / 1048576, 2)));
            if ($memory > self::$memLimit) {
                $this->log('exiting run due to memory limit');
                exit;
            }
            //sleep(1);
            usleep(10);
        }
        $this->log('[RUN] worker run quit.');
    }
}

$gotMyParam = 10;
if (isset($argv[1])) {
    $gotMyParam = intval(trim(strip_tags($argv[1])));
}
$worker = new myDaemon(['loop' => $gotMyParam, 'memLimit' => 1048576 * 128,]);
$worker->run();

测试:

在class代码中的构造函数中注释掉

1
//$this->regist_sig_handler();

运行daemon代码,在执行execute的for循环时ctrl+c或者用 kill 杀死当前进程

Snip20150708_1

进程直接结束,没有执行完execute的一个完整流程就直接退出了,

把注释去掉重新执行daemon代码:

Snip20150708_2

在执行过程中按ctrl+c或者kill命令杀死前进程

观察daemon运行的结果是直到整个execute循环执行完了以后才退出程序的

laravel的oauth2-server相关开发的备忘

开发当中踩了不少坑,做个备忘。

代码环境:

laravel 5.1

oauth2-server服务组件:

lucadegasperi/oauth2-server-laravel 也就是 thephpleague/oauth2-server 的laravel 包装版本

目前插件的版本是~4.1

需要注意的点是:

一般而言最常见的应用场景是grant_type为authorization_code的情景,

thephpleague的oauth2-server要求的数据提交必须是POST数据编码方式是application/x-www-form-urlencoded,默认情况下如果你用的是curl组件会以multipart/form-data模式编码提交的post数据,所以后端提交请求的时候注意一下, 如果你用的是curl,需要设置:

1
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/x-www-form-urlencoded'));

如果你用的是GuzzleHttp的组件:参考官方的说明

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$response = $client->post('http://httpbin.org/post', [
    'form_params' => [
        'field_name' => 'abc',
        'other_field' => '123',
        'nested_field' => [
            'nested' => 'hello'
        ]
    ]
]);

其他的基础配置设定,插件作者的wiki中已经有了说明,我这里做了一些自己的设定:

  • 不想关闭全局的csrf保护咋办? 如果你是直接安装的laravel 5.1版不要关闭全局$middleware的csrf:
1
2
3
4
5
6
7
8
9
        protected $middleware = array(
        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
        \LucaDegasperi\OAuth2Server\Middleware\OAuthExceptionHandlerMiddleware::class,
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class, // 不要关闭
    );

按wiki中的说明添加$routeMiddleware中的设定:

1
2
3
4
5
6
7
8
9
    protected $routeMiddleware = [
        'csrf' => \App\Http\Middleware\VerifyCsrfToken::class, // 添加 csrf配置
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'oauth' => \LucaDegasperi\OAuth2Server\Middleware\OAuthMiddleware::class,
        'oauth-owner' => \LucaDegasperi\OAuth2Server\Middleware\OAuthOwnerMiddleware::class,
        'check-authorization-params' => \LucaDegasperi\OAuth2Server\Middleware\CheckAuthCodeRequestMiddleware::class,
    ];

在你的\App\Http\Middleware\VerifyCsrfToken类中的$except变量添加:

1
2
3
4
5
6
    protected $except = [
        //
        'api',
        'api/*',
        'oauth/access_token',
    ];

也就是:

  1. 你的oauth服务获取access_token的入口地址,如果你换了地址修改这里对应的设置即可。
  2. 你使用oauth中间件保护的服务接口也不需要csrf做多余的防护,在此排除掉 api/* 这对应的前缀即可 如果是5.0之类的升级上来的,VerifyCsrfToken可能还是老的写法,不支持$except,自己改造一下符合新版规范:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?php namespace App\Http\Middleware;

use Closure;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;
use Illuminate\Session\TokenMismatchException;

class VerifyCsrfToken extends BaseVerifier {

    /**
     * The URIs that should be excluded from CSRF verification.
     *
     * @var array
     */
    protected $except = [
        //
        'api/*',
        'oauth/access_token',
    ];

	/**
	 * Handle an incoming request.
	 *
	 * @param  \Illuminate\Http\Request  $request
	 * @param  \Closure  $next
	 * @return mixed
	 */
	public function handle($request, Closure $next)
	{
        if ($this->isReading($request) || $this->shouldPassThrough($request) || $this->tokensMatch($request)) {
            return $this->addCookieToResponse($request, $next($request));
        }

        throw new TokenMismatchException;
		//return parent::handle($request, $next);
	}

    /**
     * Determine if the request has a URI that should pass through CSRF verification.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return bool
     */
    protected function shouldPassThrough($request)
    {
        foreach ($this->except as $except) {
            if ($request->is($except)) {
                return true;
            }
        }

        return false;
    }

}
  • 本地登录授权的页面(View::make(‘oauth.authorization-form’))该怎么写? 原来官方的wiki中没有,放狗找了一圈的issue list才凑合着写了一个放了上去,作者插件的wiki里我已改过了:

注意提交的form原先GET请求中的querystring是需要一并post的 这个坑要注意一下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@extends('app')

@section('content')
    <div class="row">
        {!! Form::open(['method' => 'POST','class'=>'form-horizontal', 'url'=> route('oauth.authorize.post',$params)]) !!}
        <div class="form-group">
            <dl class="dl-horizontal">
                <dt>Client Name</dt>
                <dd>{{$client->getName()}}</dd>
            </dl>
        </div>
        {!! Form::hidden('client_id', $params['client_id']) !!}
        {!! Form::hidden('redirect_uri', $params['redirect_uri']) !!}
        {!! Form::hidden('response_type', $params['response_type']) !!}
        {!! Form::hidden('state', $params['state']) !!}
        {!! Form::submit('Approve', ['name'=>'approve', 'value'=>1, 'class'=>'btn btn-success']) !!}
        {!! Form::submit('Deny', ['name'=>'deny', 'value'=>1, 'class'=>'btn bg-danger']) !!}
        {!! Form::close() !!}
    </div>
@endsection

而对应的$params在controller中的设置:

1
2
3
4
        $authParams = Authorizer::getAuthCodeRequestParams();
        $formParams = array_except($authParams,'client');
        $formParams['client_id'] = $authParams['client']-&gt;getId();
        return View::make('oauth.authorization-form', ['params'=&gt;$formParams,'client'=&gt;$authParams['client']]);

最后在你被oauth保护的api接口中你就可以获得到对应的当前用户id了:

1
$uid = Authorizer::getResourceOwnerId();