diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED index 1f9719688f..5dfaba3f43 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED @@ -110,6 +110,14 @@ Case_caseType_singleUser=Single-user case Case_checkImagePaths_exceptionOccurred=An exception occurred while checking if image paths are present # {0} - paths Case_checkImagePaths_noPaths=The following images had no associated paths: {0} +# {0} - appplicationName +Case_createCaseDatabase_fileLock_concurrentAccessException=The case is open in {0}. Please close it before attempting to open it in Autopsy. +Case_createCaseDatabase_fileLock_concurrentAccessException_defaultApp=another application +Case_createCaseDatabase_fileLock_ioException=An error occurred while trying to get an exclusive lock on the case. +# {0} - appplicationName +Case_openCaseDataBase_fileLock_concurrentAccessException=The case is open in {0}. Please close it before attempting to open it in Autopsy. +Case_openCaseDataBase_fileLock_concurrentAccessException_defaultApp=another application +Case_openCaseDataBase_fileLock_ioException=An error occurred while trying to get an exclusive lock on the case. CaseDetailsPanel.casePanel.border.title=Case CaseDetailsPanel.examinerLabel.text=Name: CaseDetailsPanel.examinerPanel.border.title=Examiner diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java index cccd1dc856..3e0a84e93f 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java @@ -32,8 +32,8 @@ import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.File; import java.io.IOException; -import java.io.RandomAccessFile; import java.lang.reflect.InvocationTargetException; +import java.nio.channels.OverlappingFileLockException; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; @@ -78,6 +78,7 @@ import org.sleuthkit.autopsy.actions.OpenOutputFolderAction; import org.sleuthkit.autopsy.appservices.AutopsyService; import org.sleuthkit.autopsy.appservices.AutopsyService.CaseContext; import org.sleuthkit.autopsy.casemodule.CaseMetadata.CaseMetadataException; +import org.sleuthkit.autopsy.casemodule.TskLockResources.ConcurrentDbAccessException; import org.sleuthkit.autopsy.datasourcesummary.ui.DataSourceSummaryAction; import org.sleuthkit.autopsy.casemodule.events.AddingDataSourceEvent; import org.sleuthkit.autopsy.casemodule.events.AddingDataSourceFailedEvent; @@ -183,7 +184,6 @@ public class Case { private static final String CASE_RESOURCES_THREAD_NAME = "%s-manage-case-resources"; private static final String NO_NODE_ERROR_MSG_FRAGMENT = "KeeperErrorCode = NoNode"; private static final String CT_PROVIDER_PREFIX = "CTStandardContentProvider_"; - private static final String LOCK_FILE_NAME = "lock"; private static final Logger logger = Logger.getLogger(Case.class.getName()); private static final AutopsyEventPublisher eventPublisher = new AutopsyEventPublisher(); private static final Object caseActionSerializationLock = new Object(); @@ -199,11 +199,8 @@ public class Case { private final SleuthkitEventListener sleuthkitEventListener; private CollaborationMonitor collaborationMonitor; private Services caseServices; - - private RansomAccessFile lockFileRaf = null; - private FileChannel lockFileChannel = null; - private FileLock lockFileLock = null; - + + private TskLockResources tskLockResources = null; private volatile boolean hasDataSource = false; private volatile boolean hasData = false; @@ -217,6 +214,7 @@ public class Case { mainFrame = WindowManager.getDefault().getMainWindow(); }); } + /** * An enumeration of case types. @@ -777,59 +775,6 @@ public class Case { || caseName.contains("<") || caseName.contains(">") || caseName.contains("|")); } - - /** - * Try to acquire a lock to the lock file in the case directory. - * @param caseDir The case directory that the autopsy.db is in. - * @throws IllegalAccessException - * @throws IOException - */ - private void tryAcquireFileLock(String caseDir) throws ConcurrentDbAccessException, IOException { - File lockFile = new File(caseDir, LOCK_FILE_NAME); - lockFile.getParentFile().mkdirs(); - lockFileRaf = new RandomAccessFile(lockFile, "rw"); - lockFileChannel = lockFileRaf.getChannel(); - lockFileLock = lockFileChannel.tryLock(); - if (lockFileLock == null) { - String conflictingApplication = null; - try { - StringBuffer buffer = new StringBuffer(); - while (lockFileRaf.getFilePointer() < lockFileRaf.length()) { - buffer.append(lockFileRaf.readLine() + System.lineSeparator()); - } - conflictingApplication = buffer.toString(); - } finally { - throw new ConcurrentDbAccessException("Unable to acquire lock on " + lockFile, conflictingApplication); - } - } else { - lockFileRaf.setLength(0); - lockFileRaf.writeChars(APP_NAME); - } - } - - /** - * An exception thrown if the database is currently in use. - */ - private static class ConcurrentDbAccessException extends Exception { - private final String conflictingApplicationName; - - /** - * Constructor. - * @param message The exception message. - * @param conflictingApplicationName The conflicting application name (or null if unknown). - */ - public ConcurrentDbAccessException(String message, String conflictingApplicationName) { - super(message); - this.conflictingApplicationName = conflictingApplicationName; - } - - /** - * @return The conflicting application name (or null if unknown). - */ - public String getConflictingApplicationName() { - return conflictingApplicationName; - } - } /** * Creates a new case and makes it the current case. @@ -2802,13 +2747,13 @@ public class Case { * directory. */ try { - tryAcquireFileLock(metadata.getCaseDirectory()); - } catch (IOException ex) { - throw new CaseActionException(Bundle.Case_createCaseDatabase_fileLock_ioException(), ex); + this.tskLockResources = TskLockResources.tryAcquireFileLock(metadata.getCaseDirectory(), UserPreferences.getAppName()); + } catch (IOException | OverlappingFileLockException ex) { + throw new CaseActionException(Bundle.Case_openCaseDataBase_fileLock_ioException(), ex); } catch (ConcurrentDbAccessException ex) { - throw new CaseActionException(Bundle.Case_createCaseDatabase_fileLock_concurrentAccessException( + throw new CaseActionException(Bundle.Case_openCaseDataBase_fileLock_concurrentAccessException( StringUtils.defaultIfBlank(ex.getConflictingApplicationName(), - Bundle.Case_createCaseDatabase_fileLock_concurrentAccessException_defaultApp()) + Bundle.Case_openCaseDataBase_fileLock_concurrentAccessException_defaultApp()) ), ex); } caseDb = SleuthkitCase.newCase(Paths.get(metadata.getCaseDirectory(), SINGLE_USER_CASE_DB_NAME).toString()); @@ -2868,8 +2813,8 @@ public class Case { if (CaseType.SINGLE_USER_CASE == metadata.getCaseType()) { try { - tryAcquireFileLock(metadata.getCaseDirectory()); - } catch (IOException ex) { + this.tskLockResources = TskLockResources.tryAcquireFileLock(metadata.getCaseDirectory(), UserPreferences.getAppName()); + } catch (IOException | OverlappingFileLockException ex) { throw new CaseActionException(Bundle.Case_openCaseDataBase_fileLock_ioException(), ex); } catch (ConcurrentDbAccessException ex) { throw new CaseActionException(Bundle.Case_openCaseDataBase_fileLock_concurrentAccessException( @@ -3189,34 +3134,15 @@ public class Case { eventPublisher.closeRemoteEventChannel(); } - if (this.lockFileLock != null) { + if (this.tskLockResources != null) { try { - this.lockFileLock.close(); - this.lockFileLock = null; + this.tskLockResources.close(); + this.tskLockResources = null; } catch (Exception ex) { - logger.log(Level.WARNING, "There was an error closing the lock file lock", ex); + logger.log(Level.WARNING, "There was an error closing the TSK case lock resources", ex); } } - if (this.lockFileChannel != null) { - try { - this.lockFileChannel.close(); - this.lockFileChannel = null; - } catch (Exception ex) { - logger.log(Level.WARNING, "There was an error closing the lock file channel", ex); - } - } - - - if (this.lockFileRaf != null) { - try { - this.lockFileRaf.close(); - this.lockFileRaf = null; - } catch (Exception ex) { - logger.log(Level.WARNING, "There was an error closing the lock file random access file", ex); - } - } - /* * Allow all registered application services providers to close * resources related to the case. diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/TskLockResources.java b/Core/src/org/sleuthkit/autopsy/casemodule/TskLockResources.java new file mode 100644 index 0000000000..78c36cd984 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/TskLockResources.java @@ -0,0 +1,162 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2024 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.casemodule; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.channels.OverlappingFileLockException; +import org.apache.commons.lang3.StringUtils; + +/** + * The resources associated with the file lock for the TSK database. + */ +class TskLockResources implements AutoCloseable { + + private static final String LOCK_FILE_NAME = "lock"; + + private File lockFile = null; + private RandomAccessFile lockFileRaf = null; + private FileChannel lockFileChannel = null; + private FileLock lockFileLock = null; + + /** + * Constructor. + * + * @param lockFile The lock file File reference. + * @param lockFileRef The lock file random access file reference. + * @param lockFileChannel The lock file file channel. + * @param lockFileLock The lock file file lock. + */ + TskLockResources(File lockFile, RandomAccessFile lockFileRaf, FileChannel lockFileChannel, FileLock lockFileLock) { + this.lockFile = lockFile; + this.lockFileRaf = lockFileRaf; + this.lockFileChannel = lockFileChannel; + this.lockFileLock = lockFileLock; + } + + /** + * Try to acquire a lock to the lock file in the case directory. + * + * @param caseDir The case directory that the autopsy.db is in. + * @return The lock file resources to be closed. + * @throws IllegalAccessException + * @throws IOException + */ + static TskLockResources tryAcquireFileLock(String caseDir, String applicationName) throws ConcurrentDbAccessException, IOException, OverlappingFileLockException { + // get the lock file path + File lockFile = new File(caseDir, LOCK_FILE_NAME); + // make directories leading up to that + lockFile.getParentFile().mkdirs(); + + // if the lock file exists + if (lockFile.isFile() && !lockFile.canWrite()) { + // get the random access file as read only + RandomAccessFile lockFileRaf = new RandomAccessFile(lockFile, "r"); + throw ConcurrentDbAccessException.createForFile(lockFile.getAbsolutePath(), lockFileRaf); + } else { + RandomAccessFile lockFileRaf = new RandomAccessFile(lockFile, "rw"); + FileChannel lockFileChannel = lockFileRaf.getChannel(); + FileLock lockFileLock = lockFileChannel == null + ? null + : lockFileChannel.tryLock(1024L, 1L, false); + + if (lockFileLock != null) { + lockFileRaf.setLength(0); + lockFileRaf.writeChars(applicationName); + return new TskLockResources(lockFile, lockFileRaf, lockFileChannel, lockFileLock); + } else { + throw ConcurrentDbAccessException.createForFile(lockFile.getAbsolutePath(), lockFileRaf); + } + } + } + + @Override + public void close() throws Exception { + // close lock file resources in reverse acquisition order + if (this.lockFileLock != null) { + this.lockFileLock.close(); + this.lockFileLock = null; + } + + if (this.lockFileChannel != null) { + this.lockFileChannel.close(); + this.lockFileChannel = null; + } + + if (this.lockFileRaf != null) { + this.lockFileRaf.close(); + this.lockFileRaf = null; + } + + if (this.lockFile != null) { + this.lockFile.delete(); + this.lockFile = null; + } + } + + /** + * An exception thrown if the database is currently in use. + */ + static class ConcurrentDbAccessException extends Exception { + + private final String conflictingApplicationName; + + /** + * Creates a ConcurrentDbAccessException from the lock file path and the + * random access file of that path whose contents are the application + * name. + * + * @param lockFilePath The lock file path. + * @param lockFileRaf The lock file random access file. + * @return The exception + * @throws IOException + */ + static ConcurrentDbAccessException createForFile(String lockFilePath, RandomAccessFile lockFileRaf) throws IOException { + StringBuffer buffer = new StringBuffer(); + while (lockFileRaf.getFilePointer() < lockFileRaf.length()) { + buffer.append(lockFileRaf.readLine() + System.lineSeparator()); + } + String conflictingApplication = buffer.toString().trim(); + String message = "Unable to acquire lock on " + lockFilePath + "." + (StringUtils.isNotBlank(conflictingApplication) ? (" Database is already open in " + conflictingApplication + ".") : ""); + return new ConcurrentDbAccessException(message, conflictingApplication); + } + + /** + * Constructor. + * + * @param message The exception message. + * @param conflictingApplicationName The conflicting application name + * (or null if unknown). + */ + ConcurrentDbAccessException(String message, String conflictingApplicationName) { + super(message); + this.conflictingApplicationName = conflictingApplicationName; + } + + /** + * @return The conflicting application name (or null if unknown). + */ + public String getConflictingApplicationName() { + return conflictingApplicationName; + } + } +}