173 lines
5.7 KiB
PHP
173 lines
5.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Domovoy\Services\Jobs;
|
|
|
|
use Domovoy\Models\ScanJob;
|
|
use Domovoy\Repositories\NetworkRangeRepository;
|
|
use Domovoy\Repositories\ScanJobRepository;
|
|
use Domovoy\Services\Discovery\NetworkScanner;
|
|
|
|
class ScanJobRunner
|
|
{
|
|
private ScanJobRepository $scanJobRepository;
|
|
private NetworkScanner $networkScanner;
|
|
private NetworkRangeRepository $networkRangeRepository;
|
|
|
|
public function __construct(
|
|
ScanJobRepository $scanJobRepository,
|
|
NetworkScanner $networkScanner,
|
|
NetworkRangeRepository $networkRangeRepository
|
|
) {
|
|
$this->scanJobRepository = $scanJobRepository;
|
|
$this->networkScanner = $networkScanner;
|
|
$this->networkRangeRepository = $networkRangeRepository;
|
|
}
|
|
|
|
/**
|
|
* Run a single pending scan job.
|
|
*/
|
|
public function runNext(): ?ScanJob
|
|
{
|
|
$pending = $this->scanJobRepository->findPending();
|
|
if (empty($pending)) {
|
|
return null;
|
|
}
|
|
|
|
$job = $pending[0];
|
|
$this->execute($job);
|
|
return $job;
|
|
}
|
|
|
|
/**
|
|
* Run all pending scan jobs in a loop.
|
|
*
|
|
* @param callable|null $onProgress Called after each job: fn(ScanJob $job)
|
|
*/
|
|
public function runAll(?callable $onProgress = null): int
|
|
{
|
|
$count = 0;
|
|
while (true) {
|
|
$job = $this->runNext();
|
|
if ($job === null) {
|
|
break;
|
|
}
|
|
$count++;
|
|
if ($onProgress !== null) {
|
|
$onProgress($job);
|
|
}
|
|
}
|
|
return $count;
|
|
}
|
|
|
|
public function execute(ScanJob $job): void
|
|
{
|
|
$job->status = 'running';
|
|
$job->startedAt = new \DateTimeImmutable();
|
|
$this->scanJobRepository->save($job);
|
|
|
|
try {
|
|
$finalStatus = null;
|
|
switch ($job->type) {
|
|
case 'network_discovery':
|
|
$finalStatus = $this->runNetworkDiscovery($job);
|
|
break;
|
|
default:
|
|
throw new \RuntimeException("Unknown scan job type: {$job->type}");
|
|
}
|
|
|
|
if ($finalStatus === 'done') {
|
|
$job->status = 'done';
|
|
$job->finishedAt = new \DateTimeImmutable();
|
|
} elseif ($finalStatus === 'paused') {
|
|
$job->status = 'paused';
|
|
$job->finishedAt = null;
|
|
} elseif ($finalStatus === 'cancelled') {
|
|
$job->status = 'cancelled';
|
|
$job->finishedAt = new \DateTimeImmutable();
|
|
}
|
|
} catch (\Throwable $e) {
|
|
$job->status = 'failed';
|
|
$job->errorMessage = $e->getMessage();
|
|
$job->finishedAt = new \DateTimeImmutable();
|
|
}
|
|
|
|
$this->scanJobRepository->save($job);
|
|
}
|
|
|
|
private function runNetworkDiscovery(ScanJob $job): string
|
|
{
|
|
if ($job->networkRangeId === null) {
|
|
throw new \RuntimeException('Network discovery job has no network_range_id');
|
|
}
|
|
|
|
$range = $this->networkRangeRepository->findById($job->networkRangeId);
|
|
if ($range === null) {
|
|
throw new \RuntimeException("Network range #{$job->networkRangeId} not found");
|
|
}
|
|
if (!$range->enabled) {
|
|
throw new \RuntimeException("Network range #{$job->networkRangeId} is disabled");
|
|
}
|
|
|
|
$existingProgress = $this->decodeProgress($job);
|
|
$startIndex = (int)($existingProgress['next_ip_index'] ?? 0);
|
|
$previousHostsFound = (int)($existingProgress['hosts_found'] ?? 0);
|
|
|
|
$result = $this->networkScanner->scanResumable(
|
|
$range,
|
|
$job->id ?? 0,
|
|
$startIndex,
|
|
function () use ($job): bool {
|
|
$fresh = $this->scanJobRepository->findById((int)$job->id);
|
|
return $fresh !== null && in_array($fresh->status, ['paused', 'cancelled'], true);
|
|
},
|
|
function (array $state) use ($job, $range, $previousHostsFound): void {
|
|
$fresh = $this->scanJobRepository->findById((int)$job->id);
|
|
if ($fresh !== null && in_array($fresh->status, ['paused', 'cancelled'], true)) {
|
|
$job->status = $fresh->status;
|
|
}
|
|
$job->resultJson = json_encode([
|
|
'network_range_id' => $range->id,
|
|
'cidr' => $range->cidr,
|
|
'next_ip_index' => $state['next_ip_index'],
|
|
'total_ips' => $state['total_ips'],
|
|
'scanned_count' => $state['scanned_count'],
|
|
'hosts_found' => $previousHostsFound + $state['hosts_found'],
|
|
], JSON_THROW_ON_ERROR);
|
|
$this->scanJobRepository->save($job);
|
|
}
|
|
);
|
|
|
|
$job->resultJson = json_encode([
|
|
'network_range_id' => $range->id,
|
|
'cidr' => $range->cidr,
|
|
'next_ip_index' => $result['next_ip_index'],
|
|
'total_ips' => $result['total_ips'],
|
|
'scanned_count' => $result['scanned_count'],
|
|
'hosts_found' => $previousHostsFound + $result['hosts_found'],
|
|
], JSON_THROW_ON_ERROR);
|
|
|
|
if ($result['completed']) {
|
|
return 'done';
|
|
}
|
|
|
|
$fresh = $this->scanJobRepository->findById((int)$job->id);
|
|
if ($fresh !== null && in_array($fresh->status, ['paused', 'cancelled'], true)) {
|
|
return $fresh->status;
|
|
}
|
|
|
|
return 'paused';
|
|
}
|
|
|
|
private function decodeProgress(ScanJob $job): array
|
|
{
|
|
if ($job->resultJson === null || $job->resultJson === '') {
|
|
return [];
|
|
}
|
|
|
|
$decoded = json_decode($job->resultJson, true);
|
|
return is_array($decoded) ? $decoded : [];
|
|
}
|
|
}
|