Refactored FileSystemListenerList to use more modern concurrent

mechanisms
This commit is contained in:
dragonmacher 2021-08-20 08:49:15 -04:00
parent 888f2635b4
commit f1fd921db7
7 changed files with 231 additions and 331 deletions

View file

@ -19,8 +19,8 @@ import static org.junit.Assert.*;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap; import java.util.concurrent.TimeUnit;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@ -29,13 +29,13 @@ import org.junit.*;
import generic.test.TestUtils; import generic.test.TestUtils;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import ghidra.framework.store.FileSystem; import ghidra.framework.store.FileSystem;
import ghidra.framework.store.FileSystemListenerList; import ghidra.framework.store.FileSystemEventManager;
import ghidra.framework.store.local.LocalFileSystem; import ghidra.framework.store.local.LocalFileSystem;
import ghidra.program.database.ProgramDB; import ghidra.program.database.ProgramDB;
import ghidra.program.model.lang.Language; import ghidra.program.model.lang.Language;
import ghidra.program.model.listing.Program; import ghidra.program.model.listing.Program;
import ghidra.test.AbstractGhidraHeadedIntegrationTest; import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.util.task.TaskMonitorAdapter; import ghidra.util.task.TaskMonitor;
public class ProjectFileManagerTest extends AbstractGhidraHeadedIntegrationTest { public class ProjectFileManagerTest extends AbstractGhidraHeadedIntegrationTest {
@ -45,11 +45,7 @@ public class ProjectFileManagerTest extends AbstractGhidraHeadedIntegrationTest
private LocalFileSystem privateFS; private LocalFileSystem privateFS;
private ProjectFileManager fileMgr; private ProjectFileManager fileMgr;
private DomainFolder root; private DomainFolder root;
private ArrayList<MyEvent> events = new ArrayList<>(); private List<MyEvent> events = new ArrayList<>();
public ProjectFileManagerTest() {
super();
}
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
@ -107,16 +103,16 @@ public class ProjectFileManagerTest extends AbstractGhidraHeadedIntegrationTest
} }
private void flushFileSystemEvents() { private void flushFileSystemEvents() {
FileSystemListenerList privateListenerList = FileSystemEventManager privateEventManager =
(FileSystemListenerList) TestUtils.getInstanceField("listeners", privateFS); (FileSystemEventManager) TestUtils.getInstanceField("eventManager", privateFS);
FileSystemListenerList sharedListenerList = FileSystemEventManager sharedEventManager =
(FileSystemListenerList) TestUtils.getInstanceField("listeners", sharedFS); (FileSystemEventManager) TestUtils.getInstanceField("eventManager", sharedFS);
flushTheseEvents(privateListenerList); flushTheseEvents(privateEventManager);
flushTheseEvents(sharedListenerList); flushTheseEvents(sharedEventManager);
} }
private void flushTheseEvents(FileSystemListenerList listenerList) { private void flushTheseEvents(FileSystemEventManager eventManager) {
// Events get added synchronously, but processed asynchronously, so we can check to see // Events get added synchronously, but processed asynchronously, so we can check to see
// if any have been added by an action we triggered without waiting. Also, we know that // if any have been added by an action we triggered without waiting. Also, we know that
// no more events will get added, since we are the thread (the main thread) doing the // no more events will get added, since we are the thread (the main thread) doing the
@ -124,14 +120,12 @@ public class ProjectFileManagerTest extends AbstractGhidraHeadedIntegrationTest
// //
// If there are queued actions, then we have to kick the handling thread and // If there are queued actions, then we have to kick the handling thread and
// let it finish running. // let it finish running.
while (listenerList.isProcessingEvents()) {
// give the event tread some time to send events
try { try {
Thread.sleep(100); assertTrue(eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS));
} }
catch (InterruptedException e) { catch (InterruptedException e) {
// don't care, we will try again failWithException("Interrupted waiting for filesystem events", e);
}
} }
} }
@ -180,7 +174,7 @@ public class ProjectFileManagerTest extends AbstractGhidraHeadedIntegrationTest
Language language = getSLEIGH_X86_LANGUAGE(); Language language = getSLEIGH_X86_LANGUAGE();
Program p = new ProgramDB(name, language, language.getDefaultCompilerSpec(), this); Program p = new ProgramDB(name, language, language.getDefaultCompilerSpec(), this);
try { try {
return folder.createFile(name, p, TaskMonitorAdapter.DUMMY_MONITOR); return folder.createFile(name, p, TaskMonitor.DUMMY);
} }
finally { finally {
p.release(this); p.release(this);
@ -215,7 +209,7 @@ public class ProjectFileManagerTest extends AbstractGhidraHeadedIntegrationTest
assertNotNull(df2); assertNotNull(df2);
assertEquals("file2", df.getName()); assertEquals("file2", df.getName());
df1.addToVersionControl("", false, TaskMonitorAdapter.DUMMY_MONITOR); df1.addToVersionControl("", false, TaskMonitor.DUMMY);
df = fileMgr.getFileByID(fileID1); df = fileMgr.getFileByID(fileID1);
assertNotNull(df1); assertNotNull(df1);
@ -262,7 +256,7 @@ public class ProjectFileManagerTest extends AbstractGhidraHeadedIntegrationTest
DomainFile df1 = createFile(folder, "file1"); DomainFile df1 = createFile(folder, "file1");
String fileID = df1.getFileID(); String fileID = df1.getFileID();
df1.addToVersionControl("", true, TaskMonitorAdapter.DUMMY_MONITOR); df1.addToVersionControl("", true, TaskMonitor.DUMMY);
assertEquals(fileID, df1.getFileID()); assertEquals(fileID, df1.getFileID());
df1.undoCheckout(true); df1.undoCheckout(true);
@ -282,7 +276,7 @@ public class ProjectFileManagerTest extends AbstractGhidraHeadedIntegrationTest
// create shared file /a/file1 and keep checked-out // create shared file /a/file1 and keep checked-out
DomainFile df1 = createFile(folder, "file1"); DomainFile df1 = createFile(folder, "file1");
String fileID = df1.getFileID(); String fileID = df1.getFileID();
df1.addToVersionControl("", true, TaskMonitorAdapter.DUMMY_MONITOR); df1.addToVersionControl("", true, TaskMonitor.DUMMY);
assertEquals(fileID, df1.getFileID()); assertEquals(fileID, df1.getFileID());
// Force Hijack - terminate checkout at versioned file-system // Force Hijack - terminate checkout at versioned file-system

View file

@ -15,8 +15,8 @@
*/ */
package ghidra.framework.store; package ghidra.framework.store;
import java.util.*; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.*;
/** /**
* <code>FileSystemListenerList</code> maintains a list of FileSystemListener's. * <code>FileSystemListenerList</code> maintains a list of FileSystemListener's.
@ -24,32 +24,31 @@ import java.util.concurrent.CopyOnWriteArrayList;
* all FileSystemListener's within its list. Employs either a synchronous * all FileSystemListener's within its list. Employs either a synchronous
* and asynchronous notification mechanism. * and asynchronous notification mechanism.
*/ */
public class FileSystemListenerList implements FileSystemListener { public class FileSystemEventManager implements FileSystemListener {
private List<FileSystemListener> listenerList = new CopyOnWriteArrayList<>(); private List<FileSystemListener> listeners = new CopyOnWriteArrayList<>();
private BlockingQueue<FileSystemEvent> eventQueue = new LinkedBlockingQueue<>();
private List<FileSystemEvent> events = private volatile boolean disposed = false;
Collections.synchronizedList(new LinkedList<FileSystemEvent>());
private boolean enableAsynchronousDispatching;
private boolean isEventProcessingThreadWaiting;
private boolean alive = true;
private Object lock = new Object();
private Thread thread; private Thread thread;
/** /**
* Construct FileSystemListenerList * Constructor
* @param enableAsynchronousDispatching if true a separate dispatch thread will be used * @param enableAsynchronousDispatching if true a separate dispatch thread will be used
* to notify listeners. If false, blocking notification will be performed. * to notify listeners. If false, blocking notification will be performed.
*/ */
public FileSystemListenerList(boolean enableAsynchronousDispatching) { public FileSystemEventManager(boolean enableAsynchronousDispatching) {
this.enableAsynchronousDispatching = enableAsynchronousDispatching;
if (enableAsynchronousDispatching) {
thread = new FileSystemEventProcessingThread();
thread.start();
}
} }
public void dispose() { public void dispose() {
alive = false; disposed = true;
synchronized (lock) { if (thread != null) {
lock.notify(); thread.interrupt();
} }
} }
@ -57,13 +56,8 @@ public class FileSystemListenerList implements FileSystemListener {
* Add a listener to this list. * Add a listener to this list.
* @param listener the listener * @param listener the listener
*/ */
public synchronized void add(FileSystemListener listener) { public void add(FileSystemListener listener) {
listenerList.add(listener); listeners.add(listener);
if (thread == null && enableAsynchronousDispatching) {
thread = new FileSystemEventProcessingThread();
thread.setName("File System Listener");
thread.start();
}
} }
/** /**
@ -71,157 +65,106 @@ public class FileSystemListenerList implements FileSystemListener {
* @param listener the listener * @param listener the listener
*/ */
public void remove(FileSystemListener listener) { public void remove(FileSystemListener listener) {
listenerList.remove(listener); listeners.remove(listener);
}
/**
* Remove all listeners from this list.
*/
public void clear() {
listenerList.clear();
} }
@Override @Override
public void itemMoved(String parentPath, String name, String newParentPath, String newName) { public void itemMoved(String parentPath, String name, String newParentPath, String newName) {
if (enableAsynchronousDispatching) { handleEvent(new ItemMovedEvent(parentPath, name, newParentPath, newName));
add(new ItemMovedEvent(parentPath, name, newParentPath, newName));
}
else {
for (FileSystemListener l : listenerList) {
l.itemMoved(parentPath, name, newParentPath, newName);
}
}
} }
@Override @Override
public void itemRenamed(String parentPath, String itemName, String newName) { public void itemRenamed(String parentPath, String itemName, String newName) {
if (enableAsynchronousDispatching) { handleEvent(new ItemRenamedEvent(parentPath, itemName, newName));
add(new ItemRenamedEvent(parentPath, itemName, newName));
}
else {
for (FileSystemListener l : listenerList) {
l.itemRenamed(parentPath, itemName, newName);
}
}
} }
@Override @Override
public void itemDeleted(String parentPath, String itemName) { public void itemDeleted(String parentPath, String itemName) {
if (enableAsynchronousDispatching) { handleEvent(new ItemDeletedEvent(parentPath, itemName));
add(new ItemDeletedEvent(parentPath, itemName));
}
else {
for (FileSystemListener l : listenerList) {
l.itemDeleted(parentPath, itemName);
}
}
} }
@Override @Override
public void folderRenamed(String parentPath, String folderName, String newFolderName) { public void folderRenamed(String parentPath, String folderName, String newFolderName) {
if (enableAsynchronousDispatching) { handleEvent(new FolderRenamedEvent(parentPath, folderName, newFolderName));
add(new FolderRenamedEvent(parentPath, folderName, newFolderName));
}
else {
for (FileSystemListener l : listenerList) {
l.folderRenamed(parentPath, folderName, newFolderName);
}
}
} }
@Override @Override
public void folderMoved(String parentPath, String folderName, String newParentPath) { public void folderMoved(String parentPath, String folderName, String newParentPath) {
if (enableAsynchronousDispatching) { handleEvent(new FolderMovedEvent(parentPath, folderName, newParentPath));
add(new FolderMovedEvent(parentPath, folderName, newParentPath));
}
else {
for (FileSystemListener l : listenerList) {
l.folderMoved(parentPath, folderName, newParentPath);
}
}
} }
@Override @Override
public void folderDeleted(String parentPath, String folderName) { public void folderDeleted(String parentPath, String folderName) {
if (enableAsynchronousDispatching) { handleEvent(new FolderDeletedEvent(parentPath, folderName));
add(new FolderDeletedEvent(parentPath, folderName));
}
else {
for (FileSystemListener l : listenerList) {
l.folderDeleted(parentPath, folderName);
}
}
} }
@Override @Override
public void itemCreated(String parentPath, String itemName) { public void itemCreated(String parentPath, String itemName) {
if (enableAsynchronousDispatching) { handleEvent(new ItemCreatedEvent(parentPath, itemName));
add(new ItemCreatedEvent(parentPath, itemName));
}
else {
for (FileSystemListener l : listenerList) {
l.itemCreated(parentPath, itemName);
}
}
} }
@Override @Override
public void folderCreated(String parentPath, String folderName) { public void folderCreated(String parentPath, String folderName) {
if (enableAsynchronousDispatching) { handleEvent(new FolderCreatedEvent(parentPath, folderName));
add(new FolderCreatedEvent(parentPath, folderName));
}
else {
for (FileSystemListener l : listenerList) {
l.folderCreated(parentPath, folderName);
}
}
} }
@Override @Override
public void itemChanged(String parentPath, String itemName) { public void itemChanged(String parentPath, String itemName) {
if (enableAsynchronousDispatching) { handleEvent(new ItemChangedEvent(parentPath, itemName));
add(new ItemChangedEvent(parentPath, itemName));
}
else {
for (FileSystemListener l : listenerList) {
l.itemChanged(parentPath, itemName);
}
}
} }
@Override @Override
public void syncronize() { public void syncronize() {
if (enableAsynchronousDispatching) { // Note: synchronize calls will only work when using a threaded event queue
if (isAsynchronous()) {
add(new SynchronizeEvent()); add(new SynchronizeEvent());
} }
} }
private void add(FileSystemEvent ev) { private boolean isAsynchronous() {
if (!listenerList.isEmpty()) { return thread != null;
events.add(ev);
synchronized (lock) {
lock.notify();
} }
private void add(FileSystemEvent ev) {
if (!listeners.isEmpty()) {
eventQueue.add(ev);
}
}
private void handleEvent(FileSystemEvent e) {
if (disposed) {
return;
}
if (isAsynchronous()) {
add(e);
}
else {
e.process(listeners);
} }
} }
/** /**
* Returns true if this class is processing events <b>or</b> needs to process events that are * Blocks until all current events have been processed.
* in its event queue. * <p>
* Note: clients should only use this method when {@link #isAsynchronous()} returns true, since
* this class cannot track when non-threaded events have finished broadcasting to listeners.
* In a synchronous use case, any test that needs to know when client events have been processed
* must use some other mechanism to know when event processing is finished.
* *
* @return true if this class is processing events <b>or</b> needs to process events that are * @param timeout the maximum time to wait
* in its event queue. * @param unit the time unit of the {@code time} argument
* @return true if the events were processed in the given timeout
* @throws InterruptedException if this waiting thread is interrupted
*/ */
public boolean isProcessingEvents() { public boolean flushEvents(long timeout, TimeUnit unit) throws InterruptedException {
synchronized (this) { if (!isAsynchronous()) {
if (thread == null) { return true; // each thread processes its own event
return false; // non-threaded; does not 'process' events, done synchronously
}
} }
synchronized (lock) { // lock so nobody adds new events MarkerEvent event = new MarkerEvent();
return !isEventProcessingThreadWaiting || (events.size() > 0); eventQueue.add(event);
} return event.waitForEvent(timeout, unit);
} }
//================================================================================================== //==================================================================================================
@ -231,42 +174,25 @@ public class FileSystemListenerList implements FileSystemListener {
private class FileSystemEventProcessingThread extends Thread { private class FileSystemEventProcessingThread extends Thread {
FileSystemEventProcessingThread() { FileSystemEventProcessingThread() {
super("File System Event Processor"); super("File System Listener");
setDaemon(true); setDaemon(true);
} }
@Override @Override
public void run() { public void run() {
while (alive) { while (!disposed) {
while (!events.isEmpty()) {
FileSystemEvent event;
synchronized (lock) {
event = events.remove(0);
}
synchronized (FileSystemListenerList.this) {
for (FileSystemListener l : listenerList) {
event.dispatch(l);
}
}
}
doWait();
}
}
private void doWait() { FileSystemEvent event;
try { try {
synchronized (lock) { event = eventQueue.take();
if (alive && events.isEmpty()) { event.process(listeners);
isEventProcessingThreadWaiting = true;
lock.wait();
}
}
} }
catch (InterruptedException e) { catch (InterruptedException e) {
// not sure why we are ignoring this // interrupt has been cleared; if other threads rely on this interrupted state,
// then mark the thread as interrupted again by calling:
// Thread.currentThread().interrupt();
// For now, this code relies on the 'alive' flag to know when to terminate
} }
finally {
isEventProcessingThreadWaiting = false;
} }
} }
} }
@ -284,7 +210,18 @@ public class FileSystemListenerList implements FileSystemListener {
this.newName = newName; this.newName = newName;
} }
void process(List<FileSystemListener> listeners) {
for (FileSystemListener l : listeners) {
dispatch(l);
}
}
abstract void dispatch(FileSystemListener listener); abstract void dispatch(FileSystemListener listener);
@Override
public String toString() {
return getClass().getSimpleName();
}
} }
private static class ItemMovedEvent extends FileSystemEvent { private static class ItemMovedEvent extends FileSystemEvent {
@ -396,4 +333,29 @@ public class FileSystemListenerList implements FileSystemListener {
listener.syncronize(); listener.syncronize();
} }
} }
// an event used by the flush method to mark when current events have been processed
private static class MarkerEvent extends FileSystemEvent {
private CountDownLatch latch = new CountDownLatch(1);
MarkerEvent() {
super(null, null, null, null);
}
@Override
void dispatch(FileSystemListener listener) {
// we don't actually process the event
}
@Override
void process(List<FileSystemListener> listeners) {
latch.countDown();
}
boolean waitForEvent(long timeout, TimeUnit unit) throws InterruptedException {
return latch.await(timeout, unit);
}
}
} }

View file

@ -81,7 +81,7 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
/** /**
* Constructor. * Constructor.
* @param file path path for root directory. * @param rootPath path path for root directory.
* @param isVersioned if true item versioning will be enabled. * @param isVersioned if true item versioning will be enabled.
* @param readOnly if true modifications within this file-system will not be allowed * @param readOnly if true modifications within this file-system will not be allowed
* and result in an ReadOnlyException * and result in an ReadOnlyException
@ -740,7 +740,7 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
subfolder.name = name; subfolder.name = name;
folder.folders.put(name, subfolder); folder.folders.put(name, subfolder);
if (option == GetFolderOption.CREATE_ALL_NOTIFY) { if (option == GetFolderOption.CREATE_ALL_NOTIFY) {
listeners.folderCreated(folder.getPathname(), name); eventManager.folderCreated(folder.getPathname(), name);
} }
} }
else { else {
@ -943,7 +943,7 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
indexJournal.close(); indexJournal.close();
} }
listeners.folderCreated(parentPath, getName(path)); eventManager.folderCreated(parentPath, getName(path));
} }
/* /*
@ -978,7 +978,7 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
indexJournal.close(); indexJournal.close();
} }
listeners.folderDeleted(getParentPath(folderPath), getName(folderPath)); eventManager.folderDeleted(getParentPath(folderPath), getName(folderPath));
} }
void migrateItem(LocalFolderItem item) throws IOException { void migrateItem(LocalFolderItem item) throws IOException {
@ -1085,10 +1085,10 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
} }
if (folderPath.equals(newFolderPath)) { if (folderPath.equals(newFolderPath)) {
listeners.itemRenamed(folderPath, name, newName); eventManager.itemRenamed(folderPath, name, newName);
} }
else { else {
listeners.itemMoved(folderPath, name, newFolderPath, newName); eventManager.itemMoved(folderPath, name, newFolderPath, newName);
} }
deleteEmptyVersionedFolders(folderPath); deleteEmptyVersionedFolders(folderPath);
@ -1142,7 +1142,7 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
updateAffectedItemPaths(folder); updateAffectedItemPaths(folder);
listeners.folderMoved(parentPath, folderName, newParentPath); eventManager.folderMoved(parentPath, folderName, newParentPath);
deleteEmptyVersionedFolders(parentPath); deleteEmptyVersionedFolders(parentPath);
} }
@ -1198,7 +1198,7 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
updateAffectedItemPaths(folder); updateAffectedItemPaths(folder);
listeners.folderRenamed(parentPath, folderName, newFolderName); eventManager.folderRenamed(parentPath, folderName, newFolderName);
} }
/* /*

View file

@ -74,7 +74,7 @@ public abstract class LocalFileSystem implements FileSystem {
protected final File root; protected final File root;
protected final boolean isVersioned; protected final boolean isVersioned;
protected final boolean readOnly; protected final boolean readOnly;
protected final FileSystemListenerList listeners; protected final FileSystemEventManager eventManager;
private RepositoryLogger repositoryLogger; private RepositoryLogger repositoryLogger;
@ -187,7 +187,7 @@ public abstract class LocalFileSystem implements FileSystem {
this.isVersioned = isVersioned; this.isVersioned = isVersioned;
this.readOnly = readOnly; this.readOnly = readOnly;
listeners = new FileSystemListenerList(enableAsyncronousDispatching); eventManager = new FileSystemEventManager(enableAsyncronousDispatching);
} }
@ -205,7 +205,7 @@ public abstract class LocalFileSystem implements FileSystem {
this.root = null; this.root = null;
this.isVersioned = false; this.isVersioned = false;
this.readOnly = true; this.readOnly = true;
listeners = null; eventManager = null;
} }
private void cleanupTemporaryFiles(String folderPath) { private void cleanupTemporaryFiles(String folderPath) {
@ -542,7 +542,7 @@ public abstract class LocalFileSystem implements FileSystem {
} }
} }
listeners.itemCreated(parentPath, name); eventManager.itemCreated(parentPath, name);
return dataFile; return dataFile;
} }
@ -632,10 +632,10 @@ public abstract class LocalFileSystem implements FileSystem {
success = true; success = true;
if (folderPath.equals(newFolderPath)) { if (folderPath.equals(newFolderPath)) {
listeners.itemRenamed(folderPath, name, newName); eventManager.itemRenamed(folderPath, name, newName);
} }
else { else {
listeners.itemMoved(folderPath, name, newFolderPath, newName); eventManager.itemMoved(folderPath, name, newFolderPath, newName);
} }
deleteEmptyVersionedFolders(folderPath); deleteEmptyVersionedFolders(folderPath);
deallocateItemStorage(folderPath, name); deallocateItemStorage(folderPath, name);
@ -675,8 +675,8 @@ public abstract class LocalFileSystem implements FileSystem {
*/ */
@Override @Override
public void addFileSystemListener(FileSystemListener listener) { public void addFileSystemListener(FileSystemListener listener) {
if (listeners != null) { if (eventManager != null) {
listeners.add(listener); eventManager.add(listener);
} }
} }
@ -685,8 +685,8 @@ public abstract class LocalFileSystem implements FileSystem {
*/ */
@Override @Override
public void removeFileSystemListener(FileSystemListener listener) { public void removeFileSystemListener(FileSystemListener listener) {
if (listeners != null) { if (eventManager != null) {
listeners.remove(listener); eventManager.remove(listener);
} }
} }
@ -694,7 +694,7 @@ public abstract class LocalFileSystem implements FileSystem {
* Returns file system listener. * Returns file system listener.
*/ */
FileSystemListener getListener() { FileSystemListener getListener() {
return listeners; return eventManager;
} }
/** /**
@ -844,8 +844,8 @@ public abstract class LocalFileSystem implements FileSystem {
@Override @Override
public void dispose() { public void dispose() {
if (listeners != null) { if (eventManager != null) {
listeners.dispose(); eventManager.dispose();
} }
} }

View file

@ -1,6 +1,5 @@
/* ### /* ###
* IP: GHIDRA * IP: GHIDRA
* REVIEWED: YES
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,14 +15,13 @@
*/ */
package ghidra.framework.store.local; package ghidra.framework.store.local;
import ghidra.framework.store.FolderNotEmptyException;
import ghidra.util.*;
import ghidra.util.exception.DuplicateFileException;
import java.io.*; import java.io.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import ghidra.framework.store.FolderNotEmptyException;
import ghidra.util.*;
import ghidra.util.exception.DuplicateFileException;
import utilities.util.FileUtilities; import utilities.util.FileUtilities;
/** /**
@ -150,12 +148,12 @@ public class MangledLocalFileSystem extends LocalFileSystem {
if (dirList == null) { if (dirList == null) {
throw new FileNotFoundException("Folder " + folderPath + " not found"); throw new FileNotFoundException("Folder " + folderPath + " not found");
} }
ArrayList<String> fileList = new ArrayList<String>(dirList.length); ArrayList<String> fileList = new ArrayList<>(dirList.length);
for (int i = 0; i < dirList.length; i++) { for (File element : dirList) {
String name = dirList[i].getName(); String name = element.getName();
if (name.endsWith(PROPERTY_EXT) && dirList[i].isFile()) { if (name.endsWith(PROPERTY_EXT) && element.isFile()) {
if (!NamingUtilities.isValidMangledName(dirList[i].getName())) { if (!NamingUtilities.isValidMangledName(element.getName())) {
log.warn("Ignoring property file with bad name: " + dirList[i]); log.warn("Ignoring property file with bad name: " + element);
continue; continue;
} }
int index = name.lastIndexOf(PROPERTY_EXT); int index = name.lastIndexOf(PROPERTY_EXT);
@ -172,6 +170,7 @@ public class MangledLocalFileSystem extends LocalFileSystem {
/* /*
* @see ghidra.framework.store.FileSystem#getFolders(java.lang.String) * @see ghidra.framework.store.FileSystem#getFolders(java.lang.String)
*/ */
@Override
public synchronized String[] getFolderNames(String folderPath) throws IOException { public synchronized String[] getFolderNames(String folderPath) throws IOException {
File dir = getFile(folderPath); File dir = getFile(folderPath);
@ -179,12 +178,12 @@ public class MangledLocalFileSystem extends LocalFileSystem {
if (dirList == null) { if (dirList == null) {
throw new FileNotFoundException("Folder " + folderPath + " not found"); throw new FileNotFoundException("Folder " + folderPath + " not found");
} }
ArrayList<String> folderList = new ArrayList<String>(dirList.length); ArrayList<String> folderList = new ArrayList<>(dirList.length);
for (int i = 0; i < dirList.length; i++) { for (File element : dirList) {
if (!dirList[i].isDirectory()) { if (!element.isDirectory()) {
continue; continue;
} }
String name = demangleName(dirList[i].getName()); String name = demangleName(element.getName());
if (name != null) { if (name != null) {
folderList.add(name); folderList.add(name);
} }
@ -196,6 +195,7 @@ public class MangledLocalFileSystem extends LocalFileSystem {
/* /*
* @see ghidra.framework.store.FileSystem#createFolder(java.lang.String, java.lang.String) * @see ghidra.framework.store.FileSystem#createFolder(java.lang.String, java.lang.String)
*/ */
@Override
public synchronized void createFolder(String parentPath, String folderName) public synchronized void createFolder(String parentPath, String folderName)
throws InvalidNameException, IOException { throws InvalidNameException, IOException {
@ -218,6 +218,7 @@ public class MangledLocalFileSystem extends LocalFileSystem {
/* /*
* @see ghidra.framework.store.FileSystem#deleteFolder(java.lang.String) * @see ghidra.framework.store.FileSystem#deleteFolder(java.lang.String)
*/ */
@Override
public synchronized void deleteFolder(String folderPath) throws IOException { public synchronized void deleteFolder(String folderPath) throws IOException {
if (readOnly) { if (readOnly) {
@ -243,12 +244,13 @@ public class MangledLocalFileSystem extends LocalFileSystem {
} }
FileUtilities.deleteDir(file); FileUtilities.deleteDir(file);
listeners.folderDeleted(getParentPath(folderPath), getName(folderPath)); eventManager.folderDeleted(getParentPath(folderPath), getName(folderPath));
} }
/* /*
* @see ghidra.framework.store.FileSystem#moveFolder(java.lang.String, java.lang.String, java.lang.String) * @see ghidra.framework.store.FileSystem#moveFolder(java.lang.String, java.lang.String, java.lang.String)
*/ */
@Override
public synchronized void moveFolder(String parentPath, String folderName, String newParentPath) public synchronized void moveFolder(String parentPath, String folderName, String newParentPath)
throws InvalidNameException, IOException { throws InvalidNameException, IOException {
@ -277,7 +279,7 @@ public class MangledLocalFileSystem extends LocalFileSystem {
throw new IOException("move failed for unknown reason"); throw new IOException("move failed for unknown reason");
} }
listeners.folderMoved(parentPath, folderName, newParentPath); eventManager.folderMoved(parentPath, folderName, newParentPath);
deleteEmptyVersionedFolders(parentPath); deleteEmptyVersionedFolders(parentPath);
} }
finally { finally {
@ -290,7 +292,9 @@ public class MangledLocalFileSystem extends LocalFileSystem {
/* /*
* @see ghidra.framework.store.FileSystem#renameFolder(java.lang.String, java.lang.String, java.lang.String) * @see ghidra.framework.store.FileSystem#renameFolder(java.lang.String, java.lang.String, java.lang.String)
*/ */
public synchronized void renameFolder(String parentPath, String folderName, String newFolderName) @Override
public synchronized void renameFolder(String parentPath, String folderName,
String newFolderName)
throws InvalidNameException, IOException { throws InvalidNameException, IOException {
if (readOnly) { if (readOnly) {
@ -315,7 +319,7 @@ public class MangledLocalFileSystem extends LocalFileSystem {
throw new IOException("Folder may contain files that are in use"); throw new IOException("Folder may contain files that are in use");
} }
listeners.folderRenamed(parentPath, folderName, newFolderName); eventManager.folderRenamed(parentPath, folderName, newFolderName);
} }
/** /**
@ -329,7 +333,8 @@ public class MangledLocalFileSystem extends LocalFileSystem {
throw new FileNotFoundException("Empty read-only file system"); throw new FileNotFoundException("Empty read-only file system");
} }
if (path.charAt(0) != SEPARATOR_CHAR) { if (path.charAt(0) != SEPARATOR_CHAR) {
throw new FileNotFoundException("Path names must begin with \'" + SEPARATOR_CHAR + "\'"); throw new FileNotFoundException(
"Path names must begin with \'" + SEPARATOR_CHAR + "\'");
} }
if (path.length() == 1) { if (path.length() == 1) {
return root; return root;
@ -344,9 +349,9 @@ public class MangledLocalFileSystem extends LocalFileSystem {
} }
StringBuilder buf = new StringBuilder(); StringBuilder buf = new StringBuilder();
String[] split = path.split(SEPARATOR); String[] split = path.split(SEPARATOR);
for (int i = 0; i < split.length; i++) { for (String element : split) {
buf.append(SEPARATOR_CHAR); buf.append(SEPARATOR_CHAR);
buf.append(escapeHiddenDirPrefixChars(split[i])); buf.append(escapeHiddenDirPrefixChars(element));
} }
return NamingUtilities.mangle(buf.toString()); return NamingUtilities.mangle(buf.toString());
} }
@ -399,7 +404,7 @@ public class MangledLocalFileSystem extends LocalFileSystem {
String parentPath = getParentPath(folderPath); String parentPath = getParentPath(folderPath);
createFolders(parentDir, parentPath); createFolders(parentDir, parentPath);
folderDir.mkdir(); folderDir.mkdir();
listeners.folderCreated(parentPath, getName(folderPath)); eventManager.folderCreated(parentPath, getName(folderPath));
} }
/* /*
@ -443,7 +448,8 @@ public class MangledLocalFileSystem extends LocalFileSystem {
} }
IndexedV1LocalFileSystem indexedFs = IndexedV1LocalFileSystem indexedFs =
new IndexedV1LocalFileSystem(tmpRoot.getAbsolutePath(), isVersioned, false, false, true); new IndexedV1LocalFileSystem(tmpRoot.getAbsolutePath(), isVersioned, false, false,
true);
migrationInProgress = true; migrationInProgress = true;
migrateFolder(SEPARATOR, indexedFs); migrateFolder(SEPARATOR, indexedFs);

View file

@ -41,7 +41,7 @@ import ghidra.util.task.TaskMonitor;
public class RemoteFileSystem implements FileSystem, RemoteAdapterListener { public class RemoteFileSystem implements FileSystem, RemoteAdapterListener {
private RepositoryAdapter repository; private RepositoryAdapter repository;
private FileSystemListenerList listeners = new FileSystemListenerList(true); private FileSystemEventManager eventManager = new FileSystemEventManager(true);
/** /**
* Construct a new remote file system which corresponds to a remote repository. * Construct a new remote file system which corresponds to a remote repository.
@ -49,7 +49,7 @@ public class RemoteFileSystem implements FileSystem, RemoteAdapterListener {
*/ */
public RemoteFileSystem(RepositoryAdapter repository) { public RemoteFileSystem(RepositoryAdapter repository) {
this.repository = repository; this.repository = repository;
repository.setFileSystemListener(listeners); repository.setFileSystemListener(eventManager);
repository.addListener(this); repository.addListener(this);
} }
@ -65,12 +65,12 @@ public class RemoteFileSystem implements FileSystem, RemoteAdapterListener {
@Override @Override
public void addFileSystemListener(FileSystemListener listener) { public void addFileSystemListener(FileSystemListener listener) {
listeners.add(listener); eventManager.add(listener);
} }
@Override @Override
public void removeFileSystemListener(FileSystemListener listener) { public void removeFileSystemListener(FileSystemListener listener) {
listeners.remove(listener); eventManager.remove(listener);
} }
@Override @Override
@ -230,13 +230,13 @@ public class RemoteFileSystem implements FileSystem, RemoteAdapterListener {
@Override @Override
public void connectionStateChanged(Object adapter) { public void connectionStateChanged(Object adapter) {
if (adapter == repository) { if (adapter == repository) {
listeners.syncronize(); eventManager.syncronize();
} }
} }
@Override @Override
public void dispose() { public void dispose() {
listeners.dispose(); eventManager.dispose();
} }
} }

View file

@ -18,15 +18,14 @@ package ghidra.framework.store.local;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import java.io.*; import java.io.*;
import java.util.ArrayList; import java.util.*;
import java.util.List; import java.util.concurrent.TimeUnit;
import org.junit.*; import org.junit.*;
import db.*; import db.*;
import db.buffers.BufferFile; import db.buffers.BufferFile;
import generic.test.AbstractGenericTest; import generic.test.*;
import generic.test.TestUtils;
import ghidra.framework.store.*; import ghidra.framework.store.*;
import ghidra.util.InvalidNameException; import ghidra.util.InvalidNameException;
import ghidra.util.PropertyFile; import ghidra.util.PropertyFile;
@ -41,7 +40,7 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest {
LocalFileSystem fs; LocalFileSystem fs;
File projectDir; File projectDir;
ArrayList<MyEvent> events = new ArrayList<>(); List<MyEvent> events = new ArrayList<>();
public AbstractLocalFileSystemTest(boolean useIndexedFileSystem) { public AbstractLocalFileSystemTest(boolean useIndexedFileSystem) {
super(); super();
@ -51,7 +50,7 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest {
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
File tempDir = new File(AbstractGenericTest.getTestDirectoryPath()); File tempDir = new File(AbstractGTest.getTestDirectoryPath());
projectDir = new File(tempDir, "testproject"); projectDir = new File(tempDir, "testproject");
FileUtilities.deleteDir(projectDir); FileUtilities.deleteDir(projectDir);
projectDir.mkdir(); projectDir.mkdir();
@ -114,6 +113,7 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest {
Assert.fail(); Assert.fail();
} }
catch (IOException e) { catch (IOException e) {
// expected
} }
try { try {
@ -121,6 +121,7 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest {
Assert.fail(); Assert.fail();
} }
catch (IOException e) { catch (IOException e) {
// expected
} }
} }
@ -161,78 +162,6 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest {
checkEvent("Folder Created", "/a1/a2/a3", "a4", null, null, events.get(3)); checkEvent("Folder Created", "/a1/a2/a3", "a4", null, null, events.get(3));
} }
// @Test
// public void testFolderPathTooLong() throws Exception {
//
// StringBuffer sb = new StringBuffer();
// int projectDirLength = projectDir.getAbsolutePath().length();
// int i = 0;
// while (sb.length() + projectDirLength < 248) {
// sb.append("/a" + i);
// ++i;
// }
// try {
// fs.createFolder(sb.toString(), "aaaa");
// Assert.fail("Should have gotten IO exception on too long of a filename!");
// }
// catch (IOException e) {
// }
// }
//
// @Test
// public void testDataFilePathTooLong() throws Exception {
// fs.createFolder("/", "abc");
// String data = "This is a test";
// byte[] dataBytes = data.getBytes();
//
// StringBuffer sb = new StringBuffer();
// int projectDirLength = projectDir.getAbsolutePath().length();
// int i = 0;
// while (sb.length() + projectDirLength < 248) {
// sb.append("/a" + i);
// ++i;
// }
// try {
// fs.createDataFile(sb.toString(), "freddxxxx", new ByteArrayInputStream(dataBytes), null,
// "Data", null);
// Assert.fail("Should have gotten IO Exception!");
// }
// catch (IOException e) {
// }
//
// }
//
// public void testDataBasePathTooLong() throws Exception {
// fs.createFolder("/", "abc");
// DBHandle dbh = new DBHandle();
// long id = dbh.startTransaction();
// dbh.createTable("test", new Schema(0, "key", new Class[] { IntField.class },
// new String[] { "dummy" }));
// dbh.endTransaction(id, true);
// int projectDirLength = projectDir.getAbsolutePath().length();
// StringBuffer sb = new StringBuffer();
// int i = 0;
// while (sb.length() + projectDirLength < 248) {
// sb.append("/a" + i);
// ++i;
// }
// try {
// fs.createDatabase(sb.toString(), "freddxxxx", null, "Database", dbh.getBufferSize(),
// "bob", null);
// Assert.fail("Should have gotten IO Exception!");
// }
// catch (IOException e) {
// }
// }
/**
* @param string
* @param string2
* @param string3
* @param object
* @param object2
* @param object3
*/
private void checkEvent(String op, String path, String name, String newPath, String newName, private void checkEvent(String op, String path, String name, String newPath, String newName,
Object evObj) { Object evObj) {
MyEvent event = (MyEvent) evObj; MyEvent event = (MyEvent) evObj;
@ -262,6 +191,7 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest {
Assert.fail(); Assert.fail();
} }
catch (FolderNotEmptyException e) { catch (FolderNotEmptyException e) {
// expected
} }
} }
@ -337,12 +267,14 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest {
Assert.fail(); Assert.fail();
} }
catch (FileNotFoundException e) { catch (FileNotFoundException e) {
// expected
} }
fs.createFolder("/b", "def"); fs.createFolder("/b", "def");
try { try {
fs.moveFolder("/mno/abc", "def", "/b"); fs.moveFolder("/mno/abc", "def", "/b");
} }
catch (DuplicateFileException e) { catch (DuplicateFileException e) {
// expected
} }
} }
@ -675,8 +607,6 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest {
DataFileItem df = fs.createDataFile("/abc", "fred", new ByteArrayInputStream(dataBytes), DataFileItem df = fs.createDataFile("/abc", "fred", new ByteArrayInputStream(dataBytes),
null, "Data", null); null, "Data", null);
DataFileItem df2 = fs.createDataFile("/abc", "bob", new ByteArrayInputStream(dataBytes),
null, "Data", null);
createDatabase("/abc", "greg", "123"); createDatabase("/abc", "greg", "123");
String[] items = fs.getItemNames("/abc"); String[] items = fs.getItemNames("/abc");
@ -991,25 +921,43 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest {
@Override @Override
public void syncronize() { public void syncronize() {
// not tracked
} }
} }
private void flushFileSystemEvents() { private void flushFileSystemEvents() {
FileSystemListenerList listenerList = FileSystemEventManager eventManager =
(FileSystemListenerList) TestUtils.getInstanceField("listeners", fs); (FileSystemEventManager) TestUtils.getInstanceField("eventManager", fs);
while (listenerList.isProcessingEvents()) {
// give the event tread some time to send events
try { try {
Thread.sleep(100); eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS);
} }
catch (InterruptedException e) { catch (InterruptedException e) {
// don't care, we will try again failWithException("Interrupted waiting for filesystem events", e);
}
} }
} }
} }
class MyEvent { class MyEvent {
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((newName == null) ? 0 : newName.hashCode());
result = prime * result + ((newParentPath == null) ? 0 : newParentPath.hashCode());
result = prime * result + ((op == null) ? 0 : op.hashCode());
result = prime * result + ((parentPath == null) ? 0 : parentPath.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
MyEvent other = (MyEvent) obj;
return eq(op, other.op) && eq(parentPath, other.parentPath) && eq(name, other.name) &&
eq(newParentPath, other.newParentPath) && eq(newName, other.newName);
}
String op; String op;
String parentPath; String parentPath;
String name; String name;
@ -1024,18 +972,8 @@ class MyEvent {
this.newName = newName; this.newName = newName;
} }
@Override
public boolean equals(Object obj) {
MyEvent other = (MyEvent) obj;
return eq(op, other.op) && eq(parentPath, other.parentPath) && eq(name, other.name) &&
eq(newParentPath, other.newParentPath) && eq(newName, other.newName);
}
private boolean eq(String s1, String s2) { private boolean eq(String s1, String s2) {
if (s1 == null) { return Objects.equals(s1, s2);
return s2 == null;
}
return s1.equals(s2);
} }
@Override @Override