Autopsy  4.0
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.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Map.Entry;
27 import java.util.concurrent.CancellationException;
28 import java.util.concurrent.ExecutionException;
29 import java.util.concurrent.atomic.AtomicLong;
30 import java.util.logging.Level;
31 import javax.swing.SwingUtilities;
32 import javax.swing.SwingWorker;
33 import java.util.Timer;
34 import java.util.TimerTask;
35 import org.netbeans.api.progress.aggregate.AggregateProgressFactory;
36 import org.netbeans.api.progress.aggregate.AggregateProgressHandle;
37 import org.netbeans.api.progress.aggregate.ProgressContributor;
38 import org.openide.util.Cancellable;
39 import org.openide.util.NbBundle;
44 import org.sleuthkit.datamodel.BlackboardArtifact;
45 
50 public final class SearchRunner {
51 
52  private static final Logger logger = Logger.getLogger(SearchRunner.class.getName());
53  private static SearchRunner instance = null;
55  private Ingester ingester = null;
56  private volatile boolean updateTimerRunning = false;
57  private Timer updateTimer;
58 
59  // maps a jobID to the search
60  private Map<Long, SearchJobInfo> jobs = new HashMap<>(); //guarded by "this"
61 
62  SearchRunner() {
63  ingester = Server.getIngester();
64  updateTimer = new Timer(NbBundle.getMessage(this.getClass(), "SearchRunner.updateTimer.title.text"), true); // run as a daemon
65  }
66 
71  public static synchronized SearchRunner getInstance() {
72  if (instance == null) {
73  instance = new SearchRunner();
74  }
75  return instance;
76  }
77 
88  public synchronized void startJob(long jobId, long dataSourceId, List<String> keywordListNames) {
89  if (jobs.containsKey(jobId) == false) {
90  logger.log(Level.INFO, "Adding job {0}", jobId); //NON-NLS
91  SearchJobInfo jobData = new SearchJobInfo(jobId, dataSourceId, keywordListNames);
92  jobs.put(jobId, jobData);
93  }
94 
95  // keep track of how many threads / module instances from this job have asked for this
96  jobs.get(jobId).incrementModuleReferenceCount();
97 
98  // start the timer, if needed
99  if ((jobs.size() > 0) && (updateTimerRunning == false)) {
100  final long updateIntervalMs = ((long) KeywordSearchSettings.getUpdateFrequency().getTime()) * 60 * 1000;
101  updateTimer.scheduleAtFixedRate(new UpdateTimerTask(), updateIntervalMs, updateIntervalMs);
102  updateTimerRunning = true;
103  }
104  }
105 
112  public void endJob(long jobId) {
113  SearchJobInfo job;
114  boolean readyForFinalSearch = false;
115  synchronized (this) {
116  job = jobs.get(jobId);
117  if (job == null) {
118  return;
119  }
120 
121  // Only do final search if this is the last module/thread in this job to call endJob()
122  if (job.decrementModuleReferenceCount() == 0) {
123  jobs.remove(jobId);
124  readyForFinalSearch = true;
125  }
126  }
127 
128  if (readyForFinalSearch) {
129  commit();
130  doFinalSearch(job); //this will block until it's done
131  }
132  }
133 
140  public void stopJob(long jobId) {
141  logger.log(Level.INFO, "Stopping job {0}", jobId); //NON-NLS
142  commit();
143 
144  SearchJobInfo job;
145  synchronized (this) {
146  job = jobs.get(jobId);
147  if (job == null) {
148  return;
149  }
150 
151  //stop currentSearcher
152  SearchRunner.Searcher currentSearcher = job.getCurrentSearcher();
153  if ((currentSearcher != null) && (!currentSearcher.isDone())) {
154  currentSearcher.cancel(true);
155  }
156 
157  jobs.remove(jobId);
158  }
159  }
160 
167  public synchronized void addKeywordListsToAllJobs(List<String> keywordListNames) {
168  for (String listName : keywordListNames) {
169  logger.log(Level.INFO, "Adding keyword list {0} to all jobs", listName); //NON-NLS
170  for (SearchJobInfo j : jobs.values()) {
171  j.addKeywordListName(listName);
172  }
173  }
174  }
175 
179  private void commit() {
180  ingester.commit();
181 
182  // Signal a potential change in number of text_ingested files
183  try {
184  final int numIndexedFiles = KeywordSearch.getServer().queryNumIndexedFiles();
185  KeywordSearch.fireNumIndexedFilesChange(null, numIndexedFiles);
187  logger.log(Level.WARNING, "Error executing Solr query to check number of indexed files: ", ex); //NON-NLS
188  }
189  }
190 
197  private void doFinalSearch(SearchJobInfo job) {
198  // Run one last search as there are probably some new files committed
199  logger.log(Level.INFO, "Running final search for jobid {0}", job.getJobId()); //NON-NLS
200  if (!job.getKeywordListNames().isEmpty()) {
201  try {
202  // In case this job still has a worker running, wait for it to finish
203  job.waitForCurrentWorker();
204 
205  SearchRunner.Searcher finalSearcher = new SearchRunner.Searcher(job, true);
206  job.setCurrentSearcher(finalSearcher); //save the ref
207  finalSearcher.execute(); //start thread
208 
209  // block until the search is complete
210  finalSearcher.get();
211 
212  } catch (InterruptedException | ExecutionException ex) {
213  logger.log(Level.WARNING, "Job {1} final search thread failed: {2}", new Object[]{job.getJobId(), ex}); //NON-NLS
214  }
215  }
216  }
217 
221  private class UpdateTimerTask extends TimerTask {
222 
223  private final Logger logger = Logger.getLogger(SearchRunner.UpdateTimerTask.class.getName());
224 
225  @Override
226  public void run() {
227  // If no jobs then cancel the task. If more job(s) come along, a new task will start up.
228  if (jobs.isEmpty()) {
229  this.cancel(); //terminate this timer task
230  updateTimerRunning = false;
231  return;
232  }
233 
234  commit();
235 
236  synchronized (SearchRunner.this) {
237  // Spawn a search thread for each job
238  for (Entry<Long, SearchJobInfo> j : jobs.entrySet()) {
239  SearchJobInfo job = j.getValue();
240  // If no lists or the worker is already running then skip it
241  if (!job.getKeywordListNames().isEmpty() && !job.isWorkerRunning()) {
242  Searcher searcher = new Searcher(job);
243  job.setCurrentSearcher(searcher); //save the ref
244  searcher.execute(); //start thread
245  job.setWorkerRunning(true);
246  }
247  }
248  }
249  }
250  }
251 
256  private class SearchJobInfo {
257 
258  private final long jobId;
259  private final long dataSourceId;
260  // mutable state:
261  private volatile boolean workerRunning;
262  private List<String> keywordListNames; //guarded by SearchJobInfo.this
263  private Map<Keyword, List<Long>> currentResults; //guarded by SearchJobInfo.this
265  private AtomicLong moduleReferenceCount = new AtomicLong(0);
266  private final Object finalSearchLock = new Object(); //used for a condition wait
267 
268  public SearchJobInfo(long jobId, long dataSourceId, List<String> keywordListNames) {
269  this.jobId = jobId;
270  this.dataSourceId = dataSourceId;
271  this.keywordListNames = new ArrayList<>(keywordListNames);
272  currentResults = new HashMap<>();
273  workerRunning = false;
274  currentSearcher = null;
275  }
276 
277  public long getJobId() {
278  return jobId;
279  }
280 
281  public long getDataSourceId() {
282  return dataSourceId;
283  }
284 
285  public synchronized List<String> getKeywordListNames() {
286  return new ArrayList<>(keywordListNames);
287  }
288 
289  public synchronized void addKeywordListName(String keywordListName) {
290  if (!keywordListNames.contains(keywordListName)) {
291  keywordListNames.add(keywordListName);
292  }
293  }
294 
295  public synchronized List<Long> currentKeywordResults(Keyword k) {
296  return currentResults.get(k);
297  }
298 
299  public synchronized void addKeywordResults(Keyword k, List<Long> resultsIDs) {
300  currentResults.put(k, resultsIDs);
301  }
302 
303  public boolean isWorkerRunning() {
304  return workerRunning;
305  }
306 
307  public void setWorkerRunning(boolean flag) {
308  workerRunning = flag;
309  }
310 
311  public synchronized SearchRunner.Searcher getCurrentSearcher() {
312  return currentSearcher;
313  }
314 
315  public synchronized void setCurrentSearcher(SearchRunner.Searcher searchRunner) {
316  currentSearcher = searchRunner;
317  }
318 
320  moduleReferenceCount.incrementAndGet();
321  }
322 
324  return moduleReferenceCount.decrementAndGet();
325  }
326 
332  public void waitForCurrentWorker() throws InterruptedException {
333  synchronized (finalSearchLock) {
334  while (workerRunning) {
335  finalSearchLock.wait(); //wait() releases the lock
336  }
337  }
338  }
339 
343  public void searchNotify() {
344  synchronized (finalSearchLock) {
345  workerRunning = false;
346  finalSearchLock.notify();
347  }
348  }
349  }
350 
357  private final class Searcher extends SwingWorker<Object, Void> {
358 
363  private List<Keyword> keywords; //keywords to search
364  private List<String> keywordListNames; // lists currently being searched
365  private List<KeywordList> keywordLists;
366  private Map<String, KeywordList> keywordToList; //keyword to list name mapping
367  private AggregateProgressHandle progressGroup;
368  private final Logger logger = Logger.getLogger(SearchRunner.Searcher.class.getName());
369  private boolean finalRun = false;
370 
371  Searcher(SearchJobInfo job) {
372  this.job = job;
373  keywordListNames = job.getKeywordListNames();
374  keywords = new ArrayList<>();
375  keywordToList = new HashMap<>();
376  keywordLists = new ArrayList<>();
377  //keywords are populated as searcher runs
378  }
379 
380  Searcher(SearchJobInfo job, boolean finalRun) {
381  this(job);
382  this.finalRun = finalRun;
383  }
384 
385  @Override
386  protected Object doInBackground() throws Exception {
387  final String displayName = NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.doInBackGround.displayName")
388  + (finalRun ? (" - " + NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.doInBackGround.finalizeMsg")) : "");
389  final String pgDisplayName = displayName + (" (" + NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.doInBackGround.pendingMsg") + ")");
390  progressGroup = AggregateProgressFactory.createSystemHandle(pgDisplayName, null, new Cancellable() {
391  @Override
392  public boolean cancel() {
393  logger.log(Level.INFO, "Cancelling the searcher by user."); //NON-NLS
394  if (progressGroup != null) {
395  progressGroup.setDisplayName(displayName + " " + NbBundle.getMessage(this.getClass(), "SearchRunner.doInBackGround.cancelMsg"));
396  }
397  return SearchRunner.Searcher.this.cancel(true);
398  }
399  }, null);
400 
401  updateKeywords();
402 
403  ProgressContributor[] subProgresses = new ProgressContributor[keywords.size()];
404  int i = 0;
405  for (Keyword keywordQuery : keywords) {
406  subProgresses[i] = AggregateProgressFactory.createProgressContributor(keywordQuery.getQuery());
407  progressGroup.addContributor(subProgresses[i]);
408  i++;
409  }
410 
411  progressGroup.start();
412 
413  final StopWatch stopWatch = new StopWatch();
414  stopWatch.start();
415  try {
416  progressGroup.setDisplayName(displayName);
417 
418  int keywordsSearched = 0;
419 
420  for (Keyword keywordQuery : keywords) {
421  if (this.isCancelled()) {
422  logger.log(Level.INFO, "Cancel detected, bailing before new keyword processed: {0}", keywordQuery.getQuery()); //NON-NLS
423  return null;
424  }
425 
426  final String queryStr = keywordQuery.getQuery();
427  final KeywordList list = keywordToList.get(queryStr);
428 
429  //new subProgress will be active after the initial query
430  //when we know number of hits to start() with
431  if (keywordsSearched > 0) {
432  subProgresses[keywordsSearched - 1].finish();
433  }
434 
435  KeywordSearchQuery keywordSearchQuery = null;
436 
437  boolean isRegex = !keywordQuery.isLiteral();
438  if (isRegex) {
439  keywordSearchQuery = new TermComponentQuery(list, keywordQuery);
440  } else {
441  keywordSearchQuery = new LuceneQuery(list, keywordQuery);
442  keywordSearchQuery.escape();
443  }
444 
445  // Filtering
446  //limit search to currently ingested data sources
447  //set up a filter with 1 or more image ids OR'ed
448  final KeywordQueryFilter dataSourceFilter = new KeywordQueryFilter(KeywordQueryFilter.FilterType.DATA_SOURCE, job.getDataSourceId());
449  keywordSearchQuery.addFilter(dataSourceFilter);
450 
451  QueryResults queryResults;
452 
453  // Do the actual search
454  try {
455  queryResults = keywordSearchQuery.performQuery();
456  } catch (NoOpenCoreException ex) {
457  logger.log(Level.WARNING, "Error performing query: " + keywordQuery.getQuery(), ex); //NON-NLS
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}", keywordQuery.getQuery()); //NON-NLS
464  return null;
465  } catch (Exception e) {
466  logger.log(Level.WARNING, "Error performing query: " + keywordQuery.getQuery(), e); //NON-NLS
467  continue;
468  }
469 
470  // calculate new results by substracting results already obtained in this ingest
471  // this creates a map of each keyword to the list of unique files that have that hit.
472  QueryResults newResults = filterResults(queryResults);
473 
474  if (!newResults.getKeywords().isEmpty()) {
475 
476  // Write results to BB
477  //new artifacts created, to report to listeners
478  Collection<BlackboardArtifact> newArtifacts = new ArrayList<>();
479 
480  //scale progress bar more more granular, per result sub-progress, within per keyword
481  int totalUnits = newResults.getKeywords().size();
482  subProgresses[keywordsSearched].start(totalUnits);
483  int unitProgress = 0;
484  String queryDisplayStr = keywordQuery.getQuery();
485  if (queryDisplayStr.length() > 50) {
486  queryDisplayStr = queryDisplayStr.substring(0, 49) + "...";
487  }
488  subProgresses[keywordsSearched].progress(list.getName() + ": " + queryDisplayStr, unitProgress);
489 
490  // Create blackboard artifacts
491  newArtifacts = newResults.writeAllHitsToBlackBoard(null, subProgresses[keywordsSearched], this, list.getIngestMessages());
492 
493  } //if has results
494 
495  //reset the status text before it goes away
496  subProgresses[keywordsSearched].progress("");
497 
498  ++keywordsSearched;
499 
500  } //for each keyword
501 
502  } //end try block
503  catch (Exception ex) {
504  logger.log(Level.WARNING, "searcher exception occurred", ex); //NON-NLS
505  } finally {
506  try {
508  stopWatch.stop();
509 
510  logger.log(Level.INFO, "Searcher took to run: {0} secs.", stopWatch.getElapsedTimeSecs()); //NON-NLS
511  } finally {
512  // In case a thread is waiting on this worker to be done
513  job.searchNotify();
514  }
515  }
516 
517  return null;
518  }
519 
520  @Override
521  protected void done() {
522  // call get to see if there were any errors
523  try {
524  get();
525  } catch (InterruptedException | ExecutionException e) {
526  logger.log(Level.SEVERE, "Error performing keyword search: " + e.getMessage()); //NON-NLS
528  NbBundle.getMessage(this.getClass(),
529  "SearchRunner.Searcher.done.err.msg"), e.getMessage()));
530  } // catch and ignore if we were cancelled
531  catch (java.util.concurrent.CancellationException ex) {
532  }
533  }
534 
538  private void updateKeywords() {
539  XmlKeywordSearchList loader = XmlKeywordSearchList.getCurrent();
540 
541  keywords.clear();
542  keywordToList.clear();
543  keywordLists.clear();
544 
545  for (String name : keywordListNames) {
546  KeywordList list = loader.getList(name);
547  keywordLists.add(list);
548  for (Keyword k : list.getKeywords()) {
549  keywords.add(k);
550  keywordToList.put(k.getQuery(), list);
551  }
552  }
553  }
554 
560  private void finalizeSearcher() {
561  SwingUtilities.invokeLater(new Runnable() {
562  @Override
563  public void run() {
564  progressGroup.finish();
565  }
566  });
567  }
568 
569  //calculate new results but substracting results already obtained in this ingest
570  //update currentResults map with the new results
571  private QueryResults filterResults(QueryResults queryResult) {
572 
573  QueryResults newResults = new QueryResults(queryResult.getQuery(), queryResult.getKeywordList());
574 
575  for (Keyword keyword : queryResult.getKeywords()) {
576  List<KeywordHit> queryTermResults = queryResult.getResults(keyword);
577 
578  //translate to list of IDs that we keep track of
579  List<Long> queryTermResultsIDs = new ArrayList<>();
580  for (KeywordHit ch : queryTermResults) {
581  queryTermResultsIDs.add(ch.getSolrObjectId());
582  }
583 
584  List<Long> curTermResults = job.currentKeywordResults(keyword);
585  if (curTermResults == null) {
586  job.addKeywordResults(keyword, queryTermResultsIDs);
587  newResults.addResult(keyword, queryTermResults);
588  } else {
589  //some AbstractFile hits already exist for this keyword
590  for (KeywordHit res : queryTermResults) {
591  if (!curTermResults.contains(res.getSolrObjectId())) {
592  //add to new results
593  List<KeywordHit> newResultsFs = newResults.getResults(keyword);
594  if (newResultsFs == null) {
595  newResultsFs = new ArrayList<>();
596  newResults.addResult(keyword, newResultsFs);
597  }
598  newResultsFs.add(res);
599  curTermResults.add(res.getSolrObjectId());
600  }
601  }
602  }
603  }
604 
605  return newResults;
606  }
607  }
608 }
SearchJobInfo(long jobId, long dataSourceId, List< String > keywordListNames)
synchronized void addKeywordResults(Keyword k, List< Long > resultsIDs)
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)
synchronized List< Long > currentKeywordResults(Keyword k)
static synchronized SearchRunner getInstance()
synchronized void addKeywordListsToAllJobs(List< String > keywordListNames)
void postMessage(final IngestMessage message)
synchronized static Logger getLogger(String name)
Definition: Logger.java:166
QueryResults filterResults(QueryResults queryResult)
static synchronized IngestServices getInstance()

Copyright © 2012-2015 Basis Technology. Generated on: Wed Apr 6 2016
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.