<?php
namespace SHServ\Controllers;
use \SHServ\Models\Devices;
use \SHServ\Tools\DeviceScanner;
use \SHServ\Middleware\ControlScripts;
use \SHServ\Models\Scripts;
class CronController extends \SHServ\Middleware\Controller {
protected function ensure_localhost_only() {
$remote_addr = $_SERVER['REMOTE_ADDR'] ?? '';
if(!in_array($remote_addr, ['127.0.0.1', '::1'])) {
http_response_code(403);
header('Content-Type: application/json');
echo json_encode([
"status" => false,
"error_alias" => "forbidden",
"msg" => "Forbidden: local access only"
]);
exit;
}
}
protected function createDeviceScanner(): DeviceScanner {
return new DeviceScanner();
}
public function run_regular_cron_scripts() {
$this -> ensure_localhost_only();
set_time_limit(300);
$lockFile = sys_get_temp_dir() . "/shserv-regular-scripts.lock";
$fp = fopen($lockFile, "c");
if (!$fp || !flock($fp, LOCK_EX | LOCK_NB)) {
if ($fp) {
fclose($fp);
}
logging() -> warn('php:Cron', 'Regular scripts skipped: another instance is running');
return;
}
try {
$scripts_model = new Scripts();
$regular_scripts = ControlScripts::get_regular_scripts();
foreach($regular_scripts as $alias => $script) {
if(!$scripts_model -> script_state("regular", $alias)) {
continue;
}
logging() -> info('php:Cron', 'Running regular script', ['alias' => $alias]);
$result = ControlScripts::run_regular_script($alias);
logging() -> info('php:Cron', 'Regular script finished', ['alias' => $alias, 'result' => $result]);
}
} catch(\Exception $e) {
logging() -> error('php:Cron', 'Regular scripts batch failed', ['message' => $e -> getMessage()]);
} finally {
flock($fp, LOCK_UN);
fclose($fp);
}
}
public function run_timers() {
$this -> ensure_localhost_only();
set_time_limit(300);
$lockFile = sys_get_temp_dir() . "/shserv-timers.lock";
$fp = fopen($lockFile, "c");
if (!$fp || !flock($fp, LOCK_EX | LOCK_NB)) {
if ($fp) {
fclose($fp);
}
logging() -> warn('php:Cron', 'Timers skipped: another instance is running');
return;
}
try {
$timers_model = new \SHServ\Models\Timers();
$pending = $timers_model -> get_pending_timers();
foreach($pending as $timer) {
try {
switch($timer["target_type"]) {
case "action":
ControlScripts::run_action_script($timer["target_alias"], json_decode($timer["params"], true) ?: []);
break;
case "event":
events() -> app_call($timer["target_alias"], json_decode($timer["params"], true) ?: []);
break;
case "regular":
ControlScripts::run_regular_script($timer["target_alias"]);
break;
}
$timers_model -> mark_status((int)$timer["id"], "executed");
logging() -> info('php:Cron', 'Timer executed', [
'timer_alias' => $timer["timer_alias"],
'target_type' => $timer["target_type"],
'target_alias' => $timer["target_alias"]
]);
} catch(\Exception $e) {
$timers_model -> mark_status((int)$timer["id"], "failed", $e -> getMessage());
logging() -> error('php:Cron', 'Timer failed', [
'timer_alias' => $timer["timer_alias"],
'error' => $e -> getMessage()
]);
}
}
} catch(\Exception $e) {
logging() -> error('php:Cron', 'Timers batch failed', ['message' => $e -> getMessage()]);
} finally {
flock($fp, LOCK_UN);
fclose($fp);
}
}
public function status_update_scanning() {
$this -> ensure_localhost_only();
$threshold = FCONF["device_offline_threshold"] ?? 300;
$now = time();
$devices_model = new Devices();
$active_devices = $devices_model -> get_device_list();
// 1. Помечаем оффлайн устройства с устаревшим last_contact
foreach($active_devices as $device) {
$last_contact_ts = strtotime($device -> last_contact);
if($last_contact_ts === false) {
continue;
}
$seconds_since_contact = $now - $last_contact_ts;
if($seconds_since_contact > $threshold) {
if($device -> connection_status != "lost") {
$device -> connection_status = "lost";
$device -> update();
}
}
}
// 2. Сканируем сеть — восстанавливаем lost и обновляем IP при роуминге
$device_scanner = $this -> createDeviceScanner();
$found_devices = $device_scanner -> scan_range(FCONF["device_ip_range"][0], FCONF["device_ip_range"][1]);
$devices_model -> reconcile_scan_results($found_devices);
}
}