/* * Autopsy Forensic Browser * * Copyright 2015 Basis Technology Corp. * Contact: carrier <at> sleuthkit <dot> org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.sleuthkit.autopsy.experimental.autoingest; import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import org.sleuthkit.autopsy.experimental.configuration.AutoIngestUserPreferences; import java.io.File; import java.io.IOException; import static java.nio.file.FileVisitOption.FOLLOW_LINKS; import java.nio.file.FileVisitResult; import static java.nio.file.FileVisitResult.CONTINUE; import static java.nio.file.FileVisitResult.TERMINATE; import java.nio.file.FileVisitor; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; import org.sleuthkit.autopsy.modules.vmextractor.VirtualMachineFinder; import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.datamodel.CaseDbConnectionInfo; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.Observable; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.stream.Collectors; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.ThreadSafe; import javax.swing.filechooser.FileFilter; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.apache.commons.io.FilenameUtils; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.CaseActionException; import org.sleuthkit.autopsy.ingest.IngestManager; import org.openide.modules.InstalledFileLocator; import org.sleuthkit.autopsy.casemodule.Case.CaseType; import org.sleuthkit.autopsy.casemodule.GeneralFilter; import org.sleuthkit.autopsy.casemodule.ImageDSProcessor; import org.sleuthkit.autopsy.core.RuntimeProperties; import org.sleuthkit.autopsy.core.ServicesMonitor; import org.sleuthkit.autopsy.core.UserPreferencesException; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor; import org.sleuthkit.autopsy.coreutils.ExecUtil; import org.sleuthkit.autopsy.coreutils.NetworkUtils; import org.sleuthkit.autopsy.coreutils.PlatformUtil; import org.sleuthkit.autopsy.events.AutopsyEvent; import org.sleuthkit.autopsy.events.AutopsyEventPublisher; import org.sleuthkit.autopsy.ingest.IngestJob; import org.sleuthkit.autopsy.ingest.IngestJobSettings; import org.sleuthkit.datamodel.Content; import org.sleuthkit.autopsy.experimental.coordinationservice.CoordinationService; import org.sleuthkit.autopsy.experimental.coordinationservice.CoordinationService.CoordinationServiceException; import org.sleuthkit.autopsy.experimental.coordinationservice.CoordinationService.Lock; import org.sleuthkit.autopsy.experimental.configuration.SharedConfiguration; import org.apache.solr.client.solrj.impl.HttpSolrServer; import org.openide.util.Lookup; import org.sleuthkit.autopsy.casemodule.CaseMetadata; import org.sleuthkit.autopsy.casemodule.LocalFilesDSProcessor; import org.sleuthkit.autopsy.core.ServicesMonitor.ServicesMonitorException; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback.DataSourceProcessorResult; import org.sleuthkit.autopsy.coreutils.FileUtil; import org.sleuthkit.autopsy.events.AutopsyEventException; import org.sleuthkit.autopsy.ingest.IngestJob.CancellationReason; import org.sleuthkit.autopsy.ingest.IngestJobStartResult; import org.sleuthkit.autopsy.ingest.IngestModuleError; import org.sleuthkit.autopsy.experimental.autoingest.FileExporter.FileExportException; import org.sleuthkit.autopsy.experimental.autoingest.ManifestFileParser.ManifestFileParserException; import org.sleuthkit.autopsy.experimental.autoingest.ManifestNodeData.ProcessingStatus; import static org.sleuthkit.autopsy.experimental.autoingest.ManifestNodeData.ProcessingStatus.PENDING; import static org.sleuthkit.autopsy.experimental.autoingest.ManifestNodeData.ProcessingStatus.PROCESSING; import static org.sleuthkit.autopsy.experimental.autoingest.ManifestNodeData.ProcessingStatus.COMPLETED; import static org.sleuthkit.autopsy.experimental.autoingest.ManifestNodeData.ProcessingStatus.DELETED; import org.sleuthkit.autopsy.corecomponentinterfaces.AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException; import org.sleuthkit.autopsy.coreutils.FileUtil; import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestAlertFile.AutoIngestAlertFileException; import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestJobLogger.AutoIngestJobLoggerException; import org.sleuthkit.autopsy.experimental.configuration.SharedConfiguration.SharedConfigurationException; import org.sleuthkit.autopsy.ingest.IngestJob.CancellationReason; import org.sleuthkit.autopsy.corecomponentinterfaces.AutoIngestDataSourceProcessor; /** * An auto ingest manager is responsible for processing auto ingest jobs defined * by manifest files that can be added to any level of a designated input * directory tree. * <p> * Each manifest file specifies a co-located data source and a case to which the * data source is to be added. The case directories for the cases reside in a * designated output directory tree. * <p> * There should be at most one auto ingest manager per host (auto ingest node). * Multiple auto ingest nodes may be combined to form an auto ingest cluster. * The activities of the auto ingest nodes in a cluster are coordinated by way * of a coordination service and the nodes communicate via event messages. */ public final class AutoIngestManager extends Observable implements PropertyChangeListener { private static final int NUM_INPUT_SCAN_SCHEDULING_THREADS = 1; private static final String INPUT_SCAN_SCHEDULER_THREAD_NAME = "AIM-input-scan-scheduler-%d"; private static final String INPUT_SCAN_THREAD_NAME = "AIM-input-scan-%d"; private static int DEFAULT_JOB_PRIORITY = 0; private static final String AUTO_INGEST_THREAD_NAME = "AIM-job-processing-%d"; private static final String LOCAL_HOST_NAME = NetworkUtils.getLocalHostName(); private static final String EVENT_CHANNEL_NAME = "Auto-Ingest-Manager-Events"; private static final Set<String> EVENT_LIST = new HashSet<>(Arrays.asList(new String[]{ Event.JOB_STATUS_UPDATED.toString(), Event.JOB_COMPLETED.toString(), Event.CASE_PRIORITIZED.toString(), Event.JOB_STARTED.toString()})); private static final long JOB_STATUS_EVENT_INTERVAL_SECONDS = 10; private static final String JOB_STATUS_PUBLISHING_THREAD_NAME = "AIM-job-status-event-publisher-%d"; private static final long MAX_MISSED_JOB_STATUS_UPDATES = 10; private static final java.util.logging.Logger SYS_LOGGER = AutoIngestSystemLogger.getLogger(); private static AutoIngestManager instance; private final AutopsyEventPublisher eventPublisher; private final Object scanMonitor; private final ScheduledThreadPoolExecutor inputScanSchedulingExecutor; private final ExecutorService inputScanExecutor; private final ExecutorService jobProcessingExecutor; private final ScheduledThreadPoolExecutor jobStatusPublishingExecutor; private final ConcurrentHashMap<String, Instant> hostNamesToLastMsgTime; private final ConcurrentHashMap<String, AutoIngestJob> hostNamesToRunningJobs; private final Object jobsLock; @GuardedBy("jobsLock") private final Map<String, Set<Path>> casesToManifests; @GuardedBy("jobsLock") private List<AutoIngestJob> pendingJobs; @GuardedBy("jobsLock") private AutoIngestJob currentJob; @GuardedBy("jobsLock") private List<AutoIngestJob> completedJobs; private CoordinationService coordinationService; private JobProcessingTask jobProcessingTask; private Future<?> jobProcessingTaskFuture; private Path rootInputDirectory; private Path rootOutputDirectory; private volatile State state; private volatile ErrorState errorState; /** * Gets a singleton auto ingest manager responsible for processing auto * ingest jobs defined by manifest files that can be added to any level of a * designated input directory tree. * * @return A singleton AutoIngestManager instance. */ synchronized static AutoIngestManager getInstance() { if (instance == null) { instance = new AutoIngestManager(); } return instance; } /** * Constructs an auto ingest manager responsible for processing auto ingest * jobs defined by manifest files that can be added to any level of a * designated input directory tree. */ private AutoIngestManager() { SYS_LOGGER.log(Level.INFO, "Initializing auto ingest"); state = State.IDLE; eventPublisher = new AutopsyEventPublisher(); scanMonitor = new Object(); inputScanSchedulingExecutor = new ScheduledThreadPoolExecutor(NUM_INPUT_SCAN_SCHEDULING_THREADS, new ThreadFactoryBuilder().setNameFormat(INPUT_SCAN_SCHEDULER_THREAD_NAME).build()); inputScanExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat(INPUT_SCAN_THREAD_NAME).build()); jobProcessingExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat(AUTO_INGEST_THREAD_NAME).build()); jobStatusPublishingExecutor = new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder().setNameFormat(JOB_STATUS_PUBLISHING_THREAD_NAME).build()); hostNamesToRunningJobs = new ConcurrentHashMap<>(); hostNamesToLastMsgTime = new ConcurrentHashMap<>(); jobsLock = new Object(); casesToManifests = new HashMap<>(); pendingJobs = new ArrayList<>(); completedJobs = new ArrayList<>(); } /** * Starts up auto ingest. * * @throws AutoIngestManagerStartupException if there is a problem starting * auto ingest. */ void startUp() throws AutoIngestManagerStartupException { SYS_LOGGER.log(Level.INFO, "Auto ingest starting"); try { coordinationService = CoordinationService.getInstance(CoordinationServiceNamespace.getRoot()); } catch (CoordinationServiceException ex) { throw new AutoIngestManagerStartupException("Failed to get coordination service", ex); } try { eventPublisher.openRemoteEventChannel(EVENT_CHANNEL_NAME); SYS_LOGGER.log(Level.INFO, "Opened auto ingest event channel"); } catch (AutopsyEventException ex) { throw new AutoIngestManagerStartupException("Failed to open aut ingest event channel", ex); } rootInputDirectory = Paths.get(AutoIngestUserPreferences.getAutoModeImageFolder()); rootOutputDirectory = Paths.get(AutoIngestUserPreferences.getAutoModeResultsFolder()); inputScanSchedulingExecutor.scheduleAtFixedRate(new InputDirScanSchedulingTask(), 0, AutoIngestUserPreferences.getMinutesOfInputScanInterval(), TimeUnit.MINUTES); jobProcessingTask = new JobProcessingTask(); jobProcessingTaskFuture = jobProcessingExecutor.submit(jobProcessingTask); jobStatusPublishingExecutor.scheduleAtFixedRate(new PeriodicJobStatusEventTask(), JOB_STATUS_EVENT_INTERVAL_SECONDS, JOB_STATUS_EVENT_INTERVAL_SECONDS, TimeUnit.SECONDS); eventPublisher.addSubscriber(EVENT_LIST, instance); RuntimeProperties.setCoreComponentsActive(false); state = State.RUNNING; errorState = ErrorState.NONE; } /** * Gets the state of the auto ingest manager: idle, running, shutting dowm. * * @return The state. */ State getState() { return state; } /** * Gets the error state of the autop ingest manager. * * @return The error state, may be NONE. */ ErrorState getErrorState() { return errorState; } /** * Handles auto ingest events published by other auto ingest nodes. * * @param event An auto ingest event from another node. */ @Override public void propertyChange(PropertyChangeEvent event) { if (event instanceof AutopsyEvent) { if (((AutopsyEvent) event).getSourceType() == AutopsyEvent.SourceType.REMOTE) { if (event instanceof AutoIngestJobStartedEvent) { handleRemoteJobStartedEvent((AutoIngestJobStartedEvent) event); } else if (event instanceof AutoIngestJobStatusEvent) { handleRemoteJobStatusEvent((AutoIngestJobStatusEvent) event); } else if (event instanceof AutoIngestJobCompletedEvent) { handleRemoteJobCompletedEvent((AutoIngestJobCompletedEvent) event); } else if (event instanceof AutoIngestCasePrioritizedEvent) { handleRemoteCasePrioritizationEvent((AutoIngestCasePrioritizedEvent) event); } else if (event instanceof AutoIngestCaseDeletedEvent) { handleRemoteCaseDeletedEvent((AutoIngestCaseDeletedEvent) event); } } } } /** * Processes a job started event from another node by removing the job from * the pending queue, if it is present, and adding the job in the event to * the collection of jobs running on other hosts. * <p> * Note that the processing stage of the job will be whatever it was when * the job was serialized for inclusion in the event message. * * @param event A job started from another auto ingest node. */ private void handleRemoteJobStartedEvent(AutoIngestJobStartedEvent event) { String hostName = event.getJob().getNodeName(); hostNamesToLastMsgTime.put(hostName, Instant.now()); synchronized (jobsLock) { Path manifestFilePath = event.getJob().getManifest().getFilePath(); for (Iterator<AutoIngestJob> iterator = pendingJobs.iterator(); iterator.hasNext();) { AutoIngestJob pendingJob = iterator.next(); if (pendingJob.getManifest().getFilePath().equals(manifestFilePath)) { iterator.remove(); break; } } } hostNamesToRunningJobs.put(event.getJob().getNodeName(), event.getJob()); setChanged(); notifyObservers(Event.JOB_STARTED); } /** * Processes a job status event from another node by adding the job in the * event to the collection of jobs running on other hosts. * <p> * Note that the processing stage of the job will be whatever it was when * the job was serialized for inclusion in the event message. * * @param event An job status event from another auto ingest node. */ private void handleRemoteJobStatusEvent(AutoIngestJobStatusEvent event) { String hostName = event.getJob().getNodeName(); hostNamesToLastMsgTime.put(hostName, Instant.now()); hostNamesToRunningJobs.put(hostName, event.getJob()); setChanged(); notifyObservers(Event.JOB_STATUS_UPDATED); } /** * Processes a job completed event from another node by removing the job in * the event from the collection of jobs running on other hosts and adding * it to the list of completed jobs. * <p> * Note that the processing stage of the job will be whatever it was when * the job was serialized for inclusion in the event message. * * @param event An job completed event from another auto ingest node. */ private void handleRemoteJobCompletedEvent(AutoIngestJobCompletedEvent event) { String hostName = event.getJob().getNodeName(); hostNamesToLastMsgTime.put(hostName, Instant.now()); hostNamesToRunningJobs.remove(hostName); if (event.shouldRetry() == false) { synchronized (jobsLock) { completedJobs.add(event.getJob()); } } //scanInputDirsNow(); setChanged(); notifyObservers(Event.JOB_COMPLETED); } /** * Processes a job/case prioritization event from another node by triggering * an immediate input directory scan. * * @param event A prioritization event from another auto ingest node. */ private void handleRemoteCasePrioritizationEvent(AutoIngestCasePrioritizedEvent event) { String hostName = event.getNodeName(); hostNamesToLastMsgTime.put(hostName, Instant.now()); scanInputDirsNow(); setChanged(); notifyObservers(Event.CASE_PRIORITIZED); } /** * Processes a case deletin event from another node by triggering an * immediate input directory scan. * * @param event A case deleted event from another auto ingest node. */ private void handleRemoteCaseDeletedEvent(AutoIngestCaseDeletedEvent event) { String hostName = event.getNodeName(); hostNamesToLastMsgTime.put(hostName, Instant.now()); scanInputDirsNow(); setChanged(); notifyObservers(Event.CASE_DELETED); } /** * Shuts down auto ingest. */ void shutDown() { if (State.RUNNING != state) { return; } SYS_LOGGER.log(Level.INFO, "Auto ingest shutting down"); state = State.SHUTTING_DOWN; try { eventPublisher.removeSubscriber(EVENT_LIST, instance); stopInputFolderScans(); stopJobProcessing(); eventPublisher.closeRemoteEventChannel(); cleanupJobs(); } catch (InterruptedException ex) { SYS_LOGGER.log(Level.SEVERE, "Auto ingest interrupted during shut down", ex); } SYS_LOGGER.log(Level.INFO, "Auto ingest shut down"); state = State.IDLE; } /** * Cancels any input scan scheduling tasks and input scan tasks and shuts * down their executors. */ private void stopInputFolderScans() throws InterruptedException { inputScanSchedulingExecutor.shutdownNow(); inputScanExecutor.shutdownNow(); while (!inputScanSchedulingExecutor.awaitTermination(30, TimeUnit.SECONDS)) { SYS_LOGGER.log(Level.WARNING, "Auto ingest waited at least thirty seconds for input scan scheduling executor to shut down, continuing to wait"); //NON-NLS } while (!inputScanExecutor.awaitTermination(30, TimeUnit.SECONDS)) { SYS_LOGGER.log(Level.WARNING, "Auto ingest waited at least thirty seconds for input scan executor to shut down, continuing to wait"); //NON-NLS } } /** * Cancels the job processing task and shuts down its executor. */ private void stopJobProcessing() throws InterruptedException { synchronized (jobsLock) { if (null != currentJob) { cancelCurrentJob(); } jobProcessingTaskFuture.cancel(true); jobProcessingExecutor.shutdown(); } while (!jobProcessingExecutor.awaitTermination(30, TimeUnit.SECONDS)) { SYS_LOGGER.log(Level.WARNING, "Auto ingest waited at least thirty seconds for job processing executor to shut down, continuing to wait"); //NON-NLS } } /** * Clears the job lists and resets the current job to null. */ private void cleanupJobs() { synchronized (jobsLock) { pendingJobs.clear(); currentJob = null; completedJobs.clear(); } } /** * Gets snapshots of the pending jobs queue, running jobs list, and * completed jobs list. Any of these collection can be excluded by passing a * null for the correspioding in/out list parameter. * * @param pendingJobs A list to be populated with pending jobs, can be * null. * @param runningJobs A list to be populated with running jobs, can be * null. * @param completedJobs A list to be populated with competed jobs, can be * null. */ void getJobs(List<AutoIngestJob> pendingJobs, List<AutoIngestJob> runningJobs, List<AutoIngestJob> completedJobs) { synchronized (jobsLock) { if (null != pendingJobs) { pendingJobs.clear(); pendingJobs.addAll(this.pendingJobs); } if (null != runningJobs) { runningJobs.clear(); if (null != currentJob) { runningJobs.add(currentJob); } for (AutoIngestJob job : hostNamesToRunningJobs.values()) { runningJobs.add(job); runningJobs.sort(new AutoIngestJob.AlphabeticalComparator()); // RJCTODO: This sort should be done in the AID } } if (null != completedJobs) { completedJobs.clear(); completedJobs.addAll(this.completedJobs); } } } /** * Triggers an immediate scan of the input directories. */ void scanInputDirsNow() { if (State.RUNNING != state) { return; } inputScanExecutor.submit(new InputDirScanTask()); } /** * Start a scan of the input directories and wait for scan to complete. */ void scanInputDirsAndWait(){ if (State.RUNNING != state) { return; } SYS_LOGGER.log(Level.INFO, "Starting input scan of {0}", rootInputDirectory); InputDirScanner scanner = new InputDirScanner(); scanner.scan(); SYS_LOGGER.log(Level.INFO, "Completed input scan of {0}", rootInputDirectory); } /** * Pauses processing of the pending jobs queue. The currently running job * will continue to run to completion. */ void pause() { if (State.RUNNING != state) { return; } jobProcessingTask.requestPause(); } /** * Resumes processing of the pending jobs queue. */ void resume() { if (State.RUNNING != state) { return; } jobProcessingTask.requestResume(); } /** * Bumps the priority of all pending ingest jobs for a specified case. * * @param caseName The name of the case to be prioritized. */ void prioritizeCase(final String caseName) { if (state != State.RUNNING) { return; } List<AutoIngestJob> prioritizedJobs = new ArrayList<>(); int maxPriority = 0; synchronized (jobsLock) { for (AutoIngestJob job : pendingJobs) { if (job.getPriority() > maxPriority) { maxPriority = job.getPriority(); } if (job.getManifest().getCaseName().equals(caseName)) { prioritizedJobs.add(job); } } if (!prioritizedJobs.isEmpty()) { ++maxPriority; for (AutoIngestJob job : prioritizedJobs) { String manifestNodePath = job.getManifest().getFilePath().toString(); try { ManifestNodeData nodeData = new ManifestNodeData(coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestNodePath)); nodeData.setPriority(maxPriority); coordinationService.setNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestNodePath, nodeData.toArray()); } catch (CoordinationServiceException ex) { SYS_LOGGER.log(Level.SEVERE, String.format("Coordination service error while prioritizing %s", manifestNodePath), ex); } catch (InterruptedException ex) { SYS_LOGGER.log(Level.SEVERE, "Unexpected interrupt while updating coordination service node data for {0}", manifestNodePath); } job.setPriority(maxPriority); } } Collections.sort(pendingJobs, new AutoIngestJob.PriorityComparator()); } if (!prioritizedJobs.isEmpty()) { new Thread(() -> { eventPublisher.publishRemotely(new AutoIngestCasePrioritizedEvent(LOCAL_HOST_NAME, caseName)); }).start(); } } /** * Bumps the priority of an auto ingest job. * * @param manifestPath The manifest file path for the job to be prioritized. */ void prioritizeJob(Path manifestPath) { if (state != State.RUNNING) { return; } int maxPriority = 0; AutoIngestJob prioritizedJob = null; synchronized (jobsLock) { for (AutoIngestJob job : pendingJobs) { if (job.getPriority() > maxPriority) { maxPriority = job.getPriority(); } if (job.getManifest().getFilePath().equals(manifestPath)) { prioritizedJob = job; } } if (null != prioritizedJob) { ++maxPriority; String manifestNodePath = prioritizedJob.getManifest().getFilePath().toString(); try { ManifestNodeData nodeData = new ManifestNodeData(coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestNodePath)); nodeData.setPriority(maxPriority); coordinationService.setNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestNodePath, nodeData.toArray()); } catch (CoordinationServiceException ex) { SYS_LOGGER.log(Level.SEVERE, String.format("Coordination service error while prioritizing %s", manifestNodePath), ex); } catch (InterruptedException ex) { SYS_LOGGER.log(Level.SEVERE, "Unexpected interrupt while updating coordination service node data for {0}", manifestNodePath); } prioritizedJob.setPriority(maxPriority); } Collections.sort(pendingJobs, new AutoIngestJob.PriorityComparator()); } if (null != prioritizedJob) { final String caseName = prioritizedJob.getManifest().getCaseName(); new Thread(() -> { eventPublisher.publishRemotely(new AutoIngestCasePrioritizedEvent(LOCAL_HOST_NAME, caseName)); }).start(); } } /** * Reprocesses a completed auto ingest job. * * @param manifestPath The manifiest file path for the completed job. * */ void reprocessJob(Path manifestPath) { AutoIngestJob completedJob = null; synchronized (jobsLock) { for (Iterator<AutoIngestJob> iterator = completedJobs.iterator(); iterator.hasNext();) { AutoIngestJob job = iterator.next(); if (job.getManifest().getFilePath().equals(manifestPath)) { completedJob = job; iterator.remove(); break; } } if (null != completedJob && null != completedJob.getCaseDirectoryPath()) { try { ManifestNodeData nodeData = new ManifestNodeData(PENDING, DEFAULT_JOB_PRIORITY, 0, new Date(0), true); coordinationService.setNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath.toString(), nodeData.toArray()); pendingJobs.add(new AutoIngestJob(completedJob.getManifest(), completedJob.getCaseDirectoryPath(), DEFAULT_JOB_PRIORITY, LOCAL_HOST_NAME, AutoIngestJob.Stage.PENDING, new Date(0), true)); } catch (CoordinationServiceException ex) { SYS_LOGGER.log(Level.SEVERE, String.format("Coordination service error while reprocessing %s", manifestPath), ex); completedJobs.add(completedJob); } catch (InterruptedException ex) { SYS_LOGGER.log(Level.SEVERE, "Unexpected interrupt while updating coordination service node data for {0}", manifestPath); completedJobs.add(completedJob); } } Collections.sort(pendingJobs, new AutoIngestJob.PriorityComparator()); } } /** * Deletes a case. This includes deleting the case directory, the text * index, and the case database. This does not include the directories * containing the data sources and their manifests. * * @param caseName The name of the case. * @param caseDirectoryPath The path to the case directory. * * @return A result code indicating success, partial success, or failure. */ CaseDeletionResult deleteCase(String caseName, Path caseDirectoryPath) { if (state != State.RUNNING) { return CaseDeletionResult.FAILED; } /* * Acquire an exclusive lock on the case so it can be safely deleted. * This will fail if the case is open for review or a deletion operation * on this case is already in progress on another node. */ CaseDeletionResult result = CaseDeletionResult.FULLY_DELETED; List<Lock> manifestFileLocks = new ArrayList<>(); try (Lock caseLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.CASES, caseDirectoryPath.toString())) { if (null == caseLock) { return CaseDeletionResult.FAILED; } synchronized (jobsLock) { /* * Do a fresh input directory scan. */ InputDirScanner scanner = new InputDirScanner(); scanner.scan(); Set<Path> manifestPaths = casesToManifests.get(caseName); if (null == manifestPaths) { SYS_LOGGER.log(Level.SEVERE, "No manifest paths found for case {0}", caseName); return CaseDeletionResult.FAILED; } /* * Get all of the required manifest locks. */ for (Path manifestPath : manifestPaths) { try { Lock lock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, manifestPath.toString()); if (null != lock) { manifestFileLocks.add(lock); } else { return CaseDeletionResult.FAILED; } } catch (CoordinationServiceException ex) { SYS_LOGGER.log(Level.SEVERE, String.format("Error attempting to acquire manifest lock for %s for case %s", manifestPath, caseName), ex); return CaseDeletionResult.FAILED; } } /* * Get the case metadata. */ CaseMetadata metaData; Path caseMetaDataFilePath = Paths.get(caseDirectoryPath.toString(), caseName + CaseMetadata.getFileExtension()); try { metaData = new CaseMetadata(caseMetaDataFilePath); } catch (CaseMetadata.CaseMetadataException ex) { SYS_LOGGER.log(Level.SEVERE, String.format("Failed to delete case metadata file %s for case %s", caseMetaDataFilePath, caseName)); return CaseDeletionResult.FAILED; } /* * Mark each job (manifest file) as deleted */ for (Path manifestPath : manifestPaths) { try { ManifestNodeData nodeData = new ManifestNodeData(coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath.toString())); nodeData.setStatus(ManifestNodeData.ProcessingStatus.DELETED); coordinationService.setNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath.toString(), nodeData.toArray()); } catch (InterruptedException | CoordinationServiceException ex) { SYS_LOGGER.log(Level.SEVERE, String.format("Error attempting to set delete flag on manifest data for %s for case %s", manifestPath, caseName), ex); return CaseDeletionResult.PARTIALLY_DELETED; } } /* * Try to unload/delete the Solr core from the Solr server. Do * this before deleting the case directory because the index * files are in the case directory and the deletion will fail if * the core is not unloaded first. */ String textIndexName = metaData.getTextIndexName(); try { unloadSolrCore(metaData.getTextIndexName()); } catch (Exception ex) { /* * Could be a problem, or it could be that the core was * already unloaded (e.g., by the server due to resource * constraints). */ SYS_LOGGER.log(Level.WARNING, String.format("Error deleting text index %s for %s", textIndexName, caseName), ex); //NON-NLS } /* * Delete the case database from the database server. */ String caseDatabaseName = metaData.getCaseDatabaseName(); try { deleteCaseDatabase(caseDatabaseName); } catch (SQLException ex) { SYS_LOGGER.log(Level.SEVERE, String.format("Unable to delete case database %s for %s", caseDatabaseName, caseName), ex); //NON-NLS result = CaseDeletionResult.PARTIALLY_DELETED; } catch (UserPreferencesException ex) { SYS_LOGGER.log(Level.SEVERE, String.format("Error accessing case database connection info, unable to delete case database %s for %s", caseDatabaseName, caseName), ex); //NON-NLS result = CaseDeletionResult.PARTIALLY_DELETED; } catch (ClassNotFoundException ex) { SYS_LOGGER.log(Level.SEVERE, String.format("Cannot load database driver, unable to delete case database %s for %s", caseDatabaseName, caseName), ex); //NON-NLS result = CaseDeletionResult.PARTIALLY_DELETED; } /* * Delete the case directory. */ File caseDirectory = caseDirectoryPath.toFile(); FileUtil.deleteDir(caseDirectory); if (caseDirectory.exists()) { SYS_LOGGER.log(Level.SEVERE, String.format("Failed to delete case directory %s for case %s", caseDirectoryPath, caseName)); return CaseDeletionResult.PARTIALLY_DELETED; } /* * Remove the jobs for the case from the pending jobs queue and * completed jobs list. */ removeJobs(manifestPaths, pendingJobs); removeJobs(manifestPaths, completedJobs); casesToManifests.remove(caseName); } eventPublisher.publishRemotely(new AutoIngestCaseDeletedEvent(caseName, LOCAL_HOST_NAME)); setChanged(); notifyObservers(Event.CASE_DELETED); return result; } catch (CoordinationServiceException ex) { SYS_LOGGER.log(Level.SEVERE, String.format("Error acquiring coordination service lock on case %s", caseName), ex); return CaseDeletionResult.FAILED; } finally { for (Lock lock : manifestFileLocks) { try { lock.release(); } catch (CoordinationServiceException ex) { SYS_LOGGER.log(Level.SEVERE, String.format("Failed to release manifest file lock when deleting case %s", caseName), ex); } } } } /** * Get the current snapshot of the job lists. * @return Snapshot of jobs lists */ JobsSnapshot getCurrentJobsSnapshot(){ synchronized(jobsLock){ List<AutoIngestJob> runningJobs = new ArrayList<>(); getJobs(null, runningJobs, null); return new JobsSnapshot(pendingJobs, runningJobs, completedJobs); } } /** * Tries to unload the Solr core for a case. * * @param caseName The case name. * @param coreName The name of the core to unload. * * @throws Exception if there is a problem unloading the core or it has * already been unloaded (e.g., by the server due to * resource constraints), or there is a problem deleting * files associated with the core */ private void unloadSolrCore(String coreName) throws Exception { /* * Send a core unload request to the Solr server, with the parameters * that request deleting the index and the instance directory * (deleteInstanceDir removes everything related to the core, the index * directory, the configuration files, etc.) set to true. */ String url = "http://" + UserPreferences.getIndexingServerHost() + ":" + UserPreferences.getIndexingServerPort() + "/solr"; HttpSolrServer solrServer = new HttpSolrServer(url); org.apache.solr.client.solrj.request.CoreAdminRequest.unloadCore(coreName, true, true, solrServer); } /** * Tries to delete the case database for a case. * * @param caseFolderPath The case name. * @param caseDatbaseName The case database name. */ private void deleteCaseDatabase(String caseDatbaseName) throws UserPreferencesException, ClassNotFoundException, SQLException { CaseDbConnectionInfo db = UserPreferences.getDatabaseConnectionInfo(); Class.forName("org.postgresql.Driver"); //NON-NLS try (Connection connection = DriverManager.getConnection("jdbc:postgresql://" + db.getHost() + ":" + db.getPort() + "/postgres", db.getUserName(), db.getPassword()); //NON-NLS Statement statement = connection.createStatement();) { String deleteCommand = "DROP DATABASE \"" + caseDatbaseName + "\""; //NON-NLS statement.execute(deleteCommand); } } /** * Removes a set of auto ingest jobs from a collection of jobs. * * @param manifestPaths The manifest file paths for the jobs. * @param jobs The collection of jobs. */ private void removeJobs(Set<Path> manifestPaths, List<AutoIngestJob> jobs) { for (Iterator<AutoIngestJob> iterator = jobs.iterator(); iterator.hasNext();) { AutoIngestJob job = iterator.next(); Path manifestPath = job.getManifest().getFilePath(); if (manifestPaths.contains(manifestPath)) { iterator.remove(); } } } /** * Starts the process of cancelling the current job. * * Note that the current job is included in the running list for a while * because it can take some time * for the automated ingest process for the job to be shut down in * an orderly fashion. */ void cancelCurrentJob() { if (State.RUNNING != state) { return; } synchronized (jobsLock) { if (null != currentJob) { currentJob.cancel(); SYS_LOGGER.log(Level.INFO, "Cancelling automated ingest for manifest {0}", currentJob.getManifest().getFilePath()); } } } /** * Cancels the currently running data-source-level ingest module for the * current job. */ void cancelCurrentDataSourceLevelIngestModule() { if (State.RUNNING != state) { return; } synchronized (jobsLock) { if (null != currentJob) { IngestJob ingestJob = currentJob.getIngestJob(); if (null != ingestJob) { IngestJob.DataSourceIngestModuleHandle moduleHandle = ingestJob.getSnapshot().runningDataSourceIngestModule(); if (null != moduleHandle) { currentJob.setStage(AutoIngestJob.Stage.CANCELLING_MODULE); moduleHandle.cancel(); SYS_LOGGER.log(Level.INFO, "Cancelling {0} module for manifest {1}", new Object[]{moduleHandle.displayName(), currentJob.getManifest().getFilePath()}); } } } } } /** * A task that submits an input directory scan task to the input directory * scan task executor. */ private final class InputDirScanSchedulingTask implements Runnable { /** * Constructs a task that submits an input directory scan task to the * input directory scan task executor. */ private InputDirScanSchedulingTask() { SYS_LOGGER.log(Level.INFO, "Periodic input scan scheduling task started"); } /** * Submits an input directory scan task to the input directory scan task * executor. */ @Override public void run() { scanInputDirsNow(); } } /** * A task that scans the input directory tree and refreshes the pending jobs * queue and the completed jobs list. Crashed job recovery is perfomed as * needed. */ private final class InputDirScanTask implements Callable<Void> { /** * Scans the input directory tree and refreshes the pending jobs queue * and the completed jobs list. Crashed job recovery is performed as * needed. */ @Override public Void call() throws Exception { if (Thread.currentThread().isInterrupted()) { return null; } SYS_LOGGER.log(Level.INFO, "Starting input scan of {0}", rootInputDirectory); InputDirScanner scanner = new InputDirScanner(); scanner.scan(); SYS_LOGGER.log(Level.INFO, "Completed input scan of {0}", rootInputDirectory); setChanged(); notifyObservers(Event.INPUT_SCAN_COMPLETED); return null; } } /** * A FileVisitor that searches the input directories for manifest files. The * search results are used to refresh the pending jobs queue and the * completed jobs list. Crashed job recovery is perfomed as needed. */ private final class InputDirScanner implements FileVisitor<Path> { private final List<AutoIngestJob> newPendingJobsList = new ArrayList<>(); private final List<AutoIngestJob> newCompletedJobsList = new ArrayList<>(); /** * Searches the input directories for manifest files. The search results * are used to refresh the pending jobs queue and the completed jobs * list. */ private void scan() { synchronized (jobsLock) { if (Thread.currentThread().isInterrupted()) { return; } try { newPendingJobsList.clear(); newCompletedJobsList.clear(); Files.walkFileTree(rootInputDirectory, EnumSet.of(FOLLOW_LINKS), Integer.MAX_VALUE, this); Collections.sort(newPendingJobsList, new AutoIngestJob.PriorityComparator()); AutoIngestManager.this.pendingJobs = newPendingJobsList; AutoIngestManager.this.completedJobs = newCompletedJobsList; } catch (IOException ex) { SYS_LOGGER.log(Level.SEVERE, String.format("Error scanning the input directory %s", rootInputDirectory), ex); } } synchronized (scanMonitor) { scanMonitor.notify(); } } /** * Invoked for an input directory before entries in the directory are * visited. Checks if the task thread has been interrupted because auto * ingest is shutting down and terminates the scan if that is the case. * * @param dirPath The directory about to be visited. * @param dirAttrs The basic file attributes of the directory about to * be visited. * * @return TERMINATE if the task thread has been interrupted, CONTINUE * if it has not. * * @throws IOException if an I/O error occurs, but this implementation * does not throw. */ @Override public FileVisitResult preVisitDirectory(Path dirPath, BasicFileAttributes dirAttrs) throws IOException { if (Thread.currentThread().isInterrupted()) { return TERMINATE; } return CONTINUE; } /** * Invoked for a file in a directory. If the file is a manifest file, * creates a pending pending or completed auto ingest job for the * manifest, based on the data stored in the coordination service node * for the manifest. * <p> * Note that the mapping of case names to manifest paths that is used * for case deletion is updated as well. * * @param filePath The path of the file. * @param attrs The file system attributes of the file. * * @return TERMINATE if auto ingest is shutting down, CONTINUE if it has * not. * * @throws IOException if an I/O error occurs, but this implementation * does not throw. */ @Override public FileVisitResult visitFile(Path filePath, BasicFileAttributes attrs) throws IOException { if (Thread.currentThread().isInterrupted()) { return TERMINATE; } Manifest manifest = null; for (ManifestFileParser parser : Lookup.getDefault().lookupAll(ManifestFileParser.class)) { if (parser.fileIsManifest(filePath)) { try { manifest = parser.parse(filePath); break; } catch (ManifestFileParserException ex) { SYS_LOGGER.log(Level.SEVERE, String.format("Error attempting to parse %s with parser %s", filePath, parser.getClass().getCanonicalName()), ex); } } if (Thread.currentThread().isInterrupted()) { return TERMINATE; } } if (Thread.currentThread().isInterrupted()) { return TERMINATE; } if (null != manifest) { /* * Update the mapping of case names to manifest paths that is * used for case deletion. */ String caseName = manifest.getCaseName(); Path manifestPath = manifest.getFilePath(); if (casesToManifests.containsKey(caseName)) { Set<Path> manifestPaths = casesToManifests.get(caseName); manifestPaths.add(manifestPath); } else { Set<Path> manifestPaths = new HashSet<>(); manifestPaths.add(manifestPath); casesToManifests.put(caseName, manifestPaths); } /* * Add a job to the pending jobs queue, the completed jobs list, * or do crashed job recovery, as required. */ try { byte[] rawData = coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath.toString()); if (null != rawData) { ManifestNodeData nodeData = new ManifestNodeData(rawData); if (nodeData.coordSvcNodeDataWasSet()) { ProcessingStatus processingStatus = nodeData.getStatus(); switch (processingStatus) { case PENDING: addPendingJob(manifest, nodeData); break; case PROCESSING: doRecoveryIfCrashed(manifest); break; case COMPLETED: addCompletedJob(manifest, nodeData); break; case DELETED: // Do nothing - we dont'want to add it to any job list or do recovery break; default: SYS_LOGGER.log(Level.SEVERE, "Unknown ManifestNodeData.ProcessingStatus"); break; } } else { addNewPendingJob(manifest); } } else { addNewPendingJob(manifest); } } catch (CoordinationServiceException ex) { SYS_LOGGER.log(Level.SEVERE, String.format("Error getting node data for %s", manifestPath), ex); return CONTINUE; } catch (InterruptedException ex) { Thread.currentThread().interrupt(); return TERMINATE; } } if (!Thread.currentThread().isInterrupted()) { return CONTINUE; } else { return TERMINATE; } } /** * Adds a job to process a manifest to the pending jobs queue. * * @param manifest The manifest. * @param nodeData The data stored in the coordination service node for * the manifest. */ private void addPendingJob(Manifest manifest, ManifestNodeData nodeData) { Path caseDirectory = PathUtils.findCaseDirectory(rootOutputDirectory, manifest.getCaseName()); newPendingJobsList.add(new AutoIngestJob(manifest, caseDirectory, nodeData.getPriority(), LOCAL_HOST_NAME, AutoIngestJob.Stage.PENDING, new Date(0), false)); } /** * Adds a job to process a manifest to the pending jobs queue. * * @param manifest The manifest. * * @throws InterruptedException if the thread running the input * directory scan task is interrupted while * blocked, i.e., if auto ingest is * shutting down. */ private void addNewPendingJob(Manifest manifest) throws InterruptedException { // TODO (JIRA-1960): This is something of a hack, grabbing the lock to create the node. // Is use of Curator.create().forPath() possible instead? try (Lock manifestLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, manifest.getFilePath().toString())) { if (null != manifestLock) { ManifestNodeData newNodeData = new ManifestNodeData(PENDING, DEFAULT_JOB_PRIORITY, 0, new Date(0), false); coordinationService.setNodeData(CoordinationService.CategoryNode.MANIFESTS, manifest.getFilePath().toString(), newNodeData.toArray()); newPendingJobsList.add(new AutoIngestJob(manifest, null, DEFAULT_JOB_PRIORITY, LOCAL_HOST_NAME, AutoIngestJob.Stage.PENDING, new Date(0), false)); } } catch (CoordinationServiceException ex) { SYS_LOGGER.log(Level.SEVERE, String.format("Error attempting to set node data for %s", manifest.getFilePath()), ex); } } /** * Does crash recovery for a manifest, if required. The criterion for * crash recovery is a manifest with coordination service node data * indicating it is being processed for which an exclusive lock on the * node can be acquired. If this condition is true, it is probable that * the node that was processing the job crashed and the processing * status was not updated. * * @param manifest * * @throws InterruptedException if the thread running the input * directory scan task is interrupted while * blocked, i.e., if auto ingest is * shutting down. */ private void doRecoveryIfCrashed(Manifest manifest) throws InterruptedException { String manifestPath = manifest.getFilePath().toString(); try { Lock manifestLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, manifestPath); if (null != manifestLock) { try { ManifestNodeData nodeData = new ManifestNodeData(coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath)); if (nodeData.coordSvcNodeDataWasSet() && ProcessingStatus.PROCESSING == nodeData.getStatus()) { SYS_LOGGER.log(Level.SEVERE, "Attempting crash recovery for {0}", manifestPath); int numberOfCrashes = nodeData.getNumberOfCrashes(); ++numberOfCrashes; nodeData.setNumberOfCrashes(numberOfCrashes); if (numberOfCrashes <= AutoIngestUserPreferences.getMaxNumTimesToProcessImage()) { nodeData.setStatus(PENDING); Path caseDirectoryPath = PathUtils.findCaseDirectory(rootOutputDirectory, manifest.getCaseName()); newPendingJobsList.add(new AutoIngestJob(manifest, caseDirectoryPath, nodeData.getPriority(), LOCAL_HOST_NAME, AutoIngestJob.Stage.PENDING, new Date(0), true)); if (null != caseDirectoryPath) { try { AutoIngestAlertFile.create(caseDirectoryPath); } catch (AutoIngestAlertFileException ex) { SYS_LOGGER.log(Level.SEVERE, String.format("Error creating alert file for crashed job for %s", manifestPath), ex); } try { new AutoIngestJobLogger(manifest.getFilePath(), manifest.getDataSourceFileName(), caseDirectoryPath).logCrashRecoveryWithRetry(); } catch (AutoIngestJobLoggerException ex) { SYS_LOGGER.log(Level.SEVERE, String.format("Error creating case auto ingest log entry for crashed job for %s", manifestPath), ex); } } } else { nodeData.setStatus(COMPLETED); Path caseDirectoryPath = PathUtils.findCaseDirectory(rootOutputDirectory, manifest.getCaseName()); newCompletedJobsList.add(new AutoIngestJob(manifest, caseDirectoryPath, nodeData.getPriority(), LOCAL_HOST_NAME, AutoIngestJob.Stage.COMPLETED, new Date(), true)); if (null != caseDirectoryPath) { try { AutoIngestAlertFile.create(caseDirectoryPath); } catch (AutoIngestAlertFileException ex) { SYS_LOGGER.log(Level.SEVERE, String.format("Error creating alert file for crashed job for %s", manifestPath), ex); } try { new AutoIngestJobLogger(manifest.getFilePath(), manifest.getDataSourceFileName(), caseDirectoryPath).logCrashRecoveryNoRetry(); } catch (AutoIngestJobLoggerException ex) { SYS_LOGGER.log(Level.SEVERE, String.format("Error creating case auto ingest log entry for crashed job for %s", manifestPath), ex); } } } try { coordinationService.setNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath, nodeData.toArray()); } catch (CoordinationServiceException ex) { SYS_LOGGER.log(Level.SEVERE, String.format("Error attempting to set node data for %s", manifestPath), ex); } } } catch (CoordinationServiceException ex) { SYS_LOGGER.log(Level.SEVERE, String.format("Error attempting to get node data for %s", manifestPath), ex); } finally { try { manifestLock.release(); } catch (CoordinationServiceException ex) { SYS_LOGGER.log(Level.SEVERE, String.format("Error attempting to release exclusive lock for %s", manifestPath), ex); } } } } catch (CoordinationServiceException ex) { SYS_LOGGER.log(Level.SEVERE, String.format("Error attempting to get exclusive lock for %s", manifestPath), ex); } } /** * Adds a job to process a manifest to the completed jobs list. * * @param manifest The manifest. * @param nodeData The data stored in the coordination service node for * the manifest. */ private void addCompletedJob(Manifest manifest, ManifestNodeData nodeData) { Path caseDirectoryPath = PathUtils.findCaseDirectory(rootOutputDirectory, manifest.getCaseName()); if (null != caseDirectoryPath) { newCompletedJobsList.add(new AutoIngestJob(manifest, caseDirectoryPath, nodeData.getPriority(), LOCAL_HOST_NAME, AutoIngestJob.Stage.COMPLETED, nodeData.getCompletedDate(), nodeData.getErrorsOccurred())); } else { SYS_LOGGER.log(Level.WARNING, String.format("Job completed for %s, but cannot find case directory, ignoring job", manifest.getFilePath())); } } /** * Invoked for a file that could not be visited because an I/O exception * was thrown when visiting a file. Logs the exceptino and checks if the * task thread has been interrupted because auto ingest is shutting down * and terminates the scan if that is the case. * * @param file The file. * @param ex The exception. * * @return TERMINATE if auto ingest is shutting down, CONTINUE if it has * not. * * @throws IOException if an I/O error occurs, but this implementation * does not throw. */ @Override public FileVisitResult visitFileFailed(Path file, IOException ex) throws IOException { SYS_LOGGER.log(Level.SEVERE, String.format("Error while visiting %s during input directories scan", file.toString()), ex); if (Thread.currentThread().isInterrupted()) { return TERMINATE; } return CONTINUE; } /** * Invoked for an input directory after entries in the directory are * visited. Checks if the task thread has been interrupted because auto * ingest is shutting down and terminates the scan if that is the case. * * @param dirPath The directory about to be visited. * @param dirAttrs The basic file attributes of the directory about to * be visited. * * @return TERMINATE if the task thread has been interrupted, CONTINUE * if it has not. * * @throws IOException if an I/O error occurs, but this implementation * does not throw. */ @Override public FileVisitResult postVisitDirectory(Path dirPath, IOException unused) throws IOException { if (Thread.currentThread().isInterrupted()) { return TERMINATE; } return CONTINUE; } } /** * A single instance of this job processing task is used by the auto ingest * manager to process auto ingest jobs. The task does a blocking take from a * completion service for the input directory scan tasks that refresh the * pending jobs queue. * <p> * The job processing task can be paused between jobs (it waits on the * monitor of its pause lock object) and resumed (by notifying the monitor * of its pause lock object). This supports doing things that should be done * between jobs: orderly shutdown of auto ingest and changes to the ingest * configuration (settings). Note that the ingest configuration may be * specific to the host machine or shared between multiple nodes, in which * case it is downloaded from a specified location before each job. * <p> * The task pauses itself if system errors occur, e.g., problems with the * coordination service, database server, Solr server, etc. The idea behind * this is to avoid attempts to process jobs when the auto ingest system is * not in a state to produce consistent and correct results. It is up to a * system administrator to examine the auto ingest system logs, etc., to * find a remedy for the problem and then resume the task. * <p> * Note that the task also waits on the monitor of its ingest lock object * both when the data source processor and the analysis modules are running * in other threads. Notifies are done via a data source processor callback * and an ingest job event handler, respectively. */ private final class JobProcessingTask implements Runnable { private static final String AUTO_INGEST_MODULE_OUTPUT_DIR = "AutoIngest"; private final Object ingestLock; private final Object pauseLock; @GuardedBy("pauseLock") private boolean pauseRequested; @GuardedBy("pauseLock") private boolean waitingForInputScan; /** * Constructs a job processing task used by the auto ingest manager to * process auto ingest jobs. */ private JobProcessingTask() { ingestLock = new Object(); pauseLock = new Object(); errorState = ErrorState.NONE; } /** * Processes auto ingest jobs, blocking on a completion service for * input directory scan tasks and waiting on a pause lock object when * paused by a client or because of a system error. The task is also in * a wait state when the data source processor or the analysis modules * for a job are running. */ @Override public void run() { SYS_LOGGER.log(Level.INFO, "Job processing task started"); while (true) { try { if (jobProcessingTaskFuture.isCancelled()) { break; } waitForInputDirScan(); if (jobProcessingTaskFuture.isCancelled()) { break; } try { processJobs(); } catch (Exception ex) { // Exception firewall if (jobProcessingTaskFuture.isCancelled()) { break; } if (ex instanceof CoordinationServiceException) { errorState = ErrorState.COORDINATION_SERVICE_ERROR; } else if (ex instanceof SharedConfigurationException) { errorState = ErrorState.SHARED_CONFIGURATION_DOWNLOAD_ERROR; } else if (ex instanceof ServicesMonitorException) { errorState = ErrorState.SERVICES_MONITOR_COMMUNICATION_ERROR; } else if (ex instanceof DatabaseServerDownException) { errorState = ErrorState.DATABASE_SERVER_ERROR; } else if (ex instanceof KeywordSearchServerDownException) { errorState = ErrorState.KEYWORD_SEARCH_SERVER_ERROR; } else if (ex instanceof CaseManagementException) { errorState = ErrorState.CASE_MANAGEMENT_ERROR; } else if (ex instanceof AnalysisStartupException) { errorState = ErrorState.ANALYSIS_STARTUP_ERROR; } else if (ex instanceof FileExportException) { errorState = ErrorState.FILE_EXPORT_ERROR; } else if (ex instanceof AutoIngestAlertFileException) { errorState = ErrorState.ALERT_FILE_ERROR; } else if (ex instanceof AutoIngestJobLoggerException) { errorState = ErrorState.JOB_LOGGER_ERROR; } else if (ex instanceof AutoIngestDataSourceProcessorException) { errorState = ErrorState.DATA_SOURCE_PROCESSOR_ERROR; } else if (ex instanceof InterruptedException) { throw (InterruptedException) ex; } else { errorState = ErrorState.UNEXPECTED_EXCEPTION; } SYS_LOGGER.log(Level.SEVERE, "Auto ingest system error", ex); pauseForSystemError(); } } catch (InterruptedException ex) { break; } } SYS_LOGGER.log(Level.INFO, "Job processing task stopped"); } /** * Makes a request to suspend job processing. The request will not be * serviced immediately if the task is doing a job. */ private void requestPause() { synchronized (pauseLock) { SYS_LOGGER.log(Level.INFO, "Job processing pause requested"); pauseRequested = true; if (waitingForInputScan) { /* * If the flag is set, the job processing task is blocked * waiting for an input directory scan to complete, so * notify any observers that the task is paused. This works * because as soon as the task stops waiting for a scan to * complete, it checks the pause requested flag. If the flag * is set, the task immediately waits on the pause lock * object. */ setChanged(); notifyObservers(Event.PAUSED_BY_REQUEST); } } } /** * Makes a request to resume job processing. */ private void requestResume() { synchronized (pauseLock) { SYS_LOGGER.log(Level.INFO, "Job processing resume requested"); pauseRequested = false; if (waitingForInputScan) { /* * If the flag is set, the job processing task is blocked * waiting for an input directory scan to complete, but * notify any observers that the task is resumed anyway. * This works because as soon as the task stops waiting for * a scan to complete, it checks the pause requested flag. * If the flag is not set, the task immediately begins * processing the pending jobs queue. */ setChanged(); notifyObservers(Event.RESUMED); } pauseLock.notifyAll(); } } /** * Checks for a request to suspend jobs processing. If there is one, * blocks until resumed or interrupted. * * @throws InterruptedException if the thread running the job processing * task is interrupted while blocked, i.e., * if auto ingest is shutting down. */ private void pauseIfRequested() throws InterruptedException { synchronized (pauseLock) { if (pauseRequested) { SYS_LOGGER.log(Level.INFO, "Job processing paused by request"); pauseRequested = false; setChanged(); notifyObservers(Event.PAUSED_BY_REQUEST); pauseLock.wait(); SYS_LOGGER.log(Level.INFO, "Job processing resumed after pause request"); setChanged(); notifyObservers(Event.RESUMED); } } } /** * Pauses auto ingest to allow a sys admin to address a system error. * * @throws InterruptedException if the thread running the job processing * task is interrupted while blocked, i.e., * if auto ingest is shutting down. */ private void pauseForSystemError() throws InterruptedException { synchronized (pauseLock) { SYS_LOGGER.log(Level.SEVERE, "Job processing paused for system error"); setChanged(); notifyObservers(Event.PAUSED_FOR_SYSTEM_ERROR); pauseLock.wait(); errorState = ErrorState.NONE; SYS_LOGGER.log(Level.INFO, "Job processing resumed after system error"); setChanged(); notifyObservers(Event.RESUMED); } } /** * Waits until an input directory scan has completed, with pause request * checks before and after the wait. * * @throws InterruptedException if the thread running the job processing * task is interrupted while blocked, i.e., * if auto ingest is shutting down. */ private void waitForInputDirScan() throws InterruptedException { synchronized (pauseLock) { pauseIfRequested(); /* * The waiting for scan flag is needed for the special case of a * client making a pause request while this task is blocked on * the input directory scan task completion service. Although, * the task is unable to act on the request until the next scan * completes, when it unblocks it will check the pause requested * flag and promptly pause if the flag is set. Thus, setting the * waiting for scan flag allows a pause request in a client * thread to responsively notify any observers that processing * is already effectively paused. */ waitingForInputScan = true; } SYS_LOGGER.log(Level.INFO, "Job processing waiting for input scan completion"); synchronized (scanMonitor) { scanMonitor.wait(); } SYS_LOGGER.log(Level.INFO, "Job processing finished wait for input scan completion"); synchronized (pauseLock) { waitingForInputScan = false; pauseIfRequested(); } } /** * Processes jobs until the pending jobs queue is empty. * * @throws CoordinationServiceException if there is an error * acquiring or releasing * coordination service locks * or setting coordination * service node data. * @throws SharedConfigurationException if there is an error while * downloading shared * configuration. * @throws ServicesMonitorException if there is an error * querying the services * monitor. * @throws DatabaseServerDownException if the database server is * down. * @throws KeywordSearchServerDownException if the Solr server is down. * @throws CaseManagementException if there is an error * creating, opening or closing * the case for the job. * @throws AnalysisStartupException if there is an error * starting analysis of the * data source by the data * source level and file level * ingest modules. * @throws FileExportException if there is an error * exporting files. * @throws AutoIngestAlertFileException if there is an error * creating an alert file. * @throws AutoIngestJobLoggerException if there is an error writing * to the auto ingest log for * the case. * @throws InterruptedException if the thread running the * job processing task is * interrupted while blocked, * i.e., if auto ingest is * shutting down. */ private void processJobs() throws CoordinationServiceException, SharedConfigurationException, ServicesMonitorException, DatabaseServerDownException, KeywordSearchServerDownException, CaseManagementException, AnalysisStartupException, FileExportException, AutoIngestAlertFileException, AutoIngestJobLoggerException, InterruptedException, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException { SYS_LOGGER.log(Level.INFO, "Started processing pending jobs queue"); Lock manifestLock = JobProcessingTask.this.dequeueAndLockNextJob(); while (null != manifestLock) { try { if (currentJob.isCancelled() || jobProcessingTaskFuture.isCancelled()) { return; } processJob(); } finally { manifestLock.release(); } if (jobProcessingTaskFuture.isCancelled()) { return; } pauseIfRequested(); if (jobProcessingTaskFuture.isCancelled()) { return; } manifestLock = JobProcessingTask.this.dequeueAndLockNextJob(); } } /** * Inspects the pending jobs queue, looking for the next job that is * ready for processing. If such a job is found, it is removed from the * queue, made the current job, and a coordination service lock on the * manifest for the job is returned. * <p> * Note that two passes through the queue may be made, the first * enforcing the maximum concurrent jobs per case setting, the second * ignoring this constraint. This policy override prevents idling nodes * when jobs are queued. * <p> * Holding the manifest lock does the following: a) signals to all auto * ingest nodes, including this one, that the job is in progress so it * does not get put in pending jobs queues or completed jobs lists by * input directory scans and b) prevents deletion of the input directory * and the case directory because exclusive manifest locks for all of * the manifests for a case must be acquired for delete operations. * * @return A manifest file lock if a ready job was found, null * otherwise. * * @throws CoordinationServiceException if there is an error while * acquiring or releasing a * manifest file lock. * @throws InterruptedException if the thread is interrupted while * reading the lock data */ private Lock dequeueAndLockNextJob() throws CoordinationServiceException, InterruptedException { SYS_LOGGER.log(Level.INFO, "Checking pending jobs queue for ready job, enforcing max jobs per case"); Lock manifestLock; synchronized (jobsLock) { manifestLock = dequeueAndLockNextJob(true); if (null != manifestLock) { SYS_LOGGER.log(Level.INFO, "Dequeued job for {0}", currentJob.getManifest().getFilePath()); } else { SYS_LOGGER.log(Level.INFO, "No ready job"); SYS_LOGGER.log(Level.INFO, "Checking pending jobs queue for ready job, not enforcing max jobs per case"); manifestLock = dequeueAndLockNextJob(false); if (null != manifestLock) { SYS_LOGGER.log(Level.INFO, "Dequeued job for {0}", currentJob.getManifest().getFilePath()); } else { SYS_LOGGER.log(Level.INFO, "No ready job"); } } } return manifestLock; } /** * Inspects the pending jobs queue, looking for the next job that is * ready for processing. If such a job is found, it is removed from the * queue, made the current job, and a coordination service lock on the * manifest for the job is returned. * * @param enforceMaxJobsPerCase Whether or not to enforce the maximum * concurrent jobs per case setting. * * @return A manifest file lock if a ready job was found, null * otherwise. * * @throws CoordinationServiceException if there is an error while * acquiring or releasing a * manifest file lock. * @throws InterruptedException if the thread is interrupted while * reading the lock data */ private Lock dequeueAndLockNextJob(boolean enforceMaxJobsPerCase) throws CoordinationServiceException, InterruptedException { Lock manifestLock = null; synchronized (jobsLock) { Iterator<AutoIngestJob> iterator = pendingJobs.iterator(); while (iterator.hasNext()) { AutoIngestJob job = iterator.next(); Path manifestPath = job.getManifest().getFilePath(); manifestLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, manifestPath.toString()); if (null == manifestLock) { /* * Skip the job. If it is exclusively locked for * processing or deletion by another node, the remote * job event handlers or the next input scan will flush * it out of the pending queue. */ continue; } ManifestNodeData nodeData = new ManifestNodeData(coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath.toString())); if(! nodeData.getStatus().equals(PENDING)){ /* * Due to a timing issue or a missed event, * a non-pending job has ended up on the pending queue. * Skip the job and remove it from the queue. */ iterator.remove(); continue; } if (enforceMaxJobsPerCase) { int currentJobsForCase = 0; for (AutoIngestJob runningJob : hostNamesToRunningJobs.values()) { if (0 == job.getManifest().getCaseName().compareTo(runningJob.getManifest().getCaseName())) { ++currentJobsForCase; } } if (currentJobsForCase >= AutoIngestUserPreferences.getMaxConcurrentJobsForOneCase()) { manifestLock.release(); manifestLock = null; continue; } } iterator.remove(); currentJob = job; break; } } return manifestLock; } /** * Processes and auto ingest job. * * @throws CoordinationServiceException if there is an error * acquiring or releasing * coordination service locks * or setting coordination * service node data. * @throws SharedConfigurationException if there is an error while * downloading shared * configuration. * @throws ServicesMonitorException if there is an error * querying the services * monitor. * @throws DatabaseServerDownException if the database server is * down. * @throws KeywordSearchServerDownException if the Solr server is down. * @throws CaseManagementException if there is an error * creating, opening or closing * the case for the job. * @throws AnalysisStartupException if there is an error * starting analysis of the * data source by the data * source level and file level * ingest modules. * @throws FileExportException if there is an error * exporting files. * @throws AutoIngestAlertFileException if there is an error * creating an alert file. * @throws AutoIngestJobLoggerException if there is an error writing * to the auto ingest log for * the case. * @throws InterruptedException if the thread running the * job processing task is * interrupted while blocked, * i.e., if auto ingest is * shutting down. */ private void processJob() throws CoordinationServiceException, SharedConfigurationException, ServicesMonitorException, DatabaseServerDownException, KeywordSearchServerDownException, CaseManagementException, AnalysisStartupException, FileExportException, AutoIngestAlertFileException, AutoIngestJobLoggerException, InterruptedException, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException { Manifest manifest = currentJob.getManifest(); String manifestPath = manifest.getFilePath().toString(); ManifestNodeData nodeData = new ManifestNodeData(coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath)); nodeData.setStatus(PROCESSING); coordinationService.setNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath, nodeData.toArray()); SYS_LOGGER.log(Level.INFO, "Started processing of {0}", manifestPath); currentJob.setStage(AutoIngestJob.Stage.STARTING); setChanged(); notifyObservers(Event.JOB_STARTED); eventPublisher.publishRemotely(new AutoIngestJobStartedEvent(currentJob)); try { if (currentJob.isCancelled() || jobProcessingTaskFuture.isCancelled()) { return; } attemptJob(); } finally { if (jobProcessingTaskFuture.isCancelled()) { currentJob.cancel(); } nodeData = new ManifestNodeData(coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath)); if(currentJob.isCompleted() || currentJob.isCancelled()){ nodeData.setStatus(COMPLETED); Date completedDate = new Date(); currentJob.setCompletedDate(completedDate); nodeData.setCompletedDate(currentJob.getCompletedDate()); nodeData.setErrorsOccurred(currentJob.hasErrors()); } else { // The job may get retried nodeData.setStatus(PENDING); } coordinationService.setNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath, nodeData.toArray()); boolean retry = (!currentJob.isCancelled() && !currentJob.isCompleted()); SYS_LOGGER.log(Level.INFO, "Completed processing of {0}, retry = {1}", new Object[]{manifestPath, retry}); if (currentJob.isCancelled()) { Path caseDirectoryPath = currentJob.getCaseDirectoryPath(); if (null != caseDirectoryPath) { AutoIngestAlertFile.create(caseDirectoryPath); // Do this first, it is more important than the case log AutoIngestJobLogger jobLogger = new AutoIngestJobLogger(manifest.getFilePath(), manifest.getDataSourceFileName(), caseDirectoryPath); jobLogger.logJobCancelled(); } } synchronized (jobsLock) { if (!retry) { completedJobs.add(currentJob); } eventPublisher.publishRemotely(new AutoIngestJobCompletedEvent(currentJob, retry)); currentJob = null; setChanged(); notifyObservers(Event.JOB_COMPLETED); } } } /** * Attempts processing of an auto ingest job. * * @throws CoordinationServiceException if there is an error * acquiring or releasing * coordination service locks * or setting coordination * service node data. * @throws SharedConfigurationException if there is an error while * downloading shared * configuration. * @throws ServicesMonitorException if there is an error * querying the services * monitor. * @throws DatabaseServerDownException if the database server is * down. * @throws KeywordSearchServerDownException if the Solr server is down. * @throws CaseManagementException if there is an error * creating, opening or closing * the case for the job. * @throws AnalysisStartupException if there is an error * starting analysis of the * data source by the data * source level and file level * ingest modules. * @throws InterruptedException if the thread running the * job processing task is * interrupted while blocked, * i.e., if auto ingest is * shutting down. */ private void attemptJob() throws CoordinationServiceException, SharedConfigurationException, ServicesMonitorException, DatabaseServerDownException, KeywordSearchServerDownException, CaseManagementException, AnalysisStartupException, FileExportException, AutoIngestAlertFileException, AutoIngestJobLoggerException, InterruptedException, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException { updateConfiguration(); if (currentJob.isCancelled() || jobProcessingTaskFuture.isCancelled()) { return; } verifyRequiredSevicesAreRunning(); if (currentJob.isCancelled() || jobProcessingTaskFuture.isCancelled()) { return; } Case caseForJob = openCase(); try { if (currentJob.isCancelled() || jobProcessingTaskFuture.isCancelled()) { return; } runIngestForJob(caseForJob); } finally { try { caseForJob.closeCase(); } catch (CaseActionException ex) { Manifest manifest = currentJob.getManifest(); throw new CaseManagementException(String.format("Error closing case %s for %s", manifest.getCaseName(), manifest.getFilePath()), ex); } } } /** * Updates the ingest system settings by downloading the latest version * of the settings if using shared configuration. * * @throws SharedConfigurationException if there is an error downloading * shared configuration. * @throws InterruptedException if the thread running the job * processing task is interrupted * while blocked, i.e., if auto * ingest is shutting down. */ private void updateConfiguration() throws SharedConfigurationException, InterruptedException { if (AutoIngestUserPreferences.getSharedConfigEnabled()) { Manifest manifest = currentJob.getManifest(); Path manifestPath = manifest.getFilePath(); SYS_LOGGER.log(Level.INFO, "Downloading shared configuration for {0}", manifestPath); currentJob.setStage(AutoIngestJob.Stage.UPDATING_SHARED_CONFIG); new SharedConfiguration().downloadConfiguration(); } } /** * Checks the availability of the services required to process an * automated ingest job. * * @throws ServicesMonitorException if there is an error querying the * services monitor. * @throws DatabaseServerDownException if the database server is down. * @throws SolrServerDownException if the keyword search server is * down. */ private void verifyRequiredSevicesAreRunning() throws ServicesMonitorException, DatabaseServerDownException, KeywordSearchServerDownException { Manifest manifest = currentJob.getManifest(); Path manifestPath = manifest.getFilePath(); SYS_LOGGER.log(Level.INFO, "Checking services availability for {0}", manifestPath); currentJob.setStage(AutoIngestJob.Stage.CHECKING_SERVICES); if (!isServiceUp(ServicesMonitor.Service.REMOTE_CASE_DATABASE.toString())) { throw new DatabaseServerDownException("Case database server is down"); } if (!isServiceUp(ServicesMonitor.Service.REMOTE_KEYWORD_SEARCH.toString())) { throw new KeywordSearchServerDownException("Keyword search server is down"); } } /** * Tests service of interest to verify that it is running. * * @param serviceName Name of the service. * * @return True if the service is running, false otherwise. * * @throws ServicesMonitorException if there is an error querying the * services monitor. */ private boolean isServiceUp(String serviceName) throws ServicesMonitorException { return (ServicesMonitor.getInstance().getServiceStatus(serviceName).equals(ServicesMonitor.ServiceStatus.UP.toString())); } /** * Creates or opens the case for the current auto ingest job, acquiring * an exclusive lock on the case name during the operation. * <p> * IMPORTANT: The case name lock is used to ensure that only one auto * ingest node at a time can attempt to create/open/delete a given case. * The case name lock must be acquired both here and during case * deletion. * * @return The case on success, null otherwise. * * @throws CoordinationServiceException if there is an error acquiring * or releasing the case name lock. * @throws CaseManagementException if there is an error opening the * case. * @throws InterruptedException if the thread running the auto * ingest job processing task is * interrupted while blocked, i.e., * if auto ingest is shutting down. */ private Case openCase() throws CoordinationServiceException, CaseManagementException, InterruptedException { Manifest manifest = currentJob.getManifest(); String caseName = manifest.getCaseName(); SYS_LOGGER.log(Level.INFO, "Opening case {0} for {1}", new Object[]{caseName, manifest.getFilePath()}); currentJob.setStage(AutoIngestJob.Stage.OPENING_CASE); try (Lock caseLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.CASES, caseName, 30, TimeUnit.MINUTES)) { if (null != caseLock) { try { Path caseDirectoryPath = PathUtils.findCaseDirectory(rootOutputDirectory, caseName); if (null != caseDirectoryPath) { Path metadataFilePath = caseDirectoryPath.resolve(manifest.getCaseName() + CaseMetadata.getFileExtension()); Case.open(metadataFilePath.toString()); } else { caseDirectoryPath = PathUtils.createCaseFolderPath(rootOutputDirectory, caseName); Case.create(caseDirectoryPath.toString(), currentJob.getManifest().getCaseName(), "", "", CaseType.MULTI_USER_CASE); /* * Sleep a bit before releasing the lock to ensure * that the new case folder is visible on the * network. */ Thread.sleep(AutoIngestUserPreferences.getSecondsToSleepBetweenCases() * 1000); } currentJob.setCaseDirectoryPath(caseDirectoryPath); Case caseForJob = Case.getCurrentCase(); SYS_LOGGER.log(Level.INFO, "Opened case {0} for {1}", new Object[]{caseForJob.getName(), manifest.getFilePath()}); return caseForJob; } catch (CaseActionException ex) { throw new CaseManagementException(String.format("Error creating or opening case %s for %s", manifest.getCaseName(), manifest.getFilePath()), ex); } catch (IllegalStateException ex) { /* * Deal with the unfortunate fact that * Case.getCurrentCase throws IllegalStateException. */ throw new CaseManagementException(String.format("Error getting current case %s for %s", manifest.getCaseName(), manifest.getFilePath()), ex); } } else { throw new CaseManagementException(String.format("Timed out acquiring case name lock for %s for %s", manifest.getCaseName(), manifest.getFilePath())); } } } /** * Runs the ingest porocess for the current job. * * @param caseForJob The case for the job. * * @throws CoordinationServiceException if there is an error acquiring * or releasing coordination * service locks or setting * coordination service node data. * @throws AnalysisStartupException if there is an error starting * analysis of the data source by * the data source level and file * level ingest modules. * @throws FileExportException if there is an error exporting * files. * @throws AutoIngestAlertFileException if there is an error creating an * alert file. * @throws AutoIngestJobLoggerException if there is an error writing to * the auto ingest log for the * case. * @throws InterruptedException if the thread running the job * processing task is interrupted * while blocked, i.e., if auto * ingest is shutting down. */ private void runIngestForJob(Case caseForJob) throws CoordinationServiceException, AnalysisStartupException, FileExportException, AutoIngestAlertFileException, AutoIngestJobLoggerException, InterruptedException, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException { Manifest manifest = currentJob.getManifest(); String manifestPath = manifest.getFilePath().toString(); try { if (currentJob.isCancelled() || jobProcessingTaskFuture.isCancelled()) { return; } ingestDataSource(caseForJob); } finally { currentJob.setCompleted(); } } /** * Ingests the data source specified in the manifest of the current job * by using an appropriate data source processor to add the data source * to the case database, passing the data source to the underlying * ingest manager for analysis by data source and file level analysis * modules, and exporting any files from the data source that satisfy * the user-defined file export rules. * * @param caseForJob The case for the job. * * @throws AnalysisStartupException if there is an error starting * analysis of the data source by * the data source level and file * level ingest modules. * @throws FileExportException if there is an error exporting * files. * @throws AutoIngestAlertFileException if there is an error creating an * alert file. * @throws AutoIngestJobLoggerException if there is an error writing to * the auto ingest log for the * case. * @throws InterruptedException if the thread running the job * processing task is interrupted * while blocked, i.e., if auto * ingest is shutting down. */ private void ingestDataSource(Case caseForJob) throws AnalysisStartupException, FileExportException, AutoIngestAlertFileException, AutoIngestJobLoggerException, InterruptedException, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException { if (currentJob.isCancelled() || jobProcessingTaskFuture.isCancelled()) { return; } DataSource dataSource = identifyDataSource(caseForJob); if (null == dataSource) { currentJob.setStage(AutoIngestJob.Stage.COMPLETED); return; } if (currentJob.isCancelled() || jobProcessingTaskFuture.isCancelled()) { return; } runDataSourceProcessor(caseForJob, dataSource); if (dataSource.getContent().isEmpty()) { currentJob.setStage(AutoIngestJob.Stage.COMPLETED); return; } if (currentJob.isCancelled() || jobProcessingTaskFuture.isCancelled()) { return; } try { analyze(dataSource); } finally { /* * Sleep to allow ingest event subscribers to do their event * handling. */ Thread.sleep(AutoIngestUserPreferences.getSecondsToSleepBetweenCases() * 1000); // RJCTODO: Change the setting description to be more generic } if (currentJob.isCancelled() || jobProcessingTaskFuture.isCancelled()) { return; } exportFiles(dataSource); } /** * Identifies the type of the data source specified in the manifest for * the current job and extracts it if required. * * @return A data source object. * * @throws AutoIngestAlertFileException if there is an error creating an * alert file. * @throws AutoIngestJobLoggerException if there is an error writing to * the auto ingest log for the * case. * @throws InterruptedException if the thread running the auto * ingest job processing task is * interrupted while blocked, i.e., * if auto ingest is shutting down. */ private DataSource identifyDataSource(Case caseForJob) throws AutoIngestAlertFileException, AutoIngestJobLoggerException, InterruptedException { Manifest manifest = currentJob.getManifest(); Path manifestPath = manifest.getFilePath(); SYS_LOGGER.log(Level.INFO, "Identifying data source for {0} ", manifestPath); currentJob.setStage(AutoIngestJob.Stage.IDENTIFYING_DATA_SOURCE); Path caseDirectoryPath = currentJob.getCaseDirectoryPath(); AutoIngestJobLogger jobLogger = new AutoIngestJobLogger(manifestPath, manifest.getDataSourceFileName(), caseDirectoryPath); Path dataSourcePath = manifest.getDataSourcePath(); File dataSource = dataSourcePath.toFile(); if (!dataSource.exists()) { SYS_LOGGER.log(Level.SEVERE, "Missing data source for {0}", manifestPath); currentJob.setErrorsOccurred(true); AutoIngestAlertFile.create(caseDirectoryPath); // Do this first, it is more important than the case log jobLogger.logMissingDataSource(); return null; } String deviceId = manifest.getDeviceId(); return new DataSource(deviceId, dataSourcePath); } /** * Passes the data source for the current job through a data source * processor that adds it to the case database. * * @param dataSource The data source. * * @throws AutoIngestAlertFileException if there is an error creating an * alert file. * @throws AutoIngestJobLoggerException if there is an error writing to * the auto ingest log for the * case. * @throws InterruptedException if the thread running the job * processing task is interrupted * while blocked, i.e., if auto * ingest is shutting down. */ private void runDataSourceProcessor(Case caseForJob, DataSource dataSource) throws InterruptedException, AutoIngestAlertFileException, AutoIngestJobLoggerException, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException { Manifest manifest = currentJob.getManifest(); Path manifestPath = manifest.getFilePath(); SYS_LOGGER.log(Level.INFO, "Adding data source for {0} ", manifestPath); currentJob.setStage(AutoIngestJob.Stage.ADDING_DATA_SOURCE); UUID taskId = UUID.randomUUID(); DataSourceProcessorCallback callBack = new AddDataSourceCallback(caseForJob, dataSource, taskId); DataSourceProcessorProgressMonitor progressMonitor = new DoNothingDSPProgressMonitor(); Path caseDirectoryPath = currentJob.getCaseDirectoryPath(); AutoIngestJobLogger jobLogger = new AutoIngestJobLogger(manifestPath, manifest.getDataSourceFileName(), caseDirectoryPath); try { caseForJob.notifyAddingDataSource(taskId); // lookup all AutomatedIngestDataSourceProcessors Collection<? extends AutoIngestDataSourceProcessor> processorCandidates = Lookup.getDefault().lookupAll(AutoIngestDataSourceProcessor.class); Map<AutoIngestDataSourceProcessor, Integer> validDataSourceProcessorsMap = new HashMap<>(); for (AutoIngestDataSourceProcessor processor : processorCandidates) { try { int confidence = processor.canProcess(dataSource.getPath()); if(confidence > 0){ validDataSourceProcessorsMap.put(processor, confidence); } } catch (AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException ex) { SYS_LOGGER.log(Level.SEVERE, "Exception while determining whether data source processor {0} can process {1}", new Object[]{processor.getDataSourceType(), dataSource.getPath()}); // rethrow the exception. It will get caught & handled upstream and will result in AIM auto-pause. throw ex; } } // did we find a data source processor that can process the data source if (validDataSourceProcessorsMap.isEmpty()) { // This should never happen. We should add all unsupported data sources as logical files. AutoIngestAlertFile.create(caseDirectoryPath); currentJob.setErrorsOccurred(true); jobLogger.logFailedToIdentifyDataSource(); SYS_LOGGER.log(Level.WARNING, "Unsupported data source {0} for {1}", new Object[]{dataSource.getPath(), manifestPath}); // NON-NLS return; } // Get an ordered list of data source processors to try List<AutoIngestDataSourceProcessor> validDataSourceProcessors = validDataSourceProcessorsMap.entrySet().stream() .sorted(Map.Entry.<AutoIngestDataSourceProcessor, Integer>comparingByValue().reversed()) .map(Map.Entry::getKey) .collect(Collectors.toList()); synchronized (ingestLock) { // Try each DSP in decreasing order of confidence for(AutoIngestDataSourceProcessor selectedProcessor:validDataSourceProcessors){ jobLogger.logDataSourceProcessorSelected(selectedProcessor.getDataSourceType()); SYS_LOGGER.log(Level.INFO, "Identified data source type for {0} as {1}", new Object[]{manifestPath, selectedProcessor.getDataSourceType()}); try { selectedProcessor.process(dataSource.getDeviceId(), dataSource.getPath(), progressMonitor, callBack); ingestLock.wait(); return; } catch (AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException ex) { // Log that the current DSP failed and set the error flag. We consider it an error // if a DSP fails even if a later one succeeds since we expected to be able to process // the data source which each DSP on the list. AutoIngestAlertFile.create(caseDirectoryPath); currentJob.setErrorsOccurred(true); jobLogger.logDataSourceProcessorError(selectedProcessor.getDataSourceType()); SYS_LOGGER.log(Level.SEVERE, "Exception while processing {0} with data source processor {1}", new Object[]{dataSource.getPath(), selectedProcessor.getDataSourceType()}); } } // If we get to this point, none of the processors were successful SYS_LOGGER.log(Level.SEVERE, "All data source processors failed to process {0}", dataSource.getPath()); jobLogger.logFailedToAddDataSource(); // Throw an exception. It will get caught & handled upstream and will result in AIM auto-pause. throw new AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException("Failed to process " + dataSource.getPath() + " with all data source processors"); } } finally { currentJob.setDataSourceProcessor(null); logDataSourceProcessorResult(dataSource); } } /** * Logs the results of running a data source processor on the data * source for the current job. * * @param dataSource The data source. * * @throws AutoIngestAlertFileException if there is an error creating an * alert file. * @throws AutoIngestJobLoggerException if there is an error writing to * the auto ingest log for the * case. * @throws InterruptedException if the thread running the job * processing task is interrupted * while blocked, i.e., if auto * ingest is shutting down. */ private void logDataSourceProcessorResult(DataSource dataSource) throws AutoIngestAlertFileException, AutoIngestJobLoggerException, InterruptedException { Manifest manifest = currentJob.getManifest(); Path manifestPath = manifest.getFilePath(); Path caseDirectoryPath = currentJob.getCaseDirectoryPath(); AutoIngestJobLogger jobLogger = new AutoIngestJobLogger(manifestPath, manifest.getDataSourceFileName(), caseDirectoryPath); DataSourceProcessorResult resultCode = dataSource.getResultDataSourceProcessorResultCode(); if (null != resultCode) { switch (resultCode) { case NO_ERRORS: jobLogger.logDataSourceAdded(); if (dataSource.getContent().isEmpty()) { currentJob.setErrorsOccurred(true); AutoIngestAlertFile.create(caseDirectoryPath); // Do this first, it is more important than the case log jobLogger.logNoDataSourceContent(); } break; case NONCRITICAL_ERRORS: for (String errorMessage : dataSource.getDataSourceProcessorErrorMessages()) { SYS_LOGGER.log(Level.WARNING, "Non-critical error running data source processor for {0}: {1}", new Object[]{manifestPath, errorMessage}); } jobLogger.logDataSourceAdded(); if (dataSource.getContent().isEmpty()) { currentJob.setErrorsOccurred(true); AutoIngestAlertFile.create(caseDirectoryPath); // Do this first, it is more important than the case log jobLogger.logNoDataSourceContent(); } break; case CRITICAL_ERRORS: for (String errorMessage : dataSource.getDataSourceProcessorErrorMessages()) { SYS_LOGGER.log(Level.SEVERE, "Critical error running data source processor for {0}: {1}", new Object[]{manifestPath, errorMessage}); } currentJob.setErrorsOccurred(true); AutoIngestAlertFile.create(caseDirectoryPath); // Do this first, it is more important than the case log jobLogger.logFailedToAddDataSource(); break; } } else { /* * TODO (JIRA-1711): Use cancellation feature of data source * processors that support cancellation. This should be able to * be done by adding a transient reference to the DSP to * AutoIngestJob and calling cancel on the DSP, if not null, in * cancelCurrentJob. */ SYS_LOGGER.log(Level.WARNING, "Cancellation while waiting for data source processor for {0}", manifestPath); currentJob.setErrorsOccurred(true); AutoIngestAlertFile.create(caseDirectoryPath); // Do this first, it is more important than the case log jobLogger.logDataSourceProcessorCancelled(); } } /** * Analyzes the data source content returned by the data source * processor using the configured set of data source level and file * level analysis modules. * * @param dataSource The data source to analyze. * * @throws AnalysisStartupException if there is an error analyzing * the data source. * @throws AutoIngestAlertFileException if there is an error creating an * alert file. * @throws AutoIngestJobLoggerException if there is an error writing to * the auto ingest log for the * case. * @throws InterruptedException if the thread running the job * processing task is interrupted * while blocked, i.e., if auto * ingest is shutting down. */ private void analyze(DataSource dataSource) throws AnalysisStartupException, AutoIngestAlertFileException, AutoIngestJobLoggerException, InterruptedException { Manifest manifest = currentJob.getManifest(); Path manifestPath = manifest.getFilePath(); SYS_LOGGER.log(Level.INFO, "Starting ingest modules analysis for {0} ", manifestPath); currentJob.setStage(AutoIngestJob.Stage.ANALYZING_DATA_SOURCE); Path caseDirectoryPath = currentJob.getCaseDirectoryPath(); AutoIngestJobLogger jobLogger = new AutoIngestJobLogger(manifestPath, manifest.getDataSourceFileName(), caseDirectoryPath); IngestJobEventListener ingestJobEventListener = new IngestJobEventListener(); IngestManager.getInstance().addIngestJobEventListener(ingestJobEventListener); try { synchronized (ingestLock) { IngestJobSettings ingestJobSettings = new IngestJobSettings(AutoIngestUserPreferences.getAutoModeIngestModuleContextString()); List<String> settingsWarnings = ingestJobSettings.getWarnings(); if (settingsWarnings.isEmpty()) { IngestJobStartResult ingestJobStartResult = IngestManager.getInstance().beginIngestJob(dataSource.getContent(), ingestJobSettings); IngestJob ingestJob = ingestJobStartResult.getJob(); if (null != ingestJob) { currentJob.setIngestJob(ingestJob); /* * Block until notified by the ingest job event * listener or until interrupted because auto ingest * is shutting down. */ ingestLock.wait(); IngestJob.ProgressSnapshot jobSnapshot = ingestJob.getSnapshot(); for (IngestJob.ProgressSnapshot.DataSourceProcessingSnapshot snapshot : jobSnapshot.getDataSourceSnapshots()) { // RJCTODO: Are "child" jobs IngestJobs or DataSourceIngestJobs? if (!snapshot.isCancelled()) { List<String> cancelledModules = snapshot.getCancelledDataSourceIngestModules(); if (!cancelledModules.isEmpty()) { SYS_LOGGER.log(Level.WARNING, String.format("Ingest module(s) cancelled for %s", manifestPath)); currentJob.setErrorsOccurred(true); AutoIngestAlertFile.create(caseDirectoryPath); // Do this first, it is more important than the case log for (String module : snapshot.getCancelledDataSourceIngestModules()) { SYS_LOGGER.log(Level.WARNING, String.format("%s ingest module cancelled for %s", module, manifestPath)); jobLogger.logIngestModuleCancelled(module); } } jobLogger.logAnalysisCompleted(); } else { currentJob.setStage(AutoIngestJob.Stage.CANCELLING); currentJob.setErrorsOccurred(true); AutoIngestAlertFile.create(caseDirectoryPath); // Do this first, it is more important than the case log jobLogger.logAnalysisCancelled(); CancellationReason cancellationReason = snapshot.getCancellationReason(); if (CancellationReason.NOT_CANCELLED != cancellationReason && CancellationReason.USER_CANCELLED != cancellationReason) { throw new AnalysisStartupException(String.format("Analysis cacelled due to %s for %s", cancellationReason.getDisplayName(), manifestPath)); } } } } else if (!ingestJobStartResult.getModuleErrors().isEmpty()) { for (IngestModuleError error : ingestJobStartResult.getModuleErrors()) { SYS_LOGGER.log(Level.SEVERE, String.format("%s ingest module startup error for %s", error.getModuleDisplayName(), manifestPath), error.getThrowable()); } currentJob.setErrorsOccurred(true); AutoIngestAlertFile.create(caseDirectoryPath); // Do this first, it is more important than the case log jobLogger.logIngestModuleStartupErrors(); throw new AnalysisStartupException(String.format("Error(s) during ingest module startup for %s", manifestPath)); } else { SYS_LOGGER.log(Level.SEVERE, String.format("Ingest manager ingest job start error for %s", manifestPath), ingestJobStartResult.getStartupException()); currentJob.setErrorsOccurred(true); AutoIngestAlertFile.create(caseDirectoryPath); // Do this first, it is more important than the case log jobLogger.logAnalysisStartupError(); throw new AnalysisStartupException("Ingest manager error starting job", ingestJobStartResult.getStartupException()); } } else { for (String warning : ingestJobSettings.getWarnings()) { SYS_LOGGER.log(Level.SEVERE, "Ingest job settings error for {0}: {1}", new Object[]{manifestPath, warning}); } currentJob.setErrorsOccurred(true); AutoIngestAlertFile.create(caseDirectoryPath); // Do this first, it is more important than the case log jobLogger.logIngestJobSettingsErrors(); throw new AnalysisStartupException("Error(s) in ingest job settings"); } } } finally { IngestManager.getInstance().removeIngestJobEventListener(ingestJobEventListener); currentJob.setIngestJob(null); // RJCTODO: Consider moving AutoIngestJob into AutoIngestManager so that this method can be made private } } /** * Exports any files from the data source for the current job that * satisfy any user-defined file export rules. * * @param dataSource The data source. * * @throws FileExportException if there is an error exporting * the files. * @throws AutoIngestAlertFileException if there is an error creating an * alert file. * @throws AutoIngestJobLoggerException if there is an error writing to * the auto ingest log for the * case. * @throws InterruptedException if the thread running the job * processing task is interrupted * while blocked, i.e., if auto * ingest is shutting down. */ private void exportFiles(DataSource dataSource) throws FileExportException, AutoIngestAlertFileException, AutoIngestJobLoggerException, InterruptedException { Manifest manifest = currentJob.getManifest(); Path manifestPath = manifest.getFilePath(); SYS_LOGGER.log(Level.INFO, "Exporting files for {0}", manifestPath); currentJob.setStage(AutoIngestJob.Stage.EXPORTING_FILES); Path caseDirectoryPath = currentJob.getCaseDirectoryPath(); AutoIngestJobLogger jobLogger = new AutoIngestJobLogger(manifestPath, manifest.getDataSourceFileName(), caseDirectoryPath); try { FileExporter fileExporter = new FileExporter(); if (fileExporter.isEnabled()) { fileExporter.process(manifest.getDeviceId(), dataSource.getContent(), currentJob::isCancelled); jobLogger.logFileExportCompleted(); } else { SYS_LOGGER.log(Level.WARNING, "Exporting files not enabled for {0}", manifestPath); jobLogger.logFileExportDisabled(); } } catch (FileExportException ex) { SYS_LOGGER.log(Level.SEVERE, String.format("Error doing file export for %s", manifestPath), ex); currentJob.setErrorsOccurred(true); AutoIngestAlertFile.create(caseDirectoryPath); // Do this first, it is more important than the case log jobLogger.logFileExportError(); } } /** * A "callback" that collects the results of running a data source * processor on a data source and unblocks the job processing thread * when the data source processor finishes running in its own thread. */ @Immutable class AddDataSourceCallback extends DataSourceProcessorCallback { private final Case caseForJob; private final DataSource dataSourceInfo; private final UUID taskId; /** * Constructs a "callback" that collects the results of running a * data source processor on a data source and unblocks the job * processing thread when the data source processor finishes running * in its own thread. * * @param caseForJob The case for the current job. * @param dataSourceInfo The data source * @param taskId The task id to associate with ingest job * events. */ AddDataSourceCallback(Case caseForJob, DataSource dataSourceInfo, UUID taskId) { this.caseForJob = caseForJob; this.dataSourceInfo = dataSourceInfo; this.taskId = taskId; } /** * Called by the data source processor when it finishes running in * its own thread. * * @param result The result code for the processing of * the data source. * @param errorMessages Any error messages generated during the * processing of the data source. * @param dataSourceContent The content produced by processing the * data source. */ @Override public void done(DataSourceProcessorCallback.DataSourceProcessorResult result, List<String> errorMessages, List<Content> dataSourceContent) { if (!dataSourceContent.isEmpty()) { caseForJob.notifyDataSourceAdded(dataSourceContent.get(0), taskId); } else { caseForJob.notifyFailedAddingDataSource(taskId); } dataSourceInfo.setDataSourceProcessorOutput(result, errorMessages, dataSourceContent); dataSourceContent.addAll(dataSourceContent); synchronized (ingestLock) { ingestLock.notify(); } } /** * Called by the data source processor when it finishes running in * its own thread, if that thread is the AWT (Abstract Window * Toolkit) event dispatch thread (EDT). * * @param result The result code for the processing of * the data source. * @param errorMessages Any error messages generated during the * processing of the data source. * @param dataSourceContent The content produced by processing the * data source. */ @Override public void doneEDT(DataSourceProcessorCallback.DataSourceProcessorResult result, List<String> errorMessages, List<Content> dataSources) { done(result, errorMessages, dataSources); } } /** * A data source processor progress monitor does nothing. There is * currently no mechanism for showing or recording data source processor * progress during an auto ingest job. */ private class DoNothingDSPProgressMonitor implements DataSourceProcessorProgressMonitor { /** * Does nothing. * * @param indeterminate */ @Override public void setIndeterminate(final boolean indeterminate) { } /** * Does nothing. * * @param progress */ @Override public void setProgress(final int progress) { } /** * Does nothing. * * @param text */ @Override public void setProgressText(final String text) { } } /** * An ingest job event listener that allows the job processing task to * block until the analysis of a data source by the data source level * and file level ingest modules is completed. * <p> * Note that the ingest job can spawn "child" ingest jobs (e.g., if an * embedded virtual machine is found), so the job processing task must * remain blocked until ingest is no longer running. */ private class IngestJobEventListener implements PropertyChangeListener { /** * Listens for local ingest job completed or cancelled events and * notifies the job processing thread when such an event occurs and * there are no "child" ingest jobs running. * * @param event */ @Override public void propertyChange(PropertyChangeEvent event) { if (AutopsyEvent.SourceType.LOCAL == ((AutopsyEvent) event).getSourceType()) { String eventType = event.getPropertyName(); if (eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString()) || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString())) { synchronized (ingestLock) { if (!IngestManager.getInstance().isIngestRunning()) { ingestLock.notify(); } } } } } }; /** * Exception thrown when the services monitor reports that the database * server is down. */ private final class DatabaseServerDownException extends Exception { private static final long serialVersionUID = 1L; private DatabaseServerDownException(String message) { super(message); } private DatabaseServerDownException(String message, Throwable cause) { super(message, cause); } } /** * Exception type thrown when the services monitor reports that the * keyword search server is down. */ private final class KeywordSearchServerDownException extends Exception { private static final long serialVersionUID = 1L; private KeywordSearchServerDownException(String message) { super(message); } private KeywordSearchServerDownException(String message, Throwable cause) { super(message, cause); } } /** * Exception type thrown when there is a problem creating/opening the * case for an auto ingest job. */ private final class CaseManagementException extends Exception { private static final long serialVersionUID = 1L; private CaseManagementException(String message) { super(message); } private CaseManagementException(String message, Throwable cause) { super(message, cause); } } /** * Exception type thrown when there is a problem analyzing a data source * with data source level and file level ingest modules for an auto * ingest job. */ private final class AnalysisStartupException extends Exception { private static final long serialVersionUID = 1L; private AnalysisStartupException(String message) { super(message); } private AnalysisStartupException(String message, Throwable cause) { super(message, cause); } } } /** * An instance of this runnable is responsible for periodically sending auto * ingest job status event to remote auto ingest nodes and timing out stale * remote jobs. The auto ingest job status event is sent only if auto ingest * manager has a currently running auto ingest job. */ private final class PeriodicJobStatusEventTask implements Runnable { // RJCTODO: Rename to StatusPublishingTask, especially when publishing to the system dashboard private final long MAX_SECONDS_WITHOUT_UPDATE = JOB_STATUS_EVENT_INTERVAL_SECONDS * MAX_MISSED_JOB_STATUS_UPDATES; private PeriodicJobStatusEventTask() { SYS_LOGGER.log(Level.INFO, "Periodic status publishing task started"); } @Override public void run() { try { synchronized (jobsLock) { if (currentJob != null) { setChanged(); notifyObservers(Event.JOB_STATUS_UPDATED); eventPublisher.publishRemotely(new AutoIngestJobStatusEvent(currentJob)); } if(AutoIngestUserPreferences.getStatusDatabaseLoggingEnabled()){ String message; boolean isError = false; if(getErrorState().equals(ErrorState.NONE)){ if(currentJob != null){ message = "Processing " + currentJob.getManifest().getDataSourceFileName() + " for case " + currentJob.getManifest().getCaseName(); } else { message = "Paused or waiting for next case"; } } else { message = getErrorState().toString(); isError = true; } try{ StatusDatabaseLogger.logToStatusDatabase(message, isError); } catch (SQLException | UserPreferencesException ex){ SYS_LOGGER.log(Level.WARNING, "Failed to update status database", ex); } } } // check whether any remote nodes have timed out for (AutoIngestJob job : hostNamesToRunningJobs.values()) { if (isStale(hostNamesToLastMsgTime.get(job.getNodeName()))) { // remove the job from remote job running map. /* * NOTE: there is theoretically a check-then-act race * condition but I don't it's worth introducing another * lock because of it. If a job status update is * received after we check the last message fileTime * stamp (i.e. "check") but before we remove the remote * job (i.e. "act") then the remote job will get added * back into hostNamesToRunningJobs as a result of * processing the job status update. */ SYS_LOGGER.log(Level.WARNING, "Auto ingest node {0} timed out while processing folder {1}", new Object[]{job.getNodeName(), job.getManifest().getFilePath().toString()}); hostNamesToRunningJobs.remove(job.getNodeName()); setChanged(); notifyObservers(Event.JOB_COMPLETED); } } } catch (Exception ex) { SYS_LOGGER.log(Level.SEVERE, "Unexpected exception in PeriodicJobStatusEventTask", ex); //NON-NLS } } /** * Determines whether or not the fileTime since the last message from * node is greater than the maximum acceptable interval between * messages. * * @return True or false. */ boolean isStale(Instant lastUpdateTime) { return (Duration.between(lastUpdateTime, Instant.now()).toMillis() / 1000 > MAX_SECONDS_WITHOUT_UPDATE); } } /* * The possible states of an auto ingest manager. */ private enum State { IDLE, RUNNING, SHUTTING_DOWN; } /* * Events published by an auto ingest manager. The events are published * locally to auto ingest manager clients that register as observers and are * broadcast to other auto ingest nodes. // RJCTODO: Is this true? */ enum Event { INPUT_SCAN_COMPLETED, JOB_STARTED, JOB_STATUS_UPDATED, JOB_COMPLETED, CASE_PRIORITIZED, CASE_DELETED, PAUSED_BY_REQUEST, PAUSED_FOR_SYSTEM_ERROR, RESUMED } /** * The current auto ingest error state. */ private enum ErrorState { NONE ("None"), COORDINATION_SERVICE_ERROR ("Coordination service error"), SHARED_CONFIGURATION_DOWNLOAD_ERROR("Shared configuration download error"), SERVICES_MONITOR_COMMUNICATION_ERROR ("Services monitor communication error"), DATABASE_SERVER_ERROR ("Database server error"), KEYWORD_SEARCH_SERVER_ERROR ("Keyword search server error"), CASE_MANAGEMENT_ERROR ("Case management error"), ANALYSIS_STARTUP_ERROR ("Analysis startup error"), FILE_EXPORT_ERROR ("File export error"), ALERT_FILE_ERROR ("Alert file error"), JOB_LOGGER_ERROR ("Job logger error"), DATA_SOURCE_PROCESSOR_ERROR ("Data source processor error"), UNEXPECTED_EXCEPTION ("Unknown error"); private final String desc; private ErrorState(String desc){ this.desc = desc; } @Override public String toString(){ return desc; } } /** * A snapshot of the pending jobs queue, running jobs list, and completed * jobs list. */ static final class JobsSnapshot { private final List<AutoIngestJob> pendingJobs; private final List<AutoIngestJob> runningJobs; private final List<AutoIngestJob> completedJobs; /** * Constructs a snapshot of the pending jobs queue, running jobs list, * and completed jobs list. * * @param pendingJobs The pending jobs queue. * @param runningJobs The running jobs list. * @param completedJobs The cmopleted jobs list. */ private JobsSnapshot(List<AutoIngestJob> pendingJobs, List<AutoIngestJob> runningJobs, List<AutoIngestJob> completedJobs) { this.pendingJobs = new ArrayList<>(pendingJobs); this.runningJobs = new ArrayList<>(runningJobs); this.completedJobs = new ArrayList<>(completedJobs); } /** * Gets the snapshot of the pending jobs queue. * * @return The jobs collection. */ List<AutoIngestJob> getPendingJobs() { return this.pendingJobs; } /** * Gets the snapshot of the running jobs list. * * @return The jobs collection. */ List<AutoIngestJob> getRunningJobs() { return this.runningJobs; } /** * Gets the snapshot of the completed jobs list. * * @return The jobs collection. */ List<AutoIngestJob> getCompletedJobs() { return this.completedJobs; } } /** * RJCTODO */ enum CaseDeletionResult { FAILED, PARTIALLY_DELETED, FULLY_DELETED } @ThreadSafe private static final class DataSource { private final String deviceId; private final Path path; private DataSourceProcessorResult resultCode; private List<String> errorMessages; private List<Content> content; DataSource(String deviceId, Path path) { this.deviceId = deviceId; this.path = path; } String getDeviceId() { return deviceId; } Path getPath() { return this.path; } synchronized void setDataSourceProcessorOutput(DataSourceProcessorResult result, List<String> errorMessages, List<Content> content) { this.resultCode = result; this.errorMessages = new ArrayList<>(errorMessages); this.content = new ArrayList<>(content); } synchronized DataSourceProcessorResult getResultDataSourceProcessorResultCode() { return resultCode; } synchronized List<String> getDataSourceProcessorErrorMessages() { return new ArrayList<>(errorMessages); } synchronized List<Content> getContent() { return new ArrayList<>(content); } } static final class AutoIngestManagerStartupException extends Exception { private static final long serialVersionUID = 1L; private AutoIngestManagerStartupException(String message) { super(message); } private AutoIngestManagerStartupException(String message, Throwable cause) { super(message, cause); } } }