diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageTask.java b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageTask.java
index e4f03757fb..cf7db4a392 100644
--- a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageTask.java
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageTask.java
@@ -206,6 +206,9 @@ class AddImageTask implements Runnable {
if (!verificationError.isEmpty()) {
errorMessages.add(verificationError);
}
+ if(! imageWriterPath.isEmpty()){
+ Case.getCurrentCase().scheduleImageWriterFinish(imageId);
+ }
newDataSources.add(newImage);
} else {
String errorMessage = String.format("Error commiting adding image %s to the case database, no object id returned", imagePath); //NON-NLS
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties
index f852c046e6..e25d76738d 100644
--- a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties
@@ -76,7 +76,10 @@ LocalDiskPanel.timeZoneLabel.text=Please select the input timezone:
LocalDiskPanel.noFatOrphansCheckbox.toolTipText=
LocalDiskPanel.noFatOrphansCheckbox.text=Ignore orphan files in FAT file systems
LocalDiskPanel.descLabel.text=(faster results, although some data will not be searched)
-LocalDiskPanel.imageWriterError.text=Error - file already exists
+LocalDiskPanel.imageWriterDirError.text=Error - directory does not exist
+LocalDiskPanel.imageWriterEmptyPathError.text=Error - enter path for VHD
+LocalDiskPanel.imageWriterIsDirError.text=Error - VHD path is a directory
+LocalDiskPanel.imageWriterFileExistsError.text=Error - VHD path already exists
MissingImageDialog.browseButton.text=Browse
MissingImageDialog.pathNameTextField.text=
AddImageWizardAddingProgressVisual.progressTextArea.border.title=Status
@@ -243,6 +246,7 @@ IngestJobInfoPanel.jLabel1.text=Ingest Modules
IngestJobInfoPanel.jLabel2.text=Ingest Jobs
CaseInformationPanel.closeButton.text=Close
LocalDiskPanel.copyImageCheckbox.text=Make a VHD image of the drive while it is being analyzed
-LocalDiskPanel.imageWriterPathLabel.text=jLabel1
-LocalDiskPanel.imageWriterPathDescLabel.text=Will be saved in the Module Output/Image Writer directory as :
LocalDiskPanel.imageWriterErrorLabel.text=Error Label
+LocalDiskPanel.jLabel1.text=Note that at least one ingest module must be run to create a complete copy
+LocalDiskPanel.pathTextField.text=
+LocalDiskPanel.browseButton.text=Browse
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java
index d681e81b48..52166dab6a 100644
--- a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java
@@ -18,6 +18,7 @@
*/
package org.sleuthkit.autopsy.casemodule;
+import org.sleuthkit.autopsy.datasourceprocessors.ImageWriter;
import java.awt.Cursor;
import java.awt.Frame;
import java.beans.PropertyChangeListener;
@@ -286,6 +287,7 @@ public class Case implements SleuthkitCase.ErrorObserver {
private static final int MIN_SECS_BETWEEN_TSK_ERROR_REPORTS = 60;
private static final Logger logger = Logger.getLogger(Case.class.getName());
private static final AutopsyEventPublisher eventPublisher = new AutopsyEventPublisher();
+ private final ImageWriter imageWriter = new ImageWriter();
private static String appName;
private static Case currentCase;
private static CoordinationService.Lock currentCaseLock;
@@ -1437,6 +1439,7 @@ public class Case implements SleuthkitCase.ErrorObserver {
WindowManager.getDefault().getMainWindow().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
});
IngestManager.getInstance().cancelAllIngestJobs(IngestJob.CancellationReason.CASE_CLOSED);
+ oldCase.closeImageWriter();
completeCaseChange(null); //closes windows, etc
if (null != oldCase.tskErrorReporter) {
oldCase.tskErrorReporter.shutdown(); // stop listening for TSK errors for the old case
@@ -1613,6 +1616,22 @@ public class Case implements SleuthkitCase.ErrorObserver {
}
return currentCaseExecutor;
}
+
+ /**
+ * Register the ID of an image that is being copied using ImageWriter.
+ * This will cause the image to be finished after ingest is complete.
+ * @param imageID The image ID
+ */
+ void scheduleImageWriterFinish(long imageID){
+ imageWriter.addDataSourceId(imageID);
+ }
+
+ /**
+ * Cancel all tasks associated with Image Writer
+ */
+ void closeImageWriter(){
+ imageWriter.close();
+ }
/**
* Gets the time zone(s) of the image data source(s) in this case.
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.form b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.form
index e43c8b65c9..89835246a9 100644
--- a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.form
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.form
@@ -3,10 +3,10 @@
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.java
index ed53684b0c..2572c0c61d 100644
--- a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.java
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.java
@@ -32,6 +32,7 @@ import java.util.TimeZone;
import java.util.concurrent.CancellationException;
import java.util.logging.Level;
import javax.swing.ComboBoxModel;
+import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
@@ -58,8 +59,8 @@ final class LocalDiskPanel extends JPanel {
private static final long serialVersionUID = 1L;
private List disks;
private LocalDiskModel model;
- private String fullImageWriterPath = "";
private boolean enableNext = false;
+ private final JFileChooser fc = new JFileChooser();
/**
* Creates new form LocalDiskPanel
@@ -91,8 +92,10 @@ final class LocalDiskPanel extends JPanel {
errorLabel.setVisible(false);
errorLabel.setText("");
diskComboBox.setEnabled(false);
+ imageWriterErrorLabel.setOpaque(true);
imageWriterErrorLabel.setText("");
- imageWriterPathLabel.setText("");
+ pathTextField.setEnabled(copyImageCheckbox.isSelected());
+ browseButton.setEnabled(copyImageCheckbox.isSelected());
}
/**
@@ -112,12 +115,13 @@ final class LocalDiskPanel extends JPanel {
noFatOrphansCheckbox = new javax.swing.JCheckBox();
descLabel = new javax.swing.JLabel();
copyImageCheckbox = new javax.swing.JCheckBox();
- imageWriterPathLabel = new javax.swing.JLabel();
- imageWriterPathDescLabel = new javax.swing.JLabel();
imageWriterErrorLabel = new javax.swing.JLabel();
+ jLabel1 = new javax.swing.JLabel();
+ pathTextField = new javax.swing.JTextField();
+ browseButton = new javax.swing.JButton();
- setMinimumSize(new java.awt.Dimension(0, 65));
- setPreferredSize(new java.awt.Dimension(485, 65));
+ setMinimumSize(new java.awt.Dimension(0, 420));
+ setPreferredSize(new java.awt.Dimension(485, 410));
diskLabel.setFont(diskLabel.getFont().deriveFont(diskLabel.getFont().getStyle() & ~java.awt.Font.BOLD, 11));
org.openide.awt.Mnemonics.setLocalizedText(diskLabel, org.openide.util.NbBundle.getMessage(LocalDiskPanel.class, "LocalDiskPanel.diskLabel.text")); // NOI18N
@@ -142,40 +146,65 @@ final class LocalDiskPanel extends JPanel {
org.openide.awt.Mnemonics.setLocalizedText(descLabel, org.openide.util.NbBundle.getMessage(LocalDiskPanel.class, "LocalDiskPanel.descLabel.text")); // NOI18N
org.openide.awt.Mnemonics.setLocalizedText(copyImageCheckbox, org.openide.util.NbBundle.getMessage(LocalDiskPanel.class, "LocalDiskPanel.copyImageCheckbox.text")); // NOI18N
-
- org.openide.awt.Mnemonics.setLocalizedText(imageWriterPathLabel, org.openide.util.NbBundle.getMessage(LocalDiskPanel.class, "LocalDiskPanel.imageWriterPathLabel.text")); // NOI18N
-
- org.openide.awt.Mnemonics.setLocalizedText(imageWriterPathDescLabel, org.openide.util.NbBundle.getMessage(LocalDiskPanel.class, "LocalDiskPanel.imageWriterPathDescLabel.text")); // NOI18N
+ copyImageCheckbox.addActionListener(new java.awt.event.ActionListener() {
+ public void actionPerformed(java.awt.event.ActionEvent evt) {
+ copyImageCheckboxActionPerformed(evt);
+ }
+ });
imageWriterErrorLabel.setFont(imageWriterErrorLabel.getFont().deriveFont(imageWriterErrorLabel.getFont().getStyle() & ~java.awt.Font.BOLD, 11));
imageWriterErrorLabel.setForeground(new java.awt.Color(255, 0, 0));
org.openide.awt.Mnemonics.setLocalizedText(imageWriterErrorLabel, org.openide.util.NbBundle.getMessage(LocalDiskPanel.class, "LocalDiskPanel.imageWriterErrorLabel.text")); // NOI18N
+ org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(LocalDiskPanel.class, "LocalDiskPanel.jLabel1.text")); // NOI18N
+
+ pathTextField.setText(org.openide.util.NbBundle.getMessage(LocalDiskPanel.class, "LocalDiskPanel.pathTextField.text")); // NOI18N
+ pathTextField.addKeyListener(new java.awt.event.KeyAdapter() {
+ public void keyReleased(java.awt.event.KeyEvent evt) {
+ pathTextFieldKeyReleased(evt);
+ }
+ public void keyTyped(java.awt.event.KeyEvent evt) {
+ pathTextFieldKeyTyped(evt);
+ }
+ });
+
+ org.openide.awt.Mnemonics.setLocalizedText(browseButton, org.openide.util.NbBundle.getMessage(LocalDiskPanel.class, "LocalDiskPanel.browseButton.text")); // NOI18N
+ browseButton.addActionListener(new java.awt.event.ActionListener() {
+ public void actionPerformed(java.awt.event.ActionEvent evt) {
+ browseButtonActionPerformed(evt);
+ }
+ });
+
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
this.setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
- .addComponent(diskLabel)
- .addComponent(diskComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 345, javax.swing.GroupLayout.PREFERRED_SIZE)
- .addComponent(errorLabel)
.addGroup(layout.createSequentialGroup()
- .addComponent(timeZoneLabel)
- .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
- .addComponent(timeZoneComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 215, javax.swing.GroupLayout.PREFERRED_SIZE))
- .addComponent(noFatOrphansCheckbox)
- .addComponent(copyImageCheckbox)
+ .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
+ .addComponent(pathTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 362, javax.swing.GroupLayout.PREFERRED_SIZE)
+ .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addComponent(diskLabel)
+ .addComponent(diskComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 345, javax.swing.GroupLayout.PREFERRED_SIZE)
+ .addComponent(errorLabel)
+ .addGroup(layout.createSequentialGroup()
+ .addComponent(timeZoneLabel)
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
+ .addComponent(timeZoneComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 215, javax.swing.GroupLayout.PREFERRED_SIZE))
+ .addComponent(noFatOrphansCheckbox)
+ .addComponent(copyImageCheckbox)
+ .addGroup(layout.createSequentialGroup()
+ .addGap(21, 21, 21)
+ .addComponent(descLabel))))
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+ .addComponent(browseButton))
.addGroup(layout.createSequentialGroup()
.addGap(21, 21, 21)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
- .addComponent(descLabel)
- .addGroup(layout.createSequentialGroup()
- .addComponent(imageWriterPathDescLabel)
- .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
- .addComponent(imageWriterPathLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 261, javax.swing.GroupLayout.PREFERRED_SIZE))
- .addComponent(imageWriterErrorLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 444, javax.swing.GroupLayout.PREFERRED_SIZE))))
- .addGap(0, 0, Short.MAX_VALUE))
+ .addComponent(jLabel1, javax.swing.GroupLayout.PREFERRED_SIZE, 423, javax.swing.GroupLayout.PREFERRED_SIZE)
+ .addComponent(imageWriterErrorLabel))))
+ .addGap(0, 29, Short.MAX_VALUE))
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
@@ -197,27 +226,72 @@ final class LocalDiskPanel extends JPanel {
.addComponent(copyImageCheckbox)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
- .addComponent(imageWriterPathDescLabel)
- .addComponent(imageWriterPathLabel))
+ .addComponent(pathTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+ .addComponent(browseButton))
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+ .addComponent(jLabel1)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(imageWriterErrorLabel)
- .addContainerGap(124, Short.MAX_VALUE))
+ .addContainerGap(170, Short.MAX_VALUE))
);
}// //GEN-END:initComponents
+
+ private void browseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_browseButtonActionPerformed
+ String oldText = pathTextField.getText();
+ // set the current directory of the FileChooser if the ImagePath Field is valid
+ File currentFile = new File(oldText);
+ if ((currentFile.getParentFile() != null) && (currentFile.getParentFile().exists())) {
+ fc.setCurrentDirectory(currentFile.getParentFile());
+ }
+
+ int retval = fc.showOpenDialog(this);
+ if (retval == JFileChooser.APPROVE_OPTION) {
+ String path = fc.getSelectedFile().getPath();
+ pathTextField.setText(path);
+ }
+ fireUpdateEvent();
+ }//GEN-LAST:event_browseButtonActionPerformed
+
+ private void copyImageCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_copyImageCheckboxActionPerformed
+ pathTextField.setEnabled(copyImageCheckbox.isSelected());
+ browseButton.setEnabled(copyImageCheckbox.isSelected());
+ fireUpdateEvent();
+ }//GEN-LAST:event_copyImageCheckboxActionPerformed
+
+ private void pathTextFieldKeyTyped(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_pathTextFieldKeyTyped
+
+ }//GEN-LAST:event_pathTextFieldKeyTyped
+
+ private void pathTextFieldKeyReleased(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_pathTextFieldKeyReleased
+ fireUpdateEvent();
+ }//GEN-LAST:event_pathTextFieldKeyReleased
+
// Variables declaration - do not modify//GEN-BEGIN:variables
+ private javax.swing.JButton browseButton;
private javax.swing.JCheckBox copyImageCheckbox;
private javax.swing.JLabel descLabel;
private javax.swing.JComboBox diskComboBox;
private javax.swing.JLabel diskLabel;
private javax.swing.JLabel errorLabel;
private javax.swing.JLabel imageWriterErrorLabel;
- private javax.swing.JLabel imageWriterPathDescLabel;
- private javax.swing.JLabel imageWriterPathLabel;
+ private javax.swing.JLabel jLabel1;
private javax.swing.JCheckBox noFatOrphansCheckbox;
+ private javax.swing.JTextField pathTextField;
private javax.swing.JComboBox timeZoneComboBox;
private javax.swing.JLabel timeZoneLabel;
// End of variables declaration//GEN-END:variables
+ private void fireUpdateEvent(){
+ try {
+ firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), false, true);
+ } catch (Exception e) {
+ logger.log(Level.SEVERE, "LocalDiskPanel listener threw exception", e); //NON-NLS
+ MessageNotifyUtil.Notify.show(NbBundle.getMessage(this.getClass(), "LocalDiskPanel.moduleErr"),
+ NbBundle.getMessage(this.getClass(), "LocalDiskPanel.moduleErr.msg"),
+ MessageNotifyUtil.MessageType.ERROR);
+ }
+ }
+
/**
* Return the currently selected disk path.
*
@@ -267,17 +341,30 @@ final class LocalDiskPanel extends JPanel {
String path = disk.getName().replaceAll("[:]", "");
path += " " + System.currentTimeMillis();
path += ".vhd";
- imageWriterPathLabel.setText(path);
- fullImageWriterPath = Paths.get(getDefaultImageWriterFolder(), path).toString();
+ pathTextField.setText(Paths.get(getDefaultImageWriterFolder(), path).toString());
}
private boolean imageWriterPathIsValid(){
-
- File f = new File(fullImageWriterPath);
- if(f.exists()) {
- imageWriterErrorLabel.setText(NbBundle.getMessage(this.getClass(), "LocalDiskPanel.imageWriterError.text"));
+ if(pathTextField.getText().isEmpty()){
+ imageWriterErrorLabel.setText(NbBundle.getMessage(this.getClass(), "LocalDiskPanel.imageWriterEmptyPathError.text"));
return false;
}
+
+ File f = new File(pathTextField.getText());
+ if(((f.getParentFile() != null) && (! f.getParentFile().exists())) ||
+ (f.getParentFile() == null)) {
+ imageWriterErrorLabel.setText(NbBundle.getMessage(this.getClass(), "LocalDiskPanel.imageWriterDirError.text"));
+ return false;
+ }
+ if(f.isDirectory()){
+ imageWriterErrorLabel.setText(NbBundle.getMessage(this.getClass(), "LocalDiskPanel.imageWriterIsDirError.text"));
+ return false;
+ }
+ if(f.exists()){
+ imageWriterErrorLabel.setText(NbBundle.getMessage(this.getClass(), "LocalDiskPanel.imageWriterFileExistsError.text"));
+ return false;
+ }
+
imageWriterErrorLabel.setText("");
return true;
}
@@ -287,14 +374,14 @@ final class LocalDiskPanel extends JPanel {
}
String getImageWriterPath(){
- return fullImageWriterPath;
+ return pathTextField.getText();
}
/**
- * Should we enable the wizard's next button? Always return true because we
- * control the possible selections.
+ * Should we enable the wizard's next button? We control all the possible
+ * selections except for Image Writer.
*
- * @return true
+ * @return true if panel is valid
*/
public boolean validatePanel() {
if(copyImageCheckbox.isSelected() &&
@@ -399,15 +486,7 @@ final class LocalDiskPanel extends JPanel {
selected = (LocalDisk) anItem;
enableNext = true;
setPotentialImageWriterPath((LocalDisk) selected);
-
- try {
- firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), false, true);
- } catch (Exception e) {
- logger.log(Level.SEVERE, "LocalDiskPanel listener threw exception", e); //NON-NLS
- MessageNotifyUtil.Notify.show(NbBundle.getMessage(this.getClass(), "LocalDiskPanel.moduleErr"),
- NbBundle.getMessage(this.getClass(), "LocalDiskPanel.moduleErr.msg"),
- MessageNotifyUtil.MessageType.ERROR);
- }
+ fireUpdateEvent();
}
}
diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/ImageWriter.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/ImageWriter.java
new file mode 100644
index 0000000000..efddf47398
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/ImageWriter.java
@@ -0,0 +1,252 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011-2016 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.datasourceprocessors;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.HashSet;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.ExecutionException;
+import java.util.logging.Level;
+import org.netbeans.api.progress.ProgressHandle;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.core.RuntimeProperties;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.datamodel.Image;
+import org.sleuthkit.autopsy.ingest.IngestManager;
+import org.sleuthkit.autopsy.ingest.events.DataSourceAnalysisCompletedEvent;
+import org.sleuthkit.datamodel.SleuthkitJNI;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/**
+ * The ImageWriter class is used to complete VHD copies created from local disks
+ * after the ingest process completes.
+ */
+public class ImageWriter {
+
+ private final Logger logger = Logger.getLogger(ImageWriter.class.getName());
+
+ private final HashSet dataSourceIds = new HashSet<>();
+ private final Object dataSourceIdsLock; // Get this lock before accessing dataSourceIds
+
+ private final HashSet> progressUpdaters = new HashSet<>();
+ private final HashSet imagesBeingFinished = new HashSet<>();
+ private final HashSet progressBars = new HashSet<>();
+ private final HashSet> finishTasksInProgress = new HashSet<>();
+ private boolean isCancelled;
+ private final Object currentTasksLock; // Get this lock before accessing imagesBeingFinished, progressBars, progressUpdaters, finishTasksInProgress or isCancelled
+
+ private boolean listenerStarted;
+ private ScheduledThreadPoolExecutor periodicTasksExecutor = null;
+ private final boolean doUI;
+
+ public ImageWriter(){
+ dataSourceIdsLock = new Object();
+ currentTasksLock = new Object();
+ listenerStarted = false;
+ isCancelled = false;
+
+ doUI = RuntimeProperties.coreComponentsAreActive();
+ if(doUI){
+ periodicTasksExecutor = new ScheduledThreadPoolExecutor(5, new ThreadFactoryBuilder().setNameFormat("image-writer-progress-update-%d").build()); //NON-NLS
+ }
+ }
+
+ /**
+ * Creates a listener on IngestJobEvents if it hasn't already been started.
+ * When a DataSourceAnalysisCompletedEvent arrives, if it matches
+ * the data source ID of an image that is using Image Writer, then finish the image
+ * (fill in any gaps). The AddImageTask for this data source must have included
+ * a non-empty imageWriterPath parameter to enable Image Writer.
+ */
+ private synchronized void startListener(){
+ if(! listenerStarted){
+ IngestManager.getInstance().addIngestJobEventListener(new PropertyChangeListener() {
+ @Override
+ public void propertyChange(PropertyChangeEvent evt) {
+ if(evt.getPropertyName().equals(IngestManager.IngestJobEvent.DATA_SOURCE_ANALYSIS_COMPLETED.toString())){
+ DataSourceAnalysisCompletedEvent event = (DataSourceAnalysisCompletedEvent)evt;
+
+ if(event.getDataSource() != null){
+ long imageId = event.getDataSource().getId();
+ String name = event.getDataSource().getName();
+
+ // Check whether we need to run finishImage for this data source
+ synchronized(dataSourceIdsLock){
+ if( ! ImageWriter.this.dataSourceIds.contains(imageId)){
+ // Image writer was not used on this data source or we've already finished it
+ return;
+ } else {
+ // Remove the imageId from the list here so we can't get past this point twice
+ // for the same image. Multiple DataSourceAnalysisCompletedEvent events can come from
+ // the same image if more ingest modules are run later, but the imageId is only added
+ // to the list during the intial task to add the image to the database.
+ ImageWriter.this.dataSourceIds.remove(imageId);
+ }
+ }
+ logger.log(Level.INFO, String.format("Finishing VHD image for %s",
+ event.getDataSource().getName())); //NON-NLS
+
+ new Thread(() -> {
+ try{
+ Image image = Case.getCurrentCase().getSleuthkitCase().getImageById(imageId);
+ ProgressHandle progressHandle = null;
+ ScheduledFuture> progressUpdateTask = null;
+
+ if(doUI){
+ progressHandle = ProgressHandle.createHandle("Image writer - " + name);
+ progressHandle.start(100);
+ progressUpdateTask = periodicTasksExecutor.scheduleAtFixedRate(
+ new ProgressUpdateTask(progressHandle, image.getImageHandle()), 0, 250, TimeUnit.MILLISECONDS);
+ }
+
+ synchronized(currentTasksLock){
+ ImageWriter.this.imagesBeingFinished.add(image.getImageHandle());
+
+ if(doUI){
+ if(isCancelled){
+ progressUpdateTask.cancel(true);
+ return;
+ }
+ ImageWriter.this.progressUpdaters.add(progressUpdateTask);
+ ImageWriter.this.progressBars.add(progressHandle);
+ }
+ }
+
+ // The added complexity here with the Future is because we absolutely need to make sure
+ // the call to finishImageWriter returns before allowing the TSK data structures to be freed
+ // during case close.
+ Future> finishTask = Executors.newSingleThreadExecutor().submit(() -> {
+ try{
+ SleuthkitJNI.finishImageWriter(image.getImageHandle());
+ } catch (TskCoreException ex){
+ logger.log(Level.SEVERE, "Error finishing VHD image", ex); //NON-NLS
+ }
+ });
+ synchronized(currentTasksLock){
+ ImageWriter.this.finishTasksInProgress.add(finishTask);
+ }
+
+ // Wait for finishImageWriter to complete
+ try{
+ // The call to get() will happen twice if the user closes the case, which is ok
+ finishTask.get();
+ } catch (InterruptedException | ExecutionException ex){
+ logger.log(Level.SEVERE, "Error finishing VHD image", ex); //NON-NLS
+ }
+
+ synchronized(currentTasksLock){
+ ImageWriter.this.finishTasksInProgress.remove(finishTask);
+ ImageWriter.this.imagesBeingFinished.remove(image.getImageHandle());
+
+ if(doUI){
+ progressUpdateTask.cancel(true);
+ ImageWriter.this.progressUpdaters.remove(progressUpdateTask);
+ progressHandle.finish();
+ ImageWriter.this.progressBars.remove(progressHandle);
+ }
+ }
+
+ logger.log(Level.INFO, String.format("Finished writing VHD image for %s", event.getDataSource().getName())); //NON-NLS
+ } catch (TskCoreException ex){
+ logger.log(Level.SEVERE, "Error finishing VHD image", ex); //NON-NLS
+ }
+ }).start();
+ } else {
+ logger.log(Level.SEVERE, "DataSourceAnalysisCompletedEvent did not contain a dataSource object"); //NON-NLS
+ }
+ }
+ }
+ });
+ }
+ listenerStarted = true;
+ }
+
+ /**
+ * Add a data source ID to the list of images to run finishImage on.
+ * Also starts the listener if needed.
+ * @param id The dataSource/Image ID
+ */
+ public void addDataSourceId(Long id){
+ startListener();
+ synchronized(dataSourceIdsLock){
+ dataSourceIds.add(id);
+ }
+ }
+
+ /**
+ * Stop any open progress update task, finish the progress bars, and tell
+ * the finishImage process to stop
+ */
+ public void close(){
+ synchronized(currentTasksLock){
+ isCancelled = true;
+
+ for(ScheduledFuture> task:ImageWriter.this.progressUpdaters){
+ task.cancel(true);
+ }
+
+ for(Long handle:imagesBeingFinished){
+ SleuthkitJNI.cancelFinishImage(handle);
+ logger.log(Level.SEVERE, "Case closed before VHD image could be finished"); //NON-NLS
+ }
+
+ // Wait for all the finish tasks to end
+ for(Future> task:ImageWriter.this.finishTasksInProgress){
+ try{
+ task.get();
+ } catch (InterruptedException | ExecutionException ex){
+ logger.log(Level.SEVERE, "Error finishing VHD image", ex); //NON-NLS
+ }
+ }
+
+ for(ProgressHandle progressHandle:ImageWriter.this.progressBars){
+ progressHandle.finish();
+ }
+ }
+ }
+
+ /**
+ * Task to query the Sleuthkit processing to get the percentage done.
+ */
+ private final class ProgressUpdateTask implements Runnable {
+ long imageHandle;
+ ProgressHandle progressHandle;
+
+ ProgressUpdateTask(ProgressHandle progressHandle, long imageHandle){
+ this.imageHandle = imageHandle;
+ this.progressHandle = progressHandle;
+ }
+
+ @Override
+ public void run() {
+ try {
+ int progress = SleuthkitJNI.getFinishImageProgress(imageHandle);
+ progressHandle.progress(progress);
+ } catch (Exception ex) {
+ logger.log(Level.SEVERE, "Unexpected exception in ProgressUpdateTask", ex); //NON-NLS
+ }
+ }
+ }
+}