/** * Copyright (C) 2010-2014 Leon Blakey <lord.quackstar at gmail.com> * * This file is part of PircBotX. * * PircBotX is free software: you can redistribute it and/or modify it under the * terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later * version. * * PircBotX is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR * A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * PircBotX. If not, see <http://www.gnu.org/licenses/>. */ package org.pircbotx; import com.google.common.base.Joiner; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import static com.google.common.util.concurrent.Service.State; import static com.google.common.base.Preconditions.*; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import java.util.LinkedHashMap; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.Synchronized; import lombok.extern.slf4j.Slf4j; import org.pircbotx.output.OutputIRC; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Manager that makes connecting and running multiple bots an easy, painless * process. * <p> * Lifecycle: * <ol><li>When created, any added bots or configurations are queued</li> * <li>When {@link #start()} is called, all queued bots are connected. Any bots * added after this point are automatically connected</li> * <li>When {@link #stop()} is called, {@link OutputIRC#quitServer()} is called * on all bots. No more bots can be added, the Manager is finished. Note that an * optional {@link #stopAndWait() } method is provided to block until all bots * shutdown * </ol> {@link #executeBot(org.pircbotx.PircBotX)} is overridable if you wish * to do your own connecting * * @author Leon Blakey */ @Slf4j public class MultiBotManager { protected static final AtomicInteger MANAGER_COUNT = new AtomicInteger(); protected final int managerNumber; protected final LinkedHashMap<PircBotX, ListenableFuture<Void>> runningBots = Maps.newLinkedHashMap(); protected final BiMap<PircBotX, Integer> runningBotsNumbers = HashBiMap.create(); protected final Object runningBotsLock = new Object[0]; protected final ListeningExecutorService botPool; //Code for starting protected List<PircBotX> startQueue = Lists.newArrayList(); protected State state = State.NEW; protected final Object stateLock = new Object[0]; /** * Create MultiBotManager with a cached thread pool. */ public MultiBotManager() { managerNumber = MANAGER_COUNT.getAndIncrement(); ThreadPoolExecutor defaultPool = (ThreadPoolExecutor) Executors.newCachedThreadPool(); defaultPool.allowCoreThreadTimeOut(true); this.botPool = MoreExecutors.listeningDecorator(defaultPool); } /** * Create MultiBotManager with the specified thread pool. * * @param botPool A provided thread pool. */ public MultiBotManager(ExecutorService botPool) { checkNotNull(botPool, "Bot pool cannot be null"); this.botPool = MoreExecutors.listeningDecorator(botPool); this.managerNumber = MANAGER_COUNT.getAndIncrement(); } /** * Alias of {@link #addNetwork(org.pircbotx.Configuration) } * * @param config * @see #addNetwork(org.pircbotx.Configuration) */ public void addBot(Configuration config) { addNetwork(config); } /** * Adds a managed bot using the specified configuration. * * @param config A configuration to pass to the created bot */ @Synchronized("stateLock") @SuppressWarnings("unchecked") public void addNetwork(Configuration config) { checkNotNull(config, "Configuration cannot be null"); //Since creating a bot is expensive, verify the state first if (state != State.NEW && state != State.RUNNING) throw new RuntimeException("MultiBotManager is not running. State: " + state); addNetwork(new PircBotX(config)); } /** * Alias of {@link #addNetwork(org.pircbotx.PircBotX) } * * @param bot * @see #addNetwork(org.pircbotx.PircBotX) */ public void addBot(PircBotX bot) { addNetwork(bot); } /** * Adds a bot to be managed. * * @param bot An existing <b>unconnected</b> bot */ @Synchronized("stateLock") public void addNetwork(PircBotX bot) { checkNotNull(bot, "Bot cannot be null"); checkArgument(!bot.isConnected(), "Bot must not already be connected"); if (state == State.NEW) { log.debug("Not started yet, add to queue"); startQueue.add(bot); } else if (state == State.RUNNING) { log.debug("Already running, start bot immediately"); startBot(bot); } else throw new RuntimeException("MultiBotManager is not running. State: " + state); } /** * Start the manager, connecting all queued bots. */ public void start() { synchronized (stateLock) { if (state != State.NEW) throw new RuntimeException("MultiBotManager has already been started. State: " + state); state = State.STARTING; } for (PircBotX bot : startQueue) startBot(bot); startQueue.clear(); synchronized (stateLock) { state = State.RUNNING; } } protected ListenableFuture<Void> startBot(final PircBotX bot) { checkNotNull(bot, "Bot cannot be null"); ListenableFuture<Void> future = botPool.submit(new BotRunner(bot)); synchronized (runningBotsLock) { runningBots.put(bot, future); runningBotsNumbers.put(bot, bot.getBotId()); } Futures.addCallback(future, new BotFutureCallback(bot)); return future; } /** * Stop with no quit message * @see #stop(java.lang.String) */ public void stop() { stop(""); } /** * Disconnect all bots from their respective severs cleanly. */ public void stop(String quitMessage) { synchronized (stateLock) { if (state != State.RUNNING) throw new RuntimeException("MultiBotManager cannot be stopped again or before starting. State: " + state); state = State.STOPPING; } for (PircBotX bot : runningBots.keySet()) if (bot.isConnected()) bot.sendIRC().quitServer(quitMessage); botPool.shutdown(); } /** * {@link #stop()} and wait for all bots to disconnect. * * @throws InterruptedException If this is interrupted while waiting */ public void stopAndWait() throws InterruptedException { stop(); Joiner commaJoiner = Joiner.on(", "); do synchronized (runningBotsLock) { log.debug("Waiting 5 seconds for bot(s) [{}] to terminate ", commaJoiner.join(runningBots.values())); } while (!botPool.awaitTermination(5, TimeUnit.SECONDS)); } /** * Get all the bots that this MultiBotManager is managing. * * @return An <i>immutable copy</i> of bots that are being managed */ @Synchronized("runningBotsLock") public ImmutableSortedSet<PircBotX> getBots() { return ImmutableSortedSet.copyOf(runningBots.keySet()); } /** * Lookup a managed bot by id. * * @param id The id of the bot * @return A bot that has the specified id or null */ @Synchronized("runningBotsLock") @SuppressWarnings("unchecked") public <B extends PircBotX> B getBotById(int id) { return (B) runningBotsNumbers.inverse().get(id); } /** * Called when * * @param bot */ protected void executeBot(PircBotX bot) throws Exception { bot.startBot(); } @RequiredArgsConstructor protected class BotRunner implements Callable<Void> { @NonNull protected final PircBotX bot; public Void call() throws Exception { Thread.currentThread().setName("botPool" + managerNumber + "-bot" + bot.getBotId()); Utils.addBotToMDC(bot); executeBot(bot); return null; } } @RequiredArgsConstructor protected class BotFutureCallback implements FutureCallback<Void> { protected final Logger log = LoggerFactory.getLogger(getClass()); @NonNull protected final PircBotX bot; public void onSuccess(Void result) { log.debug("Bot #" + bot.getBotId() + " finished"); remove(); } public void onFailure(Throwable t) { log.error("Bot exited with Exception", t); remove(); } protected void remove() { synchronized (runningBotsLock) { runningBots.remove(bot); runningBotsNumbers.remove(bot); //Change state to TERMINATED if this is the last but to be removed during shutdown if (runningBots.isEmpty() && state == State.STOPPING) synchronized (stateLock) { if (state == State.STOPPING) state = State.TERMINATED; } } } } }