WIZZO CMS לא דורש שורת crontab לכל משימה. מוסיפים לשרת שורה אחת שמפעילה את cron_runner בכל דקה, וכל השאר (איזו משימה, מתי, באיזה פרמטרים ומה היה הפלט) מנוהל בטבלאות ובמסך ניהול. בעמוד הזה תראו איך ההרצה עובדת, איך מגדירים משימה, איך כותבים controller שמתאים לריצה מתוזמנת ואילו מלכודות כדאי להכיר.
system/ בעמוד הזה הוא תיקיית הליבה הפרוסה (api/core בריפו), ו-{admin} הוא CONFIG::$admin_url.שורת ה-crontab#
השורה היחידה שנדרשת בשרת:
* * * * * /usr/local/bin/php /home/site/public_html/index.php url=system/cron_runner tk=<token> >/dev/null 2>&1
- הנתיב ל-PHP הוא של ה-CLI שמתאים לגרסת האתר (הליבה דורשת PHP 8.3 ומעלה, ראו דרישות מערכת).
index.phpהוא נקודת הכניסה הרגילה של האתר. במצב CLI הארגומנטים בצורתkey=valueהופכים ל-$_GET, וה-url=הופך ל-REQUEST_URI(זה עושהMISC::cli_check).<token>הוא הערך של הפרמטרcron_runner_token(ראו למטה). אל תפרסמו אותו.
מסך {admin}/cron_manager/setup_cron מציג את השורה המדויקת לאתר שלכם, כולל נתיב ה-PHP שזוהה. הזיהוי (MISC::detect_cli_php()) בודק לפי הסדר:
/usr/local/bin/php/usr/bin/php- בינאריים של cPanel:
/opt/cpanel/ea-php85/root/usr/bin/phpואחריו 84, 83, 82, 81, 80 ו-74 PHP_BINARYשל הבקשה הנוכחית, אם שם הקובץ אינוlsphpואינו מכילfpm- ברירת מחדל:
/usr/local/bin/php
ב-cPanel ה-php הכללי הוא לעיתים גרסה ישנה. אם הליבה לא עולה מ-CLI, השתמשו בנתיב המפורש של ea-php83 ומעלה.
הטוקן#
הטוקן נשמר בטבלת CRM_params תחת sysName = cron_runner_token, עם perm_developer = 1 (כך הוא מוסתר ממשתמשי ניהול שאינם מפתחים, ראו פרמטרים ושפות). הוא נוצר אוטומטית בכניסה הראשונה למסך cron_manager (get_or_create_token()): 32 תווים הקסדצימליים מ-random_bytes(16).
בדיקת הטוקן ב-cron_runner נעשית עם hash_equals, מול PARAMS::get('cron_runner_token') ועם קריאה ישירה לטבלה כגיבוי. טוקן חסר או שגוי מחזיר ERROR: invalid or missing token.
מה קורה בכל דקה#
cron_runner::__construct קובע $_GET["pmode"] = "empg" (פלט חשוף, בלי תבנית של אתר). אחר כך index() עושה:
- בודק את הטוקן.
- מעדכן את
cron_runner_heartbeat(שורה ב-CRM_params, גם היאperm_developer = 1) ומנקה את מטמון הפרמטרים. כך מסך האבחון יודע שה-crontab חי. - בוחר משימות:
is_active = 1, הקטגוריה ריקה או פעילה, ו-next_runריק או קטן או שווה לדקה הנוכחית (בפורמטY-m-d H:i), ממוין לפיnext_run. - לכל משימה מחשב את
next_runהבא עםCron\CronExpression::getNextRunDate(). ביטוי לא תקין מקבל דקה אחת קדימה. אחר כך מעדכןnext_run,last_run = NOW()ו-runs_count + 1, וקורא ל-fire.
fire (פנימי) מכין את ההרצה:
- מסיר
/פותח מה-urlשל המשימה. - מפרק את שדה
params(מחרוזת query, אחרי הסרת?או&פותחים) עםparse_str. המפתחurlנזרק, וערכים שאינם סקלריים הופכים ל-JSON. - יוצר שורת לוג ב-
CRM_cron_logsבסטטוסrunning(CronLogger::create_running_log). - אם נוצר לוג, מוסיף לפרמטרים את
cron_save_log_idואתtk. - קורא ל-
MISC::ping($url, $params).
איך המשימה רצה בפועל#
MISC::ping לא ממתין לתשובה. הוא בונה פקודת shell שמריצה את index.php שוב, בתהליך נפרד ברקע:
sh -c "nohup /usr/local/bin/php '/home/site/public_html/index.php' url='system/cache_cleanup/run' type='all' cron_save_log_id='57' tk='...' > /dev/null 2>/tmp/ping_errors.log < /dev/null &"
כל משימה היא אפוא תהליך PHP עצמאי, והרצת כל המשימות של דקה אחת מתחילה כמעט בו-זמנית. שגיאות כתיבה ל-stderr נאספות ב-/tmp/ping_errors.log.
הליבה לא בודקת אם ההרצה הקודמת של אותה משימה עדיין רצה. משימה שאורכת יותר מהמרווח שלה תרוץ במקביל לעצמה. כתבו משימות אידמפוטנטיות, או הוסיפו נעילה משלכם (למשל GET_LOCK של MySQL).
בתוך משימה ADMIN::is_admin() מחזיר false, וגם COOKIES ו-SESSION ריקים. האימות היחיד הוא הטוקן.
הרצה ידנית#
הכתובת system/cron_runner/run?id=<task_id> מפעילה משימה בודדת מיד. היא דורשת את הטוקן (tk) או מנהל מחובר (ADMIN::is_admin()). היא מעדכנת last_run ו-runs_count, קוראת ל-fire ומחזירה:
OK: cron triggered (system/cache_cleanup/run) params=type=all log_id=58
מסך tasks בניהול כולל כפתור הרצה שמשתמש בזה.
הטבלאות#
| טבלה | עמודות עיקריות |
|---|---|
CRM_cron_categories | id, name, is_active, ord. הקטגוריה id = 1 היא התיקייה המוגנת "system" |
CRM_cron_tasks | id, category_id, title, url, params, crontime, is_active, next_run, last_run, runs_count |
CRM_cron_logs | id, task_id, url, params, status, started_at, finished_at, output, duration_ms, error_message |
שדה crontime הוא ביטוי cron סטנדרטי של חמישה שדות (0 4 * * *). דוגמה לקריאת המשימות הפעילות והאחרונות שרצו:
SELECT id, title, url, crontime, next_run, last_run, runs_count
FROM CRM_cron_tasks
WHERE is_active = 1
ORDER BY last_run DESC
LIMIT 20;
מסך הניהול {admin}/cron_manager#
| תת-מסך | תפקיד |
|---|---|
tasks | רשימת משימות. ?id= הוא מזהה הקטגוריה |
insert | טופס משימה: title, category_id, url, params, is_active, crontime |
insertCategory | קטגוריה חדשה |
logs | לוגים של משימה (?id= הוא מזהה המשימה) |
setup_cron | שורת ה-crontab המומלצת והוראות התקנה |
crontab_status | האם ה-heartbeat טרי, כלומר ה-crontab רץ |
diagnostics | אבחון מערכת התזמון |
בטופס המשימה:
urlהוא כתובת של controller ו-method, לדוגמהcron_payments/daily(controller של האתר) אוsystem/cache_cleanup/run(controller של הליבה).paramsהם מחרוזת query, לדוגמהtype=pages&limit=100.crontimeנבחר עם בורר ויזואלי (FormInput_CronTimePro).next_runמחושב אחרי השמירה.
משימות הליבה#
הליבה מגיעה עם controllers שמתאימים לריצה מתוזמנת, ביניהם:
| URL | מה הוא עושה |
|---|---|
system/cache_cleanup/run | מוחק קבצי מטמון שפג תוקפם (ברירת מחדל 0 4 * * *, קטגוריית system, פרמטר type=all|pages|items) |
admin_session_cleanup/run | מנקה סשנים שפג תוקפם של הניהול ורשומות bad_attempts ישנות (ברירת מחדל 0 3 * * *) |
system/seo_indexnow/run | שליחת כתובות ל-IndexNow, ראו IndexNow. מוגן בטוקן או במנהל מחובר |
system/system_health/run | שורה ותיקה שנשארה באתרים. ה-controller רק עונה ולא מבצע בדיקה, כי בדיקת הבריאות עברה לשירות אולג, ראו בריאות וביצועים |
תור הדוא"ל (Tools::cron_emails_queue) לא נרשם אוטומטית. אם CONFIG::$mail_queue הוא true, הוסיפו ידנית משימה עם system/Tools/cron_emails_queue (ראו דוא"ל).
cache_cleanup, core_ping, admin_session_cleanup, system_health, perf_advisor ו-ai_cron לא בודקים טוקן או הרשאה בתוך ה-controller, והם נגישים גם כ-URL רגיל. מי שמכיר את הכתובת יכול להפעיל אותם. חסמו אותם ברמת שרת האינטרנט לבקשות חיצוניות כשאפשר. ב-ai_cron::run_ai_tool ערך הפרמטר tool משמש לבניית נתיב קובץ, כך שאין להשאיר את הכתובת פתוחה. ראו הקשחה.כתיבת controller למשימה#
המשימה היא controller רגיל (ראו Controllers). שלושה כללים הופכים אותו למתאים לריצה מתוזמנת:
- מגדירים
$_GET["pmode"] = "empg"ב-constructor, כדי שלא יתווסף עטיפת עמוד. - בודקים טוקן או מנהל. בלי זה כל מבקר יכול להפעיל את המשימה.
- מדפיסים שורת סיכום קצרה.
CronLoggerשומר את כל הפלט ב-CRM_cron_logs.output. פלט שמתחיל ב-ERRORמסומן כשגיאה.
<?php
// application/controllers/cron_reports.php
class cron_reports extends wz_controller
{
function __construct()
{
parent::__construct();
$_GET["pmode"] = "empg";
}
function daily()
{
if (!$this->authorized()) {
return "ERROR: forbidden";
}
set_time_limit(0);
ignore_user_abort(true);
$since = date("Y-m-d H:i:s", strtotime("-1 day"));
$rows = DB::get_all("SELECT id FROM CRM_orders WHERE created >= '" . DB::escape($since) . "' AND report_sent = 0");
$done = 0;
foreach ($rows as $row) {
DB::update("orders")->set("report_sent", 1)->update((int)$row["id"]);
$done++;
}
return "OK processed=" . $done;
}
private function authorized(): bool
{
$expected = (string)PARAMS::get("cron_runner_token");
$given = (string)($_GET["tk"] ?? "");
if ($expected !== "" && hash_equals($expected, $given)) {
return true;
}
return ADMIN::is_admin();
}
}
אחרי שמירת הקובץ רושמים את ה-controller כמודול (אחרת ה-URL מחזיר 404) ומנקים את מטמון המודולים:
INSERT INTO CRM_modules (modulesDir, moduleName, active, ord)
VALUES ('', 'cron_reports', 1, 0);
cache_engine::remove("modules");
לבסוף יוצרים משימה במסך cron_manager עם url = cron_reports/daily ו-crontime = 30 2 * * *.
return על echo ו-die
die() באמצע משימה עוקף את סיום הלוג הרגיל. CronLogger מנסה להשלים זאת ב-shutdown handler, אבל פלט וסטטוס יהיו נאמנים יותר כשהמשימה חוזרת רגיל.הלוגים#
CronLogger נכנס לפעולה רק כשבבקשה יש גם cron_save_log_id תקף וגם tk שנכון (הוא נתלה ב-api/core/core.php). אז:
- הפלט נאסף לבאפר וחוזר כרגיל.
- בסיום נכתבים
status,output,duration_msו-finished_at. statusהואerrorאם הפלט מתחיל ב-ERROR(ללא תלות באותיות רישיות), או מכילFatal errorאוUncaught. אחרתok.- fatal שלא נתפס נלכד ב-shutdown handler, והודעתו נשמרת ב-
error_message. - פלט מעל 50,000 בתים (
MAX_OUTPUT) נחתך עם סימון. - נשמרים 50 הלוגים האחרונים של כל משימה (
KEEP_PER_TASK), ישנים יותר נמחקים. - לוג שנשאר ב-
runningלאורך זמן מעיד על משימה שנתקעה או שהתהליך נהרג.
מלכודות#
- בלי נעילה: ראו למעלה. חפיפה אפשרית.
- מפתחות הפרמטרים לא עוברים escape:
MISC::pingמעביר כל ערך דרךescapeshellarg, אבל את שם המפתח לא. שמות מפתחות בשדהparamsשל משימה חייבים להיות מזהים פשוטים. הרשאת עריכת משימות שקולה להרצת קוד, ולכן שמרו אותה למפתחים. - ביטוי cron שגוי: המשימה תרוץ כל דקה (ה-
next_runנדחה בדקה אחת בלבד). - הטוקן ב-URL: הטוקן נראה ברשימת התהליכים של השרת (
ps) ובלוגים. אל תשתפו אותו, והחליפו ב-CRM_paramsאם נחשף.