diff --git a/server/SHServ/Controllers/DevicesRESTAPIController.php b/server/SHServ/Controllers/DevicesRESTAPIController.php index add788d..7297a7e 100644 --- a/server/SHServ/Controllers/DevicesRESTAPIController.php +++ b/server/SHServ/Controllers/DevicesRESTAPIController.php @@ -8,22 +8,54 @@ use \SHServ\Entities\Area; class DevicesRESTAPIController extends \SHServ\Middleware\Controller { - public function scanning__ready_to_setup() { - $device_model = new Devices(); - $devices = $device_model -> get_unregistered_devices(); + protected function withScanLock(callable $callback) { + $lockFile = dirname(__DIR__, 2) . '/Cache/scan.lock'; - return $this -> utils() -> response_success([ - "devices" => $devices - ]); + // Если lock устарел (старше 120 сек) — считаем его мёртвым + if (file_exists($lockFile) && (time() - filemtime($lockFile)) > 120) { + @unlink($lockFile); + } + + $fp = @fopen($lockFile, 'c+'); + + if (!$fp || !flock($fp, LOCK_EX | LOCK_NB)) { + return $this -> utils() -> response_error("scan_in_progress"); + } + + ftruncate($fp, 0); + fwrite($fp, getmypid()); + fflush($fp); + touch($lockFile); // обновить mtime + + try { + return $callback(); + } finally { + flock($fp, LOCK_UN); + fclose($fp); + @unlink($lockFile); + } + } + + public function scanning__ready_to_setup() { + return $this -> withScanLock(function() { + $device_model = new Devices(); + $devices = $device_model -> get_unregistered_devices(); + + return $this -> utils() -> response_success([ + "devices" => $devices + ]); + }); } public function scanning__all() { - $device_model = new Devices(); - $devices = $device_model -> scanning_localnet(FCONF["device_ip_range"][0], FCONF["device_ip_range"][1]); + return $this -> withScanLock(function() { + $device_model = new Devices(); + $devices = $device_model -> scanning_localnet(FCONF["device_ip_range"][0], FCONF["device_ip_range"][1]); - return $this -> utils() -> response_success([ - "devices" => $devices - ]); + return $this -> utils() -> response_success([ + "devices" => $devices + ]); + }); } public function setup_new_device($device_ip, $alias, $name, $description) { diff --git a/server/SHServ/Tools/DeviceScanner.php b/server/SHServ/Tools/DeviceScanner.php index 2d977a2..14b361d 100644 --- a/server/SHServ/Tools/DeviceScanner.php +++ b/server/SHServ/Tools/DeviceScanner.php @@ -20,12 +20,12 @@ * @return array Найденные устройства */ public function scan_range(string $start_ip, string $end_ip, int $port = 80, float $timeout = 2): array { - $ips = $this -> generate_ip_range($start_ip, $end_ip); - if (empty($ips)) { - return []; - } + $ips = $this -> generate_ip_range($start_ip, $end_ip); + if (empty($ips)) { + return []; + } - return $this -> scan_ips($ips, $port, $timeout); + return $this -> scan_ips($ips, $port, $timeout); } /** @@ -36,164 +36,178 @@ * @return array */ public function filter_by_status(array $devices, $status): array { - $statuses = is_array($status) ? $status : [$status]; - $statuses = array_map('strval', $statuses); + $statuses = is_array($status) ? $status : [$status]; + $statuses = array_map('strval', $statuses); - return array_values(array_filter($devices, function ($device) use ($statuses) { - if (!isset($device['status'])) { - return false; - } - return in_array((string)$device['status'], $statuses, true); - })); - } + return array_values(array_filter($devices, function ($device) use ($statuses) { + if (!isset($device['status'])) { + return false; + } + return in_array((string)$device['status'], $statuses, true); + })); + } - /** - * Генерация диапазона IP (start..end включительно). - */ - private function generate_ip_range(string $start_ip, string $end_ip): array { - $start = ip2long($start_ip); - $end = ip2long($end_ip); + /** + * Генерация диапазона IP (start..end включительно). + */ + private function generate_ip_range(string $start_ip, string $end_ip): array { + $start = ip2long($start_ip); + $end = ip2long($end_ip); - if ($start === false || $end === false || $start > $end) { - return []; - } + if ($start === false || $end === false || $start > $end) { + return []; + } - $ips = []; - for ($ip_long = $start; $ip_long <= $end; $ip_long++) { - $ips[] = long2ip($ip_long); - } + $ips = []; + for ($ip_long = $start; $ip_long <= $end; $ip_long++) { + $ips[] = long2ip($ip_long); + } - return $ips; + return $ips; } private const BATCH_SIZE = 8; /** * Параллельное сканирование IP с curl_multi и ограничением на размер батча. + * Прерывается если клиент разорвал соединение (connection_aborted). */ private function scan_ips(array $ips, int $port, float $timeout): array { - $all_devices = []; + $all_devices = []; - $batches = array_chunk($ips, self::BATCH_SIZE); + $batches = array_chunk($ips, self::BATCH_SIZE); - foreach ($batches as $batch) { - $batch_devices = $this -> scan_batch($batch, $port, $timeout); - $all_devices = array_merge($all_devices, $batch_devices); - usleep(200000); - } + foreach ($batches as $batch) { + if ((connection_status() !== CONNECTION_NORMAL)) { + return $all_devices; + } + $batch_devices = $this -> scan_batch($batch, $port, $timeout); + $all_devices = array_merge($all_devices, $batch_devices); + usleep(200000); + } - return $all_devices; + return $all_devices; } /** * Сканирует один батч IP через curl_multi с retry для неответивших. + * Retry пропускается если соединение прервано. */ private function scan_batch(array $ips, int $port, float $timeout): array { - $devices = $this -> scan_batch_multi($ips, $port, $timeout); + $devices = $this -> scan_batch_multi($ips, $port, $timeout); - // Собираем IP, которые не ответили, для retry - $responded_ips = []; - foreach ($devices as $device) { - if (isset($device['_meta']['ip'])) { - $responded_ips[] = $device['_meta']['ip']; - } - } - $failed_ips = array_values(array_diff($ips, $responded_ips)); + if ((connection_status() !== CONNECTION_NORMAL)) { + return $devices; + } - if (!empty($failed_ips)) { - usleep(500000); // 500ms backoff перед retry - $retry_devices = $this -> scan_batch_multi($failed_ips, $port, $timeout); - $devices = array_merge($devices, $retry_devices); - } + // Собираем IP, которые не ответили, для retry + $responded_ips = []; + foreach ($devices as $device) { + if (isset($device['_meta']['ip'])) { + $responded_ips[] = $device['_meta']['ip']; + } + } + $failed_ips = array_values(array_diff($ips, $responded_ips)); - return $devices; + if (!empty($failed_ips)) { + usleep(500000); // 500ms backoff перед retry + $retry_devices = $this -> scan_batch_multi($failed_ips, $port, $timeout); + $devices = array_merge($devices, $retry_devices); + } + + return $devices; } /** * Внутренний curl_multi проход по батчу. + * Прерывается если клиент разорвал соединение. */ private function scan_batch_multi(array $ips, int $port, float $timeout): array { - $multi = curl_multi_init(); - $handles = []; + $multi = curl_multi_init(); + $handles = []; - foreach ($ips as $ip) { - $url = "http://{$ip}:{$port}/about"; + foreach ($ips as $ip) { + $url = "http://{$ip}:{$port}/about"; - $ch = curl_init(); - curl_setopt_array($ch, [ - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_CONNECTTIMEOUT => $timeout, - CURLOPT_TIMEOUT => $timeout, - CURLOPT_FOLLOWLOCATION => false, - CURLOPT_NOBODY => false, - CURLOPT_HEADER => false, - ]); + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CONNECTTIMEOUT => $timeout, + CURLOPT_TIMEOUT => $timeout, + CURLOPT_FOLLOWLOCATION => false, + CURLOPT_NOBODY => false, + CURLOPT_HEADER => false, + ]); - curl_multi_add_handle($multi, $ch); + curl_multi_add_handle($multi, $ch); - $handles[] = [ - 'handle' => $ch, - 'ip' => $ip, - 'url' => $url, - ]; - } + $handles[] = [ + 'handle' => $ch, + 'ip' => $ip, + 'url' => $url, + ]; + } - $running = null; - do { - $status = curl_multi_exec($multi, $running); - if ($status > 0) { - break; - } + $running = null; + do { + $status = curl_multi_exec($multi, $running); + if ($status > 0) { + break; + } - $select_result = curl_multi_select($multi, 0.1); - if ($select_result === -1) { - usleep(100000); - } - } while ($running > 0); + if ((connection_status() !== CONNECTION_NORMAL)) { + break; + } - $devices = []; + $select_result = curl_multi_select($multi, 0.1); + if ($select_result === -1) { + usleep(100000); + } + } while ($running > 0); - foreach ($handles as $item) { - $ch = $item['handle']; - $ip = $item['ip']; - $url = $item['url']; + $devices = []; - $body = curl_multi_getcontent($ch); - $curl_err = curl_errno($ch); - $http = curl_getinfo($ch, CURLINFO_HTTP_CODE); + foreach ($handles as $item) { + $ch = $item['handle']; + $ip = $item['ip']; + $url = $item['url']; - curl_multi_remove_handle($multi, $ch); - curl_close($ch); + $body = curl_multi_getcontent($ch); + $curl_err = curl_errno($ch); + $http = curl_getinfo($ch, CURLINFO_HTTP_CODE); - if ($curl_err !== 0 || $http !== 200 || !$body) { - continue; - } + curl_multi_remove_handle($multi, $ch); + curl_close($ch); - $json = json_decode($body, true); - if (!is_array($json)) { - continue; - } + if ($curl_err !== 0 || $http !== 200 || !$body) { + continue; + } - if (!isset($json['ip_address'])) { - $json['ip_address'] = $ip; - } - if (!isset($json['server'])) { - $json['server'] = "http://{$ip}"; - } + $json = json_decode($body, true); + if (!is_array($json)) { + continue; + } - $json['_meta'] = [ - 'ip' => $ip, - 'url' => $url, - 'http_code' => $http, - ]; + if (!isset($json['ip_address'])) { + $json['ip_address'] = $ip; + } + if (!isset($json['server'])) { + $json['server'] = "http://{$ip}"; + } - $devices[] = $json; - } + $json['_meta'] = [ + 'ip' => $ip, + 'url' => $url, + 'http_code' => $http, + ]; - curl_multi_close($multi); + $devices[] = $json; + } - return $devices; - } + curl_multi_close($multi); -} + return $devices; + } + +} \ No newline at end of file diff --git a/server/SHServ/text-msgs.php b/server/SHServ/text-msgs.php index 39dbe1e..f2d5e48 100644 --- a/server/SHServ/text-msgs.php +++ b/server/SHServ/text-msgs.php @@ -50,6 +50,7 @@ "firmware_not_found" => "Прошивка не найдена", "firmware_not_compatible" => "Прошивка не совместима с устройством", "ota_failed" => "Не удалось обновить прошивку на устройстве", + "scan_in_progress" => "Сканирование сети уже выполняется, подождите", // Other "accept_removing" => "Подтвердите удаление", diff --git a/webclient/config.php b/webclient/config.php index 12293ba..b1aa07f 100644 --- a/webclient/config.php +++ b/webclient/config.php @@ -1,9 +1,9 @@ "0.3 dev", - // "server" => "http://smart-home-serv.local", - "server" => "http://192.168.1.101", + "version" => "0.4.0 dev", + "server" => "http://smart-home-serv.local", + // "server" => "http://192.168.1.101", // Какие пути разрешены (белый список) — подстрой под себя "allowed_prefixes" => [ "/api/v1/", diff --git a/webclient/src/api/modules/devices.js b/webclient/src/api/modules/devices.js index 37ab2eb..1b95f97 100644 --- a/webclient/src/api/modules/devices.js +++ b/webclient/src/api/modules/devices.js @@ -42,11 +42,11 @@ }, scanningSetup(options) { - return apiGet("/api/v1/devices/scanning/setup", options); + return apiGet("/api/v1/devices/scanning/setup", { timeoutMs: 120000, ...options }); }, scanningAll(options) { - return apiGet("/api/v1/devices/scanning/all", options); + return apiGet("/api/v1/devices/scanning/all", { timeoutMs: 120000, ...options }); }, setupNewDevice(payload) {