/* * Autopsy Forensic Browser * * Copyright 2013-2015 Basis Technology Corp. * Contact: carrier <at> sleuthkit <dot> 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.core; import org.sleuthkit.autopsy.core.events.ServiceEvent; import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.beans.PropertyChangeListener; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.stream.Collectors; import java.util.stream.Stream; import org.openide.util.Lookup; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.events.AutopsyEventPublisher; import org.sleuthkit.autopsy.keywordsearchservice.KeywordSearchService; import org.sleuthkit.autopsy.events.MessageServiceConnectionInfo; import org.sleuthkit.autopsy.events.MessageServiceException; import org.sleuthkit.autopsy.keywordsearchservice.KeywordSearchServiceException; import org.sleuthkit.datamodel.CaseDbConnectionInfo; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; /** * This class periodically checks availability of collaboration resources - * remote database, remote keyword search server, messaging service - and * reports status updates to the user in case of a gap in service. */ public class ServicesMonitor { private AutopsyEventPublisher eventPublisher; private static final Logger logger = Logger.getLogger(ServicesMonitor.class.getName()); private final ScheduledThreadPoolExecutor periodicTasksExecutor; private static final String PERIODIC_TASK_THREAD_NAME = "services-monitor-periodic-task-%d"; //NON-NLS private static final int NUMBER_OF_PERIODIC_TASK_THREADS = 1; private static final long CRASH_DETECTION_INTERVAL_MINUTES = 2; private static final Set<String> servicesList = Stream.of(ServicesMonitor.Service.values()) .map(Service::toString) .collect(Collectors.toSet()); /** * The service monitor maintains a mapping of each service to it's last * status update. */ private final ConcurrentHashMap<String, String> statusByService; /** * Call constructor on start-up so that the first check of services is done * as soon as possible. */ private static ServicesMonitor instance = new ServicesMonitor(); /** * List of services that are being monitored. The service names should be * representative of the service functionality and readable as they get * logged when service outage occurs. */ public enum Service { /** * Property change event fired when remote case database service status * changes. New value is set to updated ServiceStatus, old value is * null. */ REMOTE_CASE_DATABASE(NbBundle.getMessage(ServicesMonitor.class, "ServicesMonitor.remoteCaseDatabase.displayName.text")), /** * Property change event fired when remote keyword search service status * changes. New value is set to updated ServiceStatus, old value is * null. */ REMOTE_KEYWORD_SEARCH(NbBundle.getMessage(ServicesMonitor.class, "ServicesMonitor.remoteKeywordSearch.displayName.text")), /** * Property change event fired when messaging service status changes. * New value is set to updated ServiceStatus, old value is null. */ MESSAGING(NbBundle.getMessage(ServicesMonitor.class, "ServicesMonitor.messaging.displayName.text")); private final String displayName; private Service(String name) { this.displayName = name; } public String getDisplayName() { return displayName; } }; /** * List of possible service statuses. */ public enum ServiceStatus { /** * Service is currently up. */ UP, /** * Service is currently down. */ DOWN }; public synchronized static ServicesMonitor getInstance() { if (instance == null) { instance = new ServicesMonitor(); } return instance; } private ServicesMonitor() { this.eventPublisher = new AutopsyEventPublisher(); this.statusByService = new ConcurrentHashMap<>(); // First check is triggered immediately on current thread. checkAllServices(); /** * Start periodic task that check the availability of key collaboration * services. */ periodicTasksExecutor = new ScheduledThreadPoolExecutor(NUMBER_OF_PERIODIC_TASK_THREADS, new ThreadFactoryBuilder().setNameFormat(PERIODIC_TASK_THREAD_NAME).build()); periodicTasksExecutor.scheduleAtFixedRate(new CrashDetectionTask(), CRASH_DETECTION_INTERVAL_MINUTES, CRASH_DETECTION_INTERVAL_MINUTES, TimeUnit.MINUTES); } /** * Updates service status and publishes the service status update if it is * different from previous status. Event is published locally. Logs status * changes. * * @param service Name of the service. * @param status Updated status for the service. * @param details Details of the event. * */ public void setServiceStatus(String service, String status, String details) { // if the status update is for an existing service who's status hasn't changed - do nothing. if (statusByService.containsKey(service) && status.equals(statusByService.get(service))) { return; } // new service or status has changed - identify service's display name String serviceDisplayName; try { serviceDisplayName = ServicesMonitor.Service.valueOf(service).getDisplayName(); } catch (IllegalArgumentException ignore) { // custom service that is not listed in ServicesMonitor.Service enum. Use service name as display name. serviceDisplayName = service; } if (status.equals(ServiceStatus.UP.toString())) { logger.log(Level.INFO, "Connection to {0} is up", serviceDisplayName); //NON-NLS MessageNotifyUtil.Notify.info(NbBundle.getMessage(ServicesMonitor.class, "ServicesMonitor.restoredService.notify.title"), NbBundle.getMessage(ServicesMonitor.class, "ServicesMonitor.restoredService.notify.msg", serviceDisplayName)); } else if (status.equals(ServiceStatus.DOWN.toString())) { logger.log(Level.SEVERE, "Failed to connect to {0}", serviceDisplayName); //NON-NLS MessageNotifyUtil.Notify.error(NbBundle.getMessage(ServicesMonitor.class, "ServicesMonitor.failedService.notify.title"), NbBundle.getMessage(ServicesMonitor.class, "ServicesMonitor.failedService.notify.msg", serviceDisplayName)); } else { logger.log(Level.INFO, "Status for {0} is {1}", new Object[]{serviceDisplayName, status}); //NON-NLS MessageNotifyUtil.Notify.info(NbBundle.getMessage(ServicesMonitor.class, "ServicesMonitor.statusChange.notify.title"), NbBundle.getMessage(ServicesMonitor.class, "ServicesMonitor.statusChange.notify.msg", new Object[]{serviceDisplayName, status})); } // update and publish new status statusByService.put(service, status); eventPublisher.publishLocally(new ServiceEvent(service, status, details)); } /** * Get last status update for a service. * * @param service Name of the service. * * @return ServiceStatus Status for the service. * * @throws ServicesMonitorException If service name is null or service * doesn't exist. */ public String getServiceStatus(String service) throws ServicesMonitorException { if (service == null) { throw new ServicesMonitorException(NbBundle.getMessage(ServicesMonitor.class, "ServicesMonitor.nullServiceName.excepton.txt")); } // if request is for one of our "core" services - perform an on demand check // to make sure we have the latest status. if (servicesList.contains(service)) { checkServiceStatus(service); } String status = statusByService.get(service); if (status == null) { // no such service throw new ServicesMonitorException(NbBundle.getMessage(ServicesMonitor.class, "ServicesMonitor.unknownServiceName.excepton.txt", service)); } return status; } /** * Performs service availability status check. * * @param service Name of the service. */ private void checkServiceStatus(String service) { if (service.equals(Service.REMOTE_CASE_DATABASE.toString())) { checkDatabaseConnectionStatus(); } else if (service.equals(Service.REMOTE_KEYWORD_SEARCH.toString())) { checkKeywordSearchServerConnectionStatus(); } else if (service.equals(Service.MESSAGING.toString())) { checkMessagingServerConnectionStatus(); } } /** * Performs case database service availability status check. */ private void checkDatabaseConnectionStatus() { CaseDbConnectionInfo info; try { info = UserPreferences.getDatabaseConnectionInfo(); } catch (UserPreferencesException ex) { logger.log(Level.SEVERE, "Error accessing case database connection info", ex); //NON-NLS setServiceStatus(Service.REMOTE_CASE_DATABASE.toString(), ServiceStatus.DOWN.toString(), NbBundle.getMessage(this.getClass(), "ServicesMonitor.databaseConnectionInfo.error.msg")); return; } try { SleuthkitCase.tryConnect(info); setServiceStatus(Service.REMOTE_CASE_DATABASE.toString(), ServiceStatus.UP.toString(), ""); } catch (TskCoreException ex) { setServiceStatus(Service.REMOTE_CASE_DATABASE.toString(), ServiceStatus.DOWN.toString(), ex.getMessage()); } } /** * Performs keyword search service availability status check. */ private void checkKeywordSearchServerConnectionStatus() { KeywordSearchService kwsService = Lookup.getDefault().lookup(KeywordSearchService.class); try { if (kwsService != null) { int port = Integer.parseUnsignedInt(UserPreferences.getIndexingServerPort()); kwsService.tryConnect(UserPreferences.getIndexingServerHost(), port); setServiceStatus(Service.REMOTE_KEYWORD_SEARCH.toString(), ServiceStatus.UP.toString(), ""); } else { setServiceStatus(Service.REMOTE_KEYWORD_SEARCH.toString(), ServiceStatus.DOWN.toString(), NbBundle.getMessage(ServicesMonitor.class, "ServicesMonitor.KeywordSearchNull")); } } catch (NumberFormatException ex) { String rootCause = NbBundle.getMessage(ServicesMonitor.class, "ServicesMonitor.InvalidPortNumber"); logger.log(Level.SEVERE, "Unable to connect to messaging server: " + rootCause, ex); //NON-NLS setServiceStatus(Service.REMOTE_KEYWORD_SEARCH.toString(), ServiceStatus.DOWN.toString(), rootCause); } catch (KeywordSearchServiceException ex) { String rootCause = ex.getMessage(); logger.log(Level.SEVERE, "Unable to connect to messaging server: " + rootCause, ex); //NON-NLS setServiceStatus(Service.REMOTE_KEYWORD_SEARCH.toString(), ServiceStatus.DOWN.toString(), rootCause); } } /** * Performs messaging service availability status check. */ private void checkMessagingServerConnectionStatus() { MessageServiceConnectionInfo info; try { info = UserPreferences.getMessageServiceConnectionInfo(); } catch (UserPreferencesException ex) { logger.log(Level.SEVERE, "Error accessing messaging service connection info", ex); //NON-NLS setServiceStatus(Service.MESSAGING.toString(), ServiceStatus.DOWN.toString(), NbBundle.getMessage(this.getClass(), "ServicesMonitor.messagingService.connErr.text")); return; } try { info.tryConnect(); setServiceStatus(Service.MESSAGING.toString(), ServiceStatus.UP.toString(), ""); } catch (MessageServiceException ex) { String rootCause = ex.getMessage(); logger.log(Level.SEVERE, "Unable to connect to messaging server: " + rootCause, ex); //NON-NLS setServiceStatus(Service.MESSAGING.toString(), ServiceStatus.DOWN.toString(), rootCause); } } /** * Adds an event subscriber to this publisher. Subscriber will be subscribed * to all events from this publisher. * * @param subscriber The subscriber to add. */ public void addSubscriber(PropertyChangeListener subscriber) { eventPublisher.addSubscriber(servicesList, subscriber); } /** * Adds an event subscriber to this publisher. * * @param eventNames The events the subscriber is interested in. * @param subscriber The subscriber to add. */ public void addSubscriber(Set<String> eventNames, PropertyChangeListener subscriber) { eventPublisher.addSubscriber(eventNames, subscriber); } /** * Adds an event subscriber to this publisher. * * @param eventName The event the subscriber is interested in. * @param subscriber The subscriber to add. */ public void addSubscriber(String eventName, PropertyChangeListener subscriber) { eventPublisher.addSubscriber(eventName, subscriber); } /** * Removes an event subscriber from this publisher. * * @param eventNames The events the subscriber is no longer interested in. * @param subscriber The subscriber to remove. */ public void removeSubscriber(Set<String> eventNames, PropertyChangeListener subscriber) { eventPublisher.removeSubscriber(eventNames, subscriber); } /** * Removes an event subscriber from this publisher. * * @param eventName The event the subscriber is no longer interested in. * @param subscriber The subscriber to remove. */ public void removeSubscriber(String eventName, PropertyChangeListener subscriber) { eventPublisher.removeSubscriber(eventName, subscriber); } /** * Removes an event subscriber to this publisher. Subscriber will be removed * from all event notifications from this publisher. * * @param subscriber The subscriber to remove. */ public void removeSubscriber(PropertyChangeListener subscriber) { eventPublisher.removeSubscriber(servicesList, subscriber); } /** * Verifies connectivity to all services. */ private void checkAllServices() { if (!UserPreferences.getIsMultiUserModeEnabled()) { return; } for (String service : servicesList) { checkServiceStatus(service); } } /** * A Runnable task that periodically checks the availability of * collaboration resources (remote database, remote keyword search service, * message broker) and reports status to the user in case of a gap in * service. */ private final class CrashDetectionTask implements Runnable { /** * Monitor the availability of collaboration resources */ @Override public void run() { try { checkAllServices(); } catch (Exception ex) { logger.log(Level.SEVERE, "Unexpected exception in CrashDetectionTask", ex); //NON-NLS } } } /** * Exception thrown when service status query results in an error. */ public class ServicesMonitorException extends Exception { private static final long serialVersionUID = 1L; public ServicesMonitorException(String message) { super(message); } public ServicesMonitorException(String message, Throwable cause) { super(message, cause); } } }