diff --git a/Core/nbproject/project.xml b/Core/nbproject/project.xml index 0159ec9880..5dabff9f5d 100644 --- a/Core/nbproject/project.xml +++ b/Core/nbproject/project.xml @@ -327,6 +327,7 @@ org.sleuthkit.autopsy.events org.sleuthkit.autopsy.filesearch org.sleuthkit.autopsy.guiutils + org.sleuthkit.autopsy.healthmonitor org.sleuthkit.autopsy.ingest org.sleuthkit.autopsy.keywordsearchservice org.sleuthkit.autopsy.menuactions diff --git a/Core/src/org/sleuthkit/autopsy/communications/AbstractCVTAction.java b/Core/src/org/sleuthkit/autopsy/communications/AbstractCVTAction.java new file mode 100644 index 0000000000..ef751d262c --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/communications/AbstractCVTAction.java @@ -0,0 +1,65 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2018 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.communications; + +import java.util.Collection; +import javax.swing.AbstractAction; +import javax.swing.ImageIcon; +import javax.swing.JMenuItem; +import org.openide.util.Utilities; +import org.openide.util.actions.Presenter; + +/** + * Base class for actions that act on the selected AccountDeviceInstanceKeys. + * getPopupPresenter() provides a JMenuItem that works (i.e., has an icon) in + * custom context menus and also in the Netbeans Explorer views. + */ +abstract class AbstractCVTAction extends AbstractAction implements Presenter.Popup { + + /** + * Get the selected accounts that will be acted upon. + * + * @return The selected accounts + */ + Collection getSelectedAccounts() { + return Utilities.actionsGlobalContext().lookupAll(AccountDeviceInstanceKey.class); + } + + @Override + public JMenuItem getPopupPresenter() { + JMenuItem presenter = new JMenuItem(this); + presenter.setText(getActionDisplayName()); + presenter.setIcon(getIcon()); + return presenter; + } + + /** + * The name/text of the action as displayed in a menu. + * + * @return The diaplay name of this action + */ + abstract String getActionDisplayName(); + + /** + * The icon to use for this icon. + * + * @return An ImageIcon used to represent this action. + */ + abstract ImageIcon getIcon(); +} diff --git a/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNode.java b/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNode.java index 40cb15d27d..044cec322f 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNode.java +++ b/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNode.java @@ -18,17 +18,13 @@ */ package org.sleuthkit.autopsy.communications; -import java.awt.event.ActionEvent; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; -import javax.swing.AbstractAction; import javax.swing.Action; import org.openide.nodes.AbstractNode; import org.openide.nodes.Children; import org.openide.nodes.Sheet; import org.openide.util.NbBundle; -import org.openide.util.Utilities; import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.datamodel.NodeProperty; import org.sleuthkit.datamodel.Account; @@ -106,54 +102,4 @@ final class AccountDeviceInstanceNode extends AbstractNode { actions.add(ResetAndPinAccountsAction.getInstance()); return actions.toArray(new Action[actions.size()]); } - - /** - * Action that pins the selected AccountDeviceInstances to the - * visualization. - */ - static private class PinAccountsAction extends AbstractAction { - - private static final long serialVersionUID = 1L; - private final static PinAccountsAction instance = new PinAccountsAction(); - - private static PinAccountsAction getInstance() { - return instance; - } - - private PinAccountsAction() { - super("Add Account(s) to Visualization"); - } - - @Override - public void actionPerformed(ActionEvent e) { - Collection lookupAll = - Utilities.actionsGlobalContext().lookupAll(AccountDeviceInstanceKey.class); - CVTEvents.getCVTEventBus().post(new CVTEvents.PinAccountsEvent(lookupAll, false)); - } - } - - /** - * Action that pins the selected AccountDeviceInstances to the - * visualization. - */ - static private class ResetAndPinAccountsAction extends AbstractAction { - - private static final long serialVersionUID = 1L; - private final static ResetAndPinAccountsAction instance = new ResetAndPinAccountsAction(); - - private static ResetAndPinAccountsAction getInstance() { - return instance; - } - - private ResetAndPinAccountsAction() { - super("Visualize Account(s)"); - } - - @Override - public void actionPerformed(ActionEvent e) { - Collection lookupAll = - Utilities.actionsGlobalContext().lookupAll(AccountDeviceInstanceKey.class); - CVTEvents.getCVTEventBus().post(new CVTEvents.PinAccountsEvent(lookupAll, true)); - } - } } diff --git a/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties b/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties index 559e9bba86..358971eaa8 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties @@ -20,9 +20,6 @@ CVTTopComponent.vizPanel.TabConstraints.tabTitle=Visualize CVTTopComponent.accountsBrowser.TabConstraints.tabTitle_1=Browse CVTTopComponent.browseVisualizeTabPane.AccessibleContext.accessibleName=Visualize CVTTopComponent.vizPanel.TabConstraints.tabTitle_1=Visualize -VisualizationPanel.jButton6.text=Hierarchy -VisualizationPanel.jButton7.text=Circle -VisualizationPanel.jButton8.text=Organic VisualizationPanel.fitGraphButton.text= VisualizationPanel.jTextArea1.text=Right-click an account in the Browse Accounts table, and select 'Visualize' to begin. VisualizationPanel.jLabel1.text=Layouts: @@ -36,11 +33,8 @@ VisualizationPanel.zoomInButton.toolTipText=Zoom in VisualizationPanel.zoomInButton.text= VisualizationPanel.zoomOutButton.toolTipText=Zoom out VisualizationPanel.zoomOutButton.text= -# 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. VisualizationPanel.circleLayoutButton.text=Circle VisualizationPanel.organicLayoutButton.text=Organic VisualizationPanel.fastOrganicLayoutButton.text=Fast Organic -VisualizationPanel.hierarchyLayoutButton.text=Hierarchy +VisualizationPanel.hierarchyLayoutButton.text=Hierarchical VisualizationPanel.clearVizButton.text_1=Clear Viz. diff --git a/Core/src/org/sleuthkit/autopsy/communications/CVTEvents.java b/Core/src/org/sleuthkit/autopsy/communications/CVTEvents.java index e0f945bb69..6a6a60d3da 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/CVTEvents.java +++ b/Core/src/org/sleuthkit/autopsy/communications/CVTEvents.java @@ -21,7 +21,6 @@ package org.sleuthkit.autopsy.communications; import com.google.common.collect.ImmutableSet; import com.google.common.eventbus.EventBus; import java.util.Collection; -import java.util.Set; import org.sleuthkit.datamodel.CommunicationsFilter; /** @@ -79,7 +78,7 @@ final class CVTEvents { return accountDeviceInstances; } - public UnpinAccountsEvent(Set accountDeviceInstances) { + UnpinAccountsEvent(Collection accountDeviceInstances) { this.accountDeviceInstances = ImmutableSet.copyOf(accountDeviceInstances); } } diff --git a/Core/src/org/sleuthkit/autopsy/communications/CommunicationsGraph.java b/Core/src/org/sleuthkit/autopsy/communications/CommunicationsGraph.java index f274ab00d7..716d00656c 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/CommunicationsGraph.java +++ b/Core/src/org/sleuthkit/autopsy/communications/CommunicationsGraph.java @@ -25,7 +25,6 @@ import com.google.common.collect.MultimapBuilder; import com.mxgraph.model.mxCell; import com.mxgraph.model.mxICell; import com.mxgraph.util.mxConstants; -import com.mxgraph.view.mxCellState; import com.mxgraph.view.mxGraph; import com.mxgraph.view.mxStylesheet; import java.io.InputStream; @@ -84,17 +83,19 @@ final class CommunicationsGraph extends mxGraph { mxStylesheet.getDefaultEdgeStyle().put(mxConstants.STYLE_STARTARROW, mxConstants.NONE); } - /* Map from type specific account identifier to mxCell(vertex). */ + /** Map from type specific account identifier to mxCell(vertex). */ private final Map nodeMap = new HashMap<>(); - /* Map from relationship source (Content) to mxCell (edge). */ + /** Map from relationship source (Content) to mxCell (edge). */ private final Multimap edgeMap = MultimapBuilder.hashKeys().hashSetValues().build(); private final LockedVertexModel lockedVertexModel; private final PinnedAccountModel pinnedAccountModel; - CommunicationsGraph() { + CommunicationsGraph(PinnedAccountModel pinnedAccountModel, LockedVertexModel lockedVertexModel) { super(mxStylesheet); + this.pinnedAccountModel = pinnedAccountModel; + this.lockedVertexModel = lockedVertexModel; //set fixed properties of graph. setAutoSizeCells(true); setCellsCloneable(false); @@ -113,21 +114,6 @@ final class CommunicationsGraph extends mxGraph { setKeepEdgesInBackground(true); setResetEdgesOnMove(true); setHtmlLabels(true); - - lockedVertexModel = new LockedVertexModel(); - lockedVertexModel.registerhandler((LockedVertexModel.VertexLockEvent event) -> { - if (event.isVertexLocked()) { - getView().clear(event.getVertex(), true, true); - getView().validate(); - } else { - final mxCellState state = getView().getState(event.getVertex(), true); - getView().updateLabel(state); - getView().updateLabelBounds(state); - getView().updateBoundingBox(state); - } - }); - - pinnedAccountModel = new PinnedAccountModel(this); } /** @@ -255,20 +241,20 @@ final class CommunicationsGraph extends mxGraph { */ private class RebuildWorker extends SwingWorker { - private final ProgressIndicator progress; + private final ProgressIndicator progressIndicator; private final CommunicationsManager commsManager; private final CommunicationsFilter currentFilter; RebuildWorker(ProgressIndicator progress, CommunicationsManager commsManager, CommunicationsFilter currentFilter) { - this.progress = progress; + this.progressIndicator = progress; this.currentFilter = currentFilter; this.commsManager = commsManager; } @Override - protected Void doInBackground() throws Exception { - progress.start("Loading accounts"); + protected Void doInBackground() { + progressIndicator.start("Loading accounts"); int progressCounter = 0; try { /** @@ -279,18 +265,18 @@ final class CommunicationsGraph extends mxGraph { if (isCancelled()) { break; } + //get accounts related to pinned account final List relatedAccountDeviceInstances = commsManager.getRelatedAccountDeviceInstances(adiKey.getAccountDeviceInstance(), currentFilter); relatedAccounts.put(adiKey.getAccountDeviceInstance(), adiKey); getOrCreateVertex(adiKey); - //get accounts related to pinned account for (final AccountDeviceInstance relatedADI : relatedAccountDeviceInstances) { final long adiRelationshipsCount = commsManager.getRelationshipSourcesCount(relatedADI, currentFilter); final AccountDeviceInstanceKey relatedADIKey = new AccountDeviceInstanceKey(relatedADI, currentFilter, adiRelationshipsCount); relatedAccounts.put(relatedADI, relatedADIKey); //store related accounts } - progress.progress(++progressCounter); + progressIndicator.progress(++progressCounter); } Set accounts = relatedAccounts.keySet(); @@ -298,9 +284,9 @@ final class CommunicationsGraph extends mxGraph { Map relationshipCounts = commsManager.getRelationshipCountsPairwise(accounts, currentFilter); int total = relationshipCounts.size(); - int k = 0; - String progressText = ""; - progress.switchToDeterminate("", 0, total); + int progress = 0; + String progressText = ""; + progressIndicator.switchToDeterminate("", 0, total); for (Map.Entry entry : relationshipCounts.entrySet()) { Long count = entry.getValue(); AccountPair relationshipKey = entry.getKey(); @@ -308,15 +294,14 @@ final class CommunicationsGraph extends mxGraph { AccountDeviceInstanceKey account2 = relatedAccounts.get(relationshipKey.getSecond()); if (pinnedAccountModel.isAccountPinned(account1) - || pinnedAccountModel.isAccountPinned(account2)) { + || pinnedAccountModel.isAccountPinned(account2)) { mxCell addEdge = addOrUpdateEdge(count, account1, account2); progressText = addEdge.getId(); } - progress.progress(progressText, k++); + progressIndicator.progress(progressText, progress++); } } catch (TskCoreException tskCoreException) { logger.log(Level.SEVERE, "Error", tskCoreException); - } finally { } return null; @@ -332,7 +317,7 @@ final class CommunicationsGraph extends mxGraph { } catch (CancellationException ex) { logger.log(Level.INFO, "Graph visualization cancelled"); } finally { - progress.finish(); + progressIndicator.finish(); } } } diff --git a/Core/src/org/sleuthkit/autopsy/communications/LockedVertexModel.java b/Core/src/org/sleuthkit/autopsy/communications/LockedVertexModel.java index f26e9515b3..7d8b8c083d 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/LockedVertexModel.java +++ b/Core/src/org/sleuthkit/autopsy/communications/LockedVertexModel.java @@ -18,20 +18,19 @@ */ package org.sleuthkit.autopsy.communications; +import com.google.common.collect.ImmutableSet; import com.google.common.eventbus.EventBus; import com.mxgraph.model.mxCell; +import java.util.Collection; import java.util.HashSet; import java.util.Set; -class LockedVertexModel { - - void registerhandler(EventHandler handler) { - eventBus.register(handler); - } - - void unregisterhandler(EventHandler handler) { - eventBus.unregister(handler); - } +/** + * Model of which vertices in a graph are locked ( not moveable by layout + * algorithms). + * + */ +final class LockedVertexModel { private final EventBus eventBus = new EventBus(); @@ -42,30 +41,34 @@ class LockedVertexModel { */ private final Set lockedVertices = new HashSet<>(); - LockedVertexModel() { + void registerhandler(Object handler) { + eventBus.register(handler); + } + + void unregisterhandler(Object handler) { + eventBus.unregister(handler); } /** - * Lock the given vertex so that applying a layout algorithm doesn't move - * it. The user can still manually position the vertex. + * Lock the given vertices so that applying a layout algorithm doesn't move + * them. The user can still manually position the vertices. * * @param vertex The vertex to lock. */ - void lockVertex(mxCell vertex) { - lockedVertices.add(vertex); - eventBus.post(new VertexLockEvent(vertex, true)); - + void lock(Collection vertices) { + lockedVertices.addAll(vertices); + eventBus.post(new VertexLockEvent(true, vertices)); } /** - * Lock the given vertex so that applying a layout algorithm can move it. + * Unlock the given vertices so that applying a layout algorithm can move + * them. * * @param vertex The vertex to unlock. */ - void unlockVertex(mxCell vertex) { - lockedVertices.remove(vertex); - eventBus.post(new VertexLockEvent(vertex, false)); - + void unlock(Collection vertices) { + lockedVertices.removeAll(vertices); + eventBus.post(new VertexLockEvent(false, vertices)); } boolean isVertexLocked(mxCell vertex) { @@ -77,21 +80,36 @@ class LockedVertexModel { lockedVertices.clear(); } - static class VertexLockEvent { + boolean isEmpty() { + return lockedVertices.isEmpty(); + } - private final mxCell vertex; + /** + * Event that represents a change in the locked state of one or more + * vertices. + */ + final static class VertexLockEvent { - public mxCell getVertex() { - return vertex; + private final boolean locked; + private final Set vertices; + + /** + * @return The vertices whose locked state has changed. + */ + public Set getVertices() { + return vertices; } - public boolean isVertexLocked() { + /** + * @return True if the vertices are locked, False if the vertices are + * unlocked. + */ + public boolean isLocked() { return locked; } - private final boolean locked; - VertexLockEvent(mxCell vertex, boolean locked) { - this.vertex = vertex; + VertexLockEvent(boolean locked, Collection< mxCell> vertices) { + this.vertices = ImmutableSet.copyOf(vertices); this.locked = locked; } } diff --git a/Core/src/org/sleuthkit/autopsy/communications/MessageBrowser.java b/Core/src/org/sleuthkit/autopsy/communications/MessageBrowser.java index 68309f5195..7b90357993 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/MessageBrowser.java +++ b/Core/src/org/sleuthkit/autopsy/communications/MessageBrowser.java @@ -35,7 +35,6 @@ import org.sleuthkit.autopsy.corecomponents.DataResultPanel; import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable; import org.sleuthkit.autopsy.corecomponents.TableFilterNode; import org.sleuthkit.autopsy.directorytree.DataResultFilterNode; -import org.sleuthkit.datamodel.AccountDeviceInstance; /** * The right hand side of the CVT. Has a DataResultPanel to show a listing of @@ -151,10 +150,10 @@ public final class MessageBrowser extends JPanel implements ExplorerManager.Prov //Use lookup here? final AccountDeviceInstanceNode adiNode = (AccountDeviceInstanceNode) selectedNodes[0]; - final Set accountDeviceInstances = new HashSet<>(); + final Set accountDeviceInstances = new HashSet<>(); for (final Node n : selectedNodes) { //Use lookup here? - accountDeviceInstances.add(((AccountDeviceInstanceNode) n).getAccountDeviceInstance()); + accountDeviceInstances.add(((AccountDeviceInstanceNode) n).getAccountDeviceInstanceKey()); } return SelectionNode.createFromAccounts(accountDeviceInstances, adiNode.getFilter(), adiNode.getCommsManager()); } diff --git a/Core/src/org/sleuthkit/autopsy/communications/PinAccountsAction.java b/Core/src/org/sleuthkit/autopsy/communications/PinAccountsAction.java new file mode 100644 index 0000000000..52136d1b1a --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/communications/PinAccountsAction.java @@ -0,0 +1,59 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2018 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.communications; + +import java.awt.event.ActionEvent; +import javax.swing.ImageIcon; +import org.openide.util.ImageUtilities; +import org.openide.util.NbBundle; + +/** + * Action that pins the AccountDevicesIntanceKeys in the ActionsGlobalContext to + * the visualizaion + */ +@NbBundle.Messages({"PinAccountsAction.pluralText=Add Selected Accounts to Visualization", + "PinAccountsAction.singularText=Add Selected Account to Visualization"}) +final class PinAccountsAction extends AbstractCVTAction { + + static private final ImageIcon ICON = ImageUtilities.loadImageIcon( + "/org/sleuthkit/autopsy/communications/images/marker--plus.png", false); + private static final String SINGULAR_TEXT = Bundle.PinAccountsAction_singularText(); + private static final String PLURAL_TEXT = Bundle.PinAccountsAction_pluralText(); + + private static final PinAccountsAction instance = new PinAccountsAction(); + + static PinAccountsAction getInstance() { + return instance; + } + + @Override + public void actionPerformed(ActionEvent event) { + CVTEvents.getCVTEventBus().post(new CVTEvents.PinAccountsEvent(getSelectedAccounts(), false)); + } + + @Override + protected String getActionDisplayName() { + return getSelectedAccounts().size() > 1 ? PLURAL_TEXT : SINGULAR_TEXT; + } + + @Override + ImageIcon getIcon() { + return ICON; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/communications/PinnedAccountModel.java b/Core/src/org/sleuthkit/autopsy/communications/PinnedAccountModel.java index d5f7a7cc03..f7fb7d1232 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/PinnedAccountModel.java +++ b/Core/src/org/sleuthkit/autopsy/communications/PinnedAccountModel.java @@ -19,22 +19,31 @@ package org.sleuthkit.autopsy.communications; import com.google.common.collect.ImmutableSet; +import com.google.common.eventbus.EventBus; import java.util.HashSet; import java.util.Set; +/** + * Model of what accounts are pinned to a visualization. + */ class PinnedAccountModel { /** - * Set of AccountDeviceInstanceKeys that are 'Pinned' to this graph. Pinned + * Set of AccountDeviceInstanceKeys that are 'Pinned' to the graph. Pinned * accounts are shown regardless of filters, and accounts that are related * to pinned accounts and pass the filters are show. Pinning accounts is the * primary way to populate the graph. */ private final Set pinnedAccountDevices = new HashSet<>(); - private final CommunicationsGraph graph; - PinnedAccountModel(CommunicationsGraph graph) { - this.graph = graph; + private final EventBus eventBus = new EventBus(); + + void registerhandler(Object handler) { + eventBus.register(handler); + } + + void unregisterhandler(Object handler) { + eventBus.unregister(handler); } boolean isAccountPinned(AccountDeviceInstanceKey account) { diff --git a/Core/src/org/sleuthkit/autopsy/communications/ResetAndPinAccountsAction.java b/Core/src/org/sleuthkit/autopsy/communications/ResetAndPinAccountsAction.java new file mode 100644 index 0000000000..385ac3348b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/communications/ResetAndPinAccountsAction.java @@ -0,0 +1,59 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2018 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.communications; + +import java.awt.event.ActionEvent; +import javax.swing.ImageIcon; +import org.openide.util.ImageUtilities; +import org.openide.util.NbBundle; + +/** + * Action that clears any pinned accounts and pins the AcountDevicesInstanceKeys + * in the ActionsGlobalContext to the visualization. + */ +@NbBundle.Messages(value = {"ResetAndPinAccountsAction.singularText=Visualize Only Selected Account", + "ResetAndPinAccountsAction.pluralText=Visualize Only Selected Accounts"}) +final class ResetAndPinAccountsAction extends AbstractCVTAction { + + private static final ImageIcon ICON = ImageUtilities.loadImageIcon( + "/org/sleuthkit/autopsy/communications/images/marker--pin.png", false); + private static final String SINGULAR_TEXT = Bundle.ResetAndPinAccountsAction_singularText(); + private static final String PLURAL_TEXT = Bundle.ResetAndPinAccountsAction_pluralText(); + + private static final ResetAndPinAccountsAction instance = new ResetAndPinAccountsAction(); + + static ResetAndPinAccountsAction getInstance() { + return instance; + } + + @Override + public void actionPerformed(ActionEvent event) { + CVTEvents.getCVTEventBus().post(new CVTEvents.PinAccountsEvent(getSelectedAccounts(), true)); + } + + @Override + protected String getActionDisplayName() { + return getSelectedAccounts().size() > 1 ? PLURAL_TEXT : SINGULAR_TEXT; + } + + @Override + ImageIcon getIcon() { + return ICON; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/communications/SelectionNode.java b/Core/src/org/sleuthkit/autopsy/communications/SelectionNode.java index 40739b49cc..1618bb9ecc 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/SelectionNode.java +++ b/Core/src/org/sleuthkit/autopsy/communications/SelectionNode.java @@ -24,10 +24,13 @@ import java.util.Collections; import java.util.List; import java.util.Set; import java.util.logging.Level; +import java.util.stream.Collectors; import org.openide.nodes.AbstractNode; import org.openide.nodes.ChildFactory; import org.openide.nodes.Children; import org.openide.nodes.Node; +import org.openide.util.Lookup; +import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.AccountDeviceInstance; import org.sleuthkit.datamodel.BlackboardArtifact; @@ -44,23 +47,27 @@ import org.sleuthkit.datamodel.TskCoreException; */ final class SelectionNode extends AbstractNode { - private SelectionNode(Children children) { - super(children); + private SelectionNode(Children children, Lookup lookup) { + super(children, lookup); } static SelectionNode createFromAccountsAndRelationships( Set edgeRelationshipArtifacts, - Set accountDeviceInstances, + Set accountDeviceInstanceKeys, CommunicationsFilter filter, CommunicationsManager commsManager) { + Set accountDeviceInstances = accountDeviceInstanceKeys.stream() + .map(AccountDeviceInstanceKey::getAccountDeviceInstance) + .collect(Collectors.toSet()); + SelectionNode node = new SelectionNode(Children.create( new RelationshipChildren( edgeRelationshipArtifacts, accountDeviceInstances, commsManager, filter), - true)); + true), Lookups.fixed(accountDeviceInstanceKeys.toArray())); //This is not good for internationalization!!! String name = ""; @@ -82,7 +89,7 @@ final class SelectionNode extends AbstractNode { } static SelectionNode createFromAccounts( - Set accountDeviceInstances, + Set accountDeviceInstances, CommunicationsFilter filter, CommunicationsManager commsManager) { @@ -122,9 +129,9 @@ final class SelectionNode extends AbstractNode { } @Override - protected Node createNodeForKey(Content t) { - if (t instanceof BlackboardArtifact) { - return new RelationshipNode((BlackboardArtifact) t); + protected Node createNodeForKey(Content content) { + if (content instanceof BlackboardArtifact) { + return new RelationshipNode((BlackboardArtifact) content); } else { throw new UnsupportedOperationException("Cannot create a RelationshipNode for non BlackboardArtifact content."); } diff --git a/Core/src/org/sleuthkit/autopsy/communications/UnpinAccountsAction.java b/Core/src/org/sleuthkit/autopsy/communications/UnpinAccountsAction.java new file mode 100644 index 0000000000..ba0bbc545b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/communications/UnpinAccountsAction.java @@ -0,0 +1,59 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2018 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.communications; + +import java.awt.event.ActionEvent; +import javax.swing.ImageIcon; +import org.openide.util.ImageUtilities; +import org.openide.util.NbBundle; + +/** + * Action that unpins the AcccountDeviceInstanceKeys in the ActionsGlobalContext + * form the visualization. + */ +@NbBundle.Messages({"UnpinAccountsAction.pluralText=Remove Selected Accounts", + "UnpinAccountsAction.singularText=Remove Selected Account"}) +final class UnpinAccountsAction extends AbstractCVTAction { + + static final private ImageIcon ICON = ImageUtilities.loadImageIcon( + "/org/sleuthkit/autopsy/communications/images/marker--minus.png", false); + private static final String SINGULAR_TEXT = Bundle.UnpinAccountsAction_singularText(); + private static final String PLURAL_TEXT = Bundle.UnpinAccountsAction_pluralText(); + + private static final UnpinAccountsAction instance = new UnpinAccountsAction(); + + static UnpinAccountsAction getInstance() { + return instance; + } + + @Override + public void actionPerformed(final ActionEvent event) { + CVTEvents.getCVTEventBus().post(new CVTEvents.UnpinAccountsEvent(getSelectedAccounts())); + } + + @Override + String getActionDisplayName() { + return getSelectedAccounts().size() > 1 ? PLURAL_TEXT : SINGULAR_TEXT; + } + + @Override + ImageIcon getIcon() { + return ICON; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.form b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.form index 9d5722443e..a847f700c7 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.form +++ b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.form @@ -68,15 +68,15 @@ + + + - - - @@ -166,9 +166,6 @@ - - - @@ -179,9 +176,6 @@ - - - @@ -192,9 +186,6 @@ - - - @@ -205,9 +196,6 @@ - - - @@ -222,11 +210,11 @@ - - + + @@ -241,11 +229,11 @@ - - + + @@ -260,11 +248,11 @@ - - + + @@ -279,11 +267,11 @@ - - + + diff --git a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java index cd75ab5185..4f6b53ae39 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java +++ b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java @@ -28,7 +28,6 @@ import com.mxgraph.model.mxCell; import com.mxgraph.model.mxICell; import com.mxgraph.swing.handler.mxRubberband; import com.mxgraph.swing.mxGraphComponent; -import com.mxgraph.swing.util.mxMorphing; import com.mxgraph.util.mxEvent; import com.mxgraph.util.mxEventObject; import com.mxgraph.util.mxEventSource; @@ -36,11 +35,14 @@ import com.mxgraph.util.mxPoint; import com.mxgraph.util.mxRectangle; import com.mxgraph.util.mxUndoManager; import com.mxgraph.util.mxUndoableEdit; +import com.mxgraph.view.mxCellState; import com.mxgraph.view.mxGraph; +import com.mxgraph.view.mxGraphView; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Cursor; import java.awt.Dimension; +import java.awt.Font; import java.awt.Frame; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -51,12 +53,18 @@ import java.beans.PropertyChangeEvent; import java.beans.PropertyVetoException; import java.text.DecimalFormat; import java.util.Arrays; -import static java.util.Collections.singleton; import java.util.EnumSet; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.Map; import java.util.concurrent.Future; +import java.util.function.BiConsumer; import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.swing.AbstractAction; import javax.swing.ImageIcon; import javax.swing.JButton; @@ -79,12 +87,11 @@ import org.openide.util.Lookup; import org.openide.util.NbBundle; import org.openide.util.lookup.ProxyLookup; import org.sleuthkit.autopsy.casemodule.Case; -import static org.sleuthkit.autopsy.casemodule.Case.Events.CURRENT_CASE; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.progress.ModalDialogProgressIndicator; -import org.sleuthkit.datamodel.AccountDeviceInstance; import org.sleuthkit.datamodel.CommunicationsFilter; import org.sleuthkit.datamodel.CommunicationsManager; import org.sleuthkit.datamodel.Content; @@ -100,23 +107,17 @@ import org.sleuthkit.datamodel.TskCoreException; * CVTTopComponent when this tab is active allowing for context sensitive * actions to work correctly. */ -@NbBundle.Messages("VisualizationPanel.cancelButton.text=Cancel") final public class VisualizationPanel extends JPanel implements Lookup.Provider { private static final long serialVersionUID = 1L; private static final Logger logger = Logger.getLogger(VisualizationPanel.class.getName()); private static final String BASE_IMAGE_PATH = "/org/sleuthkit/autopsy/communications/images"; - static final private ImageIcon pinIcon - = new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/marker--pin.png")); - static final private ImageIcon addPinIcon - = new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/marker--plus.png")); - static final private ImageIcon unpinIcon - = new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/marker--minus.png")); static final private ImageIcon unlockIcon = new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/lock_large_unlocked.png")); static final private ImageIcon lockIcon = new ImageIcon(VisualizationPanel.class.getResource(BASE_IMAGE_PATH + "/lock_large_locked.png")); + @NbBundle.Messages("VisualizationPanel.cancelButton.text=Cancel") private static final String CANCEL = Bundle.VisualizationPanel_cancelButton_text(); private final ExplorerManager vizEM = new ExplorerManager(); @@ -131,27 +132,20 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider private final CommunicationsGraph graph; private final mxUndoManager undoManager = new mxUndoManager(); - private final mxRubberband rubberband; - private final mxFastOrganicLayout fastOrganicLayout; - private final mxCircleLayout circleLayout; - private final mxOrganicLayout organicLayout; - private final mxHierarchicalLayout hierarchicalLayout; + private final mxRubberband rubberband; //NOPMD We keep a referenec as insurance to prevent garbage collection @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private SwingWorker worker; - private final PinnedAccountModel pinnedAccountModel; - private final LockedVertexModel lockedVertexModel; + private final PinnedAccountModel pinnedAccountModel = new PinnedAccountModel(); + private final LockedVertexModel lockedVertexModel = new LockedVertexModel(); + + private final Map layoutButtons = new HashMap<>(); + private NamedGraphLayout currentLayout; public VisualizationPanel() { initComponents(); - graph = new CommunicationsGraph(); - pinnedAccountModel = graph.getPinnedAccountModel(); - lockedVertexModel = graph.getLockedVertexModel(); - fastOrganicLayout = new mxFastOrganicLayoutImpl(graph); - circleLayout = new mxCircleLayoutImpl(graph); - organicLayout = new mxOrganicLayoutImpl(graph); - hierarchicalLayout = new mxHierarchicalLayoutImpl(graph); + graph = new CommunicationsGraph(pinnedAccountModel, lockedVertexModel); graphComponent = new mxGraphComponent(graph); graphComponent.setAutoExtend(true); @@ -165,123 +159,90 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider graphComponent.setBackground(Color.WHITE); borderLayoutPanel.add(graphComponent, BorderLayout.CENTER); - //install rubber band selection handler + //install rubber band other handlers rubberband = new mxRubberband(graphComponent); + lockedVertexModel.registerhandler(this); + final mxEventSource.mxIEventListener scaleListener = (Object sender, mxEventObject evt) -> zoomLabel.setText(DecimalFormat.getPercentInstance().format(graph.getView().getScale())); graph.getView().addListener(mxEvent.SCALE, scaleListener); graph.getView().addListener(mxEvent.SCALE_AND_TRANSLATE, scaleListener); - graphComponent.getGraphControl().addMouseWheelListener(new MouseAdapter() { - /** - * Translate mouse wheel events into zooming. - * - * @param event The MouseWheelEvent - */ - @Override - public void mouseWheelMoved(final MouseWheelEvent event) { - super.mouseWheelMoved(event); - if (event.getPreciseWheelRotation() > 0) { - graphComponent.zoomIn(); - } else if (event.getPreciseWheelRotation() < 0) { - graphComponent.zoomOut(); - } - } - }); + final GraphMouseListener graphMouseListener = new GraphMouseListener(); + graphComponent.getGraphControl().addMouseWheelListener(graphMouseListener); + graphComponent.getGraphControl().addMouseListener(graphMouseListener); - graphComponent.getGraphControl().addMouseListener(new MouseAdapter() { - /** - * Right click handler: show context menu. - * - * @param event The MouseEvent - */ - @Override - public void mouseClicked(final MouseEvent event) { - super.mouseClicked(event); - if (SwingUtilities.isRightMouseButton(event)) { - final mxCell cellAt = (mxCell) graphComponent.getCellAt(event.getX(), event.getY()); - if (cellAt != null && cellAt.isVertex()) { - final JPopupMenu jPopupMenu = new JPopupMenu(); - final AccountDeviceInstanceKey adiKey = (AccountDeviceInstanceKey) cellAt.getValue(); - - if (lockedVertexModel.isVertexLocked(cellAt)) { - jPopupMenu.add(new JMenuItem(new AbstractAction("UnLock " + cellAt.getId(), unlockIcon) { - @Override - public void actionPerformed(final ActionEvent event) { - lockedVertexModel.unlockVertex(cellAt); - } - })); - } else { - jPopupMenu.add(new JMenuItem(new AbstractAction("Lock " + cellAt.getId(), lockIcon) { - @Override - public void actionPerformed(final ActionEvent event) { - lockedVertexModel.lockVertex(cellAt); - } - })); - } - if (pinnedAccountModel.isAccountPinned(adiKey)) { - jPopupMenu.add(new JMenuItem(new AbstractAction("Unpin " + cellAt.getId(), unpinIcon) { - @Override - public void actionPerformed(final ActionEvent event) { - handleUnPinEvent(new CVTEvents.UnpinAccountsEvent(singleton((AccountDeviceInstanceKey) cellAt.getValue()))); - } - })); - } else { - jPopupMenu.add(new JMenuItem(new AbstractAction("Pin " + cellAt.getId(), addPinIcon) { - @Override - public void actionPerformed(final ActionEvent event) { - handlePinEvent(new CVTEvents.PinAccountsEvent(singleton((AccountDeviceInstanceKey) cellAt.getValue()), false)); - } - })); - jPopupMenu.add(new JMenuItem(new AbstractAction("Pin only " + cellAt.getId(), pinIcon) { - @Override - public void actionPerformed(final ActionEvent event) { - handlePinEvent(new CVTEvents.PinAccountsEvent(singleton((AccountDeviceInstanceKey) cellAt.getValue()), true)); - } - })); - } - jPopupMenu.show(graphComponent.getGraphControl(), event.getX(), event.getY()); - } - } - } - }); final MessageBrowser messageBrowser = new MessageBrowser(vizEM, gacEM); - splitPane.setRightComponent(messageBrowser); - proxyLookup = new ProxyLookup( - messageBrowser.getLookup(), - ExplorerUtils.createLookup(vizEM, getActionMap())); + ExplorerUtils.createLookup(vizEM, getActionMap()), + messageBrowser.getLookup() + ); //feed selection to explorermanager - graph.getSelectionModel().addListener(null, new SelectionListener()); + graph.getSelectionModel().addListener(mxEvent.CHANGE, new SelectionListener()); final mxEventSource.mxIEventListener undoListener = (Object sender, mxEventObject evt) -> undoManager.undoableEditHappened((mxUndoableEdit) evt.getProperty("edit")); graph.getModel().addListener(mxEvent.UNDO, undoListener); graph.getView().addListener(mxEvent.UNDO, undoListener); + + FastOrganicLayoutImpl fastOrganicLayout = new FastOrganicLayoutImpl(graph); + CircleLayoutImpl circleLayout = new CircleLayoutImpl(graph); + OrganicLayoutImpl organicLayout = new OrganicLayoutImpl(graph); + organicLayout.setMaxIterations(10); + HierarchicalLayoutImpl hierarchyLayout = new HierarchicalLayoutImpl(graph); + + //local method to configure layout buttons + BiConsumer configure = (layoutButton, layout) -> { + layoutButtons.put(layout, layoutButton); + layoutButton.addActionListener(event -> applyLayout(layout)); + }; + //configure layout buttons. + configure.accept(circleLayoutButton, circleLayout); + configure.accept(organicLayoutButton, organicLayout); + configure.accept(fastOrganicLayoutButton, fastOrganicLayout); + configure.accept(hierarchyLayoutButton, hierarchyLayout); + + applyLayout(fastOrganicLayout); } + /** + * + * @param layoutButton the value of layoutButton + * @param layout the value of layout + */ @Override - public Lookup getLookup() { return proxyLookup; } @Subscribe - void handleUnPinEvent(final CVTEvents.UnpinAccountsEvent pinEvent) { + void handle(LockedVertexModel.VertexLockEvent event) { + final Set vertices = event.getVertices(); + mxGraphView view = graph.getView(); + vertices.forEach(vertex -> { + final mxCellState state = view.getState(vertex, true); + view.updateLabel(state); + view.updateLabelBounds(state); + view.updateBoundingBox(state); + graphComponent.redraw(state); + }); + } + + @Subscribe + void handle(final CVTEvents.UnpinAccountsEvent pinEvent) { graph.getModel().beginUpdate(); pinnedAccountModel.unpinAccount(pinEvent.getAccountDeviceInstances()); graph.clear(); rebuildGraph(); // Updates the display graph.getModel().endUpdate(); - } @Subscribe - void handlePinEvent(final CVTEvents.PinAccountsEvent pinEvent) { + void handle(final CVTEvents.PinAccountsEvent pinEvent) { graph.getModel().beginUpdate(); if (pinEvent.isReplace()) { graph.resetGraph(); @@ -290,19 +251,16 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider rebuildGraph(); // Updates the display graph.getModel().endUpdate(); - } @Subscribe - void handleFilterEvent(final CVTEvents.FilterChangeEvent filterChangeEvent) { - + void handle(final CVTEvents.FilterChangeEvent filterChangeEvent) { graph.getModel().beginUpdate(); graph.clear(); currentFilter = filterChangeEvent.getNewFilter(); rebuildGraph(); // Updates the display graph.getModel().endUpdate(); - } @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @@ -327,14 +285,12 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider if (worker.isCancelled()) { graph.resetGraph(); rebuildGraph(); - } else { - morph(fastOrganicLayout); } + applyLayout(currentLayout); } }); worker.execute(); - } } @@ -351,7 +307,7 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider logger.log(Level.SEVERE, "Can't get CommunicationsManager when there is no case open.", ex); } - Case.addEventTypeSubscriber(EnumSet.of(CURRENT_CASE), evt -> { + Case.addEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), evt -> { graph.getModel().beginUpdate(); try { graph.resetGraph(); @@ -368,15 +324,9 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider logger.log(Level.SEVERE, "Error getting CommunicationsManager for the current case.", ex); } } - }); } - @Override - public void removeNotify() { - super.removeNotify(); - } - /** * 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 @@ -413,11 +363,11 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider borderLayoutPanel.setLayout(new BorderLayout()); + jTextArea1.setBackground(new Color(240, 240, 240)); jTextArea1.setColumns(20); jTextArea1.setLineWrap(true); jTextArea1.setRows(5); jTextArea1.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.jTextArea1.text")); // NOI18N - jTextArea1.setBackground(new Color(240, 240, 240)); GroupLayout placeHolderPanelLayout = new GroupLayout(placeHolderPanel); placeHolderPanel.setLayout(placeHolderPanelLayout); @@ -442,49 +392,29 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider hierarchyLayoutButton.setFocusable(false); hierarchyLayoutButton.setHorizontalTextPosition(SwingConstants.CENTER); hierarchyLayoutButton.setVerticalTextPosition(SwingConstants.BOTTOM); - hierarchyLayoutButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - hierarchyLayoutButtonActionPerformed(evt); - } - }); fastOrganicLayoutButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.fastOrganicLayoutButton.text")); // NOI18N fastOrganicLayoutButton.setFocusable(false); fastOrganicLayoutButton.setHorizontalTextPosition(SwingConstants.CENTER); fastOrganicLayoutButton.setVerticalTextPosition(SwingConstants.BOTTOM); - fastOrganicLayoutButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - fastOrganicLayoutButtonActionPerformed(evt); - } - }); organicLayoutButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.organicLayoutButton.text")); // NOI18N organicLayoutButton.setFocusable(false); organicLayoutButton.setHorizontalTextPosition(SwingConstants.CENTER); organicLayoutButton.setVerticalTextPosition(SwingConstants.BOTTOM); - organicLayoutButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - organicLayoutButtonActionPerformed(evt); - } - }); circleLayoutButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.circleLayoutButton.text")); // NOI18N circleLayoutButton.setFocusable(false); circleLayoutButton.setHorizontalTextPosition(SwingConstants.CENTER); circleLayoutButton.setVerticalTextPosition(SwingConstants.BOTTOM); - circleLayoutButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - circleLayoutButtonActionPerformed(evt); - } - }); jSeparator1.setOrientation(SwingConstants.VERTICAL); zoomOutButton.setIcon(new ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/magnifier-zoom-out-red.png"))); // NOI18N zoomOutButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomOutButton.text")); // NOI18N + zoomOutButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomOutButton.toolTipText")); // NOI18N zoomOutButton.setFocusable(false); zoomOutButton.setHorizontalTextPosition(SwingConstants.CENTER); - zoomOutButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomOutButton.toolTipText")); // NOI18N zoomOutButton.setVerticalTextPosition(SwingConstants.BOTTOM); zoomOutButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { @@ -494,9 +424,9 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider zoomInButton.setIcon(new ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/magnifier-zoom-in-green.png"))); // NOI18N zoomInButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomInButton.text")); // NOI18N + zoomInButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomInButton.toolTipText")); // NOI18N zoomInButton.setFocusable(false); zoomInButton.setHorizontalTextPosition(SwingConstants.CENTER); - zoomInButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomInButton.toolTipText")); // NOI18N zoomInButton.setVerticalTextPosition(SwingConstants.BOTTOM); zoomInButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { @@ -506,9 +436,9 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider zoomActualButton.setIcon(new ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/magnifier-zoom-actual.png"))); // NOI18N zoomActualButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomActualButton.text")); // NOI18N + zoomActualButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomActualButton.toolTipText")); // NOI18N zoomActualButton.setFocusable(false); zoomActualButton.setHorizontalTextPosition(SwingConstants.CENTER); - zoomActualButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.zoomActualButton.toolTipText")); // NOI18N zoomActualButton.setVerticalTextPosition(SwingConstants.BOTTOM); zoomActualButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { @@ -518,9 +448,9 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider fitZoomButton.setIcon(new ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/magnifier-zoom-fit.png"))); // NOI18N fitZoomButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.fitZoomButton.text")); // NOI18N + fitZoomButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.fitZoomButton.toolTipText")); // NOI18N fitZoomButton.setFocusable(false); fitZoomButton.setHorizontalTextPosition(SwingConstants.CENTER); - fitZoomButton.setToolTipText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.fitZoomButton.toolTipText")); // NOI18N fitZoomButton.setVerticalTextPosition(SwingConstants.BOTTOM); fitZoomButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { @@ -620,21 +550,55 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider graphComponent.zoomOut(); }//GEN-LAST:event_zoomOutButtonActionPerformed - private void circleLayoutButtonActionPerformed(ActionEvent evt) {//GEN-FIRST:event_circleLayoutButtonActionPerformed - morph(circleLayout); - }//GEN-LAST:event_circleLayoutButtonActionPerformed + /** + * Apply the given layout. The given layout becomes the current layout. The + * layout is computed in the background. + * + * @param layout The layout to apply. + */ + @NbBundle.Messages({"VisualizationPanel.computingLayout=Computing Layout", + "# {0} - layout name", + "VisualizationPanel.layoutFailWithLockedVertices.text={0} layout failed with locked vertices. Unlock some vertices or try a different layout.", + "# {0} - layout name", + "VisualizationPanel.layoutFail.text={0} layout failed. Try a different layout."}) + private void applyLayout(NamedGraphLayout layout) { + currentLayout = layout; + layoutButtons.forEach((layoutKey, button) + -> button.setFont(button.getFont().deriveFont(layoutKey == layout ? Font.BOLD : Font.PLAIN))); - private void organicLayoutButtonActionPerformed(ActionEvent evt) {//GEN-FIRST:event_organicLayoutButtonActionPerformed - applyOrganicLayout(10); - }//GEN-LAST:event_organicLayoutButtonActionPerformed + ModalDialogProgressIndicator progressIndicator = new ModalDialogProgressIndicator(windowAncestor, Bundle.VisualizationPanel_computingLayout()); + progressIndicator.start(Bundle.VisualizationPanel_computingLayout()); - private void fastOrganicLayoutButtonActionPerformed(ActionEvent evt) {//GEN-FIRST:event_fastOrganicLayoutButtonActionPerformed - morph(fastOrganicLayout); - }//GEN-LAST:event_fastOrganicLayoutButtonActionPerformed + new SwingWorker() { + @Override + protected Void doInBackground() { + graph.getModel().beginUpdate(); + try { + layout.execute(graph.getDefaultParent()); + fitGraph(); + } finally { + graph.getModel().endUpdate(); + progressIndicator.finish(); + } + return null; + } - private void hierarchyLayoutButtonActionPerformed(ActionEvent evt) {//GEN-FIRST:event_hierarchyLayoutButtonActionPerformed - morph(hierarchicalLayout); - }//GEN-LAST:event_hierarchyLayoutButtonActionPerformed + @Override + protected void done() { + try { + get(); + } catch (InterruptedException | ExecutionException ex) { + logger.log(Level.WARNING, "CVT graph layout failed.", ex); + if (lockedVertexModel.isEmpty()) { + MessageNotifyUtil.Message.error(Bundle.VisualizationPanel_layoutFail_text(layout.getDisplayName())); + } else { + MessageNotifyUtil.Message.error(Bundle.VisualizationPanel_layoutFailWithLockedVertices_text(layout.getDisplayName())); + } + undoManager.undo(); + } + } + }.execute(); + } private void clearVizButtonActionPerformed(ActionEvent evt) {//GEN-FIRST:event_clearVizButtonActionPerformed setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); @@ -645,14 +609,8 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider // Updates the display graph.getModel().endUpdate(); setCursor(Cursor.getDefaultCursor()); - }//GEN-LAST:event_clearVizButtonActionPerformed - private void applyOrganicLayout(int iterations) { - organicLayout.setMaxIterations(iterations); - morph(organicLayout); - } - private void fitGraph() { graphComponent.zoomTo(1, true); mxPoint translate = graph.getView().getTranslate(); @@ -675,57 +633,11 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider final Dimension size = graphComponent.getSize(); final double widthFactor = size.getWidth() / boundsForCells.getWidth(); + final double heightFactor = size.getHeight() / boundsForCells.getHeight(); - graphComponent.zoom(widthFactor); - + graphComponent.zoom((heightFactor + widthFactor) / 2.0); } - private void morph(mxIGraphLayout layout) { - // layout using morphing - graph.getModel().beginUpdate(); - - CancelationListener cancelationListener = new CancelationListener(); - ModalDialogProgressIndicator progress = new ModalDialogProgressIndicator(windowAncestor, "Computing layout", new String[]{CANCEL}, CANCEL, cancelationListener); - SwingWorker morphWorker = new SwingWorker() { - @Override - protected Void doInBackground() { - progress.start("Computing layout"); - layout.execute(graph.getDefaultParent()); - if (isCancelled()) { - progress.finish(); - return null; - } - mxMorphing morph = new mxMorphing(graphComponent, 20, 1.2, 20) { - @Override - public void updateAnimation() { - fireEvent(new mxEventObject(mxEvent.EXECUTE)); - super.updateAnimation(); //To change body of generated methods, choose Tools | Templates. - } - - }; - morph.addListener(mxEvent.EXECUTE, (Object sender, mxEventObject evt) -> { - if (isCancelled()) { - morph.stopAnimation(); - } - }); - morph.addListener(mxEvent.DONE, (Object sender, mxEventObject event) -> { - graph.getModel().endUpdate(); - if (isCancelled()) { - undoManager.undo(); - } else { - fitGraph(); - } - progress.finish(); - }); - - morph.startAnimation(); - return null; - - } - }; - cancelationListener.configure(morphWorker, progress); - morphWorker.execute(); - } // Variables declaration - do not modify//GEN-BEGIN:variables private JPanel borderLayoutPanel; @@ -749,6 +661,10 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider private JButton zoomOutButton; // End of variables declaration//GEN-END:variables + /** + * Listens to graph selection model and updates ExplorerManager to reflect + * changes in selection. + */ final private class SelectionListener implements mxEventSource.mxIEventListener { @SuppressWarnings("unchecked") @@ -760,7 +676,7 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider if (selectionCells.length > 0) { mxICell[] selectedCells = Arrays.asList(selectionCells).toArray(new mxCell[selectionCells.length]); HashSet relationshipSources = new HashSet<>(); - HashSet adis = new HashSet<>(); + HashSet adis = new HashSet<>(); for (mxICell cell : selectedCells) { if (cell.isEdge()) { mxICell source = (mxICell) graph.getModel().getTerminal(cell, true); @@ -777,7 +693,7 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider logger.log(Level.SEVERE, " Error getting relationsips....", tskCoreException); } } else if (cell.isVertex()) { - adis.add(((AccountDeviceInstanceKey) cell.getValue()).getAccountDeviceInstance()); + adis.add((AccountDeviceInstanceKey) cell.getValue()); } } @@ -793,9 +709,20 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider } } - final private class mxFastOrganicLayoutImpl extends mxFastOrganicLayout { + /** + * Extend mxIGraphLayout with a getDisplayName method, + */ + private interface NamedGraphLayout extends mxIGraphLayout { - mxFastOrganicLayoutImpl(mxGraph graph) { + String getDisplayName(); + } + + /** + * Extension of mxFastOrganicLayout that ignores locked vertices. + */ + final private class FastOrganicLayoutImpl extends mxFastOrganicLayout implements NamedGraphLayout { + + FastOrganicLayoutImpl(mxGraph graph) { super(graph); } @@ -806,18 +733,26 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider } @Override - public mxRectangle setVertexLocation(Object vertex, double x, double y) { + public mxRectangle setVertexLocation(Object vertex, double x, double y) { //NOPMD x, y are standard coordinate names if (isVertexIgnored(vertex)) { return getVertexBounds(vertex); } else { return super.setVertexLocation(vertex, x, y); } } + + @Override + public String getDisplayName() { + return "Fast Organic"; + } } - final private class mxCircleLayoutImpl extends mxCircleLayout { + /** + * Extension of mxCircleLayout that ignores locked vertices. + */ + final private class CircleLayoutImpl extends mxCircleLayout implements NamedGraphLayout { - mxCircleLayoutImpl(mxGraph graph) { + CircleLayoutImpl(mxGraph graph) { super(graph); setResetEdges(true); } @@ -829,18 +764,26 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider } @Override - public mxRectangle setVertexLocation(Object vertex, double x, double y) { + public mxRectangle setVertexLocation(Object vertex, double x, double y) { //NOPMD x, y are standard coordinate names if (isVertexIgnored(vertex)) { return getVertexBounds(vertex); } else { return super.setVertexLocation(vertex, x, y); } } + + @Override + public String getDisplayName() { + return "Circle"; + } } - final private class mxOrganicLayoutImpl extends mxOrganicLayout { + /** + * Extension of mxOrganicLayout that ignores locked vertices. + */ + final private class OrganicLayoutImpl extends mxOrganicLayout implements NamedGraphLayout { - mxOrganicLayoutImpl(mxGraph graph) { + OrganicLayoutImpl(mxGraph graph) { super(graph); setResetEdges(true); } @@ -852,18 +795,26 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider } @Override - public mxRectangle setVertexLocation(Object vertex, double x, double y) { + public mxRectangle setVertexLocation(Object vertex, double x, double y) { //NOPMD x, y are standard coordinate names if (isVertexIgnored(vertex)) { return getVertexBounds(vertex); } else { return super.setVertexLocation(vertex, x, y); } } + + @Override + public String getDisplayName() { + return "Organic"; + } } - final private class mxHierarchicalLayoutImpl extends mxHierarchicalLayout { + /** + * Extension of mxHierarchicalLayout that ignores locked vertices. + */ + final private class HierarchicalLayoutImpl extends mxHierarchicalLayout implements NamedGraphLayout { - mxHierarchicalLayoutImpl(mxGraph graph) { + HierarchicalLayoutImpl(mxGraph graph) { super(graph); } @@ -874,15 +825,24 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider } @Override - public mxRectangle setVertexLocation(Object vertex, double x, double y) { + public mxRectangle setVertexLocation(Object vertex, double x, double y) { //NOPMD x, y are standard coordinate names if (isVertexIgnored(vertex)) { return getVertexBounds(vertex); } else { return super.setVertexLocation(vertex, x, y); } } + + @Override + public String getDisplayName() { + return "Hierarchical"; + } } + /** + * Listener that closses the given ModalDialogProgressIndicator and cancels + * the future. + */ private class CancelationListener implements ActionListener { private Future cancellable; @@ -899,6 +859,108 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider cancellable.cancel(true); progress.finish(); } + } + /** + * Mouse Adapter for the graphComponent. Handles wheel zooming and context + * menus. + */ + private class GraphMouseListener extends MouseAdapter { + + /** + * Translate mouse wheel events into zooming. + * + * @param event The MouseWheelEvent + */ + @Override + public void mouseWheelMoved(final MouseWheelEvent event) { + super.mouseWheelMoved(event); + if (event.getPreciseWheelRotation() < 0) { + graphComponent.zoomIn(); + } else if (event.getPreciseWheelRotation() > 0) { + graphComponent.zoomOut(); + } + } + + /** + * Right click handler: show context menu. + * + * @param event The MouseEvent + */ + @Override + public void mouseClicked(final MouseEvent event) { + super.mouseClicked(event); + if (SwingUtilities.isRightMouseButton(event)) { + final mxCell cellAt = (mxCell) graphComponent.getCellAt(event.getX(), event.getY()); + if (cellAt != null && cellAt.isVertex()) { + final JPopupMenu jPopupMenu = new JPopupMenu(); + final AccountDeviceInstanceKey adiKey = (AccountDeviceInstanceKey) cellAt.getValue(); + + Set selectedVertices + = Stream.of(graph.getSelectionModel().getCells()) + .map(mxCell.class::cast) + .filter(mxCell::isVertex) + .collect(Collectors.toSet()); + + if (lockedVertexModel.isVertexLocked(cellAt)) { + jPopupMenu.add(new JMenuItem(new UnlockAction(selectedVertices))); + } else { + jPopupMenu.add(new JMenuItem(new LockAction(selectedVertices))); + } + if (pinnedAccountModel.isAccountPinned(adiKey)) { + jPopupMenu.add(UnpinAccountsAction.getInstance().getPopupPresenter()); + } else { + jPopupMenu.add(PinAccountsAction.getInstance().getPopupPresenter()); + jPopupMenu.add(ResetAndPinAccountsAction.getInstance().getPopupPresenter()); + } + jPopupMenu.show(graphComponent.getGraphControl(), event.getX(), event.getY()); + } + } + } + } + + /** + * Action that un-locks the selected vertices. + */ + @NbBundle.Messages({ + "VisualizationPanel.unlockAction.singularText=Unlock Selected Account", + "VisualizationPanel.unlockAction.pluralText=Unlock Selected Accounts",}) + private final class UnlockAction extends AbstractAction { + + private final Set selectedVertices; + + UnlockAction(Set selectedVertices) { + super(selectedVertices.size() > 1 ? Bundle.VisualizationPanel_unlockAction_pluralText() : Bundle.VisualizationPanel_unlockAction_singularText(), + unlockIcon); + this.selectedVertices = selectedVertices; + } + + @Override + + public void actionPerformed(final ActionEvent event) { + lockedVertexModel.unlock(selectedVertices); + } + } + + /** + * Action that locks the selected vertices. + */ + @NbBundle.Messages({ + "VisualizationPanel.lockAction.singularText=Lock Selected Account", + "VisualizationPanel.lockAction.pluralText=Lock Selected Accounts"}) + private final class LockAction extends AbstractAction { + + private final Set selectedVertices; + + LockAction(Set selectedVertices) { + super(selectedVertices.size() > 1 ? Bundle.VisualizationPanel_lockAction_pluralText() : Bundle.VisualizationPanel_lockAction_singularText(), + lockIcon); + this.selectedVertices = selectedVertices; + } + + @Override + public void actionPerformed(final ActionEvent event) { + lockedVertexModel.lock(selectedVertices); + } } } diff --git a/Core/src/org/sleuthkit/autopsy/coordinationservice/CoordinationService.java b/Core/src/org/sleuthkit/autopsy/coordinationservice/CoordinationService.java index 9b2afff6b4..d7ef8c4750 100644 --- a/Core/src/org/sleuthkit/autopsy/coordinationservice/CoordinationService.java +++ b/Core/src/org/sleuthkit/autopsy/coordinationservice/CoordinationService.java @@ -457,7 +457,8 @@ public final class CoordinationService { CASES("cases"), MANIFESTS("manifests"), CONFIG("config"), - CENTRAL_REPO("centralRepository"); + CENTRAL_REPO("centralRepository"), + HEALTH_MONITOR("healthMonitor"); private final String displayName; diff --git a/Core/src/org/sleuthkit/autopsy/core/Installer.java b/Core/src/org/sleuthkit/autopsy/core/Installer.java index 83250f719d..63ac880171 100644 --- a/Core/src/org/sleuthkit/autopsy/core/Installer.java +++ b/Core/src/org/sleuthkit/autopsy/core/Installer.java @@ -216,6 +216,7 @@ public class Installer extends ModuleInstall { packageInstallers.add(org.sleuthkit.autopsy.datamodel.Installer.getDefault()); packageInstallers.add(org.sleuthkit.autopsy.ingest.Installer.getDefault()); packageInstallers.add(org.sleuthkit.autopsy.centralrepository.eventlisteners.Installer.getDefault()); + packageInstallers.add(org.sleuthkit.autopsy.healthmonitor.Installer.getDefault()); } /** diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Accounts.java b/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Accounts.java index 3b6878ce23..a06d3eeeea 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Accounts.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Accounts.java @@ -408,6 +408,7 @@ final public class Accounts implements AutopsyVisitableItem { protected void addNotify() { IngestManager.getInstance().addIngestJobEventListener(pcl); IngestManager.getInstance().addIngestModuleEventListener(pcl); + Case.addEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), pcl); super.addNotify(); } @@ -415,6 +416,7 @@ final public class Accounts implements AutopsyVisitableItem { protected void removeNotify() { IngestManager.getInstance().removeIngestJobEventListener(pcl); IngestManager.getInstance().removeIngestModuleEventListener(pcl); + Case.removeEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), pcl); super.removeNotify(); } @@ -569,6 +571,7 @@ final public class Accounts implements AutopsyVisitableItem { protected void addNotify() { IngestManager.getInstance().addIngestJobEventListener(pcl); IngestManager.getInstance().addIngestModuleEventListener(pcl); + Case.addEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), pcl); super.addNotify(); } @@ -576,6 +579,7 @@ final public class Accounts implements AutopsyVisitableItem { protected void removeNotify() { IngestManager.getInstance().removeIngestJobEventListener(pcl); IngestManager.getInstance().removeIngestModuleEventListener(pcl); + Case.removeEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), pcl); super.removeNotify(); } @@ -691,6 +695,7 @@ final public class Accounts implements AutopsyVisitableItem { protected void addNotify() { IngestManager.getInstance().addIngestJobEventListener(pcl); IngestManager.getInstance().addIngestModuleEventListener(pcl); + Case.addEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), pcl); super.addNotify(); } @@ -698,6 +703,7 @@ final public class Accounts implements AutopsyVisitableItem { protected void removeNotify() { IngestManager.getInstance().removeIngestJobEventListener(pcl); IngestManager.getInstance().removeIngestModuleEventListener(pcl); + Case.removeEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), pcl); super.removeNotify(); } @@ -901,6 +907,7 @@ final public class Accounts implements AutopsyVisitableItem { protected void addNotify() { IngestManager.getInstance().addIngestJobEventListener(pcl); IngestManager.getInstance().addIngestModuleEventListener(pcl); + Case.addEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), pcl); super.addNotify(); } @@ -908,6 +915,7 @@ final public class Accounts implements AutopsyVisitableItem { protected void removeNotify() { IngestManager.getInstance().removeIngestJobEventListener(pcl); IngestManager.getInstance().removeIngestModuleEventListener(pcl); + Case.removeEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), pcl); super.removeNotify(); } diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java new file mode 100644 index 0000000000..35bc6b6bfa --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java @@ -0,0 +1,785 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2018 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.healthmonitor; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Map; +import java.util.HashMap; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import org.apache.commons.dbcp2.BasicDataSource; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coordinationservice.CoordinationService; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.autopsy.core.UserPreferencesException; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.ModuleSettings; +import org.sleuthkit.autopsy.coreutils.ThreadUtils; +import org.sleuthkit.datamodel.CaseDbConnectionInfo; +import org.sleuthkit.datamodel.CaseDbSchemaVersionNumber; + +/** + * Class for recording data on the health of the system. + * + * For timing data: + * Modules will call getTimingMetric() before the code to be timed to get a TimingMetric object + * Modules will call submitTimingMetric() with the obtained TimingMetric object to log it + */ +public final class EnterpriseHealthMonitor implements PropertyChangeListener { + + private final static Logger logger = Logger.getLogger(EnterpriseHealthMonitor.class.getName()); + private final static String DATABASE_NAME = "EnterpriseHealthMonitor"; + private final static String MODULE_NAME = "EnterpriseHealthMonitor"; + private final static String IS_ENABLED_KEY = "is_enabled"; + private final static long DATABASE_WRITE_INTERVAL = 60; // Minutes + public static final CaseDbSchemaVersionNumber CURRENT_DB_SCHEMA_VERSION + = new CaseDbSchemaVersionNumber(1, 0); + + private static final AtomicBoolean isEnabled = new AtomicBoolean(false); + private static EnterpriseHealthMonitor instance; + + private final ExecutorService healthMonitorExecutor; + private static final String HEALTH_MONITOR_EVENT_THREAD_NAME = "Health-Monitor-Event-Listener-%d"; + + private ScheduledThreadPoolExecutor healthMonitorOutputTimer; + private final Map timingInfoMap; + private static final int CONN_POOL_SIZE = 10; + private BasicDataSource connectionPool = null; + private String hostName; + + private EnterpriseHealthMonitor() throws HealthMonitorException { + + // Create the map to collect timing metrics. The map will exist regardless + // of whether the monitor is enabled. + timingInfoMap = new HashMap<>(); + + // Set up the executor to handle case events + healthMonitorExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat(HEALTH_MONITOR_EVENT_THREAD_NAME).build()); + + // Get the host name + try { + hostName = java.net.InetAddress.getLocalHost().getHostName(); + } catch (java.net.UnknownHostException ex) { + // Continue on, but log the error and generate a UUID to use for this session + hostName = UUID.randomUUID().toString(); + logger.log(Level.SEVERE, "Unable to look up host name - falling back to UUID " + hostName, ex); + } + + // Read from module settings to determine if the module is enabled + if (ModuleSettings.settingExists(MODULE_NAME, IS_ENABLED_KEY)) { + if(ModuleSettings.getConfigSetting(MODULE_NAME, IS_ENABLED_KEY).equals("true")){ + isEnabled.set(true); + try { + activateMonitor(); + } catch (HealthMonitorException ex) { + // If we failed to activate it, then disable the monitor + logger.log(Level.SEVERE, "Health monitor activation failed - disabling health monitor"); + setEnabled(false); + throw ex; + } + return; + } + } + isEnabled.set(false); + } + + /** + * Get the instance of the EnterpriseHealthMonitor + * @return the instance + * @throws HealthMonitorException + */ + synchronized static EnterpriseHealthMonitor getInstance() throws HealthMonitorException { + if (instance == null) { + instance = new EnterpriseHealthMonitor(); + Case.addPropertyChangeListener(instance); + } + return instance; + } + + /** + * Activate the health monitor. + * Creates/initialized the database (if needed), clears any existing metrics + * out of the maps, and sets up the timer for writing to the database. + * @throws HealthMonitorException + */ + private synchronized void activateMonitor() throws HealthMonitorException { + + logger.log(Level.INFO, "Activating Servies Health Monitor"); + + if (!UserPreferences.getIsMultiUserModeEnabled()) { + throw new HealthMonitorException("Multi user mode is not enabled - can not activate health monitor"); + } + + // Set up database (if needed) + try (CoordinationService.Lock lock = getExclusiveDbLock()) { + if(lock == null) { + throw new HealthMonitorException("Error getting database lock"); + } + + // Check if the database exists + if (! databaseExists()) { + + // If not, create a new one + createDatabase(); + } + + if( ! databaseIsInitialized()) { + initializeDatabaseSchema(); + } + + } catch (CoordinationService.CoordinationServiceException ex) { + throw new HealthMonitorException("Error releasing database lock", ex); + } + + // Clear out any old data + timingInfoMap.clear(); + + // Start the timer for database writes + startTimer(); + } + + /** + * Deactivate the health monitor. + * This should only be used when disabling the monitor, not when Autopsy is closing. + * Clears out any metrics that haven't been written, stops the database write timer, + * and shuts down the connection pool. + * @throws HealthMonitorException + */ + private synchronized void deactivateMonitor() throws HealthMonitorException { + + logger.log(Level.INFO, "Deactivating Servies Health Monitor"); + + // Clear out the collected data + timingInfoMap.clear(); + + // Stop the timer + stopTimer(); + + // Shut down the connection pool + shutdownConnections(); + } + + /** + * Start the ScheduledThreadPoolExecutor that will handle the database writes. + */ + private synchronized void startTimer() { + // Make sure the previous executor (if it exists) has been stopped + stopTimer(); + + healthMonitorOutputTimer = new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder().setNameFormat("health_monitor_timer").build()); + healthMonitorOutputTimer.scheduleWithFixedDelay(new DatabaseWriteTask(), DATABASE_WRITE_INTERVAL, DATABASE_WRITE_INTERVAL, TimeUnit.MINUTES); + } + + /** + * Stop the ScheduledThreadPoolExecutor to prevent further database writes. + */ + private synchronized void stopTimer() { + if(healthMonitorOutputTimer != null) { + ThreadUtils.shutDownTaskExecutor(healthMonitorOutputTimer); + } + } + + /** + * Called from the installer to set up the Health Monitor instance at startup. + * @throws HealthMonitorException + */ + static synchronized void startUpIfEnabled() throws HealthMonitorException { + getInstance(); + } + + /** + * Enabled/disable the health monitor. + * @param enabled true to enable the monitor, false to disable it + * @throws HealthMonitorException + */ + static synchronized void setEnabled(boolean enabled) throws HealthMonitorException { + if(enabled == isEnabled.get()) { + // The setting has not changed, so do nothing + return; + } + + if(enabled) { + getInstance().activateMonitor(); + + // If activateMonitor fails, we won't update either of these + ModuleSettings.setConfigSetting(MODULE_NAME, IS_ENABLED_KEY, "true"); + isEnabled.set(true); + } else { + ModuleSettings.setConfigSetting(MODULE_NAME, IS_ENABLED_KEY, "false"); + isEnabled.set(false); + getInstance().deactivateMonitor(); + } + } + + /** + * Get a metric that will measure the time to execute a section of code. + * Call this before the section of code to be timed and then + * submit it afterward using submitTimingMetric(). + * This method is safe to call regardless of whether the Enterprise Health + * Monitor is enabled. + * @param name A short but descriptive name describing the code being timed. + * This name will appear in the UI. + * @return The TimingMetric object + */ + public static TimingMetric getTimingMetric(String name) { + if(isEnabled.get()) { + return new TimingMetric(name); + } + return null; + } + + /** + * Submit the metric that was previously obtained through getTimingMetric(). + * Call this immediately after the section of code being timed. + * This method is safe to call regardless of whether the Enterprise Health + * Monitor is enabled. + * @param metric The TimingMetric object obtained from getTimingMetric() + */ + public static void submitTimingMetric(TimingMetric metric) { + if(isEnabled.get() && (metric != null)) { + metric.stopTiming(); + try { + getInstance().addTimingMetric(metric); + } catch (HealthMonitorException ex) { + // We don't want calling methods to have to check for exceptions, so just log it + logger.log(Level.SEVERE, "Error adding timing metric", ex); + } + } + } + + /** + * Add the timing metric data to the map. + * @param metric The metric to add. stopTiming() should already have been called. + */ + private void addTimingMetric(TimingMetric metric) throws HealthMonitorException { + + // Do as little as possible within the synchronized block to minimize + // blocking with multiple threads. + synchronized(this) { + // There's a small check-then-act situation here where isEnabled + // may have changed before reaching this code. This is fine - + // the map still exists and any extra data added after the monitor + // is disabled will be deleted if the monitor is re-enabled. This + // seems preferable to doing another check on isEnabled within + // the synchronized block. + if(timingInfoMap.containsKey(metric.getName())) { + timingInfoMap.get(metric.getName()).addMetric(metric); + } else { + timingInfoMap.put(metric.getName(), new TimingInfo(metric)); + } + } + } + + /** + * Write the collected metrics to the database. + * @throws HealthMonitorException + */ + private void writeCurrentStateToDatabase() throws HealthMonitorException { + + Map timingMapCopy; + + // Do as little as possible within the synchronized block since it will + // block threads attempting to record metrics. + synchronized(this) { + if(! isEnabled.get()) { + return; + } + + // Make a shallow copy of the timing map. The map should be small - one entry + // per metric name. + timingMapCopy = new HashMap<>(timingInfoMap); + timingInfoMap.clear(); + } + + // Check if there's anything to report (right now we only have the timing map) + if(timingMapCopy.keySet().isEmpty()) { + return; + } + + logger.log(Level.INFO, "Writing health monitor metrics to database"); + + // Write to the database + try (CoordinationService.Lock lock = getSharedDbLock()) { + if(lock == null) { + throw new HealthMonitorException("Error getting database lock"); + } + + Connection conn = connect(); + if(conn == null) { + throw new HealthMonitorException("Error getting database connection"); + } + + // Add timing metrics to the database + String addTimingInfoSql = "INSERT INTO timing_data (name, host, timestamp, count, average, max, min) VALUES (?, ?, ?, ?, ?, ?, ?)"; + try (PreparedStatement statement = conn.prepareStatement(addTimingInfoSql)) { + + for(String name:timingMapCopy.keySet()) { + TimingInfo info = timingMapCopy.get(name); + + statement.setString(1, name); + statement.setString(2, hostName); + statement.setLong(3, System.currentTimeMillis()); + statement.setLong(4, info.getCount()); + statement.setLong(5, info.getAverage()); + statement.setLong(6, info.getMax()); + statement.setLong(7, info.getMin()); + + statement.execute(); + } + + } catch (SQLException ex) { + throw new HealthMonitorException("Error saving metric data to database", ex); + } finally { + try { + conn.close(); + } catch (SQLException ex) { + logger.log(Level.SEVERE, "Error closing Connection.", ex); + } + } + } catch (CoordinationService.CoordinationServiceException ex) { + throw new HealthMonitorException("Error releasing database lock", ex); + } + } + + /** + * Check whether the health monitor database exists. + * Does not check the schema. + * @return true if the database exists, false otherwise + * @throws HealthMonitorException + */ + private boolean databaseExists() throws HealthMonitorException { + try { + // Use the same database settings as the case + CaseDbConnectionInfo db = UserPreferences.getDatabaseConnectionInfo(); + Class.forName("org.postgresql.Driver"); //NON-NLS + ResultSet rs = null; + try (Connection connection = DriverManager.getConnection("jdbc:postgresql://" + db.getHost() + ":" + db.getPort() + "/postgres", db.getUserName(), db.getPassword()); //NON-NLS + Statement statement = connection.createStatement();) { + String createCommand = "SELECT 1 AS result FROM pg_database WHERE datname='" + DATABASE_NAME + "'"; + rs = statement.executeQuery(createCommand); + if(rs.next()) { + logger.log(Level.INFO, "Existing Enterprise Health Monitor database found"); + return true; + } + } finally { + if(rs != null) { + rs.close(); + } + } + } catch (UserPreferencesException | ClassNotFoundException | SQLException ex) { + throw new HealthMonitorException("Failed check for health monitor database", ex); + } + return false; + } + + /** + * Create a new health monitor database. + * @throws HealthMonitorException + */ + private void createDatabase() throws HealthMonitorException { + try { + // Use the same database settings as the case + CaseDbConnectionInfo db = UserPreferences.getDatabaseConnectionInfo(); + Class.forName("org.postgresql.Driver"); //NON-NLS + try (Connection connection = DriverManager.getConnection("jdbc:postgresql://" + db.getHost() + ":" + db.getPort() + "/postgres", db.getUserName(), db.getPassword()); //NON-NLS + Statement statement = connection.createStatement();) { + String createCommand = "CREATE DATABASE \"" + DATABASE_NAME + "\" OWNER \"" + db.getUserName() + "\""; //NON-NLS + statement.execute(createCommand); + } + logger.log(Level.INFO, "Created new health monitor database " + DATABASE_NAME); + } catch (UserPreferencesException | ClassNotFoundException | SQLException ex) { + throw new HealthMonitorException("Failed to delete health monitor database", ex); + } + } + + /** + * Setup a connection pool for db connections. + * @throws HealthMonitorException + */ + private void setupConnectionPool() throws HealthMonitorException { + try { + CaseDbConnectionInfo db = UserPreferences.getDatabaseConnectionInfo(); + + connectionPool = new BasicDataSource(); + connectionPool.setDriverClassName("org.postgresql.Driver"); + + StringBuilder connectionURL = new StringBuilder(); + connectionURL.append("jdbc:postgresql://"); + connectionURL.append(db.getHost()); + connectionURL.append(":"); + connectionURL.append(db.getPort()); + connectionURL.append("/"); + connectionURL.append(DATABASE_NAME); + + connectionPool.setUrl(connectionURL.toString()); + connectionPool.setUsername(db.getUserName()); + connectionPool.setPassword(db.getPassword()); + + // tweak pool configuration + connectionPool.setInitialSize(3); // start with 3 connections + connectionPool.setMaxIdle(CONN_POOL_SIZE); // max of 10 idle connections + connectionPool.setValidationQuery("SELECT version()"); + } catch (UserPreferencesException ex) { + throw new HealthMonitorException("Error loading database configuration", ex); + } + } + + /** + * Shut down the connection pool + * @throws HealthMonitorException + */ + private void shutdownConnections() throws HealthMonitorException { + try { + synchronized(this) { + if(connectionPool != null){ + connectionPool.close(); + connectionPool = null; // force it to be re-created on next connect() + } + } + } catch (SQLException ex) { + throw new HealthMonitorException("Failed to close existing database connections.", ex); // NON-NLS + } + } + + /** + * Get a database connection. + * Sets up the connection pool if needed. + * @return The Connection object + * @throws HealthMonitorException + */ + private Connection connect() throws HealthMonitorException { + synchronized (this) { + if (connectionPool == null) { + setupConnectionPool(); + } + } + + try { + return connectionPool.getConnection(); + } catch (SQLException ex) { + throw new HealthMonitorException("Error getting connection from connection pool.", ex); // NON-NLS + } + } + + /** + * Test whether the database schema has been initialized. + * We do this by looking for the version number. + * @return True if it has been initialized, false otherwise. + * @throws HealthMonitorException + */ + private boolean databaseIsInitialized() throws HealthMonitorException { + Connection conn = connect(); + if(conn == null) { + throw new HealthMonitorException("Error getting database connection"); + } + ResultSet resultSet = null; + + try (Statement statement = conn.createStatement()) { + resultSet = statement.executeQuery("SELECT value FROM db_info WHERE name='SCHEMA_VERSION'"); + return resultSet.next(); + } catch (SQLException ex) { + // This likely just means that the db_info table does not exist + return false; + } finally { + if(resultSet != null) { + try { + resultSet.close(); + } catch (SQLException ex) { + logger.log(Level.SEVERE, "Error closing result set", ex); + } + } + try { + conn.close(); + } catch (SQLException ex) { + logger.log(Level.SEVERE, "Error closing Connection.", ex); + } + } + } + + /** + * Get the current schema version + * @return the current schema version + * @throws HealthMonitorException + */ + private CaseDbSchemaVersionNumber getVersion() throws HealthMonitorException { + Connection conn = connect(); + if(conn == null) { + throw new HealthMonitorException("Error getting database connection"); + } + ResultSet resultSet = null; + + try (Statement statement = conn.createStatement()) { + int minorVersion = 0; + int majorVersion = 0; + resultSet = statement.executeQuery("SELECT value FROM db_info WHERE name='SCHEMA_MINOR_VERSION'"); + if (resultSet.next()) { + String minorVersionStr = resultSet.getString("value"); + try { + minorVersion = Integer.parseInt(minorVersionStr); + } catch (NumberFormatException ex) { + throw new HealthMonitorException("Bad value for schema minor version (" + minorVersionStr + ") - database is corrupt"); + } + } + + resultSet = statement.executeQuery("SELECT value FROM db_info WHERE name='SCHEMA_VERSION'"); + if (resultSet.next()) { + String majorVersionStr = resultSet.getString("value"); + try { + majorVersion = Integer.parseInt(majorVersionStr); + } catch (NumberFormatException ex) { + throw new HealthMonitorException("Bad value for schema version (" + majorVersionStr + ") - database is corrupt"); + } + } + + return new CaseDbSchemaVersionNumber(majorVersion, minorVersion); + } catch (SQLException ex) { + throw new HealthMonitorException("Error initializing database", ex); + } finally { + if(resultSet != null) { + try { + resultSet.close(); + } catch (SQLException ex) { + logger.log(Level.SEVERE, "Error closing result set", ex); + } + } + try { + conn.close(); + } catch (SQLException ex) { + logger.log(Level.SEVERE, "Error closing Connection.", ex); + } + } + } + + /** + * Initialize the database. + * @throws HealthMonitorException + */ + private void initializeDatabaseSchema() throws HealthMonitorException { + Connection conn = connect(); + if(conn == null) { + throw new HealthMonitorException("Error getting database connection"); + } + + try (Statement statement = conn.createStatement()) { + conn.setAutoCommit(false); + + String createTimingTable = + "CREATE TABLE IF NOT EXISTS timing_data (" + + "id SERIAL PRIMARY KEY," + + "name text NOT NULL," + + "host text NOT NULL," + + "timestamp bigint NOT NULL," + + "count bigint NOT NULL," + + "average bigint NOT NULL," + + "max bigint NOT NULL," + + "min bigint NOT NULL" + + ")"; + statement.execute(createTimingTable); + + String createDbInfoTable = + "CREATE TABLE IF NOT EXISTS db_info (" + + "id SERIAL PRIMARY KEY NOT NULL," + + "name text NOT NULL," + + "value text NOT NULL" + + ")"; + statement.execute(createDbInfoTable); + + statement.execute("INSERT INTO db_info (name, value) VALUES ('SCHEMA_VERSION', '" + CURRENT_DB_SCHEMA_VERSION.getMajor() + "')"); + statement.execute("INSERT INTO db_info (name, value) VALUES ('SCHEMA_MINOR_VERSION', '" + CURRENT_DB_SCHEMA_VERSION.getMinor() + "')"); + + conn.commit(); + } catch (SQLException ex) { + try { + conn.rollback(); + } catch (SQLException ex2) { + logger.log(Level.SEVERE, "Rollback error"); + } + throw new HealthMonitorException("Error initializing database", ex); + } finally { + try { + conn.close(); + } catch (SQLException ex) { + logger.log(Level.SEVERE, "Error closing connection.", ex); + } + } + } + + /** + * The task called by the ScheduledThreadPoolExecutor to handle + * the database writes. + */ + static final class DatabaseWriteTask implements Runnable { + + /** + * Write current metric data to the database + */ + @Override + public void run() { + try { + getInstance().writeCurrentStateToDatabase(); + } catch (HealthMonitorException ex) { + logger.log(Level.SEVERE, "Error writing current metrics to database", ex); //NON-NLS + } + } + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + + switch (Case.Events.valueOf(evt.getPropertyName())) { + + case CURRENT_CASE: + if ((null == evt.getNewValue()) && (evt.getOldValue() instanceof Case)) { + // When a case is closed, write the current metrics to the database + healthMonitorExecutor.submit(new EnterpriseHealthMonitor.DatabaseWriteTask()); + } + break; + } + } + + /** + * Get an exclusive lock for the health monitor database. + * Acquire this before creating, initializing, or updating the database schema. + * @return The lock + * @throws HealthMonitorException + */ + private CoordinationService.Lock getExclusiveDbLock() throws HealthMonitorException{ + try { + CoordinationService.Lock lock = CoordinationService.getInstance().tryGetExclusiveLock(CoordinationService.CategoryNode.HEALTH_MONITOR, DATABASE_NAME, 5, TimeUnit.MINUTES); + + if(lock != null){ + return lock; + } + throw new HealthMonitorException("Error acquiring database lock"); + } catch (InterruptedException | CoordinationService.CoordinationServiceException ex){ + throw new HealthMonitorException("Error acquiring database lock", ex); + } + } + + /** + * Get an shared lock for the health monitor database. + * Acquire this before database reads or writes. + * @return The lock + * @throws HealthMonitorException + */ + private CoordinationService.Lock getSharedDbLock() throws HealthMonitorException{ + try { + String databaseNodeName = DATABASE_NAME; + CoordinationService.Lock lock = CoordinationService.getInstance().tryGetSharedLock(CoordinationService.CategoryNode.HEALTH_MONITOR, databaseNodeName, 5, TimeUnit.MINUTES); + + if(lock != null){ + return lock; + } + throw new HealthMonitorException("Error acquiring database lock"); + } catch (InterruptedException | CoordinationService.CoordinationServiceException ex){ + throw new HealthMonitorException("Error acquiring database lock"); + } + } + + /** + * Internal class for collecting timing metrics. + * Instead of storing each TimingMetric, we only store the min and max + * seen and the number of metrics and total duration to compute the average + * later. + * One TimingInfo instance should be created per metric name, and + * additional timing metrics will be added to it. + */ + private class TimingInfo { + private long count; // Number of metrics collected + private long sum; // Sum of the durations collected (nanoseconds) + private long max; // Maximum value found (nanoseconds) + private long min; // Minimum value found (nanoseconds) + + TimingInfo(TimingMetric metric) throws HealthMonitorException { + count = 1; + sum = metric.getDuration(); + max = metric.getDuration(); + min = metric.getDuration(); + } + + /** + * Add a new TimingMetric to an existing TimingInfo object. + * This is called in a synchronized block for almost all new + * TimingMetric objects, so do as little processing here as possible. + * @param metric The new metric + * @throws HealthMonitorException Will be thrown if the metric hasn't been stopped + */ + void addMetric(TimingMetric metric) throws HealthMonitorException { + + // Keep track of needed info to calculate the average + count++; + sum += metric.getDuration(); + + // Check if this is the longest duration seen + if(max < metric.getDuration()) { + max = metric.getDuration(); + } + + // Check if this is the lowest duration seen + if(min > metric.getDuration()) { + min = metric.getDuration(); + } + } + + /** + * Get the average duration + * @return average duration (nanoseconds) + */ + long getAverage() { + return sum / count; + } + + /** + * Get the maximum duration + * @return maximum duration (nanoseconds) + */ + long getMax() { + return max; + } + + /** + * Get the minimum duration + * @return minimum duration (nanoseconds) + */ + long getMin() { + return min; + } + + /** + * Get the total number of metrics collected + * @return number of metrics collected + */ + long getCount() { + return count; + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/communications/EventHandler.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorException.java similarity index 64% rename from Core/src/org/sleuthkit/autopsy/communications/EventHandler.java rename to Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorException.java index f1acedf1cf..6df918acaa 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/EventHandler.java +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorException.java @@ -16,13 +16,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -package org.sleuthkit.autopsy.communications; +package org.sleuthkit.autopsy.healthmonitor; /** - * + * Exception used internally by the Services Health Monitor */ -public interface EventHandler { +class HealthMonitorException extends Exception { + private static final long serialVersionUID = 1L; - void handle(T event); + HealthMonitorException(String message) { + super(message); + } + + HealthMonitorException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/Installer.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/Installer.java new file mode 100644 index 0000000000..61ea5a5244 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/Installer.java @@ -0,0 +1,53 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2018 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.healthmonitor; + +import java.util.logging.Level; +import org.openide.modules.ModuleInstall; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; + +public class Installer extends ModuleInstall { + + private static final Logger logger = Logger.getLogger(Installer.class.getName()); + private static final long serialVersionUID = 1L; + + private static Installer instance; + + public synchronized static Installer getDefault() { + if (instance == null) { + instance = new Installer(); + } + return instance; + } + + private Installer() { + super(); + } + + @Override + public void restored() { + + try { + EnterpriseHealthMonitor.startUpIfEnabled(); + } catch (HealthMonitorException ex) { + logger.log(Level.SEVERE, "Error starting health services monitor", ex); + } + } +} \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/TimingMetric.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/TimingMetric.java new file mode 100644 index 0000000000..d94dfd7161 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/TimingMetric.java @@ -0,0 +1,65 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2018 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.healthmonitor; + +/** + * Used to calculate and report timing metrics. + */ +public class TimingMetric { + + private final String name; + private final long startingTimestamp; + private Long duration; + + TimingMetric(String name) { + this.name = name; + this.startingTimestamp = System.nanoTime(); + this.duration = null; + } + + /** + * Record how long the metric was running. + */ + void stopTiming() { + long endingTimestamp = System.nanoTime(); + this.duration = endingTimestamp - startingTimestamp; + } + + /** + * Get the name of metric + * @return name + */ + String getName() { + return name; + } + + /** + * Get the duration of the metric. Will throw an exception if the + * metric has not been stopped. + * @return how long the metric was running (nanoseconds) + * @throws HealthMonitorException + */ + long getDuration() throws HealthMonitorException { + if (duration != null) { + return duration; + } else { + throw new HealthMonitorException("getDuration() called before stopTiming()"); + } + } +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/VolatilityProcessor.java b/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/VolatilityProcessor.java index fd285ae1aa..50246f5255 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/VolatilityProcessor.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/VolatilityProcessor.java @@ -136,7 +136,6 @@ class VolatilityProcessor { break; } String pluginToRun = pluginsToRun.get(i); - progressMonitor.setProgressText(Bundle.VolatilityProcessor_progressMessage_runningImageInfo(pluginToRun)); runVolatilityPlugin(pluginToRun); progressMonitor.setProgress(i); } @@ -172,6 +171,8 @@ class VolatilityProcessor { "VolatilityProcessor_exceptionMessage_errorIndexingOutput=Error indexing output for {0} plugin" }) private void runVolatilityPlugin(String pluginToRun) throws VolatilityProcessorException { + progressMonitor.setProgressText("Running module " + pluginToRun); + List commandLine = new ArrayList<>(); commandLine.add("\"" + executableFile + "\""); //NON-NLS File memoryImage = new File(memoryImagePath); @@ -314,6 +315,8 @@ class VolatilityProcessor { String filePath = volfile.getParent(); + logger.log(Level.INFO, "Looking up file " + fileName + " at path " + filePath); + try { List resolvedFiles; if (filePath == null) { @@ -333,12 +336,13 @@ class VolatilityProcessor { } fileName += ".%"; //NON-NLS + logger.log(Level.INFO, "Looking up file (extension wildcard) " + fileName + " at path " + filePath); + if (filePath == null) { resolvedFiles = fileManager.findFiles(fileName); //NON-NLS } else { resolvedFiles = fileManager.findFiles(fileName, filePath); //NON-NLS } - } if (resolvedFiles.isEmpty()) { @@ -387,6 +391,7 @@ class VolatilityProcessor { * @param pluginOutputFile File that contains the output to parse. */ private void createArtifactsFromPluginOutput(String pluginName, File pluginOutputFile) throws VolatilityProcessorException { + progressMonitor.setProgressText("Parsing module " + pluginName); Set fileSet = null; switch (pluginName) { case "dlllist": //NON-NLS @@ -421,6 +426,7 @@ class VolatilityProcessor { } if (fileSet != null && !fileSet.isEmpty()) { + progressMonitor.setProgressText("Flagging files from module " + pluginName); flagFiles(fileSet, pluginName); } } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java index c7f4c07f6a..b9c4541c7b 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java @@ -27,6 +27,8 @@ import org.apache.solr.common.SolrInputDocument; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datamodel.ContentUtils; +import org.sleuthkit.autopsy.healthmonitor.EnterpriseHealthMonitor; +import org.sleuthkit.autopsy.healthmonitor.TimingMetric; import org.sleuthkit.autopsy.ingest.IngestJobContext; import org.sleuthkit.autopsy.keywordsearch.Chunker.Chunk; import org.sleuthkit.datamodel.AbstractFile; @@ -235,7 +237,9 @@ class Ingester { try { //TODO: consider timeout thread, or vary socket timeout based on size of indexed content + TimingMetric metric = EnterpriseHealthMonitor.getTimingMetric("Solr: Index chunk"); solrServer.addDocument(updateDoc); + EnterpriseHealthMonitor.submitTimingMetric(metric); uncommitedIngests = true; } catch (KeywordSearchModuleException | NoOpenCoreException ex) { diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java index 416f2b394f..eea4d2914e 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java @@ -70,6 +70,8 @@ import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ModuleSettings; import org.sleuthkit.autopsy.coreutils.PlatformUtil; +import org.sleuthkit.autopsy.healthmonitor.EnterpriseHealthMonitor; +import org.sleuthkit.autopsy.healthmonitor.TimingMetric; import org.sleuthkit.autopsy.keywordsearchservice.KeywordSearchServiceException; import org.sleuthkit.datamodel.Content; @@ -773,7 +775,9 @@ public class Server { IndexingServerProperties properties = getMultiUserServerProperties(theCase.getCaseDirectory()); currentSolrServer = new HttpSolrServer("http://" + properties.getHost() + ":" + properties.getPort() + "/solr"); //NON-NLS } + TimingMetric metric = EnterpriseHealthMonitor.getTimingMetric("Solr: Connectivity check"); connectToSolrServer(currentSolrServer); + EnterpriseHealthMonitor.submitTimingMetric(metric); } catch (SolrServerException | IOException ex) { throw new KeywordSearchModuleException(NbBundle.getMessage(Server.class, "Server.connect.exception.msg", ex.getLocalizedMessage()), ex);