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 @@
- + - + @@ -26,30 +26,38 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - + + + + + + + + + - + @@ -74,12 +82,14 @@ - - + + + + - + @@ -189,20 +199,9 @@ - - - - - - - - - - - - - - + + + @@ -219,5 +218,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + } + } + } +}