domovoy/app/Services/Jobs/ScanJobRunner.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 : [];
}
}