182 lines
7.1 KiB
PHP
182 lines
7.1 KiB
PHP
<?php
|
|
/**
|
|
* @copyright Copyright (c) 2017, Matias De lellis <mati86dl@gmail.com>
|
|
* @copyright Copyright (c) 2018, Branko Kokanovic <branko@kokanovic.org>
|
|
*
|
|
* @author Branko Kokanovic <branko@kokanovic.org>
|
|
*
|
|
* @license GNU AGPL version 3 or any later version
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
namespace OCA\FaceRecognition\BackgroundJob;
|
|
|
|
use OCP\IUser;
|
|
|
|
use OCA\FaceRecognition\AppInfo\Application;
|
|
use OCA\FaceRecognition\Helper\Requirements;
|
|
|
|
use OCA\FaceRecognition\BackgroundJob\FaceRecognitionContext;
|
|
|
|
use OCA\FaceRecognition\BackgroundJob\Tasks\AddMissingImagesTask;
|
|
use OCA\FaceRecognition\BackgroundJob\Tasks\CheckCronTask;
|
|
use OCA\FaceRecognition\BackgroundJob\Tasks\CheckRequirementsTask;
|
|
use OCA\FaceRecognition\BackgroundJob\Tasks\CreateClustersTask;
|
|
use OCA\FaceRecognition\BackgroundJob\Tasks\DisabledUserRemovalTask;
|
|
use OCA\FaceRecognition\BackgroundJob\Tasks\EnumerateImagesMissingFacesTask;
|
|
use OCA\FaceRecognition\BackgroundJob\Tasks\ImageProcessingTask;
|
|
use OCA\FaceRecognition\BackgroundJob\Tasks\StaleImagesRemovalTask;
|
|
|
|
use Symfony\Component\Console\Output\OutputInterface;
|
|
|
|
/**
|
|
* Background service. Both command and cron job are calling this service for long-running background operations.
|
|
* Background processing for face recognition is comprised of several steps, called tasks. Each task is independent,
|
|
* idempotent, DI-aware logic unit that yields. Since tasks are non-preemptive, they should yield from time to time,
|
|
* so we son't end up working for more than given timeout.
|
|
*
|
|
* Tasks can be seen as normal sequential functions, but they are easier to work with,
|
|
* reason about them and test them independently. Other than that, they are really glorified functions.
|
|
*/
|
|
class BackgroundService {
|
|
|
|
/** @var Application $application */
|
|
private $application;
|
|
|
|
/** @var FaceRecognitionContext $context */
|
|
private $context;
|
|
|
|
public function __construct(Application $application, FaceRecognitionContext $context) {
|
|
$this->application = $application;
|
|
$this->context = $context;
|
|
}
|
|
|
|
public function setLogger(OutputInterface $logger): void {
|
|
if (!is_null($this->context->logger)) {
|
|
// If you get this exception, it means you already initialized context->logger. Double-check your flow.
|
|
throw new \LogicException('You cannot call setLogger after you set it once');
|
|
}
|
|
|
|
$this->context->logger = new FaceRecognitionLogger($logger);
|
|
}
|
|
|
|
/**
|
|
* Starts background tasks sequentially.
|
|
*
|
|
* @param int $timeout Maximum allowed time (in seconds) to execute
|
|
* @param bool $verbose Whether to be more verbose
|
|
* @param IUser|null $user ID of user to execute background operations for
|
|
* @param int|null $maxImageArea Max image area (in pixels^2) to be fed to neural network when doing face detection
|
|
* @param string $runMode The command execution mode
|
|
*
|
|
* @return void
|
|
*/
|
|
public function execute(int $timeout, bool $verbose, IUser $user = null, int $maxImageArea = null, string $runMode) {
|
|
// Put to context all the stuff we are figuring only now
|
|
//
|
|
$this->context->user = $user;
|
|
$this->context->verbose = $verbose;
|
|
$this->context->setRunningThroughCommand();
|
|
$this->context->propertyBag['max_image_area'] = $maxImageArea;
|
|
$this->context->propertyBag['run_mode'] = $runMode;
|
|
|
|
// Here we are defining all the tasks that will get executed.
|
|
//
|
|
$task_classes = [
|
|
CheckRequirementsTask::class,
|
|
CheckCronTask::class,
|
|
];
|
|
|
|
switch ($runMode)
|
|
{
|
|
case 'sync-mode':
|
|
$task_classes[] = DisabledUserRemovalTask::class;
|
|
$task_classes[] = StaleImagesRemovalTask::class;
|
|
$task_classes[] = AddMissingImagesTask::class;
|
|
break;
|
|
case 'analyze-mode':
|
|
$task_classes[] = EnumerateImagesMissingFacesTask::class;
|
|
$task_classes[] = ImageProcessingTask::class;
|
|
break;
|
|
case 'cluster-mode':
|
|
$task_classes[] = CreateClustersTask::class;
|
|
break;
|
|
case 'defer-mode':
|
|
$task_classes[] = DisabledUserRemovalTask::class;
|
|
$task_classes[] = StaleImagesRemovalTask::class;
|
|
$task_classes[] = AddMissingImagesTask::class;
|
|
$task_classes[] = EnumerateImagesMissingFacesTask::class;
|
|
$task_classes[] = ImageProcessingTask::class;
|
|
$task_classes[] = CreateClustersTask::class;
|
|
break;
|
|
case 'default-mode':
|
|
default:
|
|
$task_classes[] = DisabledUserRemovalTask::class;
|
|
$task_classes[] = StaleImagesRemovalTask::class;
|
|
$task_classes[] = CreateClustersTask::class;
|
|
$task_classes[] = AddMissingImagesTask::class;
|
|
$task_classes[] = EnumerateImagesMissingFacesTask::class;
|
|
$task_classes[] = ImageProcessingTask::class;
|
|
break;
|
|
}
|
|
|
|
// Main logic to iterate over all tasks and executes them.
|
|
//
|
|
$startTime = time();
|
|
for ($i=0, $task_classes_count = count($task_classes); $i < $task_classes_count; $i++) {
|
|
$task_class = $task_classes[$i];
|
|
$task = $this->application->getContainer()->query($task_class);
|
|
$this->context->logger->logInfo(sprintf("%d/%d - Executing task %s (%s)",
|
|
$i+1, count($task_classes), (new \ReflectionClass($task_class))->getShortName(), $task->description()));
|
|
|
|
try {
|
|
$generator = $task->execute($this->context);
|
|
// $generator can be either actual Generator or return value of boolean.
|
|
// If it is Generator object, that means execute() had some yields.
|
|
// Iterate through those yields and we will get end result through getReturn().
|
|
if ($generator instanceof \Generator) {
|
|
foreach ($generator as $_) {
|
|
$currentTime = time();
|
|
if (($timeout > 0) && ($currentTime - $startTime > $timeout)) {
|
|
$this->context->logger->logInfo("Time out. Quitting...");
|
|
return;
|
|
}
|
|
|
|
if ($this->context->verbose) {
|
|
$this->context->logger->logDebug('yielding');
|
|
}
|
|
}
|
|
}
|
|
|
|
$shouldContinue = ($generator instanceof \Generator) ? $generator->getReturn() : $generator;
|
|
if (!$shouldContinue) {
|
|
$this->context->logger->logInfo(
|
|
sprintf("Task %s signalled we should not continue, bailing out",
|
|
(new \ReflectionClass($task_class))->getShortName()));
|
|
return;
|
|
}
|
|
} catch (\Exception $e) {
|
|
// Any exception is fatal, and we should quit background job
|
|
//
|
|
$this->context->logger->logInfo("Error during background task execution");
|
|
$this->context->logger->logInfo("If error is not transient, this means that core component of face recognition is not working properly");
|
|
$this->context->logger->logInfo("and that quantity and quality of detected faces and person will be low or suboptimal.");
|
|
$this->context->logger->logInfo("You probably want to file an issue (please include exception below) to: https://github.com/matiasdelellis/facerecognition/issues");
|
|
throw $e;
|
|
}
|
|
}
|
|
}
|
|
}
|