Autopsy  4.4.1
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.Collections;
23 import java.util.HashMap;
24 import java.util.HashSet;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Map.Entry;
28 import java.util.Set;
29 import java.util.Timer;
30 import java.util.TimerTask;
31 import java.util.concurrent.CancellationException;
32 import java.util.concurrent.ExecutionException;
33 import java.util.concurrent.atomic.AtomicLong;
34 import java.util.logging.Level;
35 import javax.swing.SwingUtilities;
36 import javax.swing.SwingWorker;
37 import org.netbeans.api.progress.aggregate.AggregateProgressFactory;
38 import org.netbeans.api.progress.aggregate.AggregateProgressHandle;
39 import org.netbeans.api.progress.aggregate.ProgressContributor;
40 import org.openide.util.Cancellable;
41 import org.openide.util.NbBundle;
42 import org.openide.util.NbBundle.Messages;
48 
53 public final class SearchRunner {
54 
55  private static final Logger logger = Logger.getLogger(SearchRunner.class.getName());
56  private static SearchRunner instance = null;
58  private Ingester ingester = null;
59  private volatile boolean updateTimerRunning = false;
60  private Timer updateTimer;
61 
62  // maps a jobID to the search
63  private Map<Long, SearchJobInfo> jobs = new HashMap<>(); //guarded by "this"
64 
65  SearchRunner() {
66  ingester = Ingester.getDefault();
67  updateTimer = new Timer(NbBundle.getMessage(this.getClass(), "SearchRunner.updateTimer.title.text"), true); // run as a daemon
68  }
69 
74  public static synchronized SearchRunner getInstance() {
75  if (instance == null) {
76  instance = new SearchRunner();
77  }
78  return instance;
79  }
80 
91  public synchronized void startJob(long jobId, long dataSourceId, List<String> keywordListNames) {
92  if (jobs.containsKey(jobId) == false) {
93  logger.log(Level.INFO, "Adding job {0}", jobId); //NON-NLS
94  SearchJobInfo jobData = new SearchJobInfo(jobId, dataSourceId, keywordListNames);
95  jobs.put(jobId, jobData);
96  }
97 
98  // keep track of how many threads / module instances from this job have asked for this
99  jobs.get(jobId).incrementModuleReferenceCount();
100 
101  // start the timer, if needed
102  if ((jobs.size() > 0) && (updateTimerRunning == false)) {
103  final long updateIntervalMs = ((long) KeywordSearchSettings.getUpdateFrequency().getTime()) * 60 * 1000;
104  updateTimer.scheduleAtFixedRate(new UpdateTimerTask(), updateIntervalMs, updateIntervalMs);
105  updateTimerRunning = true;
106  }
107  }
108 
115  public void endJob(long jobId) {
116  SearchJobInfo job;
117  boolean readyForFinalSearch = false;
118  synchronized (this) {
119  job = jobs.get(jobId);
120  if (job == null) {
121  return;
122  }
123 
124  // Only do final search if this is the last module/thread in this job to call endJob()
125  if (job.decrementModuleReferenceCount() == 0) {
126  jobs.remove(jobId);
127  readyForFinalSearch = true;
128  }
129  }
130 
131  if (readyForFinalSearch) {
132  logger.log(Level.INFO, "Commiting search index before final search for search job {0}", job.getJobId()); //NON-NLS
133  commit();
134  doFinalSearch(job); //this will block until it's done
135  }
136  }
137 
144  public void stopJob(long jobId) {
145  logger.log(Level.INFO, "Stopping job {0}", jobId); //NON-NLS
146  commit();
147 
148  SearchJobInfo job;
149  synchronized (this) {
150  job = jobs.get(jobId);
151  if (job == null) {
152  return;
153  }
154 
155  //stop currentSearcher
156  SearchRunner.Searcher currentSearcher = job.getCurrentSearcher();
157  if ((currentSearcher != null) && (!currentSearcher.isDone())) {
158  currentSearcher.cancel(true);
159  }
160 
161  jobs.remove(jobId);
162  }
163  }
164 
171  public synchronized void addKeywordListsToAllJobs(List<String> keywordListNames) {
172  for (String listName : keywordListNames) {
173  logger.log(Level.INFO, "Adding keyword list {0} to all jobs", listName); //NON-NLS
174  for (SearchJobInfo j : jobs.values()) {
175  j.addKeywordListName(listName);
176  }
177  }
178  }
179 
183  private void commit() {
184  ingester.commit();
185 
186  // Signal a potential change in number of text_ingested files
187  try {
188  final int numIndexedFiles = KeywordSearch.getServer().queryNumIndexedFiles();
189  KeywordSearch.fireNumIndexedFilesChange(null, numIndexedFiles);
191  logger.log(Level.SEVERE, "Error executing Solr query to check number of indexed files", ex); //NON-NLS
192  }
193  }
194 
201  private void doFinalSearch(SearchJobInfo job) {
202  // Run one last search as there are probably some new files committed
203  logger.log(Level.INFO, "Starting final search for search job {0}", job.getJobId()); //NON-NLS
204  if (!job.getKeywordListNames().isEmpty()) {
205  try {
206  // In case this job still has a worker running, wait for it to finish
207  logger.log(Level.INFO, "Checking for previous search for search job {0} before executing final search", job.getJobId()); //NON-NLS
208  job.waitForCurrentWorker();
209 
210  SearchRunner.Searcher finalSearcher = new SearchRunner.Searcher(job, true);
211  job.setCurrentSearcher(finalSearcher); //save the ref
212  logger.log(Level.INFO, "Kicking off final search for search job {0}", job.getJobId()); //NON-NLS
213  finalSearcher.execute(); //start thread
214 
215  // block until the search is complete
216  logger.log(Level.INFO, "Waiting for final search for search job {0}", job.getJobId()); //NON-NLS
217  finalSearcher.get();
218  logger.log(Level.INFO, "Final search for search job {0} completed", job.getJobId()); //NON-NLS
219 
220  } catch (InterruptedException | CancellationException ex) {
221  logger.log(Level.INFO, "Final search for search job {0} interrupted or cancelled", job.getJobId()); //NON-NLS
222  } catch (ExecutionException ex) {
223  logger.log(Level.SEVERE, String.format("Final search for search job %d failed", job.getJobId()), ex); //NON-NLS
224  }
225  }
226  }
227 
231  private class UpdateTimerTask extends TimerTask {
232 
233  private final Logger logger = Logger.getLogger(SearchRunner.UpdateTimerTask.class.getName());
234 
235  @Override
236  public void run() {
237  // If no jobs then cancel the task. If more job(s) come along, a new task will start up.
238  if (jobs.isEmpty()) {
239  this.cancel(); //terminate this timer task
240  updateTimerRunning = false;
241  return;
242  }
243 
244  commit();
245 
246  synchronized (SearchRunner.this) {
247  // Spawn a search thread for each job
248  for (Entry<Long, SearchJobInfo> j : jobs.entrySet()) {
249  SearchJobInfo job = j.getValue();
250  // If no lists or the worker is already running then skip it
251  if (!job.getKeywordListNames().isEmpty() && !job.isWorkerRunning()) {
252  logger.log(Level.INFO, "Executing periodic search for search job {0}", job.getJobId());
253  Searcher searcher = new Searcher(job);
254  job.setCurrentSearcher(searcher); //save the ref
255  searcher.execute(); //start thread
256  job.setWorkerRunning(true);
257  }
258  }
259  }
260  }
261  }
262 
267  private class SearchJobInfo {
268 
269  private final long jobId;
270  private final long dataSourceId;
271  // mutable state:
272  private volatile boolean workerRunning;
273  private List<String> keywordListNames; //guarded by SearchJobInfo.this
274 
275  // Map of keyword to the object ids that contain a hit
276  private Map<Keyword, Set<Long>> currentResults; //guarded by SearchJobInfo.this
278  private AtomicLong moduleReferenceCount = new AtomicLong(0);
279  private final Object finalSearchLock = new Object(); //used for a condition wait
280 
281  private SearchJobInfo(long jobId, long dataSourceId, List<String> keywordListNames) {
282  this.jobId = jobId;
283  this.dataSourceId = dataSourceId;
284  this.keywordListNames = new ArrayList<>(keywordListNames);
285  currentResults = new HashMap<>();
286  workerRunning = false;
287  currentSearcher = null;
288  }
289 
290  private long getJobId() {
291  return jobId;
292  }
293 
294  private long getDataSourceId() {
295  return dataSourceId;
296  }
297 
298  private synchronized List<String> getKeywordListNames() {
299  return new ArrayList<>(keywordListNames);
300  }
301 
302  private synchronized void addKeywordListName(String keywordListName) {
303  if (!keywordListNames.contains(keywordListName)) {
304  keywordListNames.add(keywordListName);
305  }
306  }
307 
308  private synchronized Set<Long> currentKeywordResults(Keyword k) {
309  return currentResults.get(k);
310  }
311 
312  private synchronized void addKeywordResults(Keyword k, Set<Long> resultsIDs) {
313  currentResults.put(k, resultsIDs);
314  }
315 
316  private boolean isWorkerRunning() {
317  return workerRunning;
318  }
319 
320  private void setWorkerRunning(boolean flag) {
321  workerRunning = flag;
322  }
323 
324  private synchronized SearchRunner.Searcher getCurrentSearcher() {
325  return currentSearcher;
326  }
327 
328  private synchronized void setCurrentSearcher(SearchRunner.Searcher searchRunner) {
329  currentSearcher = searchRunner;
330  }
331 
333  moduleReferenceCount.incrementAndGet();
334  }
335 
337  return moduleReferenceCount.decrementAndGet();
338  }
339 
345  private void waitForCurrentWorker() throws InterruptedException {
346  synchronized (finalSearchLock) {
347  while (workerRunning) {
348  logger.log(Level.INFO, "Waiting for previous worker to finish"); //NON-NLS
349  finalSearchLock.wait(); //wait() releases the lock
350  logger.log(Level.INFO, "Notified previous worker finished"); //NON-NLS
351  }
352  }
353  }
354 
358  private void searchNotify() {
359  synchronized (finalSearchLock) {
360  logger.log(Level.INFO, "Notifying after finishing search"); //NON-NLS
361  workerRunning = false;
362  finalSearchLock.notify();
363  }
364  }
365  }
366 
373  private final class Searcher extends SwingWorker<Object, Void> {
374 
379  private List<Keyword> keywords; //keywords to search
380  private List<String> keywordListNames; // lists currently being searched
381  private List<KeywordList> keywordLists;
382  private Map<Keyword, KeywordList> keywordToList; //keyword to list name mapping
383  private AggregateProgressHandle progressGroup;
384  private final Logger logger = Logger.getLogger(SearchRunner.Searcher.class.getName());
385  private boolean finalRun = false;
386 
387  Searcher(SearchJobInfo job) {
388  this.job = job;
389  keywordListNames = job.getKeywordListNames();
390  keywords = new ArrayList<>();
391  keywordToList = new HashMap<>();
392  keywordLists = new ArrayList<>();
393  //keywords are populated as searcher runs
394  }
395 
396  Searcher(SearchJobInfo job, boolean finalRun) {
397  this(job);
398  this.finalRun = finalRun;
399  }
400 
401  @Override
402  @Messages("SearchRunner.query.exception.msg=Error performing query:")
403  protected Object doInBackground() throws Exception {
404  final String displayName = NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.doInBackGround.displayName")
405  + (finalRun ? (" - " + NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.doInBackGround.finalizeMsg")) : "");
406  final String pgDisplayName = displayName + (" (" + NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.doInBackGround.pendingMsg") + ")");
407  progressGroup = AggregateProgressFactory.createSystemHandle(pgDisplayName, null, new Cancellable() {
408  @Override
409  public boolean cancel() {
410  logger.log(Level.INFO, "Cancelling the searcher by user."); //NON-NLS
411  if (progressGroup != null) {
412  progressGroup.setDisplayName(displayName + " " + NbBundle.getMessage(this.getClass(), "SearchRunner.doInBackGround.cancelMsg"));
413  }
414  return SearchRunner.Searcher.this.cancel(true);
415  }
416  }, null);
417 
418  updateKeywords();
419 
420  ProgressContributor[] subProgresses = new ProgressContributor[keywords.size()];
421  int i = 0;
422  for (Keyword keywordQuery : keywords) {
423  subProgresses[i] = AggregateProgressFactory.createProgressContributor(keywordQuery.getSearchTerm());
424  progressGroup.addContributor(subProgresses[i]);
425  i++;
426  }
427 
428  progressGroup.start();
429 
430  final StopWatch stopWatch = new StopWatch();
431  stopWatch.start();
432  try {
433  progressGroup.setDisplayName(displayName);
434 
435  int keywordsSearched = 0;
436 
437  for (Keyword keyword : keywords) {
438  if (this.isCancelled()) {
439  logger.log(Level.INFO, "Cancel detected, bailing before new keyword processed: {0}", keyword.getSearchTerm()); //NON-NLS
440  return null;
441  }
442 
443  final KeywordList keywordList = keywordToList.get(keyword);
444 
445  //new subProgress will be active after the initial query
446  //when we know number of hits to start() with
447  if (keywordsSearched > 0) {
448  subProgresses[keywordsSearched - 1].finish();
449  }
450 
451  KeywordSearchQuery keywordSearchQuery = KeywordSearchUtil.getQueryForKeyword(keyword, keywordList);
452 
453  // Filtering
454  //limit search to currently ingested data sources
455  //set up a filter with 1 or more image ids OR'ed
456  final KeywordQueryFilter dataSourceFilter = new KeywordQueryFilter(KeywordQueryFilter.FilterType.DATA_SOURCE, job.getDataSourceId());
457  keywordSearchQuery.addFilter(dataSourceFilter);
458 
459  QueryResults queryResults;
460 
461  // Do the actual search
462  try {
463  queryResults = keywordSearchQuery.performQuery();
465  logger.log(Level.SEVERE, "Error performing query: " + keyword.getSearchTerm(), ex); //NON-NLS
466  MessageNotifyUtil.Notify.error(Bundle.SearchRunner_query_exception_msg() + keyword.getSearchTerm(), ex.getCause().getMessage());
467  //no reason to continue with next query if recovery failed
468  //or wait for recovery to kick in and run again later
469  //likely case has closed and threads are being interrupted
470  return null;
471  } catch (CancellationException e) {
472  logger.log(Level.INFO, "Cancel detected, bailing during keyword query: {0}", keyword.getSearchTerm()); //NON-NLS
473  return null;
474  }
475 
476  // Reduce the results of the query to only those hits we
477  // have not already seen.
478  QueryResults newResults = filterResults(queryResults);
479 
480  if (!newResults.getKeywords().isEmpty()) {
481 
482  // Write results to BB
483 
484  //scale progress bar more more granular, per result sub-progress, within per keyword
485  int totalUnits = newResults.getKeywords().size();
486  subProgresses[keywordsSearched].start(totalUnits);
487  int unitProgress = 0;
488  String queryDisplayStr = keyword.getSearchTerm();
489  if (queryDisplayStr.length() > 50) {
490  queryDisplayStr = queryDisplayStr.substring(0, 49) + "...";
491  }
492  subProgresses[keywordsSearched].progress(keywordList.getName() + ": " + queryDisplayStr, unitProgress);
493 
494  // Create blackboard artifacts
495  newResults.writeAllHitsToBlackBoard(null, subProgresses[keywordsSearched], this, keywordList.getIngestMessages());
496 
497  } //if has results
498 
499  //reset the status text before it goes away
500  subProgresses[keywordsSearched].progress("");
501 
502  ++keywordsSearched;
503 
504  } //for each keyword
505 
506  } //end try block
507  catch (Exception ex) {
508  logger.log(Level.WARNING, "searcher exception occurred", ex); //NON-NLS
509  } finally {
510  try {
512  stopWatch.stop();
513  logger.log(Level.INFO, "Searcher took {0} secs to run (final = {1})", new Object[]{stopWatch.getElapsedTimeSecs(), this.finalRun}); //NON-NLS
514  } finally {
515  // In case a thread is waiting on this worker to be done
516  job.searchNotify();
517  }
518  }
519 
520  return null;
521  }
522 
523  @Override
524  protected void done() {
525  // call get to see if there were any errors
526  try {
527  logger.log(Level.INFO, "Searcher calling get() on itself in done()"); //NON-NLS
528  get();
529  logger.log(Level.INFO, "Searcher finished calling get() on itself in done()"); //NON-NLS
530  } catch (InterruptedException | ExecutionException e) {
531  logger.log(Level.SEVERE, "Error performing keyword search: " + e.getMessage()); //NON-NLS
533  NbBundle.getMessage(this.getClass(),
534  "SearchRunner.Searcher.done.err.msg"), e.getMessage()));
535  } // catch and ignore if we were cancelled
536  catch (java.util.concurrent.CancellationException ex) {
537  }
538  }
539 
543  private void updateKeywords() {
544  XmlKeywordSearchList loader = XmlKeywordSearchList.getCurrent();
545 
546  keywords.clear();
547  keywordToList.clear();
548  keywordLists.clear();
549 
550  for (String name : keywordListNames) {
551  KeywordList list = loader.getList(name);
552  keywordLists.add(list);
553  for (Keyword k : list.getKeywords()) {
554  keywords.add(k);
555  keywordToList.put(k, list);
556  }
557  }
558  }
559 
565  private void finalizeSearcher() {
566  SwingUtilities.invokeLater(new Runnable() {
567  @Override
568  public void run() {
569  progressGroup.finish();
570  }
571  });
572  }
573 
589  private QueryResults filterResults(QueryResults queryResult) {
590 
591  // Create a new (empty) QueryResults object to hold the most recently
592  // found hits.
593  QueryResults newResults = new QueryResults(queryResult.getQuery());
594 
595  // For each keyword represented in the results.
596  for (Keyword keyword : queryResult.getKeywords()) {
597  // These are all of the hits across all objects for the most recent search.
598  // This may well include duplicates of hits we've seen in earlier periodic searches.
599  List<KeywordHit> queryTermResults = queryResult.getResults(keyword);
600 
601  // Sort the hits for this keyword so that we are always
602  // guaranteed to return the hit for the lowest chunk.
603  Collections.sort(queryTermResults);
604 
605  // This will be used to build up the hits we haven't seen before
606  // for this keyword.
607  List<KeywordHit> newUniqueHits = new ArrayList<>();
608 
609  // Get the set of object ids seen in the past by this searcher
610  // for the given keyword.
611  Set<Long> curTermResults = job.currentKeywordResults(keyword);
612  if (curTermResults == null) {
613  // We create a new empty set if we haven't seen results for
614  // this keyword before.
615  curTermResults = new HashSet<>();
616  }
617 
618  // For each hit for this keyword.
619  for (KeywordHit hit : queryTermResults) {
620  if (curTermResults.contains(hit.getSolrObjectId())) {
621  // Skip the hit if we've already seen a hit for
622  // this keyword in the object.
623  continue;
624  }
625 
626  // We haven't seen the hit before so add it to list of new
627  // unique hits.
628  newUniqueHits.add(hit);
629 
630  // Add the object id to the results we've seen for this
631  // keyword.
632  curTermResults.add(hit.getSolrObjectId());
633  }
634 
635  // Update the job with the list of objects for which we have
636  // seen hits for the current keyword.
637  job.addKeywordResults(keyword, curTermResults);
638 
639  // Add the new hits for the current keyword into the results
640  // to be returned.
641  newResults.addResult(keyword, newUniqueHits);
642  }
643 
644  return newResults;
645  }
646  }
647 }
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: Fri Sep 29 2017
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.