Autopsy  4.4
Graphical digital forensics platform for The Sleuth Kit and other tools.
SearchRunner.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2011 - 2017 Basis Technology Corp.
5  * Contact: carrier <at> sleuthkit <dot> org
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  * http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19 package org.sleuthkit.autopsy.keywordsearch;
20 
21 import java.util.ArrayList;
22 import java.util.Collection;
23 import java.util.Collections;
24 import java.util.HashMap;
25 import java.util.HashSet;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Map.Entry;
29 import java.util.Set;
30 import java.util.Timer;
31 import java.util.TimerTask;
32 import java.util.concurrent.CancellationException;
33 import java.util.concurrent.ExecutionException;
34 import java.util.concurrent.atomic.AtomicLong;
35 import java.util.logging.Level;
36 import javax.swing.SwingUtilities;
37 import javax.swing.SwingWorker;
38 import org.netbeans.api.progress.aggregate.AggregateProgressFactory;
39 import org.netbeans.api.progress.aggregate.AggregateProgressHandle;
40 import org.netbeans.api.progress.aggregate.ProgressContributor;
41 import org.openide.util.Cancellable;
42 import org.openide.util.NbBundle;
43 import org.openide.util.NbBundle.Messages;
49 import org.sleuthkit.datamodel.BlackboardArtifact;
50 
55 public final class SearchRunner {
56 
57  private static final Logger logger = Logger.getLogger(SearchRunner.class.getName());
58  private static SearchRunner instance = null;
60  private Ingester ingester = null;
61  private volatile boolean updateTimerRunning = false;
62  private Timer updateTimer;
63 
64  // maps a jobID to the search
65  private Map<Long, SearchJobInfo> jobs = new HashMap<>(); //guarded by "this"
66 
67  SearchRunner() {
68  ingester = Ingester.getDefault();
69  updateTimer = new Timer(NbBundle.getMessage(this.getClass(), "SearchRunner.updateTimer.title.text"), true); // run as a daemon
70  }
71 
76  public static synchronized SearchRunner getInstance() {
77  if (instance == null) {
78  instance = new SearchRunner();
79  }
80  return instance;
81  }
82 
93  public synchronized void startJob(long jobId, long dataSourceId, List<String> keywordListNames) {
94  if (jobs.containsKey(jobId) == false) {
95  logger.log(Level.INFO, "Adding job {0}", jobId); //NON-NLS
96  SearchJobInfo jobData = new SearchJobInfo(jobId, dataSourceId, keywordListNames);
97  jobs.put(jobId, jobData);
98  }
99 
100  // keep track of how many threads / module instances from this job have asked for this
101  jobs.get(jobId).incrementModuleReferenceCount();
102 
103  // start the timer, if needed
104  if ((jobs.size() > 0) && (updateTimerRunning == false)) {
105  final long updateIntervalMs = ((long) KeywordSearchSettings.getUpdateFrequency().getTime()) * 60 * 1000;
106  updateTimer.scheduleAtFixedRate(new UpdateTimerTask(), updateIntervalMs, updateIntervalMs);
107  updateTimerRunning = true;
108  }
109  }
110 
117  public void endJob(long jobId) {
118  SearchJobInfo job;
119  boolean readyForFinalSearch = false;
120  synchronized (this) {
121  job = jobs.get(jobId);
122  if (job == null) {
123  return;
124  }
125 
126  // Only do final search if this is the last module/thread in this job to call endJob()
127  if (job.decrementModuleReferenceCount() == 0) {
128  jobs.remove(jobId);
129  readyForFinalSearch = true;
130  }
131  }
132 
133  if (readyForFinalSearch) {
134  commit();
135  doFinalSearch(job); //this will block until it's done
136  }
137  }
138 
145  public void stopJob(long jobId) {
146  logger.log(Level.INFO, "Stopping job {0}", jobId); //NON-NLS
147  commit();
148 
149  SearchJobInfo job;
150  synchronized (this) {
151  job = jobs.get(jobId);
152  if (job == null) {
153  return;
154  }
155 
156  //stop currentSearcher
157  SearchRunner.Searcher currentSearcher = job.getCurrentSearcher();
158  if ((currentSearcher != null) && (!currentSearcher.isDone())) {
159  currentSearcher.cancel(true);
160  }
161 
162  jobs.remove(jobId);
163  }
164  }
165 
172  public synchronized void addKeywordListsToAllJobs(List<String> keywordListNames) {
173  for (String listName : keywordListNames) {
174  logger.log(Level.INFO, "Adding keyword list {0} to all jobs", listName); //NON-NLS
175  for (SearchJobInfo j : jobs.values()) {
176  j.addKeywordListName(listName);
177  }
178  }
179  }
180 
184  private void commit() {
185  ingester.commit();
186 
187  // Signal a potential change in number of text_ingested files
188  try {
189  final int numIndexedFiles = KeywordSearch.getServer().queryNumIndexedFiles();
190  KeywordSearch.fireNumIndexedFilesChange(null, numIndexedFiles);
192  logger.log(Level.WARNING, "Error executing Solr query to check number of indexed files: ", ex); //NON-NLS
193  }
194  }
195 
202  private void doFinalSearch(SearchJobInfo job) {
203  // Run one last search as there are probably some new files committed
204  logger.log(Level.INFO, "Running final search for jobid {0}", job.getJobId()); //NON-NLS
205  if (!job.getKeywordListNames().isEmpty()) {
206  try {
207  // In case this job still has a worker running, wait for it to finish
208  job.waitForCurrentWorker();
209 
210  SearchRunner.Searcher finalSearcher = new SearchRunner.Searcher(job, true);
211  job.setCurrentSearcher(finalSearcher); //save the ref
212  finalSearcher.execute(); //start thread
213 
214  // block until the search is complete
215  finalSearcher.get();
216 
217  } catch (InterruptedException | CancellationException ex) {
218  logger.log(Level.INFO, "Final search for search job {1} interrupted or cancelled", job.getJobId()); //NON-NLS
219  } catch (ExecutionException ex) {
220  logger.log(Level.SEVERE, String.format("Final search for search job %d failed", job.getJobId()), ex); //NON-NLS
221  }
222  }
223  }
224 
228  private class UpdateTimerTask extends TimerTask {
229 
230  private final Logger logger = Logger.getLogger(SearchRunner.UpdateTimerTask.class.getName());
231 
232  @Override
233  public void run() {
234  // If no jobs then cancel the task. If more job(s) come along, a new task will start up.
235  if (jobs.isEmpty()) {
236  this.cancel(); //terminate this timer task
237  updateTimerRunning = false;
238  return;
239  }
240 
241  commit();
242 
243  synchronized (SearchRunner.this) {
244  // Spawn a search thread for each job
245  for (Entry<Long, SearchJobInfo> j : jobs.entrySet()) {
246  SearchJobInfo job = j.getValue();
247  // If no lists or the worker is already running then skip it
248  if (!job.getKeywordListNames().isEmpty() && !job.isWorkerRunning()) {
249  Searcher searcher = new Searcher(job);
250  job.setCurrentSearcher(searcher); //save the ref
251  searcher.execute(); //start thread
252  job.setWorkerRunning(true);
253  }
254  }
255  }
256  }
257  }
258 
263  private class SearchJobInfo {
264 
265  private final long jobId;
266  private final long dataSourceId;
267  // mutable state:
268  private volatile boolean workerRunning;
269  private List<String> keywordListNames; //guarded by SearchJobInfo.this
270 
271  // Map of keyword to the object ids that contain a hit
272  private Map<Keyword, Set<Long>> currentResults; //guarded by SearchJobInfo.this
274  private AtomicLong moduleReferenceCount = new AtomicLong(0);
275  private final Object finalSearchLock = new Object(); //used for a condition wait
276 
277  private SearchJobInfo(long jobId, long dataSourceId, List<String> keywordListNames) {
278  this.jobId = jobId;
279  this.dataSourceId = dataSourceId;
280  this.keywordListNames = new ArrayList<>(keywordListNames);
281  currentResults = new HashMap<>();
282  workerRunning = false;
283  currentSearcher = null;
284  }
285 
286  private long getJobId() {
287  return jobId;
288  }
289 
290  private long getDataSourceId() {
291  return dataSourceId;
292  }
293 
294  private synchronized List<String> getKeywordListNames() {
295  return new ArrayList<>(keywordListNames);
296  }
297 
298  private synchronized void addKeywordListName(String keywordListName) {
299  if (!keywordListNames.contains(keywordListName)) {
300  keywordListNames.add(keywordListName);
301  }
302  }
303 
304  private synchronized Set<Long> currentKeywordResults(Keyword k) {
305  return currentResults.get(k);
306  }
307 
308  private synchronized void addKeywordResults(Keyword k, Set<Long> resultsIDs) {
309  currentResults.put(k, resultsIDs);
310  }
311 
312  private boolean isWorkerRunning() {
313  return workerRunning;
314  }
315 
316  private void setWorkerRunning(boolean flag) {
317  workerRunning = flag;
318  }
319 
320  private synchronized SearchRunner.Searcher getCurrentSearcher() {
321  return currentSearcher;
322  }
323 
324  private synchronized void setCurrentSearcher(SearchRunner.Searcher searchRunner) {
325  currentSearcher = searchRunner;
326  }
327 
329  moduleReferenceCount.incrementAndGet();
330  }
331 
333  return moduleReferenceCount.decrementAndGet();
334  }
335 
341  private void waitForCurrentWorker() throws InterruptedException {
342  synchronized (finalSearchLock) {
343  while (workerRunning) {
344  finalSearchLock.wait(); //wait() releases the lock
345  }
346  }
347  }
348 
352  private void searchNotify() {
353  synchronized (finalSearchLock) {
354  workerRunning = false;
355  finalSearchLock.notify();
356  }
357  }
358  }
359 
366  private final class Searcher extends SwingWorker<Object, Void> {
367 
372  private List<Keyword> keywords; //keywords to search
373  private List<String> keywordListNames; // lists currently being searched
374  private List<KeywordList> keywordLists;
375  private Map<Keyword, KeywordList> keywordToList; //keyword to list name mapping
376  private AggregateProgressHandle progressGroup;
377  private final Logger logger = Logger.getLogger(SearchRunner.Searcher.class.getName());
378  private boolean finalRun = false;
379 
380  Searcher(SearchJobInfo job) {
381  this.job = job;
382  keywordListNames = job.getKeywordListNames();
383  keywords = new ArrayList<>();
384  keywordToList = new HashMap<>();
385  keywordLists = new ArrayList<>();
386  //keywords are populated as searcher runs
387  }
388 
389  Searcher(SearchJobInfo job, boolean finalRun) {
390  this(job);
391  this.finalRun = finalRun;
392  }
393 
394  @Override
395  @Messages("SearchRunner.query.exception.msg=Error performing query:")
396  protected Object doInBackground() throws Exception {
397  final String displayName = NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.doInBackGround.displayName")
398  + (finalRun ? (" - " + NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.doInBackGround.finalizeMsg")) : "");
399  final String pgDisplayName = displayName + (" (" + NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.doInBackGround.pendingMsg") + ")");
400  progressGroup = AggregateProgressFactory.createSystemHandle(pgDisplayName, null, new Cancellable() {
401  @Override
402  public boolean cancel() {
403  logger.log(Level.INFO, "Cancelling the searcher by user."); //NON-NLS
404  if (progressGroup != null) {
405  progressGroup.setDisplayName(displayName + " " + NbBundle.getMessage(this.getClass(), "SearchRunner.doInBackGround.cancelMsg"));
406  }
407  return SearchRunner.Searcher.this.cancel(true);
408  }
409  }, null);
410 
411  updateKeywords();
412 
413  ProgressContributor[] subProgresses = new ProgressContributor[keywords.size()];
414  int i = 0;
415  for (Keyword keywordQuery : keywords) {
416  subProgresses[i] = AggregateProgressFactory.createProgressContributor(keywordQuery.getSearchTerm());
417  progressGroup.addContributor(subProgresses[i]);
418  i++;
419  }
420 
421  progressGroup.start();
422 
423  final StopWatch stopWatch = new StopWatch();
424  stopWatch.start();
425  try {
426  progressGroup.setDisplayName(displayName);
427 
428  int keywordsSearched = 0;
429 
430  for (Keyword keyword : keywords) {
431  if (this.isCancelled()) {
432  logger.log(Level.INFO, "Cancel detected, bailing before new keyword processed: {0}", keyword.getSearchTerm()); //NON-NLS
433  return null;
434  }
435 
436  final KeywordList keywordList = keywordToList.get(keyword);
437 
438  //new subProgress will be active after the initial query
439  //when we know number of hits to start() with
440  if (keywordsSearched > 0) {
441  subProgresses[keywordsSearched - 1].finish();
442  }
443 
444  KeywordSearchQuery keywordSearchQuery = KeywordSearchUtil.getQueryForKeyword(keyword, keywordList);
445 
446  // Filtering
447  //limit search to currently ingested data sources
448  //set up a filter with 1 or more image ids OR'ed
449  final KeywordQueryFilter dataSourceFilter = new KeywordQueryFilter(KeywordQueryFilter.FilterType.DATA_SOURCE, job.getDataSourceId());
450  keywordSearchQuery.addFilter(dataSourceFilter);
451 
452  QueryResults queryResults;
453 
454  // Do the actual search
455  try {
456  queryResults = keywordSearchQuery.performQuery();
458  logger.log(Level.SEVERE, "Error performing query: " + keyword.getSearchTerm(), ex); //NON-NLS
459  MessageNotifyUtil.Notify.error(Bundle.SearchRunner_query_exception_msg() + keyword.getSearchTerm(), ex.getCause().getMessage());
460  //no reason to continue with next query if recovery failed
461  //or wait for recovery to kick in and run again later
462  //likely case has closed and threads are being interrupted
463  return null;
464  } catch (CancellationException e) {
465  logger.log(Level.INFO, "Cancel detected, bailing during keyword query: {0}", keyword.getSearchTerm()); //NON-NLS
466  return null;
467  }
468 
469  // Reduce the results of the query to only those hits we
470  // have not already seen.
471  QueryResults newResults = filterResults(queryResults);
472 
473  if (!newResults.getKeywords().isEmpty()) {
474 
475  // Write results to BB
476  //new artifacts created, to report to listeners
477  Collection<BlackboardArtifact> newArtifacts = new ArrayList<>();
478 
479  //scale progress bar more more granular, per result sub-progress, within per keyword
480  int totalUnits = newResults.getKeywords().size();
481  subProgresses[keywordsSearched].start(totalUnits);
482  int unitProgress = 0;
483  String queryDisplayStr = keyword.getSearchTerm();
484  if (queryDisplayStr.length() > 50) {
485  queryDisplayStr = queryDisplayStr.substring(0, 49) + "...";
486  }
487  subProgresses[keywordsSearched].progress(keywordList.getName() + ": " + queryDisplayStr, unitProgress);
488 
489  // Create blackboard artifacts
490  newArtifacts = newResults.writeAllHitsToBlackBoard(null, subProgresses[keywordsSearched], this, keywordList.getIngestMessages());
491 
492  } //if has results
493 
494  //reset the status text before it goes away
495  subProgresses[keywordsSearched].progress("");
496 
497  ++keywordsSearched;
498 
499  } //for each keyword
500 
501  } //end try block
502  catch (Exception ex) {
503  logger.log(Level.WARNING, "searcher exception occurred", ex); //NON-NLS
504  } finally {
505  try {
507  stopWatch.stop();
508 
509  logger.log(Level.INFO, "Searcher took to run: {0} secs.", stopWatch.getElapsedTimeSecs()); //NON-NLS
510  } finally {
511  // In case a thread is waiting on this worker to be done
512  job.searchNotify();
513  }
514  }
515 
516  return null;
517  }
518 
519  @Override
520  protected void done() {
521  // call get to see if there were any errors
522  try {
523  get();
524  } catch (InterruptedException | ExecutionException e) {
525  logger.log(Level.SEVERE, "Error performing keyword search: " + e.getMessage()); //NON-NLS
527  NbBundle.getMessage(this.getClass(),
528  "SearchRunner.Searcher.done.err.msg"), e.getMessage()));
529  } // catch and ignore if we were cancelled
530  catch (java.util.concurrent.CancellationException ex) {
531  }
532  }
533 
537  private void updateKeywords() {
538  XmlKeywordSearchList loader = XmlKeywordSearchList.getCurrent();
539 
540  keywords.clear();
541  keywordToList.clear();
542  keywordLists.clear();
543 
544  for (String name : keywordListNames) {
545  KeywordList list = loader.getList(name);
546  keywordLists.add(list);
547  for (Keyword k : list.getKeywords()) {
548  keywords.add(k);
549  keywordToList.put(k, list);
550  }
551  }
552  }
553 
559  private void finalizeSearcher() {
560  SwingUtilities.invokeLater(new Runnable() {
561  @Override
562  public void run() {
563  progressGroup.finish();
564  }
565  });
566  }
567 
582  private QueryResults filterResults(QueryResults queryResult) {
583 
584  // Create a new (empty) QueryResults object to hold the most recently
585  // found hits.
586  QueryResults newResults = new QueryResults(queryResult.getQuery());
587 
588  // For each keyword represented in the results.
589  for (Keyword keyword : queryResult.getKeywords()) {
590  // These are all of the hits across all objects for the most recent search.
591  // This may well include duplicates of hits we've seen in earlier periodic searches.
592  List<KeywordHit> queryTermResults = queryResult.getResults(keyword);
593 
594  // Sort the hits for this keyword so that we are always
595  // guaranteed to return the hit for the lowest chunk.
596  Collections.sort(queryTermResults);
597 
598  // This will be used to build up the hits we haven't seen before
599  // for this keyword.
600  List<KeywordHit> newUniqueHits = new ArrayList<>();
601 
602  // Get the set of object ids seen in the past by this searcher
603  // for the given keyword.
604  Set<Long> curTermResults = job.currentKeywordResults(keyword);
605  if (curTermResults == null) {
606  // We create a new empty set if we haven't seen results for
607  // this keyword before.
608  curTermResults = new HashSet<>();
609  }
610 
611  // For each hit for this keyword.
612  for (KeywordHit hit : queryTermResults) {
613  if (curTermResults.contains(hit.getSolrObjectId())) {
614  // Skip the hit if we've already seen a hit for
615  // this keyword in the object.
616  continue;
617  }
618 
619  // We haven't seen the hit before so add it to list of new
620  // unique hits.
621  newUniqueHits.add(hit);
622 
623  // Add the object id to the results we've seen for this
624  // keyword.
625  curTermResults.add(hit.getSolrObjectId());
626  }
627 
628  // Update the job with the list of objects for which we have
629  // seen hits for the current keyword.
630  job.addKeywordResults(keyword, curTermResults);
631 
632  // Add the new hits for the current keyword into the results
633  // to be returned.
634  newResults.addResult(keyword, newUniqueHits);
635  }
636 
637  return newResults;
638  }
639  }
640 }
SearchJobInfo(long jobId, long dataSourceId, List< String > keywordListNames)
static IngestMessage createErrorMessage(String source, String subject, String detailsHtml)
static void fireNumIndexedFilesChange(Integer oldNum, Integer newNum)
synchronized void addKeywordListName(String keywordListName)
synchronized void startJob(long jobId, long dataSourceId, List< String > keywordListNames)
synchronized void setCurrentSearcher(SearchRunner.Searcher searchRunner)
static synchronized SearchRunner getInstance()
synchronized void addKeywordListsToAllJobs(List< String > keywordListNames)
void postMessage(final IngestMessage message)
static void error(String title, String message)
synchronized static Logger getLogger(String name)
Definition: Logger.java:161
QueryResults filterResults(QueryResults queryResult)
synchronized void addKeywordResults(Keyword k, Set< Long > resultsIDs)
static synchronized IngestServices getInstance()

Copyright © 2012-2016 Basis Technology. Generated on: Tue Jun 13 2017
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.