package net.i2p.i2ptunnel;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import net.i2p.I2PAppContext;
import net.i2p.app.*;
import static net.i2p.app.ClientAppState.*;
import net.i2p.client.I2PSession;
import net.i2p.client.I2PSessionException;
import net.i2p.data.DataHelper;
import net.i2p.util.I2PAppThread;
import net.i2p.util.Log;
import net.i2p.util.OrderedProperties;
import net.i2p.util.SystemVersion;
/**
* Coordinate a set of tunnels within the JVM, loading and storing their config
* to disk, and building new ones as requested.
*
* This is the entry point from clients.config.
*/
public class TunnelControllerGroup implements ClientApp {
private final Log _log;
private volatile ClientAppState _state;
private final I2PAppContext _context;
private final ClientAppManager _mgr;
private static volatile TunnelControllerGroup _instance;
static final String DEFAULT_CONFIG_FILE = "i2ptunnel.config";
private final List<TunnelController> _controllers;
private final ReadWriteLock _controllersLock;
private boolean _controllersLoaded;
private final Object _controllersLoadedLock = new Object();
private final String _configFile;
private static final String REGISTERED_NAME = "i2ptunnel";
/**
* Map of I2PSession to a Set of TunnelController objects
* using the session (to prevent closing the session until
* no more tunnels are using it)
*
*/
private final Map<I2PSession, Set<TunnelController>> _sessions;
/**
* We keep a pool of socket handlers for all clients,
* as there is no need for isolation on the client side.
* Extending classes may use it for other purposes.
*
* May also be used by servers, carefully,
* as there is no limit on threads.
*/
private ThreadPoolExecutor _executor;
private static final AtomicLong _executorThreadCount = new AtomicLong();
private final Object _executorLock = new Object();
/** how long to wait before dropping an idle thread */
private static final long HANDLER_KEEPALIVE_MS = 2*60*1000;
/**
* In I2PAppContext will instantiate if necessary and always return non-null.
* As of 0.9.4, when in RouterContext, will return null (except in Android)
* if the TCG has not yet been started by the router.
*
* @throws IllegalArgumentException if unable to load from i2ptunnel.config
*/
public static TunnelControllerGroup getInstance() {
synchronized (TunnelControllerGroup.class) {
if (_instance == null) {
I2PAppContext ctx = I2PAppContext.getGlobalContext();
if (SystemVersion.isAndroid() || !ctx.isRouterContext()) {
_instance = new TunnelControllerGroup(ctx, null, null);
if (!SystemVersion.isAndroid())
_instance.startup();
} // else wait for the router to start it
}
return _instance;
}
}
/**
* Instantiation only. Caller must call startup().
* Config file problems will not throw exception until startup().
*
* @param mgr may be null
* @param args one arg, the config file, if not absolute will be relative to the context's config dir,
* if empty or null, the default is i2ptunnel.config
* @throws IllegalArgumentException if too many args
* @since 0.9.4
*/
public TunnelControllerGroup(I2PAppContext context, ClientAppManager mgr, String[] args) {
_state = UNINITIALIZED;
_context = context;
_mgr = mgr;
_log = _context.logManager().getLog(TunnelControllerGroup.class);
_controllers = new ArrayList<TunnelController>();
_controllersLock = new ReentrantReadWriteLock(true);
if (args == null || args.length <= 0)
_configFile = DEFAULT_CONFIG_FILE;
else if (args.length == 1)
_configFile = args[0];
else
throw new IllegalArgumentException("Usage: TunnelControllerGroup [filename]");
_sessions = new HashMap<I2PSession, Set<TunnelController>>(4);
synchronized (TunnelControllerGroup.class) {
if (_instance == null)
_instance = this;
}
if (_instance != this) {
_log.logAlways(Log.WARN, "New TunnelControllerGroup, now you have two");
if (_log.shouldLog(Log.WARN))
_log.warn("I did it", new Exception());
}
_state = INITIALIZED;
}
/**
* @param args one arg, the config file, if not absolute will be relative to the context's config dir,
* if no args, the default is i2ptunnel.config
* @throws IllegalArgumentException if unable to load from config from file
*/
public static void main(String args[]) {
synchronized (TunnelControllerGroup.class) {
if (_instance != null) return; // already loaded through the web
_instance = new TunnelControllerGroup(I2PAppContext.getGlobalContext(), null, args);
_instance.startup();
}
}
/**
* ClientApp interface
* @throws IllegalArgumentException if unable to load config from file
* @since 0.9.4
*/
public void startup() {
try {
loadControllers(_configFile);
} catch (IllegalArgumentException iae) {
if (DEFAULT_CONFIG_FILE.equals(_configFile) && !_context.isRouterContext()) {
// for i2ptunnel command line
synchronized (_controllersLoadedLock) {
_controllersLoaded = true;
}
_log.logAlways(Log.WARN, "Not in router context and no preconfigured tunnels");
} else {
throw iae;
}
}
startControllers();
if (_mgr != null)
_mgr.register(this);
// RouterAppManager registers its own shutdown hook
else
_context.addShutdownTask(new Shutdown());
}
/**
* ClientApp interface
* @since 0.9.4
*/
public ClientAppState getState() {
return _state;
}
/**
* ClientApp interface
* @since 0.9.4
*/
public String getName() {
return REGISTERED_NAME;
}
/**
* ClientApp interface
* @since 0.9.4
*/
public String getDisplayName() {
return REGISTERED_NAME;
}
/**
* @since 0.9.4
*/
private void changeState(ClientAppState state) {
changeState(state, null);
}
/**
* @since 0.9.4
*/
private synchronized void changeState(ClientAppState state, Exception e) {
_state = state;
if (_mgr != null)
_mgr.notify(this, state, null, e);
}
/**
* Warning - destroys the singleton!
* @since 0.8.8
*/
private class Shutdown implements Runnable {
public void run() {
shutdown();
}
}
/**
* ClientApp interface
* @since 0.9.4
*/
public void shutdown(String[] args) {
shutdown();
}
/**
* Warning - destroys the singleton!
* Caller must root a new context before calling instance() or main() again.
* Agressively kill and null everything to reduce memory usage in the JVM
* after stopping, and to recognize what must be reinitialized on restart (Android)
*
* @since 0.8.8
*/
public synchronized void shutdown() {
if (_state != STARTING && _state != RUNNING)
return;
changeState(STOPPING);
if (_mgr != null)
_mgr.unregister(this);
unloadControllers();
synchronized (TunnelControllerGroup.class) {
if (_instance == this)
_instance = null;
}
killClientExecutor();
changeState(STOPPED);
}
/**
* Load up all of the tunnels configured in the given file.
* Prior to 0.9.20, also started the tunnels.
* As of 0.9.20, does not start the tunnels, you must call startup()
* or getInstance() instead of loadControllers().
*
* DEPRECATED for use outside this class. Use startup() or getInstance().
*
* @throws IllegalArgumentException if unable to load from file
*/
public synchronized void loadControllers(String configFile) {
synchronized (_controllersLoadedLock) {
if (_controllersLoaded)
return;
}
Properties cfg = loadConfig(configFile);
int i = 0;
_controllersLock.writeLock().lock();
try {
while (true) {
String type = cfg.getProperty("tunnel." + i + ".type");
if (type == null)
break;
TunnelController controller = new TunnelController(cfg, "tunnel." + i + ".");
_controllers.add(controller);
i++;
}
} finally {
_controllersLock.writeLock().unlock();
}
synchronized (_controllersLoadedLock) {
_controllersLoaded = true;
}
if (i > 0) {
if (_log.shouldLog(Log.INFO))
_log.info(i + " controllers loaded from " + configFile);
} else {
_log.logAlways(Log.WARN, "No i2ptunnel configurations found in " + configFile);
}
}
/**
* Start all of the tunnels. Must call loadControllers() first.
* @since 0.9.20
*/
private synchronized void startControllers() {
changeState(STARTING);
I2PAppThread startupThread = new I2PAppThread(new StartControllers(), "Startup tunnels");
startupThread.start();
changeState(RUNNING);
}
private class StartControllers implements Runnable {
public void run() {
synchronized(TunnelControllerGroup.this) {
_controllersLock.readLock().lock();
try {
if (_controllers.size() <= 0) {
_log.logAlways(Log.WARN, "No configured tunnels to start");
return;
}
for (int i = 0; i < _controllers.size(); i++) {
TunnelController controller = _controllers.get(i);
if (controller.getStartOnLoad())
controller.startTunnelBackground();
}
} finally {
_controllersLock.readLock().unlock();
}
}
}
}
/**
* Stop all tunnels, reload config, and restart those configured to do so.
* WARNING - Does NOT simply reload the configuration!!! This is probably not what you want.
*
* @throws IllegalArgumentException if unable to reload config file
*/
public synchronized void reloadControllers() {
unloadControllers();
loadControllers(_configFile);
startControllers();
}
/**
* Stop and remove reference to all known tunnels (but dont delete any config
* file or do other silly things)
*
*/
public synchronized void unloadControllers() {
synchronized (_controllersLoadedLock) {
if (!_controllersLoaded)
return;
}
_controllersLock.writeLock().lock();
try {
destroyAllControllers();
_controllers.clear();
} finally {
_controllersLock.writeLock().unlock();
}
synchronized (_controllersLoadedLock) {
_controllersLoaded = false;
}
if (_log.shouldLog(Log.INFO))
_log.info("All controllers stopped and unloaded");
}
/**
* Add the given tunnel to the set of known controllers (but dont add it to
* a config file or start it or anything)
*
*/
public synchronized void addController(TunnelController controller) {
_controllersLock.writeLock().lock();
try {
_controllers.add(controller);
} finally {
_controllersLock.writeLock().unlock();
}
}
/**
* Stop and remove the given tunnel
*
* @return list of messages from the controller as it is stopped
*/
public synchronized List<String> removeController(TunnelController controller) {
if (controller == null) return new ArrayList<String>();
controller.stopTunnel();
List<String> msgs = controller.clearMessages();
_controllersLock.writeLock().lock();
try {
_controllers.remove(controller);
} finally {
_controllersLock.writeLock().unlock();
}
msgs.add("Tunnel " + controller.getName() + " removed");
return msgs;
}
/**
* Stop all tunnels. May be restarted.
*
* @return list of messages the tunnels generate when stopped
*/
public synchronized List<String> stopAllControllers() {
List<String> msgs = new ArrayList<String>();
_controllersLock.readLock().lock();
try {
for (int i = 0; i < _controllers.size(); i++) {
TunnelController controller = _controllers.get(i);
controller.stopTunnel();
msgs.addAll(controller.clearMessages());
}
if (_log.shouldLog(Log.INFO))
_log.info(_controllers.size() + " controllers stopped");
} finally {
_controllersLock.readLock().unlock();
}
return msgs;
}
/**
* Stop all tunnels. They may not be restarted, you must reload.
* Caller must synch. Caller must clear controller list.
*
* @since 0.9.17
*/
private void destroyAllControllers() {
for (int i = 0; i < _controllers.size(); i++) {
TunnelController controller = _controllers.get(i);
controller.destroyTunnel();
}
if (_log.shouldLog(Log.INFO))
_log.info(_controllers.size() + " controllers stopped");
}
/**
* Start all tunnels
*
* @return list of messages the tunnels generate when started
*/
public synchronized List<String> startAllControllers() {
List<String> msgs = new ArrayList<String>();
_controllersLock.readLock().lock();
try {
for (int i = 0; i < _controllers.size(); i++) {
TunnelController controller = _controllers.get(i);
controller.startTunnelBackground();
msgs.addAll(controller.clearMessages());
}
if (_log.shouldLog(Log.INFO))
_log.info(_controllers.size() + " controllers started");
} finally {
_controllersLock.readLock().unlock();
}
return msgs;
}
/**
* Restart all tunnels
*
* @return list of messages the tunnels generate when restarted
*/
public synchronized List<String> restartAllControllers() {
List<String> msgs = new ArrayList<String>();
_controllersLock.readLock().lock();
try {
for (int i = 0; i < _controllers.size(); i++) {
TunnelController controller = _controllers.get(i);
controller.restartTunnel();
msgs.addAll(controller.clearMessages());
}
if (_log.shouldLog(Log.INFO))
_log.info(_controllers.size() + " controllers restarted");
} finally {
_controllersLock.readLock().unlock();
}
return msgs;
}
/**
* Fetch all outstanding messages from any of the known tunnels
*
* @return list of messages the tunnels have generated
*/
public List<String> clearAllMessages() {
List<String> msgs = new ArrayList<String>();
_controllersLock.readLock().lock();
try {
for (int i = 0; i < _controllers.size(); i++) {
TunnelController controller = _controllers.get(i);
msgs.addAll(controller.clearMessages());
}
} finally {
_controllersLock.readLock().unlock();
}
return msgs;
}
/**
* Save the configuration of all known tunnels to the default config
* file
*
*/
public void saveConfig() throws IOException {
saveConfig(_configFile);
}
/**
* Save the configuration of all known tunnels to the given file
*
*/
public synchronized void saveConfig(String configFile) throws IOException {
File cfgFile = new File(configFile);
if (!cfgFile.isAbsolute())
cfgFile = new File(I2PAppContext.getGlobalContext().getConfigDir(), configFile);
File parent = cfgFile.getParentFile();
if ( (parent != null) && (!parent.exists()) )
parent.mkdirs();
Properties map = new OrderedProperties();
_controllersLock.readLock().lock();
try {
for (int i = 0; i < _controllers.size(); i++) {
TunnelController controller = _controllers.get(i);
Properties cur = controller.getConfig("tunnel." + i + ".");
map.putAll(cur);
}
} finally {
_controllersLock.readLock().unlock();
}
DataHelper.storeProps(map, cfgFile);
}
/**
* Load up the config data from the file
*
* @return properties loaded
* @throws IllegalArgumentException if unable to load from file
*/
private synchronized Properties loadConfig(String configFile) {
File cfgFile = new File(configFile);
if (!cfgFile.isAbsolute())
cfgFile = new File(I2PAppContext.getGlobalContext().getConfigDir(), configFile);
if (!cfgFile.exists()) {
if (_log.shouldLog(Log.ERROR))
_log.error("Unable to load the controllers from " + cfgFile.getAbsolutePath());
throw new IllegalArgumentException("Unable to load the controllers from " + cfgFile.getAbsolutePath());
}
Properties props = new Properties();
try {
DataHelper.loadProps(props, cfgFile);
return props;
} catch (IOException ioe) {
if (_log.shouldLog(Log.ERROR))
_log.error("Error reading the controllers from " + cfgFile.getAbsolutePath(), ioe);
throw new IllegalArgumentException("Error reading the controllers from " + cfgFile.getAbsolutePath(), ioe);
}
}
/**
* Retrieve a list of tunnels known.
*
* Side effect: if the tunnels have not been loaded from config yet, they
* will be.
*
* @return list of TunnelController objects
* @throws IllegalArgumentException if unable to load config from file
*/
public List<TunnelController> getControllers() {
synchronized (_controllersLoadedLock) {
if (!_controllersLoaded)
loadControllers(_configFile);
}
_controllersLock.readLock().lock();
try {
return new ArrayList<TunnelController>(_controllers);
} finally {
_controllersLock.readLock().unlock();
}
}
/**
* Note the fact that the controller is using the session so that
* it isn't destroyed prematurely.
*
*/
void acquire(TunnelController controller, I2PSession session) {
synchronized (_sessions) {
Set<TunnelController> owners = _sessions.get(session);
if (owners == null) {
owners = new HashSet<TunnelController>(2);
_sessions.put(session, owners);
}
owners.add(controller);
}
if (_log.shouldLog(Log.INFO))
_log.info("Acquiring session " + session + " for " + controller);
}
/**
* Note the fact that the controller is no longer using the session, and if
* no other controllers are using it, destroy the session.
*
*/
void release(TunnelController controller, I2PSession session) {
boolean shouldClose = false;
synchronized (_sessions) {
Set<TunnelController> owners = _sessions.get(session);
if (owners != null) {
owners.remove(controller);
if (owners.isEmpty()) {
if (_log.shouldLog(Log.INFO))
_log.info("After releasing session " + session + " by " + controller + ", no more owners remain");
shouldClose = true;
_sessions.remove(session);
} else {
if (_log.shouldLog(Log.INFO))
_log.info("After releasing session " + session + " by " + controller + ", " + owners.size() + " owners remain");
shouldClose = false;
}
} else {
if (_log.shouldLog(Log.WARN))
_log.warn("After releasing session " + session + " by " + controller + ", no owners were even known?!");
shouldClose = true;
}
}
if (shouldClose) {
try {
session.destroySession();
if (_log.shouldLog(Log.INFO))
_log.info("Session destroyed: " + session);
} catch (I2PSessionException ise) {
_log.error("Error closing the client session", ise);
}
}
}
/**
* @return non-null
* @since 0.8.8 Moved from I2PTunnelClientBase in 0.9.18
*/
ThreadPoolExecutor getClientExecutor() {
synchronized (_executorLock) {
if (_executor == null)
_executor = new CustomThreadPoolExecutor();
}
return _executor;
}
/**
* @since 0.8.8 Moved from I2PTunnelClientBase in 0.9.18
*/
private void killClientExecutor() {
synchronized (_executorLock) {
if (_executor != null) {
_executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
_executor.shutdownNow();
_executor = null;
}
}
// kill the shared client, so that on restart in android
// we won't latch onto the old one
I2PTunnelClientBase.killSharedClient();
}
/**
* Not really needed for now but in case we want to add some hooks like afterExecute().
* Package private for fallback in case TCG.getInstance() is null, never instantiated
* but a plugin still needs it... should be rare.
*
* @since 0.9.18 Moved from I2PTunnelClientBase
*/
static class CustomThreadPoolExecutor extends ThreadPoolExecutor {
public CustomThreadPoolExecutor() {
super(0, Integer.MAX_VALUE, HANDLER_KEEPALIVE_MS, TimeUnit.MILLISECONDS,
new SynchronousQueue<Runnable>(), new CustomThreadFactory());
}
}
/**
* Just to set the name and set Daemon
* @since 0.9.18 Moved from I2PTunnelClientBase
*/
private static class CustomThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
Thread rv = Executors.defaultThreadFactory().newThread(r);
rv.setName("I2PTunnel Client Runner " + _executorThreadCount.incrementAndGet());
rv.setDaemon(true);
return rv;
}
}
}