package hudson.plugins.im; import hudson.model.Hudson; import hudson.plugins.im.tools.ExceptionHelper; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import org.acegisecurity.Authentication; import org.acegisecurity.providers.UsernamePasswordAuthenticationToken; /** * Abstract implementation of a provider of {@link IMConnection}s. * * @author kutzi */ public abstract class IMConnectionProvider implements IMConnectionListener { private static final Logger LOGGER = Logger.getLogger(IMConnectionProvider.class.getName()); private static final IMConnection NULL_CONNECTION = new DummyConnection(); protected IMPublisherDescriptor descriptor; private IMConnection imConnection = NULL_CONNECTION; private Authentication authentication = null; private final ConnectorRunnable connector = new ConnectorRunnable(); protected IMConnectionProvider() { } /** * Must be called once to initialize the provider. */ protected void init() { Thread connectorThread = new Thread(this.connector, "IM-Reconnector-Thread"); connectorThread.setDaemon(true); connectorThread.start(); tryReconnect(); } /** * Creates a new connection. * * @return the new connection. Never null. * @throws IMException if the connection couldn't be created for any reason. * @throws IMException */ public abstract IMConnection createConnection() throws IMException; private synchronized boolean create() throws IMException { if (this.descriptor == null || !this.descriptor.isEnabled()) { // plugin is disabled this.imConnection = NULL_CONNECTION; return true; } try { this.imConnection = createConnection(); this.imConnection.addConnectionListener(this); return true; } catch (IMException e) { this.imConnection = NULL_CONNECTION; tryReconnect(); return false; } } /** * Return the current connection. * Returns an instance of {@link DummyConnection} if the plugin * is currently not connection to a IM network. * * @return the current connection. Never null. */ public synchronized IMConnection currentConnection() { return this.imConnection; } /** * Releases (and thus closes) the current connection. */ public synchronized void releaseConnection() { if (this.imConnection != null) { this.imConnection.removeConnectionListener(this); this.imConnection.close(); this.imConnection = NULL_CONNECTION; } } protected IMPublisherDescriptor getDescriptor() { return this.descriptor; } public void setDescriptor(IMPublisherDescriptor desc) { this.descriptor = desc; if (desc != null && desc.isEnabled()) { tryReconnect(); } } @Override public void connectionBroken(Exception e) { tryReconnect(); } private void tryReconnect() { this.connector.semaphore.release(); } // we need an additional level of indirection to the Authentication entity // to fix HUDSON-5978 and HUDSON-5233 public synchronized AuthenticationHolder getAuthenticationHolder() { if (this.descriptor == null || this.descriptor.getHudsonUserName() == null) { return null; } return new AuthenticationHolder() { @Override public Authentication getAuthentication() { if (authentication != null) { return authentication; } try { Authentication tmp = new UsernamePasswordAuthenticationToken( descriptor.getHudsonUserName(), descriptor.getHudsonPassword()); authentication = Hudson.getInstance().getSecurityRealm().getSecurityComponents().manager.authenticate(tmp); } catch (Exception e) { LOGGER.warning(descriptor.getPluginDescription() + " couldn't authenticate against Hudson: " + e); } return authentication; } }; } private final class ConnectorRunnable implements Runnable { private final Semaphore semaphore = new Semaphore(0); private boolean firstConnect = true; public void run() { try { while (true) { this.semaphore.acquire(); if (!firstConnect) { // wait a little bit in case the XMPP server/network has just a 'hickup' TimeUnit.SECONDS.sleep(30); LOGGER.info("Trying to reconnect"); } else { firstConnect = false; LOGGER.info("Trying to connect"); } boolean success = false; int timeout = 1; while (!success) { synchronized (IMConnectionProvider.this) { if (imConnection != null) { try { releaseConnection(); } catch (Exception e) { LOGGER.warning(ExceptionHelper.dump(e)); } } try { success = create(); } catch (IMException e) { // ignore } } // make sure to leave the synchronized block before sleeping! if(!success) { LOGGER.info("Reconnect failed. Next connection attempt in " + timeout + " minutes"); this.semaphore.drainPermits(); // wait up to timeout time OR until semaphore is released again (happens e.g. if global config was changed) this.semaphore.tryAcquire(timeout * 60, TimeUnit.SECONDS); // exponentially increase timeout, but longer than 16 minutes if (timeout < 15) { timeout *= 2; } } else { // remove any permits which came in in the mean time this.semaphore.drainPermits(); } } } } catch (InterruptedException e) { LOGGER.info("Connect thread interrupted"); // just bail out } } } }