Autopsy  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 - 2014 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;
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 | ExecutionException ex) {
218  logger.log(Level.WARNING, "Job {1} final search thread failed: {2}", new Object[]{job.getJobId(), ex}); //NON-NLS
219  }
220  }
221  }
222 
226  private class UpdateTimerTask extends TimerTask {
227 
228  private final Logger logger = Logger.getLogger(SearchRunner.UpdateTimerTask.class.getName());
229 
230  @Override
231  public void run() {
232  // If no jobs then cancel the task. If more job(s) come along, a new task will start up.
233  if (jobs.isEmpty()) {
234  this.cancel(); //terminate this timer task
235  updateTimerRunning = false;
236  return;
237  }
238 
239  commit();
240 
241  synchronized (SearchRunner.this) {
242  // Spawn a search thread for each job
243  for (Entry<Long, SearchJobInfo> j : jobs.entrySet()) {
244  SearchJobInfo job = j.getValue();
245  // If no lists or the worker is already running then skip it
246  if (!job.getKeywordListNames().isEmpty() && !job.isWorkerRunning()) {
247  Searcher searcher = new Searcher(job);
248  job.setCurrentSearcher(searcher); //save the ref
249  searcher.execute(); //start thread
250  job.setWorkerRunning(true);
251  }
252  }
253  }
254  }
255  }
256 
261  private class SearchJobInfo {
262 
263  private final long jobId;
264  private final long dataSourceId;
265  // mutable state:
266  private volatile boolean workerRunning;
267  private List<String> keywordListNames; //guarded by SearchJobInfo.this
268 
269  // Map of keyword to the object ids that contain a hit
270  private Map<Keyword, Set<Long>> currentResults; //guarded by SearchJobInfo.this
272  private AtomicLong moduleReferenceCount = new AtomicLong(0);
273  private final Object finalSearchLock = new Object(); //used for a condition wait
274 
275  private SearchJobInfo(long jobId, long dataSourceId, List<String> keywordListNames) {
276  this.jobId = jobId;
277  this.dataSourceId = dataSourceId;
278  this.keywordListNames = new ArrayList<>(keywordListNames);
279  currentResults = new HashMap<>();
280  workerRunning = false;
281  currentSearcher = null;
282  }
283 
284  private long getJobId() {
285  return jobId;
286  }
287 
288  private long getDataSourceId() {
289  return dataSourceId;
290  }
291 
292  private synchronized List<String> getKeywordListNames() {
293  return new ArrayList<>(keywordListNames);
294  }
295 
296  private synchronized void addKeywordListName(String keywordListName) {
297  if (!keywordListNames.contains(keywordListName)) {
298  keywordListNames.add(keywordListName);
299  }
300  }
301 
302  private synchronized Set<Long> currentKeywordResults(Keyword k) {
303  return currentResults.get(k);
304  }
305 
306  private synchronized void addKeywordResults(Keyword k, Set<Long> resultsIDs) {
307  currentResults.put(k, resultsIDs);
308  }
309 
310  private boolean isWorkerRunning() {
311  return workerRunning;
312  }
313 
314  private void setWorkerRunning(boolean flag) {
315  workerRunning = flag;
316  }
317 
318  private synchronized SearchRunner.Searcher getCurrentSearcher() {
319  return currentSearcher;
320  }
321 
322  private synchronized void setCurrentSearcher(SearchRunner.Searcher searchRunner) {
323  currentSearcher = searchRunner;
324  }
325 
327  moduleReferenceCount.incrementAndGet();
328  }
329 
331  return moduleReferenceCount.decrementAndGet();
332  }
333 
339  private void waitForCurrentWorker() throws InterruptedException {
340  synchronized (finalSearchLock) {
341  while (workerRunning) {
342  finalSearchLock.wait(); //wait() releases the lock
343  }
344  }
345  }
346 
350  private void searchNotify() {
351  synchronized (finalSearchLock) {
352  workerRunning = false;
353  finalSearchLock.notify();
354  }
355  }
356  }
357 
364  private final class Searcher extends SwingWorker<Object, Void> {
365 
370  private List<Keyword> keywords; //keywords to search
371  private List<String> keywordListNames; // lists currently being searched
372  private List<KeywordList> keywordLists;
373  private Map<Keyword, KeywordList> keywordToList; //keyword to list name mapping
374  private AggregateProgressHandle progressGroup;
375  private final Logger logger = Logger.getLogger(SearchRunner.Searcher.class.getName());
376  private boolean finalRun = false;
377 
378  Searcher(SearchJobInfo job) {
379  this.job = job;
380  keywordListNames = job.getKeywordListNames();
381  keywords = new ArrayList<>();
382  keywordToList = new HashMap<>();
383  keywordLists = new ArrayList<>();
384  //keywords are populated as searcher runs
385  }
386 
387  Searcher(SearchJobInfo job, boolean finalRun) {
388  this(job);
389  this.finalRun = finalRun;
390  }
391 
392  @Override
393  @Messages("SearchRunner.query.exception.msg=Error performing query:")
394  protected Object doInBackground() throws Exception {
395  final String displayName = NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.doInBackGround.displayName")
396  + (finalRun ? (" - " + NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.doInBackGround.finalizeMsg")) : "");
397  final String pgDisplayName = displayName + (" (" + NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.doInBackGround.pendingMsg") + ")");
398  progressGroup = AggregateProgressFactory.createSystemHandle(pgDisplayName, null, new Cancellable() {
399  @Override
400  public boolean cancel() {
401  logger.log(Level.INFO, "Cancelling the searcher by user."); //NON-NLS
402  if (progressGroup != null) {
403  progressGroup.setDisplayName(displayName + " " + NbBundle.getMessage(this.getClass(), "SearchRunner.doInBackGround.cancelMsg"));
404  }
405  return SearchRunner.Searcher.this.cancel(true);
406  }
407  }, null);
408 
409  updateKeywords();
410 
411  ProgressContributor[] subProgresses = new ProgressContributor[keywords.size()];
412  int i = 0;
413  for (Keyword keywordQuery : keywords) {
414  subProgresses[i] = AggregateProgressFactory.createProgressContributor(keywordQuery.getSearchTerm());
415  progressGroup.addContributor(subProgresses[i]);
416  i++;
417  }
418 
419  progressGroup.start();
420 
421  final StopWatch stopWatch = new StopWatch();
422  stopWatch.start();
423  try {
424  progressGroup.setDisplayName(displayName);
425 
426  int keywordsSearched = 0;
427 
428  for (Keyword keyword : keywords) {
429  if (this.isCancelled()) {
430  logger.log(Level.INFO, "Cancel detected, bailing before new keyword processed: {0}", keyword.getSearchTerm()); //NON-NLS
431  return null;
432  }
433 
434  final KeywordList keywordList = keywordToList.get(keyword);
435 
436  //new subProgress will be active after the initial query
437  //when we know number of hits to start() with
438  if (keywordsSearched > 0) {
439  subProgresses[keywordsSearched - 1].finish();
440  }
441 
442  KeywordSearchQuery keywordSearchQuery = KeywordSearchUtil.getQueryForKeyword(keyword, keywordList);
443 
444  // Filtering
445  //limit search to currently ingested data sources
446  //set up a filter with 1 or more image ids OR'ed
447  final KeywordQueryFilter dataSourceFilter = new KeywordQueryFilter(KeywordQueryFilter.FilterType.DATA_SOURCE, job.getDataSourceId());
448  keywordSearchQuery.addFilter(dataSourceFilter);
449 
450  QueryResults queryResults;
451 
452  // Do the actual search
453  try {
454  queryResults = keywordSearchQuery.performQuery();
456  logger.log(Level.SEVERE, "Error performing query: " + keyword.getSearchTerm(), ex); //NON-NLS
457  MessageNotifyUtil.Notify.error(Bundle.SearchRunner_query_exception_msg() + keyword.getSearchTerm(), ex.getCause().getMessage());
458  //no reason to continue with next query if recovery failed
459  //or wait for recovery to kick in and run again later
460  //likely case has closed and threads are being interrupted
461  return null;
462  } catch (CancellationException e) {
463  logger.log(Level.INFO, "Cancel detected, bailing during keyword query: {0}", keyword.getSearchTerm()); //NON-NLS
464  return null;
465  }
466 
467  // Reduce the results of the query to only those hits we
468  // have not already seen.
469  QueryResults newResults = filterResults(queryResults);
470 
471  if (!newResults.getKeywords().isEmpty()) {
472 
473  // Write results to BB
474  //new artifacts created, to report to listeners
475  Collection<BlackboardArtifact> newArtifacts = new ArrayList<>();
476 
477  //scale progress bar more more granular, per result sub-progress, within per keyword
478  int totalUnits = newResults.getKeywords().size();
479  subProgresses[keywordsSearched].start(totalUnits);
480  int unitProgress = 0;
481  String queryDisplayStr = keyword.getSearchTerm();
482  if (queryDisplayStr.length() > 50) {
483  queryDisplayStr = queryDisplayStr.substring(0, 49) + "...";
484  }
485  subProgresses[keywordsSearched].progress(keywordList.getName() + ": " + queryDisplayStr, unitProgress);
486 
487  // Create blackboard artifacts
488  newArtifacts = newResults.writeAllHitsToBlackBoard(null, subProgresses[keywordsSearched], this, keywordList.getIngestMessages());
489 
490  } //if has results
491 
492  //reset the status text before it goes away
493  subProgresses[keywordsSearched].progress("");
494 
495  ++keywordsSearched;
496 
497  } //for each keyword
498 
499  } //end try block
500  catch (Exception ex) {
501  logger.log(Level.WARNING, "searcher exception occurred", ex); //NON-NLS
502  } finally {
503  try {
505  stopWatch.stop();
506 
507  logger.log(Level.INFO, "Searcher took to run: {0} secs.", stopWatch.getElapsedTimeSecs()); //NON-NLS
508  } finally {
509  // In case a thread is waiting on this worker to be done
510  job.searchNotify();
511  }
512  }
513 
514  return null;
515  }
516 
517  @Override
518  protected void done() {
519  // call get to see if there were any errors
520  try {
521  get();
522  } catch (InterruptedException | ExecutionException e) {
523  logger.log(Level.SEVERE, "Error performing keyword search: " + e.getMessage()); //NON-NLS
525  NbBundle.getMessage(this.getClass(),
526  "SearchRunner.Searcher.done.err.msg"), e.getMessage()));
527  } // catch and ignore if we were cancelled
528  catch (java.util.concurrent.CancellationException ex) {
529  }
530  }
531 
535  private void updateKeywords() {
536  XmlKeywordSearchList loader = XmlKeywordSearchList.getCurrent();
537 
538  keywords.clear();
539  keywordToList.clear();
540  keywordLists.clear();
541 
542  for (String name : keywordListNames) {
543  KeywordList list = loader.getList(name);
544  keywordLists.add(list);
545  for (Keyword k : list.getKeywords()) {
546  keywords.add(k);
547  keywordToList.put(k, list);
548  }
549  }
550  }
551 
557  private void finalizeSearcher() {
558  SwingUtilities.invokeLater(new Runnable() {
559  @Override
560  public void run() {
561  progressGroup.finish();
562  }
563  });
564  }
565 
580  private QueryResults filterResults(QueryResults queryResult) {
581 
582  // Create a new (empty) QueryResults object to hold the most recently
583  // found hits.
584  QueryResults newResults = new QueryResults(queryResult.getQuery());
585 
586  // For each keyword represented in the results.
587  for (Keyword keyword : queryResult.getKeywords()) {
588  // These are all of the hits across all objects for the most recent search.
589  // This may well include duplicates of hits we've seen in earlier periodic searches.
590  List<KeywordHit> queryTermResults = queryResult.getResults(keyword);
591 
592  // Sort the hits for this keyword so that we are always
593  // guaranteed to return the hit for the lowest chunk.
594  Collections.sort(queryTermResults);
595 
596  // This will be used to build up the hits we haven't seen before
597  // for this keyword.
598  List<KeywordHit> newUniqueHits = new ArrayList<>();
599 
600  // Get the set of object ids seen in the past by this searcher
601  // for the given keyword.
602  Set<Long> curTermResults = job.currentKeywordResults(keyword);
603  if (curTermResults == null) {
604  // We create a new empty set if we haven't seen results for
605  // this keyword before.
606  curTermResults = new HashSet<>();
607  }
608 
609  // For each hit for this keyword.
610  for (KeywordHit hit : queryTermResults) {
611  if (curTermResults.contains(hit.getSolrObjectId())) {
612  // Skip the hit if we've already seen a hit for
613  // this keyword in the object.
614  continue;
615  }
616 
617  // We haven't seen the hit before so add it to list of new
618  // unique hits.
619  newUniqueHits.add(hit);
620 
621  // Add the object id to the results we've seen for this
622  // keyword.
623  curTermResults.add(hit.getSolrObjectId());
624  }
625 
626  // Update the job with the list of objects for which we have
627  // seen hits for the current keyword.
628  job.addKeywordResults(keyword, curTermResults);
629 
630  // Add the new hits for the current keyword into the results
631  // to be returned.
632  newResults.addResult(keyword, newUniqueHits);
633  }
634 
635  return newResults;
636  }
637  }
638 }
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)
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: Mon Apr 24 2017
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.