From 1b48afa81125f50d944beeff27f21c7838c55e3c Mon Sep 17 00:00:00 2001 From: jmillman Date: Fri, 13 Nov 2015 17:19:49 -0500 Subject: [PATCH 1/5] don't block while rebuilding repo, use JFX dialogs WIP --- .../autopsy/timeline/OpenTimelineAction.java | 5 +- .../autopsy/timeline/ProgressUpdate.java | 48 ++++ .../autopsy/timeline/ProgressWindow.form | 65 ------ .../autopsy/timeline/ProgressWindow.java | 217 ------------------ .../autopsy/timeline/TimeLineController.java | 158 ++++++++----- .../autopsy/timeline/db/EventsRepository.java | 143 +++++++----- 6 files changed, 239 insertions(+), 397 deletions(-) create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ProgressUpdate.java delete mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ProgressWindow.form delete mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ProgressWindow.java diff --git a/Core/src/org/sleuthkit/autopsy/timeline/OpenTimelineAction.java b/Core/src/org/sleuthkit/autopsy/timeline/OpenTimelineAction.java index 3c5c8a6621..ad5ffe8323 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/OpenTimelineAction.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/OpenTimelineAction.java @@ -31,6 +31,7 @@ import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.core.Installer; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; @ActionID(category = "Tools", id = "org.sleuthkit.autopsy.timeline.Timeline") @ActionRegistration(displayName = "#CTL_MakeTimeline", lazy = false) @@ -58,8 +59,8 @@ public class OpenTimelineAction extends CallableSystemAction { } @Override + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) public void performAction() { - //check case if (!Case.isCaseOpen()) { return; @@ -72,14 +73,12 @@ public class OpenTimelineAction extends CallableSystemAction { LOGGER.log(Level.INFO, "Could not create timeline, there are no data sources.");// NON-NLS return; } - synchronized (OpenTimelineAction.class) { if (timeLineController == null) { timeLineController = new TimeLineController(currentCase); } else if (timeLineController.getAutopsyCase() != currentCase) { timeLineController.closeTimeLine(); timeLineController = new TimeLineController(currentCase); } - } timeLineController.openTimeLine(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ProgressUpdate.java b/Core/src/org/sleuthkit/autopsy/timeline/ProgressUpdate.java new file mode 100644 index 0000000000..31783e2e3f --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ProgressUpdate.java @@ -0,0 +1,48 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package org.sleuthkit.autopsy.timeline; + +import javax.annotation.concurrent.Immutable; + +/** + * bundles up progress information to be shown in the progress dialog + */ +@Immutable +public class ProgressUpdate { + private final int progress; + private final int total; + private final String headerMessage; + private final String detailMessage; + + public int getProgress() { + return progress; + } + + public int getTotal() { + return total; + } + + public String getHeaderMessage() { + return headerMessage; + } + + public String getDetailMessage() { + return detailMessage; + } + + public ProgressUpdate(int progress, int total, String headerMessage, String detailMessage) { + this.progress = progress; + this.total = total; + this.headerMessage = headerMessage; + this.detailMessage = detailMessage; + } + + public ProgressUpdate(int progress, int total, String headerMessage) { + this(progress, total, headerMessage, ""); + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ProgressWindow.form b/Core/src/org/sleuthkit/autopsy/timeline/ProgressWindow.form deleted file mode 100644 index 397abea1b4..0000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/ProgressWindow.form +++ /dev/null @@ -1,65 +0,0 @@ - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ProgressWindow.java b/Core/src/org/sleuthkit/autopsy/timeline/ProgressWindow.java deleted file mode 100644 index 9afa6f7c89..0000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/ProgressWindow.java +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2013 Basis Technology Corp. - * Contact: carrier sleuthkit 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.timeline; - -import java.awt.Component; -import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; -import javax.annotation.concurrent.Immutable; -import javax.swing.AbstractAction; -import javax.swing.ActionMap; -import javax.swing.InputMap; -import javax.swing.JComponent; -import javax.swing.JFrame; -import javax.swing.JOptionPane; -import javax.swing.KeyStroke; -import javax.swing.SwingUtilities; -import javax.swing.SwingWorker; -import org.openide.util.NbBundle; -import org.openide.windows.WindowManager; -import org.sleuthkit.autopsy.coreutils.ThreadConfined; - -/** - * Dialog with progress bar that pops up when timeline is being generated - */ -public class ProgressWindow extends JFrame { - - private final SwingWorker worker; - - /** - * Creates new form TimelineProgressDialog - */ - @NbBundle.Messages({"Timeline.progressWindow.name=Timeline", - "Timeline.progressWindow.title=Generating Timeline data"}) - public ProgressWindow(Component parent, boolean modal, SwingWorker worker) { - super(); - initComponents(); - - setLocationRelativeTo(parent); - - setAlwaysOnTop(modal); - - //set icon the same as main app - SwingUtilities.invokeLater(() -> { - setIconImage(WindowManager.getDefault().getMainWindow().getIconImage()); - }); - - setName(Bundle.Timeline_progressWindow_name()); - setTitle(Bundle.Timeline_progressWindow_title()); - // Close the dialog when Esc is pressed - String cancelName = "cancel"; // NON-NLS - InputMap inputMap = getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); - - inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), cancelName); - ActionMap actionMap = getRootPane().getActionMap(); - - actionMap.put(cancelName, new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - cancel(); - } - }); - this.worker = worker; - } - - /** - * This method is called from within the constructor to initialize the form. - * WARNING: Do NOT modify this code. The content of this method is always - * regenerated by the Form Editor. - */ - @SuppressWarnings("unchecked") - // //GEN-BEGIN:initComponents - private void initComponents() { - - progressBar = new javax.swing.JProgressBar(); - progressHeader = new javax.swing.JLabel(); - - addWindowListener(new java.awt.event.WindowAdapter() { - public void windowClosing(java.awt.event.WindowEvent evt) { - closeDialog(evt); - } - }); - - org.openide.awt.Mnemonics.setLocalizedText(progressHeader, NbBundle.getMessage(ProgressWindow.class, "ProgressWindow.progressHeader.text")); // NOI18N - progressHeader.setMinimumSize(new java.awt.Dimension(10, 14)); - - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); - getContentPane().setLayout(layout); - layout.setHorizontalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(progressBar, javax.swing.GroupLayout.DEFAULT_SIZE, 504, Short.MAX_VALUE) - .addGroup(layout.createSequentialGroup() - .addComponent(progressHeader, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(0, 0, Short.MAX_VALUE))) - .addContainerGap()) - ); - layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addComponent(progressHeader, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(progressBar, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - ); - - pack(); - }// //GEN-END:initComponents - - /** - * Closes the dialog - */ - private void closeDialog(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_closeDialog - cancel(); - }//GEN-LAST:event_closeDialog - - @NbBundle.Messages({"Timeline.ProgressWindow.cancel.confdlg.msg=Do you want to cancel timeline creation?", - "Timeline.ProgressWindow.cancel.confdlg.detail=Cancel timeline creation?"}) - public void cancel() { - SwingUtilities.invokeLater(() -> { - if (isVisible()) { - int showConfirmDialog = JOptionPane.showConfirmDialog(ProgressWindow.this, - Bundle.Timeline_ProgressWindow_cancel_confdlg_msg(), - Bundle.Timeline_ProgressWindow_cancel_confdlg_detail(), - JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); - if (showConfirmDialog == JOptionPane.YES_OPTION) { - close(); - } - } else { - close(); - } - }); - } - - public void close() { - worker.cancel(false); - setVisible(false); - dispose(); - } - // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JProgressBar progressBar; - private javax.swing.JLabel progressHeader; - // End of variables declaration//GEN-END:variables - - @ThreadConfined(type = ThreadConfined.ThreadType.AWT) - public void update(ProgressUpdate chunk) { - progressHeader.setText(chunk.getHeaderMessage()); - if (chunk.getTotal() >= 0) { - progressBar.setIndeterminate(false); - progressBar.setMaximum(chunk.getTotal()); - progressBar.setStringPainted(true); - progressBar.setValue(chunk.getProgress()); - progressBar.setString(chunk.getDetailMessage()); - } else { - progressBar.setIndeterminate(true); - progressBar.setStringPainted(true); - progressBar.setString(chunk.getDetailMessage()); - } - } - - /** - * bundles up progress information to be shown in the progress dialog - */ - @Immutable - public static class ProgressUpdate { - - private final int progress; - private final int total; - private final String headerMessage; - private final String detailMessage; - - public int getProgress() { - return progress; - } - - public int getTotal() { - return total; - } - - public String getHeaderMessage() { - return headerMessage; - } - - public String getDetailMessage() { - return detailMessage; - } - - public ProgressUpdate(int progress, int total, String headerMessage, String detailMessage) { - this.progress = progress; - this.total = total; - this.headerMessage = headerMessage; - this.detailMessage = detailMessage; - } - - public ProgressUpdate(int progress, int total, String headerMessage) { - this(progress, total, headerMessage, ""); - } - } -} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java index 9f904eca79..07e8af5824 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java @@ -21,6 +21,8 @@ package org.sleuthkit.autopsy.timeline; import java.awt.HeadlessException; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.io.IOException; +import java.net.URL; import java.sql.ResultSet; import java.sql.SQLException; import java.text.NumberFormat; @@ -28,6 +30,7 @@ import java.time.ZoneId; import java.util.Collection; import java.util.Map; import java.util.MissingResourceException; +import java.util.Optional; import java.util.TimeZone; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -49,10 +52,18 @@ import javafx.beans.property.ReadOnlyStringWrapper; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.concurrent.Task; +import javafx.concurrent.WorkerStateEvent; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; +import javafx.scene.control.DialogEvent; +import javafx.scene.image.Image; +import javafx.stage.Modality; +import javafx.stage.Stage; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.Immutable; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; +import org.controlsfx.dialog.ProgressDialog; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.joda.time.Interval; @@ -136,6 +147,9 @@ public class TimeLineController { private final ReadOnlyStringWrapper status = new ReadOnlyStringWrapper(); + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private ProgressDialog taskProgressDialog; + /** * status is a string that will be displayed in the status bar as a kind of * user hint/information when it is not empty @@ -202,7 +216,6 @@ public class TimeLineController { @GuardedBy("filteredEvents") private final FilteredEventsModel filteredEvents; - @GuardedBy("eventsRepository") private final EventsRepository eventsRepository; @GuardedBy("this") @@ -305,6 +318,7 @@ public class TimeLineController { * the user aborted after prompt about ingest running. True if the * repo was rebuilt. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) boolean rebuildRepo() { if (IngestManager.getInstance().isIngestRunning()) { //confirm timeline during ingest @@ -314,40 +328,29 @@ public class TimeLineController { } LOGGER.log(Level.INFO, "Beginning generation of timeline"); // NON-NLS try { - SwingUtilities.invokeLater(() -> { - synchronized (TimeLineController.this) { - if (isWindowOpen()) { - mainFrame.close(); - } - } - }); + closeTimelineWindow(); final SleuthkitCase sleuthkitCase = Case.getCurrentCase().getSleuthkitCase(); final long lastObjId = sleuthkitCase.getLastObjectId(); final long lastArtfID = getCaseLastArtifactID(sleuthkitCase); final Boolean injestRunning = IngestManager.getInstance().isIngestRunning(); - //TODO: verify this locking is correct? -jm - synchronized (eventsRepository) { - eventsRepository.rebuildRepository(() -> { - synchronized (eventsRepository) { - eventsRepository.recordLastObjID(lastObjId); - eventsRepository.recordLastArtifactID(lastArtfID); - eventsRepository.recordWasIngestRunning(injestRunning); - } - synchronized (TimeLineController.this) { - //TODO: this looks hacky. what is going on? should this be an event? - needsHistogramRebuild.set(true); - needsHistogramRebuild.set(false); - showWindow(); - } + final Task rebuildRepository = eventsRepository.rebuildRepository(lastObjId, lastArtfID, injestRunning); + Platform.runLater(() -> { + rebuildRepository.setOnSucceeded((WorkerStateEvent event) -> { + //this is run on jfx thread - Platform.runLater(() -> { - //TODO: should this be an event? - newEventsFlag.set(false); - historyManager.reset(filteredEvents.zoomParametersProperty().get()); - TimeLineController.this.showFullRange(); - }); + //TODO: this looks hacky. what is going on? should this be an event? + needsHistogramRebuild.set(true); + needsHistogramRebuild.set(false); + SwingUtilities.invokeLater(TimeLineController.this::showWindow); + + //TODO: should this be an event? + newEventsFlag.set(false); + historyManager.reset(filteredEvents.zoomParametersProperty().get()); + TimeLineController.this.showFullRange(); }); - } + taskProgressDialog = showProgressDialogForTask(rebuildRepository); + }); + } catch (TskCoreException ex) { LOGGER.log(Level.SEVERE, "Error when generating timeline, ", ex); // NON-NLS return false; @@ -360,25 +363,62 @@ public class TimeLineController { * tags table and rebuild it by querying for all the tags and inserting them * in to the TimeLine DB. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) void rebuildTagsTable() { LOGGER.log(Level.INFO, "starting to rebuild tags table"); // NON-NLS - SwingUtilities.invokeLater(() -> { - synchronized (TimeLineController.this) { - if (isWindowOpen()) { - mainFrame.close(); - } - } - }); - synchronized (eventsRepository) { - eventsRepository.rebuildTags(() -> { - showWindow(); - Platform.runLater(() -> { - showFullRange(); - }); + closeTimelineWindow(); + Task rebuildTags = eventsRepository.rebuildTags(); + + Platform.runLater(() -> { + rebuildTags.setOnSucceeded((WorkerStateEvent event) -> { + SwingUtilities.invokeLater(TimeLineController.this::showWindow); + showFullRange();; }); + taskProgressDialog = showProgressDialogForTask(rebuildTags); + }); + + } + + private void closeTimelineWindow() { + if (isWindowOpen()) { + mainFrame.close(); } } + @NbBundle.Messages({"Timeline.progressWindow.title=Populating Timeline data", + "Timeline.ProgressWindow.cancel.confdlg.msg=Not all events will be present of accurate if you cancel the timeline population. Do you want to cancel?", + "Timeline.ProgressWindow.cancel.confdlg.detail=Cancel timeline population?"}) + private ProgressDialog showProgressDialogForTask(Task task) { + ProgressDialog progressDialog = new ProgressDialog(task); + progressDialog.getDialogPane().getButtonTypes().add(ButtonType.CANCEL); + + progressDialog.setTitle(Bundle.Timeline_progressWindow_title()); + progressDialog.getDialogPane().headerTextProperty().bind(task.titleProperty()); + try { + Image image = new Image(new URL("nbresloc:/org/netbeans/core/startup/frame.gif").openStream()); + ((Stage) progressDialog.getDialogPane().getScene().getWindow()).getIcons().setAll(image); + } catch (IOException iOException) { + LOGGER.log(Level.WARNING, "Failed to laod branded icon for progress dialog.", iOException); + } + progressDialog.setOnCloseRequest((DialogEvent event) -> { + Alert confirmation = new Alert(Alert.AlertType.CONFIRMATION, Bundle.Timeline_ProgressWindow_cancel_confdlg_msg(), ButtonType.YES, ButtonType.NO); + confirmation.setHeaderText(null); + confirmation.setTitle(Bundle.Timeline_ProgressWindow_cancel_confdlg_detail()); + confirmation.initOwner(progressDialog.getDialogPane().getScene().getWindow()); + confirmation.initModality(Modality.WINDOW_MODAL); + Optional showAndWait = confirmation.showAndWait(); + + showAndWait.ifPresent(buttonType -> { + if (buttonType == ButtonType.YES) { + task.cancel(true); + } else { + event.consume(); + } + }); + }); + return progressDialog; + } + public void showFullRange() { synchronized (filteredEvents) { pushTimeRange(filteredEvents.getSpanningInterval()); @@ -403,6 +443,7 @@ public class TimeLineController { /** * show the timeline window and prompt for rebuilding database if necessary. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) void openTimeLine() { // listen for case changes (specifically images being added, and case changes). if (Case.isCaseOpen() && !listeningToAutopsy) { @@ -413,6 +454,16 @@ public class TimeLineController { } try { + if (eventsRepository.isRebuilding()) { + Platform.runLater(() -> { + if (taskProgressDialog != null) { + ((Stage) taskProgressDialog.getDialogPane().getScene().getWindow()).toFront(); + } + }); + + return; + } + boolean repoRebuilt = false; //has the repo been rebuilt long timeLineLastObjectId = eventsRepository.getLastObjID(); @@ -545,16 +596,13 @@ public class TimeLineController { /** * private method to build gui if necessary and make it visible. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) synchronized private void showWindow() { - SwingUtilities.invokeLater(() -> { - synchronized (TimeLineController.this) { - if (mainFrame == null) { - mainFrame = new TimeLineTopComponent(this); - } - mainFrame.open(); - mainFrame.toFront(); - } - }); + if (mainFrame == null) { + mainFrame = new TimeLineTopComponent(this); + } + mainFrame.open(); + mainFrame.toFront(); } synchronized public void pushEventTypeZoom(EventTypeZoomLevel typeZoomeLevel) { @@ -575,8 +623,7 @@ public class TimeLineController { } else if (currentZoom.hasTimeRange(timeRange) == false) { advance(currentZoom.withTimeRange(timeRange)); return true; - } - else{ + } else { return false; } } @@ -590,7 +637,7 @@ public class TimeLineController { final Long count = eventCounts.values().stream().reduce(0l, Long::sum); boolean shouldContinue = true; - if ((newLOD == DescriptionLoD.FULL|| newLOD ==DescriptionLoD.MEDIUM )&& count > 10_000) { + if ((newLOD == DescriptionLoD.FULL || newLOD == DescriptionLoD.MEDIUM) && count > 10_000) { String format = NumberFormat.getInstance().format(count); int showConfirmDialog = JOptionPane.showConfirmDialog(mainFrame, @@ -748,7 +795,8 @@ public class TimeLineController { * * @return true if the timeline window is open */ - synchronized private boolean isWindowOpen() { + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + private boolean isWindowOpen() { return mainFrame != null && mainFrame.isOpened() && mainFrame.isVisible(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java index b36c4a56c6..ea083f67d9 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java @@ -21,6 +21,7 @@ package org.sleuthkit.autopsy.timeline.db; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -30,6 +31,8 @@ import java.util.Map; import static java.util.Objects.isNull; import java.util.Set; import java.util.concurrent.CancellationException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.stream.Collectors; @@ -37,10 +40,8 @@ import javafx.beans.property.ReadOnlyObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.ObservableMap; -import javax.annotation.concurrent.GuardedBy; +import javafx.concurrent.Task; import javax.swing.JOptionPane; -import javax.swing.SwingUtilities; -import javax.swing.SwingWorker; import org.apache.commons.lang3.StringUtils; import org.controlsfx.dialog.ProgressDialog; import org.joda.time.Interval; @@ -50,7 +51,8 @@ import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.services.TagsManager; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.timeline.ProgressWindow; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; +import org.sleuthkit.autopsy.timeline.ProgressUpdate; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; @@ -93,8 +95,7 @@ public class EventsRepository { private final EventDB eventDB; - @GuardedBy("this") - private SwingWorker dbPopulationWorker; + private Task dbPopulationWorker; private final LoadingCache maxCache; @@ -177,15 +178,15 @@ public class EventsRepository { // return eventDB.getMinTime(); } - public void recordLastArtifactID(long lastArtfID) { + private void recordLastArtifactID(long lastArtfID) { eventDB.recordLastArtifactID(lastArtfID); } - public void recordWasIngestRunning(Boolean wasIngestRunning) { + private void recordWasIngestRunning(Boolean wasIngestRunning) { eventDB.recordWasIngestRunning(wasIngestRunning); } - public void recordLastObjID(Long lastObjID) { + private void recordLastObjID(Long lastObjID) { eventDB.recordLastObjID(lastObjID); } @@ -324,22 +325,48 @@ public class EventsRepository { } } - synchronized public void rebuildRepository(Runnable r) { + Executor workerExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("eventrepository-worker-%d").build()); + + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + public Task rebuildRepository(long lastObjId, long lastArtfID, Boolean injestRunning) { if (dbPopulationWorker != null) { dbPopulationWorker.cancel(true); - + LOGGER.log(Level.INFO, "cancelling previous db worker"); } - dbPopulationWorker = new DBPopulationWorker(r); - dbPopulationWorker.execute(); + LOGGER.log(Level.INFO, "rebuilding repo"); + dbPopulationWorker = new DBPopulationWorker(lastObjId, lastArtfID, injestRunning); + workerExecutor.execute(dbPopulationWorker); + return dbPopulationWorker; } - synchronized public void rebuildTags(Runnable r) { + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + public Task rebuildTags() { if (dbPopulationWorker != null) { dbPopulationWorker.cancel(true); - + LOGGER.log(Level.INFO, "cancelling previous db worker"); } - dbPopulationWorker = new RebuildTagsWorker(r); - dbPopulationWorker.execute(); + LOGGER.log(Level.INFO, "rebuilding tags"); + dbPopulationWorker = new RebuildTagsWorker(); + workerExecutor.execute(dbPopulationWorker); + return dbPopulationWorker; + } + + /** + * + * @param lastObjId the value of lastObjId + * @param lastArtfID the value of lastArtfID + * @param injestRunning the value of injestRunning + * @param timeLineController the value of timeLineController + */ + public void recordDBPopulationState(final long lastObjId, final long lastArtfID, final Boolean injestRunning) { + recordLastObjID(lastObjId); + recordLastArtifactID(lastArtfID); + recordWasIngestRunning(injestRunning); + } + + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + public boolean isRebuilding() { + return dbPopulationWorker != null && dbPopulationWorker.isDone() == false; } /** @@ -352,26 +379,19 @@ public class EventsRepository { * these to a JavaFX implementation,and replace {@link ProgressWindow} with * {@link ProgressDialog} */ - private abstract class DBProgressWorker extends SwingWorker { - - //TODO: can we avoid this with a state listener? does it amount to the same thing? - //post population operation to execute - final Runnable postPopulationOperation; + private abstract class DBProgressWorker extends Task { final SleuthkitCase skCase; final TagsManager tagsManager; - final ProgressWindow progressDialog; volatile ProgressHandle progressHandle; - DBProgressWorker(Runnable postPopulationOperation, String initialProgressDisplayName) { - progressDialog = new ProgressWindow(null, false, this); - progressDialog.setVisible(true); + DBProgressWorker(String initialProgressDisplayName) { progressHandle = ProgressHandleFactory.createHandle(initialProgressDisplayName, () -> cancel(true)); skCase = autoCase.getSleuthkitCase(); tagsManager = autoCase.getServices().getTagsManager(); - this.postPopulationOperation = postPopulationOperation; + } /** @@ -379,10 +399,11 @@ public class EventsRepository { * * @param chunk */ - final protected void update(ProgressWindow.ProgressUpdate chunk) { - SwingUtilities.invokeLater(() -> { - progressDialog.update(chunk); - }); + final protected void update(ProgressUpdate chunk) { + updateProgress(chunk.getProgress(), chunk.getTotal()); + updateMessage(chunk.getDetailMessage()); + updateTitle(chunk.getHeaderMessage()); + if (chunk.getTotal() >= 0) { progressHandle.progress(chunk.getProgress()); } @@ -390,12 +411,6 @@ public class EventsRepository { progressHandle.progress(chunk.getDetailMessage()); } - @Override - protected void done() { - super.done(); - progressDialog.close(); - postPopulationOperation.run(); //execute post db population operation - } } public boolean areFiltersEquivalent(RootFilter f1, RootFilter f2) { @@ -405,15 +420,15 @@ public class EventsRepository { private class RebuildTagsWorker extends DBProgressWorker { @NbBundle.Messages("RebuildTagsWorker.task.displayName=refreshing tags") - RebuildTagsWorker(Runnable postPopulationOperation) { - super(postPopulationOperation, Bundle.RebuildTagsWorker_task_displayName()); + RebuildTagsWorker() { + super(Bundle.RebuildTagsWorker_task_displayName()); } @Override @NbBundle.Messages({"progressWindow.msg.refreshingFileTags=refreshing file tags", "progressWindow.msg.refreshingResultTags=refreshing result tags", "progressWindow.msg.commitingTags=committing tag changes"}) - protected Void doInBackground() throws Exception { + protected Void call() throws Exception { int currentWorkTotal; progressHandle.start(); @@ -432,7 +447,7 @@ public class EventsRepository { if (isCancelled()) { break; } - update(new ProgressWindow.ProgressUpdate(i, currentWorkTotal, Bundle.progressWindow_msg_refreshingFileTags())); + update(new ProgressUpdate(i, currentWorkTotal, Bundle.progressWindow_msg_refreshingFileTags())); ContentTag contentTag = contentTags.get(i); eventDB.addTag(contentTag.getContent().getId(), null, contentTag); } @@ -448,7 +463,7 @@ public class EventsRepository { if (isCancelled()) { break; } - update(new ProgressWindow.ProgressUpdate(i, currentWorkTotal, Bundle.progressWindow_msg_refreshingResultTags())); + update(new ProgressUpdate(i, currentWorkTotal, Bundle.progressWindow_msg_refreshingResultTags())); BlackboardArtifactTag artifactTag = artifactTags.get(i); eventDB.addTag(artifactTag.getContent().getId(), artifactTag.getArtifact().getArtifactID(), artifactTag); } @@ -457,7 +472,7 @@ public class EventsRepository { progressHandle.finish(); progressHandle = ProgressHandleFactory.createHandle(Bundle.progressWindow_msg_commitingTags()); progressHandle.start(); - update(new ProgressWindow.ProgressUpdate(0, -1, Bundle.progressWindow_msg_commitingTags())); + update(new ProgressUpdate(0, -1, Bundle.progressWindow_msg_commitingTags())); if (isCancelled()) { eventDB.rollBackTransaction(trans); @@ -491,18 +506,25 @@ public class EventsRepository { private class DBPopulationWorker extends DBProgressWorker { + private final long lastObjId; + private final long lastArtfID; + private final boolean injestRunning; + @NbBundle.Messages("DBPopulationWorker.task.displayName=(re)initializing events database") - public DBPopulationWorker(Runnable postPopulationOperation) { - super(postPopulationOperation, Bundle.DBPopulationWorker_task_displayName()); + DBPopulationWorker(long lastObjId, long lastArtfID, Boolean injestRunning) { + super(Bundle.DBPopulationWorker_task_displayName()); + this.lastObjId = lastObjId; + this.lastArtfID = lastArtfID; + this.injestRunning = injestRunning; } @Override @NbBundle.Messages({"progressWindow.msg.populateMacEventsFiles=Populating MAC time events for files:", "progressWindow.msg.reinit_db=(re)initializing events database", "progressWindow.msg.commitingDb=committing events db"}) - protected Void doInBackground() throws Exception { + protected Void call() throws Exception { progressHandle.start(); - update(new ProgressWindow.ProgressUpdate(0, -1, Bundle.progressWindow_msg_reinit_db())); + update(new ProgressUpdate(0, -1, Bundle.progressWindow_msg_reinit_db())); //reset database //TODO: can we do more incremental updates? -jm eventDB.reInitializeDB(); @@ -510,7 +532,7 @@ public class EventsRepository { List fileIDs = skCase.findAllFileIdsWhere("name != '.' AND name != '..'"); final int numFiles = fileIDs.size(); progressHandle.switchToDeterminate(numFiles); - update(new ProgressWindow.ProgressUpdate(0, numFiles, Bundle.progressWindow_msg_populateMacEventsFiles())); + update(new ProgressUpdate(0, numFiles, Bundle.progressWindow_msg_populateMacEventsFiles())); //insert file events into db EventDB.EventTransaction trans = eventDB.beginTransaction(); @@ -526,7 +548,7 @@ public class EventsRepository { LOGGER.log(Level.WARNING, "Failed to get data for file : {0}", fID); // NON-NLS } else { insertEventsForFile(f, trans); - update(new ProgressWindow.ProgressUpdate(i, numFiles, + update(new ProgressUpdate(i, numFiles, Bundle.progressWindow_msg_populateMacEventsFiles(), f.getName())); } } catch (TskCoreException tskCoreException) { @@ -550,7 +572,7 @@ public class EventsRepository { progressHandle.finish(); progressHandle = ProgressHandleFactory.createHandle(Bundle.progressWindow_msg_commitingDb()); progressHandle.start(); - update(new ProgressWindow.ProgressUpdate(0, -1, Bundle.progressWindow_msg_commitingDb())); + update(new ProgressUpdate(0, -1, Bundle.progressWindow_msg_commitingDb())); if (isCancelled()) { eventDB.rollBackTransaction(trans); @@ -561,6 +583,7 @@ public class EventsRepository { populateFilterData(skCase); invalidateCaches(); + recordDBPopulationState(lastObjId, lastArtfID, injestRunning); progressHandle.finish(); return null; } @@ -573,10 +596,12 @@ public class EventsRepository { timeMap.put(FileSystemTypes.FILE_CHANGED, f.getCtime()); timeMap.put(FileSystemTypes.FILE_MODIFIED, f.getMtime()); - /* if there are no legitimate ( greater tan zero ) time stamps ( eg, + /* + * if there are no legitimate ( greater tan zero ) time stamps ( eg, * logical/local files) skip the rest of the event generation: this * should result in droping logical files, since they do not have - * legitimate time stamps. */ + * legitimate time stamps. + */ if (Collections.max(timeMap.values()) > 0) { final String uniquePath = f.getUniquePath(); final String parentPath = f.getParentPath(); @@ -593,8 +618,10 @@ public class EventsRepository { List tags = tagsManager.getContentTagsByContent(f); for (Map.Entry timeEntry : timeMap.entrySet()) { - /* if the time is legitimate ( greater than zero ) insert it - * into the db */ + /* + * if the time is legitimate ( greater than zero ) insert it + * into the db + */ if (timeEntry.getValue() > 0) { eventDB.insertEvent(timeEntry.getValue(), timeEntry.getKey(), datasourceID, f.getId(), null, uniquePath, medDesc, @@ -640,7 +667,7 @@ public class EventsRepository { try { //for each artifact, extract the relevant information for the descriptions insertEventForArtifact(type, blackboardArtifacts.get(i), trans); - update(new ProgressWindow.ProgressUpdate(i, numArtifacts, + update(new ProgressUpdate(i, numArtifacts, Bundle.progressWindow_populatingXevents(type.getDisplayName()))); } catch (TskCoreException ex) { LOGGER.log(Level.SEVERE, "There was a problem inserting event for artifact: " + blackboardArtifacts.get(i).getArtifactID(), ex); // NON-NLS @@ -653,8 +680,10 @@ public class EventsRepository { private void insertEventForArtifact(final ArtifactEventType type, BlackboardArtifact bbart, EventDB.EventTransaction trans) throws TskCoreException { ArtifactEventType.AttributeEventDescription eventDescription = ArtifactEventType.buildEventDescription(type, bbart); - /* if the time is legitimate ( greater than zero ) insert it into - * the db */ + /* + * if the time is legitimate ( greater than zero ) insert it into + * the db + */ if (eventDescription != null && eventDescription.getTime() > 0) { long objectID = bbart.getObjectID(); AbstractFile f = skCase.getAbstractFileById(objectID); From 3c3b05a7dc54a3aabbbadcd222bb7ad576a985f0 Mon Sep 17 00:00:00 2001 From: jmillman Date: Mon, 16 Nov 2015 11:23:21 -0500 Subject: [PATCH 2/5] drop extra confirmation dialog --- .../autopsy/timeline/TimeLineController.java | 31 +++---------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java index 07e8af5824..5407ebda8b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java @@ -30,7 +30,6 @@ import java.time.ZoneId; import java.util.Collection; import java.util.Map; import java.util.MissingResourceException; -import java.util.Optional; import java.util.TimeZone; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -52,12 +51,8 @@ import javafx.beans.property.ReadOnlyStringWrapper; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.concurrent.Task; -import javafx.concurrent.WorkerStateEvent; -import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; -import javafx.scene.control.DialogEvent; import javafx.scene.image.Image; -import javafx.stage.Modality; import javafx.stage.Stage; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.Immutable; @@ -335,7 +330,7 @@ public class TimeLineController { final Boolean injestRunning = IngestManager.getInstance().isIngestRunning(); final Task rebuildRepository = eventsRepository.rebuildRepository(lastObjId, lastArtfID, injestRunning); Platform.runLater(() -> { - rebuildRepository.setOnSucceeded((WorkerStateEvent event) -> { + rebuildRepository.setOnSucceeded(succeeded -> { //this is run on jfx thread //TODO: this looks hacky. what is going on? should this be an event? @@ -370,7 +365,7 @@ public class TimeLineController { Task rebuildTags = eventsRepository.rebuildTags(); Platform.runLater(() -> { - rebuildTags.setOnSucceeded((WorkerStateEvent event) -> { + rebuildTags.setOnSucceeded(succeeded -> { SwingUtilities.invokeLater(TimeLineController.this::showWindow); showFullRange();; }); @@ -385,13 +380,10 @@ public class TimeLineController { } } - @NbBundle.Messages({"Timeline.progressWindow.title=Populating Timeline data", - "Timeline.ProgressWindow.cancel.confdlg.msg=Not all events will be present of accurate if you cancel the timeline population. Do you want to cancel?", - "Timeline.ProgressWindow.cancel.confdlg.detail=Cancel timeline population?"}) + @NbBundle.Messages({"Timeline.progressWindow.title=Populating Timeline Data"}) private ProgressDialog showProgressDialogForTask(Task task) { ProgressDialog progressDialog = new ProgressDialog(task); progressDialog.getDialogPane().getButtonTypes().add(ButtonType.CANCEL); - progressDialog.setTitle(Bundle.Timeline_progressWindow_title()); progressDialog.getDialogPane().headerTextProperty().bind(task.titleProperty()); try { @@ -400,22 +392,7 @@ public class TimeLineController { } catch (IOException iOException) { LOGGER.log(Level.WARNING, "Failed to laod branded icon for progress dialog.", iOException); } - progressDialog.setOnCloseRequest((DialogEvent event) -> { - Alert confirmation = new Alert(Alert.AlertType.CONFIRMATION, Bundle.Timeline_ProgressWindow_cancel_confdlg_msg(), ButtonType.YES, ButtonType.NO); - confirmation.setHeaderText(null); - confirmation.setTitle(Bundle.Timeline_ProgressWindow_cancel_confdlg_detail()); - confirmation.initOwner(progressDialog.getDialogPane().getScene().getWindow()); - confirmation.initModality(Modality.WINDOW_MODAL); - Optional showAndWait = confirmation.showAndWait(); - - showAndWait.ifPresent(buttonType -> { - if (buttonType == ButtonType.YES) { - task.cancel(true); - } else { - event.consume(); - } - }); - }); + progressDialog.setOnCloseRequest(closeRequestEvent -> task.cancel(true)); return progressDialog; } From 77e1d3f8ee96d2fbb5df4748aa3c2d6848a9d86c Mon Sep 17 00:00:00 2001 From: jmillman Date: Mon, 16 Nov 2015 11:55:04 -0500 Subject: [PATCH 3/5] comments and minor refactoring --- .../autopsy/timeline/TimeLineController.java | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java index 5407ebda8b..5fb884ef31 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java @@ -52,6 +52,7 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.concurrent.Task; import javafx.scene.control.ButtonType; +import javafx.scene.control.DialogPane; import javafx.scene.image.Image; import javafx.stage.Stage; import javax.annotation.concurrent.GuardedBy; @@ -103,7 +104,8 @@ import org.sleuthkit.datamodel.TskCoreException; * *
  • Since eventsRepository is internally synchronized, only compound * access to it needs external synchronization
  • *
  • Other state including listeningToAutopsy, mainFrame, viewMode, and the - * listeners should only be accessed with this object's intrinsic lock held + * listeners should only be accessed with this object's intrinsic lock held, or + * on the EDT as indicated. *
  • *
      */ @@ -190,11 +192,11 @@ public class TimeLineController { return taskTitle.getReadOnlyProperty(); } - @GuardedBy("this") + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private TimeLineTopComponent mainFrame; //are the listeners currently attached - @GuardedBy("this") + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private boolean listeningToAutopsy = false; private final PropertyChangeListener caseListener = new AutopsyCaseListener(); @@ -371,9 +373,9 @@ public class TimeLineController { }); taskProgressDialog = showProgressDialogForTask(rebuildTags); }); - } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private void closeTimelineWindow() { if (isWindowOpen()) { mainFrame.close(); @@ -383,37 +385,44 @@ public class TimeLineController { @NbBundle.Messages({"Timeline.progressWindow.title=Populating Timeline Data"}) private ProgressDialog showProgressDialogForTask(Task task) { ProgressDialog progressDialog = new ProgressDialog(task); - progressDialog.getDialogPane().getButtonTypes().add(ButtonType.CANCEL); progressDialog.setTitle(Bundle.Timeline_progressWindow_title()); - progressDialog.getDialogPane().headerTextProperty().bind(task.titleProperty()); - try { - Image image = new Image(new URL("nbresloc:/org/netbeans/core/startup/frame.gif").openStream()); - ((Stage) progressDialog.getDialogPane().getScene().getWindow()).getIcons().setAll(image); - } catch (IOException iOException) { - LOGGER.log(Level.WARNING, "Failed to laod branded icon for progress dialog.", iOException); - } + DialogPane dialogPane = progressDialog.getDialogPane(); + dialogPane.getButtonTypes().add(ButtonType.CANCEL); + Stage dialogStage = (Stage) dialogPane.getScene().getWindow(); + progressDialog.headerTextProperty().addListener(observable -> dialogStage.sizeToScene()); + progressDialog.headerTextProperty().bind(task.titleProperty()); + dialogStage.getIcons().setAll(LOGO); progressDialog.setOnCloseRequest(closeRequestEvent -> task.cancel(true)); return progressDialog; } + private static final Image LOGO; + + static { + Image x = null; + try { + x = new Image(new URL("nbresloc:/org/netbeans/core/startup/frame.gif").openStream()); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "Failed to laod branded icon for progress dialog.", ex); + } + LOGO = x; + } + public void showFullRange() { synchronized (filteredEvents) { pushTimeRange(filteredEvents.getSpanningInterval()); } } - synchronized public void closeTimeLine() { + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + public void closeTimeLine() { if (mainFrame != null) { listeningToAutopsy = false; IngestManager.getInstance().removeIngestModuleEventListener(ingestModuleListener); IngestManager.getInstance().removeIngestJobEventListener(ingestJobListener); Case.removePropertyChangeListener(caseListener); - SwingUtilities.invokeLater(() -> { - synchronized (TimeLineController.this) { - mainFrame.close(); - mainFrame = null; - } - }); + mainFrame.close(); + mainFrame = null; } } From 41e32bc09a4afd0263041f41b87fd10831e4d559 Mon Sep 17 00:00:00 2001 From: jmillman Date: Mon, 16 Nov 2015 14:03:57 -0500 Subject: [PATCH 4/5] use a reusable service rather than tasks. refactor threading some --- .../autopsy/timeline/TimeLineController.java | 66 ++++---- .../autopsy/timeline/db/EventsRepository.java | 153 +++++++++++------- 2 files changed, 125 insertions(+), 94 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java index 5fb884ef31..4a04a4a630 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java @@ -51,6 +51,7 @@ import javafx.beans.property.ReadOnlyStringWrapper; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.concurrent.Task; +import javafx.concurrent.Worker; import javafx.scene.control.ButtonType; import javafx.scene.control.DialogPane; import javafx.scene.image.Image; @@ -323,18 +324,12 @@ public class TimeLineController { return false; } } - LOGGER.log(Level.INFO, "Beginning generation of timeline"); // NON-NLS - try { - closeTimelineWindow(); - final SleuthkitCase sleuthkitCase = Case.getCurrentCase().getSleuthkitCase(); - final long lastObjId = sleuthkitCase.getLastObjectId(); - final long lastArtfID = getCaseLastArtifactID(sleuthkitCase); - final Boolean injestRunning = IngestManager.getInstance().isIngestRunning(); - final Task rebuildRepository = eventsRepository.rebuildRepository(lastObjId, lastArtfID, injestRunning); - Platform.runLater(() -> { - rebuildRepository.setOnSucceeded(succeeded -> { - //this is run on jfx thread - + SwingUtilities.invokeLater(this::closeTimelineWindow); + Platform.runLater(() -> { + final Worker rebuildRepository = eventsRepository.rebuildRepository(); + rebuildRepository.stateProperty().addListener((stateProperty, oldState, newSate) -> { + //this will be on JFX thread + if (newSate == Worker.State.SUCCEEDED) { //TODO: this looks hacky. what is going on? should this be an event? needsHistogramRebuild.set(true); needsHistogramRebuild.set(false); @@ -344,14 +339,11 @@ public class TimeLineController { newEventsFlag.set(false); historyManager.reset(filteredEvents.zoomParametersProperty().get()); TimeLineController.this.showFullRange(); - }); - taskProgressDialog = showProgressDialogForTask(rebuildRepository); - }); - } catch (TskCoreException ex) { - LOGGER.log(Level.SEVERE, "Error when generating timeline, ", ex); // NON-NLS - return false; - } + } + }); + showProgressDialog(rebuildRepository); + }); return true; } @@ -360,18 +352,19 @@ public class TimeLineController { * tags table and rebuild it by querying for all the tags and inserting them * in to the TimeLine DB. */ - @ThreadConfined(type = ThreadConfined.ThreadType.AWT) void rebuildTagsTable() { - LOGGER.log(Level.INFO, "starting to rebuild tags table"); // NON-NLS - closeTimelineWindow(); - Task rebuildTags = eventsRepository.rebuildTags(); + SwingUtilities.invokeLater(this::closeTimelineWindow); Platform.runLater(() -> { - rebuildTags.setOnSucceeded(succeeded -> { - SwingUtilities.invokeLater(TimeLineController.this::showWindow); - showFullRange();; + Worker rebuildTags = eventsRepository.rebuildTags(); + rebuildTags.stateProperty().addListener((stateProperty, oldState, newSate) -> { + //this will be on JFX thread + if (newSate == Worker.State.SUCCEEDED) { + SwingUtilities.invokeLater(TimeLineController.this::showWindow); + showFullRange(); + } }); - taskProgressDialog = showProgressDialogForTask(rebuildTags); + showProgressDialog(rebuildTags); }); } @@ -382,18 +375,18 @@ public class TimeLineController { } } + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) @NbBundle.Messages({"Timeline.progressWindow.title=Populating Timeline Data"}) - private ProgressDialog showProgressDialogForTask(Task task) { - ProgressDialog progressDialog = new ProgressDialog(task); - progressDialog.setTitle(Bundle.Timeline_progressWindow_title()); - DialogPane dialogPane = progressDialog.getDialogPane(); + private void showProgressDialog(Worker task) { + taskProgressDialog = new ProgressDialog(task); + taskProgressDialog.setTitle(Bundle.Timeline_progressWindow_title()); + DialogPane dialogPane = taskProgressDialog.getDialogPane(); dialogPane.getButtonTypes().add(ButtonType.CANCEL); Stage dialogStage = (Stage) dialogPane.getScene().getWindow(); - progressDialog.headerTextProperty().addListener(observable -> dialogStage.sizeToScene()); - progressDialog.headerTextProperty().bind(task.titleProperty()); + dialogPane.setPrefWidth(400); + taskProgressDialog.headerTextProperty().bind(task.titleProperty()); dialogStage.getIcons().setAll(LOGO); - progressDialog.setOnCloseRequest(closeRequestEvent -> task.cancel(true)); - return progressDialog; + taskProgressDialog.setOnCloseRequest(closeRequestEvent -> task.cancel()); } private static final Image LOGO; @@ -446,7 +439,6 @@ public class TimeLineController { ((Stage) taskProgressDialog.getDialogPane().getScene().getWindow()).toFront(); } }); - return; } @@ -502,7 +494,7 @@ public class TimeLineController { } } - private long getCaseLastArtifactID(final SleuthkitCase sleuthkitCase) { + public static long getCaseLastArtifactID(final SleuthkitCase sleuthkitCase) { long caseLastArtfId = -1; String query = "select Max(artifact_id) as max_id from blackboard_artifacts"; // NON-NLS try (CaseDbQuery dbQuery = sleuthkitCase.executeQuery(query)) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java index ea083f67d9..de081c1d02 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java @@ -31,19 +31,23 @@ import java.util.Map; import static java.util.Objects.isNull; import java.util.Set; import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.stream.Collectors; +import javafx.application.Platform; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.ObservableMap; +import javafx.concurrent.Service; import javafx.concurrent.Task; +import javafx.concurrent.Worker; import javax.swing.JOptionPane; import org.apache.commons.lang3.StringUtils; -import org.controlsfx.dialog.ProgressDialog; import org.joda.time.Interval; import org.netbeans.api.progress.ProgressHandle; import org.netbeans.api.progress.ProgressHandleFactory; @@ -52,7 +56,9 @@ import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.services.TagsManager; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; +import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.timeline.ProgressUpdate; +import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; @@ -95,7 +101,7 @@ public class EventsRepository { private final EventDB eventDB; - private Task dbPopulationWorker; + private final DBPopulationService dbPopulationService = new DBPopulationService(this); private final LoadingCache maxCache; @@ -325,38 +331,72 @@ public class EventsRepository { } } - Executor workerExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("eventrepository-worker-%d").build()); + static private final Executor workerExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("eventrepository-worker-%d").build()); - @ThreadConfined(type = ThreadConfined.ThreadType.AWT) - public Task rebuildRepository(long lastObjId, long lastArtfID, Boolean injestRunning) { - if (dbPopulationWorker != null) { - dbPopulationWorker.cancel(true); - LOGGER.log(Level.INFO, "cancelling previous db worker"); - } - LOGGER.log(Level.INFO, "rebuilding repo"); - dbPopulationWorker = new DBPopulationWorker(lastObjId, lastArtfID, injestRunning); - workerExecutor.execute(dbPopulationWorker); - return dbPopulationWorker; + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + public Worker rebuildRepository() { + return rebuildRepository(DBPopulationService.DBPopulationMode.FULL); } - @ThreadConfined(type = ThreadConfined.ThreadType.AWT) - public Task rebuildTags() { - if (dbPopulationWorker != null) { - dbPopulationWorker.cancel(true); - LOGGER.log(Level.INFO, "cancelling previous db worker"); - } - LOGGER.log(Level.INFO, "rebuilding tags"); - dbPopulationWorker = new RebuildTagsWorker(); - workerExecutor.execute(dbPopulationWorker); - return dbPopulationWorker; + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + public Worker rebuildTags() { + return rebuildRepository(DBPopulationService.DBPopulationMode.TAGS_ONLY); } /** * - * @param lastObjId the value of lastObjId - * @param lastArtfID the value of lastArtfID - * @param injestRunning the value of injestRunning - * @param timeLineController the value of timeLineController + * @param mode the value of mode + */ + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private Worker rebuildRepository(final DBPopulationService.DBPopulationMode mode) { + LOGGER.log(Level.INFO, "(re) starting {0}db population task", mode); + dbPopulationService.setDBPopulationMode(mode); + dbPopulationService.restart(); + return dbPopulationService; + } + + private static class DBPopulationService extends Service { + + enum DBPopulationMode { + + FULL, + TAGS_ONLY; + } + + private final EventsRepository eventRepo; + + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private DBPopulationMode dbPopulationMode = DBPopulationMode.FULL; + + DBPopulationService(EventsRepository eventRepo) { + this.eventRepo = eventRepo; + setExecutor(workerExecutor); + } + + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + public final void setDBPopulationMode(DBPopulationMode value) { + dbPopulationMode = value; + } + + @Override + protected Task createTask() { + DBPopulationMode dbPopMode = dbPopulationMode; + switch (dbPopMode) { + case FULL: + return eventRepo.new DBPopulationWorker(); + case TAGS_ONLY: + return eventRepo.new RebuildTagsWorker(); + default: + throw new IllegalArgumentException("Unknown db population mode: " + dbPopMode + ". Skipping db population."); + } + } + } + + /** + * + * @param lastObjId the value of lastObjId + * @param lastArtfID the value of lastArtfID + * @param injestRunning the value of injestRunning */ public void recordDBPopulationState(final long lastObjId, final long lastArtfID, final Boolean injestRunning) { recordLastObjID(lastObjId); @@ -364,20 +404,23 @@ public class EventsRepository { recordWasIngestRunning(injestRunning); } - @ThreadConfined(type = ThreadConfined.ThreadType.AWT) public boolean isRebuilding() { - return dbPopulationWorker != null && dbPopulationWorker.isDone() == false; + FutureTask task = new FutureTask<>(dbPopulationService::isRunning); + Platform.runLater(task); + try { + return task.get(); + } catch (InterruptedException interruptedException) { + } catch (ExecutionException executionException) { + } + return false; } /** - * A base class for swing workers that shows a {@link ProgressWorker} and - * updates a {@link ProgressHandle} as it performs its background work, and - * calls a call-back when finished. + * A base class for Tasks that show a updates a {@link ProgressHandle} as it + * performs its background work on the events DB. * - * //TODO: I prefer the JavaFX task API as it has built in progress - * properties that can be bound to a javafx progress indicator. Convert - * these to a JavaFX implementation,and replace {@link ProgressWindow} with - * {@link ProgressDialog} + * //TODO: I don't like the coupling to ProgressHandle, but the + * alternatives I can think of seem even worse. -jm */ private abstract class DBProgressWorker extends Task { @@ -387,11 +430,9 @@ public class EventsRepository { volatile ProgressHandle progressHandle; DBProgressWorker(String initialProgressDisplayName) { - progressHandle = ProgressHandleFactory.createHandle(initialProgressDisplayName, () -> cancel(true)); - + progressHandle = ProgressHandleFactory.createHandle(initialProgressDisplayName, this::cancel); skCase = autoCase.getSleuthkitCase(); tagsManager = autoCase.getServices().getTagsManager(); - } /** @@ -429,8 +470,6 @@ public class EventsRepository { "progressWindow.msg.refreshingResultTags=refreshing result tags", "progressWindow.msg.commitingTags=committing tag changes"}) protected Void call() throws Exception { - int currentWorkTotal; - progressHandle.start(); EventDB.EventTransaction trans = eventDB.beginTransaction(); LOGGER.log(Level.INFO, "dropping old tags"); // NON-NLS @@ -439,9 +478,9 @@ public class EventsRepository { LOGGER.log(Level.INFO, "updating content tags"); // NON-NLS List contentTags = tagsManager.getAllContentTags(); progressHandle.finish(); - progressHandle = ProgressHandleFactory.createHandle(Bundle.progressWindow_msg_refreshingFileTags(), - () -> cancel(true)); - progressHandle.start(currentWorkTotal = contentTags.size()); + progressHandle = ProgressHandleFactory.createHandle(Bundle.progressWindow_msg_refreshingFileTags(), this::cancel); + int currentWorkTotal = contentTags.size(); + progressHandle.start(currentWorkTotal); for (int i = 0; i < currentWorkTotal; i++) { if (isCancelled()) { @@ -455,9 +494,9 @@ public class EventsRepository { LOGGER.log(Level.INFO, "updating artifact tags"); // NON-NLS List artifactTags = tagsManager.getAllBlackboardArtifactTags(); progressHandle.finish(); - progressHandle = ProgressHandleFactory.createHandle(Bundle.progressWindow_msg_refreshingResultTags(), - () -> cancel(true)); - progressHandle.start(currentWorkTotal = artifactTags.size()); + progressHandle = ProgressHandleFactory.createHandle(Bundle.progressWindow_msg_refreshingResultTags(), this::cancel); + currentWorkTotal = artifactTags.size(); + progressHandle.start(currentWorkTotal); for (int i = 0; i < currentWorkTotal; i++) { if (isCancelled()) { @@ -506,28 +545,28 @@ public class EventsRepository { private class DBPopulationWorker extends DBProgressWorker { - private final long lastObjId; - private final long lastArtfID; - private final boolean injestRunning; - @NbBundle.Messages("DBPopulationWorker.task.displayName=(re)initializing events database") - DBPopulationWorker(long lastObjId, long lastArtfID, Boolean injestRunning) { + DBPopulationWorker() { super(Bundle.DBPopulationWorker_task_displayName()); - this.lastObjId = lastObjId; - this.lastArtfID = lastArtfID; - this.injestRunning = injestRunning; } @Override - @NbBundle.Messages({"progressWindow.msg.populateMacEventsFiles=Populating MAC time events for files:", - "progressWindow.msg.reinit_db=(re)initializing events database", + @NbBundle.Messages({"progressWindow.msg.populateMacEventsFiles=Populating MAC time events for files", + "progressWindow.msg.reinit_db=(Re)Initializing events database", + "progressWindow.msg.gatheringData=Gather event data", "progressWindow.msg.commitingDb=committing events db"}) protected Void call() throws Exception { + LOGGER.log(Level.INFO, "Beginning population of timeline db."); // NON-NLS progressHandle.start(); update(new ProgressUpdate(0, -1, Bundle.progressWindow_msg_reinit_db())); //reset database //TODO: can we do more incremental updates? -jm eventDB.reInitializeDB(); + update(new ProgressUpdate(0, -1, Bundle.progressWindow_msg_gatheringData())); + long lastObjId = skCase.getLastObjectId(); + long lastArtfID = TimeLineController.getCaseLastArtifactID(skCase); + boolean injestRunning = IngestManager.getInstance().isIngestRunning(); + //grab ids of all files List fileIDs = skCase.findAllFileIdsWhere("name != '.' AND name != '..'"); final int numFiles = fileIDs.size(); From cda31217ef4f6277e5984d7543ab6545cd17e79f Mon Sep 17 00:00:00 2001 From: jmillman Date: Mon, 16 Nov 2015 14:22:58 -0500 Subject: [PATCH 5/5] refactor away ProgressUpdate.java --- .../autopsy/timeline/ProgressUpdate.java | 48 ------------ .../autopsy/timeline/db/EventsRepository.java | 74 +++++++++++-------- 2 files changed, 44 insertions(+), 78 deletions(-) delete mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ProgressUpdate.java diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ProgressUpdate.java b/Core/src/org/sleuthkit/autopsy/timeline/ProgressUpdate.java deleted file mode 100644 index 31783e2e3f..0000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/ProgressUpdate.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ - -package org.sleuthkit.autopsy.timeline; - -import javax.annotation.concurrent.Immutable; - -/** - * bundles up progress information to be shown in the progress dialog - */ -@Immutable -public class ProgressUpdate { - private final int progress; - private final int total; - private final String headerMessage; - private final String detailMessage; - - public int getProgress() { - return progress; - } - - public int getTotal() { - return total; - } - - public String getHeaderMessage() { - return headerMessage; - } - - public String getDetailMessage() { - return detailMessage; - } - - public ProgressUpdate(int progress, int total, String headerMessage, String detailMessage) { - this.progress = progress; - this.total = total; - this.headerMessage = headerMessage; - this.detailMessage = detailMessage; - } - - public ProgressUpdate(int progress, int total, String headerMessage) { - this(progress, total, headerMessage, ""); - } - -} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java index de081c1d02..d2b6046aa2 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java @@ -57,7 +57,6 @@ import org.sleuthkit.autopsy.casemodule.services.TagsManager; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.ingest.IngestManager; -import org.sleuthkit.autopsy.timeline.ProgressUpdate; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; @@ -409,8 +408,8 @@ public class EventsRepository { Platform.runLater(task); try { return task.get(); - } catch (InterruptedException interruptedException) { - } catch (ExecutionException executionException) { + } catch (InterruptedException | ExecutionException exception) { + LOGGER.log(Level.SEVERE, "There was an error determining the state of the db population service.", exception); } return false; } @@ -435,23 +434,34 @@ public class EventsRepository { tagsManager = autoCase.getServices().getTagsManager(); } - /** - * update progress UIs - * - * @param chunk - */ - final protected void update(ProgressUpdate chunk) { - updateProgress(chunk.getProgress(), chunk.getTotal()); - updateMessage(chunk.getDetailMessage()); - updateTitle(chunk.getHeaderMessage()); - - if (chunk.getTotal() >= 0) { - progressHandle.progress(chunk.getProgress()); - } - progressHandle.setDisplayName(chunk.getHeaderMessage()); - progressHandle.progress(chunk.getDetailMessage()); + @Override + protected void updateTitle(String title) { + super.updateTitle(title); + progressHandle.setDisplayName(title); } + @Override + protected void updateMessage(String message) { + super.updateMessage(message); + progressHandle.progress(message); + } + + @Override + protected void updateProgress(double workDone, double max) { + super.updateProgress(workDone, max); + if (workDone >= 0) { + progressHandle.progress((int) workDone); + } + } + + @Override + protected void updateProgress(long workDone, long max) { + super.updateProgress(workDone, max); + super.updateProgress(workDone, max); + if (workDone >= 0) { + progressHandle.progress((int) workDone); + } + } } public boolean areFiltersEquivalent(RootFilter f1, RootFilter f2) { @@ -479,6 +489,7 @@ public class EventsRepository { List contentTags = tagsManager.getAllContentTags(); progressHandle.finish(); progressHandle = ProgressHandleFactory.createHandle(Bundle.progressWindow_msg_refreshingFileTags(), this::cancel); + updateTitle(Bundle.progressWindow_msg_refreshingFileTags()); int currentWorkTotal = contentTags.size(); progressHandle.start(currentWorkTotal); @@ -486,7 +497,7 @@ public class EventsRepository { if (isCancelled()) { break; } - update(new ProgressUpdate(i, currentWorkTotal, Bundle.progressWindow_msg_refreshingFileTags())); + updateProgress(i, currentWorkTotal); ContentTag contentTag = contentTags.get(i); eventDB.addTag(contentTag.getContent().getId(), null, contentTag); } @@ -495,14 +506,14 @@ public class EventsRepository { List artifactTags = tagsManager.getAllBlackboardArtifactTags(); progressHandle.finish(); progressHandle = ProgressHandleFactory.createHandle(Bundle.progressWindow_msg_refreshingResultTags(), this::cancel); + updateTitle(Bundle.progressWindow_msg_refreshingResultTags()); currentWorkTotal = artifactTags.size(); progressHandle.start(currentWorkTotal); - for (int i = 0; i < currentWorkTotal; i++) { if (isCancelled()) { break; } - update(new ProgressUpdate(i, currentWorkTotal, Bundle.progressWindow_msg_refreshingResultTags())); + updateProgress(i, currentWorkTotal); BlackboardArtifactTag artifactTag = artifactTags.get(i); eventDB.addTag(artifactTag.getContent().getId(), artifactTag.getArtifact().getArtifactID(), artifactTag); } @@ -511,7 +522,8 @@ public class EventsRepository { progressHandle.finish(); progressHandle = ProgressHandleFactory.createHandle(Bundle.progressWindow_msg_commitingTags()); progressHandle.start(); - update(new ProgressUpdate(0, -1, Bundle.progressWindow_msg_commitingTags())); + updateTitle(Bundle.progressWindow_msg_commitingTags()); + updateProgress(-.5, 1); if (isCancelled()) { eventDB.rollBackTransaction(trans); @@ -558,11 +570,12 @@ public class EventsRepository { protected Void call() throws Exception { LOGGER.log(Level.INFO, "Beginning population of timeline db."); // NON-NLS progressHandle.start(); - update(new ProgressUpdate(0, -1, Bundle.progressWindow_msg_reinit_db())); + updateProgress(-.5, 1); + updateTitle(Bundle.progressWindow_msg_reinit_db()); //reset database //TODO: can we do more incremental updates? -jm eventDB.reInitializeDB(); - update(new ProgressUpdate(0, -1, Bundle.progressWindow_msg_gatheringData())); + updateTitle(Bundle.progressWindow_msg_gatheringData()); long lastObjId = skCase.getLastObjectId(); long lastArtfID = TimeLineController.getCaseLastArtifactID(skCase); boolean injestRunning = IngestManager.getInstance().isIngestRunning(); @@ -571,7 +584,7 @@ public class EventsRepository { List fileIDs = skCase.findAllFileIdsWhere("name != '.' AND name != '..'"); final int numFiles = fileIDs.size(); progressHandle.switchToDeterminate(numFiles); - update(new ProgressUpdate(0, numFiles, Bundle.progressWindow_msg_populateMacEventsFiles())); + updateTitle(Bundle.progressWindow_msg_populateMacEventsFiles()); //insert file events into db EventDB.EventTransaction trans = eventDB.beginTransaction(); @@ -587,8 +600,8 @@ public class EventsRepository { LOGGER.log(Level.WARNING, "Failed to get data for file : {0}", fID); // NON-NLS } else { insertEventsForFile(f, trans); - update(new ProgressUpdate(i, numFiles, - Bundle.progressWindow_msg_populateMacEventsFiles(), f.getName())); + updateProgress(i, numFiles); + updateMessage(f.getName()); } } catch (TskCoreException tskCoreException) { LOGGER.log(Level.SEVERE, "Failed to insert MAC time events for file : " + fID, tskCoreException); // NON-NLS @@ -611,7 +624,8 @@ public class EventsRepository { progressHandle.finish(); progressHandle = ProgressHandleFactory.createHandle(Bundle.progressWindow_msg_commitingDb()); progressHandle.start(); - update(new ProgressUpdate(0, -1, Bundle.progressWindow_msg_commitingDb())); + updateProgress(-0.5, 1); + updateTitle(Bundle.progressWindow_msg_commitingDb()); if (isCancelled()) { eventDB.rollBackTransaction(trans); @@ -702,12 +716,12 @@ public class EventsRepository { progressHandle.finish(); progressHandle = ProgressHandleFactory.createHandle(Bundle.progressWindow_populatingXevents(type.getDisplayName()), () -> cancel(true)); progressHandle.start(numArtifacts); + updateTitle(Bundle.progressWindow_populatingXevents(type.getDisplayName())); for (int i = 0; i < numArtifacts; i++) { try { //for each artifact, extract the relevant information for the descriptions insertEventForArtifact(type, blackboardArtifacts.get(i), trans); - update(new ProgressUpdate(i, numArtifacts, - Bundle.progressWindow_populatingXevents(type.getDisplayName()))); + updateProgress(i, numArtifacts); } catch (TskCoreException ex) { LOGGER.log(Level.SEVERE, "There was a problem inserting event for artifact: " + blackboardArtifacts.get(i).getArtifactID(), ex); // NON-NLS }