<META NAME="robots" CONTENT="noindex,nofollow">


<?php
file_put_contents("/home/cipherteam/cron_runner_ping.log", date("Y-m-d H:i:s")." cron ping\n", FILE_APPEND);

/**
 * master_runner.php (FIXED)
 * Run via cron every minute:
 *   * * * * * /usr/bin/php8.2 /path/master_runner.php
 *
 * Manual run (Run Now):
 *   https://yourdomain/path/master_runner.php?force_id=1&token=SECRET
 */

date_default_timezone_set("Asia/Kolkata");

// ---------------- CONFIG ----------------
$RUN_NOW_TOKEN = "CHANGE_THIS_SECRET_TOKEN"; // MUST match in scraper_servers.php
$DEFAULT_TIMEOUT_SEC = 120;                  // per job timeout
$LOCK_TTL_SEC = 300;                         // lock expiry to avoid overlap

// ---------------- DB ----------------
$con = mysqli_connect("localhost:3306", "marketdatauser", "d7o7A8%s2d7", "marketdataci");
if (mysqli_connect_errno()) exit("DB Connection Failed: " . mysqli_connect_error() . "\n");
mysqli_set_charset($con, "utf8mb4");

$now = new DateTime();

// ----------- CRON MATCH --------------
function cronMatch($cron, DateTime $time) {
    $parts = preg_split('/\s+/', trim($cron));
    if (count($parts) !== 5) return false;

    return matchPart($parts[0], (int)$time->format('i')) && // minute
           matchPart($parts[1], (int)$time->format('G')) && // hour
           matchPart($parts[2], (int)$time->format('j')) && // day
           matchPart($parts[3], (int)$time->format('n')) && // month
           matchPart($parts[4], (int)$time->format('w'));   // weekday
}

function matchPart($rule, $value) {
    $rule = trim($rule);
    if ($rule === '*') return true;

    // */N
    if (preg_match('/^\*\/(\d+)$/', $rule, $m)) {
        $step = (int)$m[1];
        return $step > 0 ? (($value % $step) === 0) : false;
    }

    // range A-B
    if (preg_match('/^(\d+)\-(\d+)$/', $rule, $m)) {
        $a = (int)$m[1]; $b = (int)$m[2];
        return ($value >= $a && $value <= $b);
    }

    // list 1,2,3
    foreach (explode(',', $rule) as $r) {
        $r = trim($r);
        if ($r === '') continue;
        if ((int)$r === (int)$value) return true;
    }
    return false;
}

// ----------- LOCKING -----------------
function tryLockJob(mysqli $con, int $id, int $ttlSec) {
    $lockUntil = date('Y-m-d H:i:s', time() + $ttlSec);
    $stmt = mysqli_prepare($con, "
        UPDATE scraper_servers
        SET lock_until = ?
        WHERE id = ?
          AND (lock_until IS NULL OR lock_until < NOW())
    ");
    mysqli_stmt_bind_param($stmt, "si", $lockUntil, $id);
    mysqli_stmt_execute($stmt);
    return (mysqli_stmt_affected_rows($stmt) === 1);
}

function unlockJob(mysqli $con, int $id) {
    $stmt = mysqli_prepare($con, "UPDATE scraper_servers SET lock_until = NULL WHERE id=?");
    mysqli_stmt_bind_param($stmt, "i", $id);
    mysqli_stmt_execute($stmt);
}

// ----------- RUN WITH TIMEOUT --------
function runCommandWithTimeout(string $command, int $timeoutSec, &$stdout, &$exitCode) {
    // If proc_open is disabled on your server, this will fail.
    if (!function_exists('proc_open')) {
        $stdout = "proc_open is disabled on this server. Enable it in php.ini (disable_functions).";
        $exitCode = 127;
        return "FAILED";
    }

    $spec = [
        0 => ["pipe", "r"],
        1 => ["pipe", "w"],
        2 => ["pipe", "w"],
    ];

    $process = proc_open($command, $spec, $pipes);
    if (!is_resource($process)) {
        $stdout = "Failed to start process.";
        $exitCode = 127;
        return "FAILED";
    }

    fclose($pipes[0]);
    stream_set_blocking($pipes[1], false);
    stream_set_blocking($pipes[2], false);

    $start = microtime(true);
    $out = ""; $err = "";

    while (true) {
        $status = proc_get_status($process);
        $out .= stream_get_contents($pipes[1]);
        $err .= stream_get_contents($pipes[2]);

        if (!$status['running']) break;

        if ((microtime(true) - $start) > $timeoutSec) {
            proc_terminate($process);
            usleep(200000);
            $status2 = proc_get_status($process);
            if ($status2['running']) {
                proc_terminate($process, 9);
            }
            $out .= stream_get_contents($pipes[1]);
            $err .= stream_get_contents($pipes[2]);

            fclose($pipes[1]); fclose($pipes[2]);
            proc_close($process);

            $stdout = trim($out . "\n" . $err);
            $exitCode = 124;
            return "TIMEOUT";
        }

        usleep(200000);
    }

    $out .= stream_get_contents($pipes[1]);
    $err .= stream_get_contents($pipes[2]);

    fclose($pipes[1]); fclose($pipes[2]);
    $code = proc_close($process);

    $stdout = trim($out . "\n" . $err);
    $exitCode = is_int($code) ? $code : 0;

    return ($exitCode === 0) ? "SUCCESS" : "FAILED";
}

// ----------- FORCE MODE --------------
$forceId = isset($_GET['force_id']) ? (int)$_GET['force_id'] : 0;
$token   = isset($_GET['token']) ? (string)$_GET['token'] : "";

$jobs = [];

if ($forceId > 0) {
    if ($token !== $RUN_NOW_TOKEN) {
        http_response_code(403);
        echo "Invalid token";
        exit;
    }
    $stmt = mysqli_prepare($con, "SELECT * FROM scraper_servers WHERE id=? LIMIT 1");
    mysqli_stmt_bind_param($stmt, "i", $forceId);
    mysqli_stmt_execute($stmt);
    $res = mysqli_stmt_get_result($stmt);
    if (!$res || mysqli_num_rows($res) === 0) {
        http_response_code(404);
        echo "Job not found";
        exit;
    }
    $jobs[] = mysqli_fetch_assoc($res);
} else {
    $res = mysqli_query($con, "SELECT * FROM scraper_servers WHERE is_enabled=1");
    while ($row = mysqli_fetch_assoc($res)) $jobs[] = $row;
}

// ----------- RUN LOOP ----------------
$ran = 0;

foreach ($jobs as $job) {
    $id = (int)$job['id'];
    $command = trim($job['url']);
    $cron = trim($job['cron_timing']);

    if ($command === "") continue;

    // Normal cron mode -> must match time
    if ($forceId === 0) {
        if (!$cron || !cronMatch($cron, $now)) continue;

        // prevent same-minute duplicate
        if (!empty($job['last_run_at'])) {
            $lr = new DateTime($job['last_run_at']);
            if ($lr->format('Y-m-d H:i') === $now->format('Y-m-d H:i')) continue;
        }
    }

    // Lock
    if (!tryLockJob($con, $id, $LOCK_TTL_SEC)) {
        $by = ($forceId > 0) ? "MANUAL" : "CRON";
        $stmt = mysqli_prepare($con, "
            UPDATE scraper_servers
            SET last_run_at=NOW(), last_status='LOCKED', last_run_by=?, last_exec_ms=0, last_exit_code=0
            WHERE id=?
        ");
        mysqli_stmt_bind_param($stmt, "si", $by, $id);
        mysqli_stmt_execute($stmt);
        continue;
    }

    $startMs = (int)round(microtime(true) * 1000);

    $stdout = "";
    $exitCode = 0;
    $status = runCommandWithTimeout($command, $DEFAULT_TIMEOUT_SEC, $stdout, $exitCode);

    // ✅ If script ran and printed expected text, treat as SUCCESS even if exitCode non-zero
    $okMarkers = ["Done.", "FOUND + NOTIFIED", "NOT FOUND", "Done. Checked"];
    $looksOk = false;
    foreach ($okMarkers as $m) {
        if (stripos($stdout, $m) !== false) { $looksOk = true; break; }
    }
    if ($looksOk && ($status === "FAILED" || $exitCode !== 0)) {
        $status = "SUCCESS";
        $exitCode = 0;
    }


    $endMs = (int)round(microtime(true) * 1000);
    $execMs = max(0, $endMs - $startMs);

    $by = ($forceId > 0) ? "MANUAL" : "CRON";

    $stmt = mysqli_prepare($con, "
        UPDATE scraper_servers
        SET last_run_at = NOW(),
            last_status = ?,
            last_output = ?,
            last_exec_ms = ?,
            last_exit_code = ?,
            last_run_by = ?
        WHERE id = ?
    ");
    mysqli_stmt_bind_param($stmt, "ssiisi", $status, $stdout, $execMs, $exitCode, $by, $id);
    mysqli_stmt_execute($stmt);

    unlockJob($con, $id);
    $ran++;
}

