Q: What does the plugin manager do? A: It manages plugins.

This commit is contained in:
akwizgran
2011-10-14 14:49:29 +01:00
parent d54ca67fe9
commit 55182528cf
12 changed files with 510 additions and 6 deletions

View File

@@ -12,5 +12,5 @@ public interface BatchPluginCallback extends PluginCallback {
void readerCreated(BatchTransportReader r);
void writerCreated(ContactId contactId, BatchTransportWriter w);
void writerCreated(ContactId c, BatchTransportWriter w);
}

View File

@@ -0,0 +1,16 @@
package net.sf.briar.api.plugins;
public interface PluginManager {
/**
* Starts all the plugins the manager knows about and returns the number of
* plugins successfully started.
*/
int startPlugins();
/**
* Stops all the plugins started by startPlugins() and returns the number
* of plugins successfully stopped.
*/
int stopPlugins();
}

View File

@@ -9,8 +9,7 @@ import net.sf.briar.api.transport.StreamTransportConnection;
*/
public interface StreamPluginCallback extends PluginCallback {
void incomingConnectionCreated(StreamTransportConnection c);
void incomingConnectionCreated(StreamTransportConnection s);
void outgoingConnectionCreated(ContactId contactId,
StreamTransportConnection c);
void outgoingConnectionCreated(ContactId c, StreamTransportConnection s);
}

View File

@@ -0,0 +1,17 @@
package net.sf.briar.api.transport;
import net.sf.briar.api.ContactId;
import net.sf.briar.api.TransportId;
public interface ConnectionDispatcher {
void dispatchReader(TransportId t, BatchTransportReader r);
void dispatchWriter(TransportId t, ContactId c,
BatchTransportWriter w);
void dispatchIncomingConnection(TransportId t, StreamTransportConnection s);
void dispatchOutgoingConnection(TransportId t, ContactId c,
StreamTransportConnection s);
}

View File

@@ -0,0 +1,25 @@
package net.sf.briar.api.ui;
public interface UiCallback {
/**
* Presents the user with a choice among two or more named options and
* returns the user's response. The message may consist of a translatable
* format string and arguments.
* @return An index into the array of options indicating the user's choice,
* or -1 if the user cancelled the choice.
*/
int showChoice(String[] options, String... message);
/**
* Asks the user to confirm an action and returns the user's response. The
* message may consist of a translatable format string and arguments.
*/
boolean showConfirmationMessage(String... message);
/**
* Shows a message to the user. The message may consist of a translatable
* format string and arguments.
*/
void showMessage(String... message);
}

View File

@@ -0,0 +1,289 @@
package net.sf.briar.plugins;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.briar.api.ContactId;
import net.sf.briar.api.TransportConfig;
import net.sf.briar.api.TransportId;
import net.sf.briar.api.TransportProperties;
import net.sf.briar.api.db.DatabaseComponent;
import net.sf.briar.api.db.DbException;
import net.sf.briar.api.plugins.BatchPlugin;
import net.sf.briar.api.plugins.BatchPluginCallback;
import net.sf.briar.api.plugins.BatchPluginFactory;
import net.sf.briar.api.plugins.Plugin;
import net.sf.briar.api.plugins.PluginCallback;
import net.sf.briar.api.plugins.PluginManager;
import net.sf.briar.api.plugins.StreamPlugin;
import net.sf.briar.api.plugins.StreamPluginCallback;
import net.sf.briar.api.plugins.StreamPluginFactory;
import net.sf.briar.api.protocol.TransportUpdate;
import net.sf.briar.api.transport.BatchTransportReader;
import net.sf.briar.api.transport.BatchTransportWriter;
import net.sf.briar.api.transport.ConnectionDispatcher;
import net.sf.briar.api.transport.StreamTransportConnection;
import net.sf.briar.api.ui.UiCallback;
import com.google.inject.Inject;
class PluginManagerImpl implements PluginManager {
private static final Logger LOG =
Logger.getLogger(PluginManagerImpl.class.getName());
private static final String[] BATCH_FACTORIES = new String[] {
"net.sf.briar.plugins.file.RemovableDrivePluginFactory"
};
private static final String[] STREAM_FACTORIES = new String[] {
"net.sf.briar.plugins.bluetooth.BluetoothPluginFactory",
"net.sf.briar.plugins.socket.SimpleSocketPluginFactory"
};
private final Executor executor;
private final DatabaseComponent db;
private final Poller poller;
private final ConnectionDispatcher dispatcher;
private final UiCallback uiCallback;
private final List<BatchPlugin> batchPlugins;
private final List<StreamPlugin> streamPlugins;
@Inject
PluginManagerImpl(Executor executor, DatabaseComponent db, Poller poller,
ConnectionDispatcher dispatcher, UiCallback uiCallback) {
this.executor = executor;
this.db = db;
this.poller = poller;
this.dispatcher = dispatcher;
this.uiCallback = uiCallback;
batchPlugins = new ArrayList<BatchPlugin>();
streamPlugins = new ArrayList<StreamPlugin>();
}
public synchronized int startPlugins() {
Set<TransportId> ids = new HashSet<TransportId>();
// Instantiate and start the batch plugins
for(String s : BATCH_FACTORIES) {
try {
Class<?> c = Class.forName(s);
BatchPluginFactory factory =
(BatchPluginFactory) c.newInstance();
BatchCallback callback = new BatchCallback();
BatchPlugin plugin = factory.createPlugin(executor, callback);
if(plugin == null) {
if(LOG.isLoggable(Level.INFO))
LOG.info(factory.getClass().getSimpleName() +
" did not create a plugin");
continue;
}
TransportId id = plugin.getId();
if(!ids.add(id)) {
if(LOG.isLoggable(Level.WARNING))
LOG.warning("Duplicate transport ID: " + id);
continue;
}
callback.setId(id);
plugin.start();
batchPlugins.add(plugin);
} catch(ClassCastException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
continue;
} catch(Exception e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
continue;
}
}
// Instantiate and start the stream plugins
for(String s : STREAM_FACTORIES) {
try {
Class<?> c = Class.forName(s);
StreamPluginFactory factory =
(StreamPluginFactory) c.newInstance();
StreamCallback callback = new StreamCallback();
StreamPlugin plugin = factory.createPlugin(executor, callback);
if(plugin == null) {
if(LOG.isLoggable(Level.INFO))
LOG.info(factory.getClass().getSimpleName() +
" did not create a plugin");
continue;
}
TransportId id = plugin.getId();
if(!ids.add(id)) {
if(LOG.isLoggable(Level.WARNING))
LOG.warning("Duplicate transport ID: " + id);
continue;
}
callback.setId(id);
plugin.start();
streamPlugins.add(plugin);
} catch(ClassCastException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
continue;
} catch(Exception e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
continue;
}
}
// Start the poller
List<Plugin> plugins = new ArrayList<Plugin>();
plugins.addAll(batchPlugins);
plugins.addAll(streamPlugins);
poller.startPolling(plugins);
// Return the number of plugins started
return batchPlugins.size() + streamPlugins.size();
}
public synchronized int stopPlugins() {
int stopped = 0;
// Stop the batch plugins
for(BatchPlugin plugin : batchPlugins) {
try {
plugin.stop();
stopped++;
} catch(IOException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
}
}
batchPlugins.clear();
// Stop the stream plugins
for(StreamPlugin plugin : streamPlugins) {
try {
plugin.stop();
stopped++;
} catch(IOException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
}
}
streamPlugins.clear();
// Return the number of plugins stopped
return stopped;
}
private abstract class PluginCallbackImpl implements PluginCallback {
protected volatile TransportId id = null;
protected void setId(TransportId id) {
assert this.id == null;
this.id = id;
}
public TransportConfig getConfig() {
assert id != null;
try {
return db.getConfig(id);
} catch(DbException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
return new TransportConfig();
}
}
public TransportProperties getLocalProperties() {
assert id != null;
try {
TransportProperties p = db.getLocalTransports().get(id);
return p == null ? new TransportProperties() : p;
} catch(DbException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
return new TransportProperties();
}
}
public Map<ContactId, TransportProperties> getRemoteProperties() {
assert id != null;
try {
return db.getRemoteProperties(id);
} catch(DbException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
return Collections.emptyMap();
}
}
public void setConfig(TransportConfig c) {
assert id != null;
try {
db.setConfig(id, c);
} catch(DbException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
}
}
public void setLocalProperties(TransportProperties p) {
assert id != null;
if(p.size() > TransportUpdate.MAX_PROPERTIES_PER_PLUGIN) {
if(LOG.isLoggable(Level.WARNING))
LOG.warning("Plugin " + id + " set too many properties");
return;
}
for(String s : p.keySet()) {
if(s.length() > TransportUpdate.MAX_KEY_OR_VALUE_LENGTH) {
if(LOG.isLoggable(Level.WARNING))
LOG.warning("Plugin " + id + " set long key: " + s);
return;
}
}
for(String s : p.values()) {
if(s.length() > TransportUpdate.MAX_KEY_OR_VALUE_LENGTH) {
if(LOG.isLoggable(Level.WARNING))
LOG.warning("Plugin " + id + " set long value: " + s);
return;
}
}
try {
db.setLocalProperties(id, p);
} catch(DbException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
}
}
public int showChoice(String[] options, String... message) {
return uiCallback.showChoice(options, message);
}
public boolean showConfirmationMessage(String... message) {
return uiCallback.showConfirmationMessage(message);
}
public void showMessage(String... message) {
uiCallback.showMessage(message);
}
}
private class BatchCallback extends PluginCallbackImpl
implements BatchPluginCallback {
public void readerCreated(BatchTransportReader r) {
assert id != null;
dispatcher.dispatchReader(id, r);
}
public void writerCreated(ContactId c, BatchTransportWriter w) {
assert id != null;
dispatcher.dispatchWriter(id, c, w);
}
}
private class StreamCallback extends PluginCallbackImpl
implements StreamPluginCallback {
public void incomingConnectionCreated(StreamTransportConnection s) {
assert id != null;
dispatcher.dispatchIncomingConnection(id, s);
}
public void outgoingConnectionCreated(ContactId c,
StreamTransportConnection s) {
assert id != null;
dispatcher.dispatchOutgoingConnection(id, c, s);
}
}
}

View File

@@ -0,0 +1,16 @@
package net.sf.briar.plugins;
import net.sf.briar.api.plugins.PluginManager;
import com.google.inject.AbstractModule;
import com.google.inject.Singleton;
public class PluginsModule extends AbstractModule {
@Override
protected void configure() {
bind(PluginManager.class).to(
PluginManagerImpl.class).in(Singleton.class);
bind(Poller.class).to(PollerImpl.class);
}
}

View File

@@ -0,0 +1,14 @@
package net.sf.briar.plugins;
import java.util.Collection;
import net.sf.briar.api.plugins.Plugin;
interface Poller {
/** Starts a new thread to poll the given collection of plugins. */
void startPolling(Collection<Plugin> plugins);
/** Tells the poller thread to exit. */
void stopPolling();
}

View File

@@ -0,0 +1,78 @@
package net.sf.briar.plugins;
import java.util.Collection;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.briar.api.plugins.Plugin;
class PollerImpl implements Poller, Runnable {
private static final Logger LOG =
Logger.getLogger(PollerImpl.class.getName());
private final SortedSet<PollTime> pollTimes = new TreeSet<PollTime>();
public synchronized void startPolling(Collection<Plugin> plugins) {
for(Plugin plugin : plugins) schedule(plugin);
new Thread(this).start();
}
private synchronized void schedule(Plugin plugin) {
if(plugin.shouldPoll()) {
long now = System.currentTimeMillis();
long interval = plugin.getPollingInterval();
pollTimes.add(new PollTime(now + interval, plugin));
}
}
public synchronized void stopPolling() {
pollTimes.clear();
notifyAll();
}
public void run() {
while(true) {
synchronized(this) {
if(pollTimes.isEmpty()) return;
PollTime p = pollTimes.first();
long now = System.currentTimeMillis();
if(now <= p.time) {
pollTimes.remove(p);
try {
p.plugin.poll();
} catch(RuntimeException e) {
if(LOG.isLoggable(Level.WARNING))
LOG.warning("Plugin " + p.plugin.getId() + " " + e);
}
schedule(p.plugin);
} else {
try {
wait(p.time - now);
} catch(InterruptedException ignored) {}
}
}
}
}
private static class PollTime implements Comparable<PollTime> {
private final long time;
private final Plugin plugin;
private PollTime(long time, Plugin plugin) {
this.time = time;
this.plugin = plugin;
}
public int compareTo(PollTime p) {
if(time < p.time) return -1;
if(time > p.time) return 1;
if(plugin.getId().getInt() < p.plugin.getId().getInt()) return -1;
if(plugin.getId().getInt() > p.plugin.getId().getInt()) return 1;
return 0;
}
}
}

View File

@@ -63,8 +63,13 @@ class BluetoothPlugin extends AbstractPlugin implements StreamPlugin {
}
} catch(UnsatisfiedLinkError e) {
// On Linux the user may need to install libbluetooth-dev
if(OsUtils.isLinux())
callback.showMessage("BLUETOOTH_INSTALL LIBS");
if(OsUtils.isLinux()) {
executor.execute(new Runnable() {
public void run() {
callback.showMessage("BLUETOOTH_INSTALL_LIBS");
}
});
}
throw new IOException(e.getMessage());
}
executor.execute(createBinder());

View File

@@ -26,6 +26,7 @@
<test name='net.sf.briar.i18n.FontManagerTest'/>
<test name='net.sf.briar.i18n.I18nTest'/>
<test name='net.sf.briar.invitation.InvitationWorkerTest'/>
<test name='net.sf.briar.plugins.PluginManagerImplTest'/>
<test name='net.sf.briar.plugins.file.LinuxRemovableDriveFinderTest'/>
<test name='net.sf.briar.plugins.file.MacRemovableDriveFinderTest'/>
<test name='net.sf.briar.plugins.file.PollingRemovableDriveMonitorTest'/>

View File

@@ -0,0 +1,44 @@
package net.sf.briar.plugins;
import java.util.Map;
import java.util.concurrent.Executor;
import junit.framework.TestCase;
import net.sf.briar.api.TransportId;
import net.sf.briar.api.TransportProperties;
import net.sf.briar.api.db.DatabaseComponent;
import net.sf.briar.api.transport.ConnectionDispatcher;
import net.sf.briar.api.ui.UiCallback;
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.junit.Test;
public class PluginManagerImplTest extends TestCase {
@Test
public void testStartAndStop() throws Exception {
Mockery context = new Mockery();
final DatabaseComponent db = context.mock(DatabaseComponent.class);
final Map<?, ?> localTransports = context.mock(Map.class);
final ConnectionDispatcher dispatcher =
context.mock(ConnectionDispatcher.class);
final UiCallback uiCallback = context.mock(UiCallback.class);
context.checking(new Expectations() {{
allowing(db).getLocalTransports();
will(returnValue(localTransports));
allowing(localTransports).get(with(any(TransportId.class)));
will(returnValue(new TransportProperties()));
allowing(db).getRemoteProperties(with(any(TransportId.class)));
will(returnValue(new TransportProperties()));
}});
Executor executor = new ImmediateExecutor();
Poller poller = new PollerImpl();
PluginManagerImpl p =
new PluginManagerImpl(executor, db, poller, dispatcher, uiCallback);
// The Bluetooth plugin will not start without a Bluetooth device, so
// we expect two plugins to be started
assertEquals(2, p.startPlugins());
assertEquals(2, p.stopPlugins());
}
}