cronと同様の設定方法で秒単位でコマンドを定期実行させるPHPスクリプト

[NASのミラーリング用スクリプト](https://qiita.com/akebi_mh/items/93d77526b8109dc58589)を数秒間隔で定期実行させる目的で作成。
crontabにsleepを組み合わせたりすることでも秒単位での定期実行は可能ですが、せっかくなのでもう少し細かな設定もできるようにしました。

###スクリプト###

~~~php:interval.php
<?php
/**
 *  interval.php
 */

define('CONFIG_FILENAME', __DIR__. '/interval.conf');

// テンポラリ用ディレクトリ
// /dev/shm(shared memory)があれば優先して使用、無ければ/var/tmpを使用
define('TEMP_DIR', (file_exists('/dev/shm/') ? '/dev/shm/.' : '/var/tmp/.'). md5(__DIR__));
if(!file_exists(TEMP_DIR)) {
    mkdir(TEMP_DIR);
    chmod(TEMP_DIR, 0700);
}
// タイムスタンプの新しいlockファイルが既に存在していたら終了
$lockFile = TEMP_DIR. sprintf('/interval_%s.lock', md5(__FILE__));
if(file_exists($lockFile)) {
    if(time() - filemtime($lockFile) < 10) {
        exit;
    }
}
// lockファイルを作成、中にはプロセスIDを入れておく
file_put_contents($lockFile, getmypid());
chmod($lockFile, 0600);

set_time_limit(0);
date_default_timezone_set('Asia/Tokyo');

// 設定ファイル取得
$schedules = getSchedules(CONFIG_FILENAME);

// メイン
// 設定ファイル及びlockファイルがある場合のみループ
while(file_exists(CONFIG_FILENAME) && file_exists($lockFile)) {
    // 秒の切替わりタイミングまでスリープ
    $time = time() + 1;
    time_sleep_until($time);

    printf("%s\n", date('m/d H:i:s w', $time));

    // 各設定コマンドが有効なタイミングか確認、有効であれば実行
    foreach($schedules as $segments) {
        $command = $segments['command'];
        $r = true;
        foreach($segments['fields'] as $key => $val) {
            if(!getFlag($key, $val, $time)) {
                $r = false;
                break;
            }
        }
        if($r) {
            print "> $command\n";
            // コマンドの途中に&が含まれている場合、最後に&を追加してそのままexec関数に渡しても
            // バックグラウンド実行にならないため、目的のコマンドをechoで表示しパイプでshに渡す動作に置き換え
            $execCommand = 'echo "'. str_replace('"', '\\"', $command). '"|sh > /dev/null &';
            exec($execCommand);
        }
    }

    // 5秒間隔でチェック
    if($time % 5 == 0) {
        // lockファイルの中身を確認、プロセスIDが異なっていたら終了
        if(file_get_contents($lockFile) != getmypid()) exit;
        // lockファイルのタイムスタンプ更新(起動時の別プロセス生死確認用)
        touch($lockFile, time());

        // 設定ファイルが更新されていたら再取得
        clearstatcache();
        if(time() - filemtime(CONFIG_FILENAME) <= 6) {
            $schedules = getSchedules(CONFIG_FILENAME);
        }
    }
}

// フラグ取得
function getFlag($ds, $p, $t) {
    if(strpos('mdHisw', $ds) !== false) $d = date($ds, $t);
    else return false;

    // 曜日特例(7を0扱い) 範囲指定
    if($ds === 'w') {
        $p = preg_replace('/^(\d+)-7(\D|$)/', "0,$1-6$2", $p);
    }
    $params = explode(',', $p);
    $result = false;
    foreach($params as $param) {
        // 曜日特例 単独設定
        if($ds === 'w') {
            $param = preg_replace('|^7$|', '0', $param);
        }
        // 各条件判別
            // 全指定
        if( $param === '*' ||
            // 単独
            preg_match('|^\d+$|', $param) && $d == $param ||
            // 間隔 先頭起点
            preg_match('|^\*/(\d+)$|', $param, $m) && $m[1] && ($d % $m[1] == 0) ||
            // 間隔 任意起点
            preg_match('|^(\d+)/(\d+)$|', $param, $m) && $m[2] && $d >= $m[1] && (($d - $m[1]) % $m[2] == 0) ||
            // 範囲
            preg_match('|^(\d+)-(\d+)$|', $param, $m) && $d >= $m[1] && $d <= $m[2] ||
            // 範囲間隔
            preg_match('|^(\d+)-(\d+)/(\d+)$|', $param, $m) && $m[3] && $d >= $m[1] && $d <= $m[2] && (($d - $m[1]) % $m[3] == 0)
        ) {
            $result = true;
            break;
        }
    }
    return $result;
}

// 設定ファイル取得
function getSchedules($configFilename) {
    if(!file_exists($configFilename)) {
        print "Cannot find configuration file `{$configFilename}` .\n";
        exit;
    }

    $configFile = file_get_contents($configFilename);
    $configFile = explode("\n", $configFile);

    $arr = [];
    foreach($configFile as $line) {
        $line = trim($line);
        if($line === '' || preg_match('|^[/#]|', $line)) continue;

        $tmp = [];
        while(preg_match('|^([\*,-/\d]+)\s+|', $line, $m)) {
            $tmp[] = $m[1];
            $line = preg_replace('|^([\*,-/\d]+)\s+|', '', $line);
        }
        // 日時フィールド5個(秒は0固定)
        if(count($tmp) == 5) {
            $arr[] = [
                'command' => $line,
                'fields' => [
                    's' => 0,
                    'i' => $tmp[0],
                    'H' => $tmp[1],
                    'd' => $tmp[2],
                    'm' => $tmp[3],
                    'w' => $tmp[4],
                ],
            ];
        }
        // 日時フィールド6個
        elseif(count($tmp) == 6) {
            $arr[] = [
                'command' => $line,
                'fields' => [
                    's' => $tmp[0],
                    'i' => $tmp[1],
                    'H' => $tmp[2],
                    'd' => $tmp[3],
                    'm' => $tmp[4],
                    'w' => $tmp[5],
                ],
            ];
        }
    }

    print "-- schedules --\n";
    print_r($arr);
    print "\n";

    return $arr;
}

~~~

**設定ファイルサンプル**

~~~text:interval.conf
#### interval.conf
* * * * * command1
* * * * * * command2
~~~

`interval.php`、`interval.conf`は同一のディレクトリに置いて下さい。

interval.confの日時指定の書式はcrontabとほぼ同じになるようにしたつもりですが、異なるケースもあるかと思いますのでご了承ください。

既知の相違点としては、crontabでは月日と曜日を両方指定した場合はORになりますが、このスクリプトではANDになっています。
OR条件としたい場合は、同じコマンドで月日を指定したものと曜日を指定したものを重複してスケジューリングすることで対応できるかと思います。

**日時指定フィールドが5つの場合**
crontabと同様、 **分・時・日・月・曜日・コマンド** の順に指定。

~~~
* * * * * command
~~~


**日時指定フィールドが6つの場合**
先頭フィールドが**秒**の指定になり、それ以降は5つの場合と同じです。

~~~
* * * * * * command
~~~
 


日時指定フィールドには以下の書式が使えます。

全て  &nbsp;**`*`**
固定値  &nbsp;**`0`**、**`10`**、**`20`**など
範囲 &nbsp;**`0-9`**、**`30-59`**など
間隔 &nbsp;**`*/5`**、**`10/5`**、**`15-45/2`**など

それぞれカンマで区切って複合指定も可能です。

**設定例**
毎分0秒

~~~
* * * * * command
~~~

毎秒

~~~
* * * * * * command
~~~

3～5時台 毎分 5秒間隔

~~~
*/5 * 3-5 * * * command
~~~

3～4時台 毎分 0～9秒は毎秒、30～40秒は2秒間隔

~~~
0-9,30-40/2 * 3,4 * * * command
~~~

設定を終えたらまず手動で`interval.php`を実行して動作を確認してください。

~~~bash
$ php /スクリプト設置パス/interval.php
~~~

問題なさそうであれば、必要に応じて`rc.local`や`crontab`でreboot指定などして自動実行してください。