
com.sonymobile.tools.gerrit.gerritevents.GerritConnection Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of gerrit-events Show documentation
Show all versions of gerrit-events Show documentation
Java client library for receiving stream-events from Gerrit code review.
As well as performing queries and sending reviews.
The newest version!
/*
* The MIT License
*
* Copyright 2010 Sony Ericsson Mobile Communications. All rights reserved.
* Copyright 2012 Sony Mobile Communications AB. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.sonymobile.tools.gerrit.gerritevents;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.CharBuffer;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSchException;
import com.sonymobile.tools.gerrit.gerritevents.dto.attr.Provider;
import com.sonymobile.tools.gerrit.gerritevents.ssh.Authentication;
import com.sonymobile.tools.gerrit.gerritevents.ssh.AuthenticationUpdater;
import com.sonymobile.tools.gerrit.gerritevents.ssh.SshAuthenticationException;
import com.sonymobile.tools.gerrit.gerritevents.ssh.SshConnectException;
import com.sonymobile.tools.gerrit.gerritevents.ssh.SshConnection;
import com.sonymobile.tools.gerrit.gerritevents.ssh.SshConnectionFactory;
import com.sonymobile.tools.gerrit.gerritevents.watchdog.StreamWatchdog;
import com.sonymobile.tools.gerrit.gerritevents.watchdog.WatchTimeExceptionData;
//CS IGNORE LineLength FOR NEXT 7 LINES. REASON: static import.
/**
* Main class for connection. Contains the main loop for connecting to Gerrit.
*
* @author rinrinne <[email protected]>
*/
public class GerritConnection extends Thread implements Connector {
/**
* Time to wait between connection attempts.
*/
public static final int CONNECT_SLEEP = 2000;
/**
* Command to open gerrit event stream.
*/
public static final String CMD_STREAM_EVENTS = "gerrit stream-events";
private static final String GERRIT_VERSION_PREFIX = "gerrit version ";
/* The buffer must support 256KB as real life messages can be pretty
* big. See https://issues.jenkins-ci.org/browse/JENKINS-44568
*/
private static final int SSH_RX_BUFFER_SIZE = 262400;
private static final int SSH_RX_SLEEP_MILLIS = 100;
/**
* The standard scheme used for stream-events.
*/
public static final String GERRIT_PROTOCOL_SCHEME_NAME = "ssh";
private static final Logger logger = LoggerFactory.getLogger(GerritConnection.class);
private String gerritName;
private String gerritHostName;
private int gerritSshPort;
private String gerritProxy;
private Authentication authentication;
private String gerritFrontEndUrl;
private SshConnection sshConnection;
private volatile boolean shutdownInProgress = false;
private volatile boolean connected = false;
private String gerritVersion = null;
private int watchdogTimeoutSeconds;
private WatchTimeExceptionData exceptionData;
private StreamWatchdog watchdog;
private int reconnectCallCount = 0;
private GerritHandler handler;
private AuthenticationUpdater authenticationUpdater = null;
private final Set listeners = new CopyOnWriteArraySet();
private int sshRxBufferSize = SSH_RX_BUFFER_SIZE;
private StringBuilder eventBuffer = null;
/**
* Creates a GerritHandler with all the default values set.
*
* @see GerritDefaultValues#DEFAULT_GERRIT_NAME
* @see GerritDefaultValues#DEFAULT_GERRIT_HOSTNAME
* @see GerritDefaultValues#DEFAULT_GERRIT_SSH_PORT
* @see GerritDefaultValues#DEFAULT_GERRIT_PROXY
* @see GerritDefaultValues#DEFAULT_GERRIT_USERNAME
* @see GerritDefaultValues#DEFAULT_GERRIT_AUTH_KEY_FILE
* @see GerritDefaultValues#DEFAULT_GERRIT_AUTH_KEY_FILE_PASSWORD
*/
public GerritConnection() {
this(GerritDefaultValues.DEFAULT_GERRIT_NAME,
GerritDefaultValues.DEFAULT_GERRIT_HOSTNAME,
GerritDefaultValues.DEFAULT_GERRIT_SSH_PORT,
GerritDefaultValues.DEFAULT_GERRIT_PROXY,
GerritDefaultValues.DEFAULT_GERRIT_FRONT_END_URL,
new Authentication(GerritDefaultValues.DEFAULT_GERRIT_AUTH_KEY_FILE,
GerritDefaultValues.DEFAULT_GERRIT_USERNAME,
GerritDefaultValues.DEFAULT_GERRIT_AUTH_KEY_FILE_PASSWORD));
}
/**
* Creates a GerritHandler with the specified values.
*
* @param gerritName the name of the gerrit server.
* @param gerritHostName the hostName
* @param gerritSshPort the ssh port that the gerrit server listens to.
* @param authentication the authentication credentials.
*/
public GerritConnection(String gerritName,
String gerritHostName,
int gerritSshPort,
Authentication authentication) {
this(gerritName,
gerritHostName,
gerritSshPort,
GerritDefaultValues.DEFAULT_GERRIT_PROXY,
GerritDefaultValues.DEFAULT_GERRIT_FRONT_END_URL,
authentication);
}
/**
* Standard Constructor.
*
* @param gerritName the name of the gerrit server.
* @param config the configuration containing the connection values.
*/
public GerritConnection(String gerritName, GerritConnectionConfig config) {
this(gerritName, config, GerritDefaultValues.DEFAULT_GERRIT_PROXY, 0, null);
}
/**
* Standard Constructor.
*
* @param gerritName the name of the gerrit server.
* @param config the configuration containing the connection values.
*/
public GerritConnection(String gerritName, GerritConnectionConfig2 config) {
this(gerritName, config, config.getGerritProxy(), config.getWatchdogTimeoutSeconds(), config.getExceptionData());
}
/**
* Creates a GerritHandler with the specified values.
*
* @param gerritName the name of the gerrit server.
* @param config the configuration containing the connection values.
* @param gerritProxy the URL of gerrit proxy.
* @param watchdogTimeoutSeconds number of seconds before the connection watch dog restarts the connection set to 0
* or less to disable it.
* @param exceptionData time info for when the watch dog's timeout should not be in effect. set to null to
* disable the watch dog.
*/
public GerritConnection(String gerritName,
GerritConnectionConfig config,
String gerritProxy,
int watchdogTimeoutSeconds,
WatchTimeExceptionData exceptionData) {
this(gerritName,
config.getGerritHostName(),
config.getGerritSshPort(),
gerritProxy,
config.getGerritFrontEndUrl(),
config.getGerritAuthentication(),
watchdogTimeoutSeconds, exceptionData);
}
/**
* Standard Constructor.
*
* @param gerritName the name of the gerrit server.
* @param gerritHostName the hostName for gerrit.
* @param gerritSshPort the ssh port that the gerrit server listens to.
* @param gerritProxy the proxy url socks5|http://host:port.
* @param gerritFrontEndUrl the gerrit front end url.
* @param authentication the authentication credentials.
*/
public GerritConnection(String gerritName,
String gerritHostName,
int gerritSshPort,
String gerritProxy,
String gerritFrontEndUrl,
Authentication authentication) {
this(gerritName, gerritHostName, gerritSshPort, gerritProxy, gerritFrontEndUrl, authentication, 0, null);
}
/**
* Standard Constructor.
*
* @param gerritName the name of the gerrit server.
* @param gerritHostName the hostName for gerrit.
* @param gerritSshPort the ssh port that the gerrit server listens to.
* @param gerritProxy the proxy url socks5|http://host:port.
* @param gerritFrontEndUrl the gerrit front end url.
* @param authentication the authentication credentials.
* @param watchdogTimeoutSeconds number of seconds before the connection watch dog restarts the connection
* set to 0 or less to disable it.
* @param exceptionData time info for when the watch dog's timeout should not be in effect.
* set to null to disable the watch dog.
*/
public GerritConnection(String gerritName,
String gerritHostName,
int gerritSshPort,
String gerritProxy,
String gerritFrontEndUrl,
Authentication authentication,
int watchdogTimeoutSeconds,
WatchTimeExceptionData exceptionData) {
this.gerritName = gerritName;
this.gerritHostName = gerritHostName;
this.gerritSshPort = gerritSshPort;
this.gerritProxy = gerritProxy;
this.gerritFrontEndUrl = gerritFrontEndUrl;
this.authentication = authentication;
this.watchdogTimeoutSeconds = watchdogTimeoutSeconds;
this.exceptionData = exceptionData;
}
/**
* Sets Buffer size for receiving SSH stream.
*
* @param size buffer size.
* @return The previous size.
*/
public int setSshRxBufferSize(int size) {
int prev = sshRxBufferSize;
sshRxBufferSize = size;
return prev;
}
/**
* Sets gerrit handler.
*
* @param handler the handler.
*/
public void setHandler(GerritHandler handler) {
this.handler = handler;
}
/**
* Gets gerrit handler.
*
* @return the handler.
*/
public GerritHandler getHandler() {
return handler;
}
/**
* The gerrit version we are connected to.
*
* @return the gerrit version.
*/
public String getGerritVersion() {
return gerritVersion;
}
/**
* Add listener for GerrirtConnectionEvent.
*
* @param listener the listener.
*/
public void addListener(ConnectionListener listener) {
if (!listeners.add(listener)) {
logger.warn("The connection listener was doubly-added: {}", listener);
}
}
/**
* Remove listener for GerrirtConnectionEvent.
*
* @param listener the listener.
*/
public void removeListener(ConnectionListener listener) {
listeners.remove(listener);
}
/**
* Removes all connection listeners.
*/
public void removeListeners() {
listeners.clear();
}
/**
* Returns an unmodifiable view of the set of {@link ConnectionListener}s.
*
* @return the set of connection listeners.
* @see Collections#unmodifiableSet(Set)
*/
public Set getListenersView() {
return Collections.unmodifiableSet(listeners);
}
/**
* Sets {@link AuthenticationUpdater}.
* @param authenticationUpdater The {@link AuthenticationUpdater}.
*/
public void setAuthenticationUpdater(AuthenticationUpdater authenticationUpdater) {
this.authenticationUpdater = authenticationUpdater;
}
/**
* If watchdog field is not null, shut it down and put it to null.
*/
private void nullifyWatchdog() {
if (watchdog != null) {
watchdog.shutdown();
watchdog = null;
}
}
/**
* Offers lines in buffer to queue.
*
* @param cb a buffer to have received text data.
* @return the line string. null if no EOL, otherwise buffer is compacted.
*/
private String getLine(CharBuffer cb) {
String line = null;
int pos = cb.position();
int limit = cb.limit();
cb.flip();
for (int i = 0; i < cb.length(); i++) {
if (cb.charAt(i) == '\n') {
line = getSubSequence(cb, 0, i).toString();
cb.position(i + 1);
break;
}
}
if (line != null) {
cb.compact();
if (eventBuffer != null) {
eventBuffer.append(line);
String eventString = eventBuffer.toString();
eventBuffer = null;
line = eventString;
}
line.trim();
} else {
if (cb.length() > 0) {
if (cb.length() == cb.capacity()) {
if (eventBuffer == null) {
logger.debug("Encountered big event.");
eventBuffer = new StringBuilder();
}
eventBuffer.append(getSubSequence(cb, 0, pos));
} else {
cb.position(pos);
cb.limit(limit);
}
} else {
cb.clear();
}
}
return line;
}
/**
* Get sub sequence of buffer.
*
* This method avoids error in java-api-check.
* animal-sniffer is confused by the signature of CharBuffer.subSequence()
* due to declaration of this method has been changed since Java7.
* (abstract -> non-abstract)
*
* @param cb a buffer
* @param start start of sub sequence
* @param end end of sub sequence
* @return sub sequence.
*/
@IgnoreJRERequirement
private CharSequence getSubSequence(CharBuffer cb, int start, int end) {
return cb.subSequence(start, end);
}
/**
* Main loop for connecting and reading Gerrit JSON Events and dispatching them to Workers.
*/
@Override
public void run() {
logger.info("Starting Up " + gerritName);
do {
sshConnection = connect();
if (sshConnection == null) {
return;
}
if (watchdogTimeoutSeconds > 0 && exceptionData != null) {
nullifyWatchdog();
watchdog = new StreamWatchdog(this, watchdogTimeoutSeconds, exceptionData);
}
ChannelExec channel = null;
try {
logger.trace("Executing stream-events command.");
channel = sshConnection.executeCommandChannel(CMD_STREAM_EVENTS, false);
if (channel == null) {
throw new IOException("Cannot open SSH channel.");
}
Reader reader = new InputStreamReader(channel.getInputStream(), "utf-8");
channel.connect();
CharBuffer cb = CharBuffer.allocate(sshRxBufferSize);
notifyConnectionEstablished();
Provider provider = new Provider(
gerritName,
gerritHostName,
String.valueOf(gerritSshPort),
GERRIT_PROTOCOL_SCHEME_NAME,
gerritFrontEndUrl,
getGerritVersionString());
logger.info("Ready to receive data from Gerrit: " + gerritName);
String line;
Integer readCount;
while ((readCount = reader.read(cb)) != -1) {
logger.debug("Read count from Gerrit stream: {}", String.valueOf(readCount));
int linecount = 0;
while ((line = getLine(cb)) != null) {
linecount++;
logger.debug("Data-line from Gerrit: {}", line);
if (handler != null) {
handler.post(line, provider);
}
}
if (shutdownInProgress || interrupted()) {
throw new InterruptedException("shutdown requested: " + shutdownInProgress);
}
if (readCount > 0 && watchdog != null) {
watchdog.signal();
}
if (!channel.isConnected() || !sshConnection.isConnected()) {
throw new IllegalStateException("SSH connection is already lost.");
}
if (readCount == 0 || linecount > 0) {
sleep(SSH_RX_SLEEP_MILLIS);
}
}
} catch (IOException ex) {
logger.error("Stream events command error. ", ex);
} catch (IllegalStateException ex) {
logger.error("Unexpected disconnection occurred after initial moment of connection. ", ex);
} catch (InterruptedException ex) {
logger.error("Interrupted.", ex);
} catch (JSchException ex) {
logger.error("Error when establishing SSH connection. ", ex);
} finally {
nullifyWatchdog();
if (channel != null && !channel.isClosed()) {
logger.trace("Close channel.");
try {
channel.disconnect();
} catch (Exception ex) {
logger.warn("Error when disconnecting SSH command channel.", ex);
}
}
if (!sshConnection.isConnected()) {
sshConnection = null;
}
notifyConnectionDown();
}
} while (!shutdownInProgress);
handler = null;
logger.debug("End of GerritConnection Thread.");
}
/**
* Connects to the Gerrit server and authenticates as the specified user.
*
* @return not null if everything is well, null if connect and reconnect failed.
*/
private SshConnection connect() {
if (sshConnection != null && sshConnection.isConnected()) {
return sshConnection;
}
while (!shutdownInProgress) {
SshConnection ssh = null;
try {
logger.debug("Connecting...");
ssh = SshConnectionFactory.getConnection(gerritHostName, gerritSshPort, gerritProxy,
authentication, authenticationUpdater);
gerritVersion = formatVersion(ssh.executeCommand("gerrit version"));
logger.debug("connection seems ok, returning it.");
return ssh;
} catch (SshConnectException sshConEx) {
logger.error("Could not connect to Gerrit server! "
+ "Host: {} Port: {}", gerritHostName, gerritSshPort);
logger.error(" Proxy: {}", gerritProxy);
logger.error(" User: {} KeyFile: {}", authentication.getUsername(), authentication.getPrivateKeyFile());
logger.error("ConnectionException: ", sshConEx);
} catch (SshAuthenticationException sshAuthEx) {
logger.error("Could not authenticate to Gerrit server!"
+ "\n\tUsername: {}\n\tKeyFile: {}\n\tPassword: {}",
new Object[]{authentication.getUsername(),
authentication.getPrivateKeyFile(),
authentication.getPrivateKeyFilePassword(), });
logger.error("AuthenticationException: ", sshAuthEx);
} catch (IOException ex) {
logger.error("Could not connect to Gerrit server! "
+ "Host: {} Port: {}", gerritHostName, gerritSshPort);
logger.error(" Proxy: {}", gerritProxy);
logger.error(" User: {} KeyFile: {}", authentication.getUsername(), authentication.getPrivateKeyFile());
logger.error("IOException: ", ex);
}
if (ssh != null) {
logger.trace("Disconnecting bad connection.");
try {
//The ssh lib used is starting at least one thread for each connection.
//The thread isn't shutdown properly when the connection goes down,
//so we need to close it "manually"
ssh.disconnect();
} catch (Exception ex) {
logger.warn("Error when disconnecting bad connection.", ex);
} finally {
ssh = null;
}
}
if (!shutdownInProgress) {
//If we end up here, sleep for a while and then go back up in the loop.
logger.trace("Sleeping for a bit.");
try {
Thread.sleep(CONNECT_SLEEP);
} catch (InterruptedException ex) {
logger.warn("Got interrupted while sleeping.", ex);
}
}
}
return null;
}
/**
* Removes the "gerrit version " from the start of the response from gerrit.
* @param version the response from gerrit.
* @return the input string with "gerrit version " removed.
*/
private String formatVersion(String version) {
if (version == null) {
return version;
}
String[] split = version.split(GERRIT_VERSION_PREFIX);
if (split.length < 2) {
return version.trim();
}
return split[1].trim();
}
/**
* Gets the gerrit version.
* @return the gerrit version as valid string.
*/
private String getGerritVersionString() {
String version = getGerritVersion();
if (version == null) {
version = "";
}
return version;
}
/**
* The authentication credentials for ssh connection.
*
* @return the credentials.
*/
public Authentication getAuthentication() {
return authentication;
}
/**
* The authentication credentials for ssh connection.
*
* @param authentication the credentials.
*/
public void setAuthentication(Authentication authentication) {
this.authentication = authentication;
}
/**
* gets the hostname where Gerrit is running.
*
* @return the hostname.
*/
public String getGerritHostName() {
return gerritHostName;
}
/**
* Sets the hostname where Gerrit is running.
*
* @param gerritHostName the hostname.
*/
public void setGerritHostName(String gerritHostName) {
this.gerritHostName = gerritHostName;
}
/**
* Gets the port for gerrit ssh commands.
*
* @return the port nr.
*/
public int getGerritSshPort() {
return gerritSshPort;
}
/**
* Sets the port for gerrit ssh commands.
*
* @param gerritSshPort the port nr.
*/
public void setGerritSshPort(int gerritSshPort) {
this.gerritSshPort = gerritSshPort;
}
/**
* Gets the proxy for gerrit ssh commands.
*
* @return the proxy url.
*/
public String getGerritProxy() {
return gerritProxy;
}
/**
* Sets the proxy for gerrit ssh commands.
*
* @param gerritProxy the port nr.
*/
public void setGerritProxy(String gerritProxy) {
this.gerritProxy = gerritProxy;
}
/**
* Sets the shutdown flag.
*/
private void setShutdownInProgress() {
this.shutdownInProgress = true;
}
/**
* If the system is shutting down. I.e. the shutdown method has been called.
*
* @return true if so.
*/
public boolean isShutdownInProgress() {
return shutdownInProgress;
}
/**
* If already connected.
* @return true if already connected.
*/
public boolean isConnected() {
return connected;
}
@Override
public void reconnect() {
reconnectCallCount++;
nullifyWatchdog();
sshConnection.disconnect();
}
/**
* Count how many times {@link #reconnect()} has been called since object creation.
*
* @return the count.
*/
public int getReconnectCallCount() {
return reconnectCallCount;
}
/**
* Closes the connection.
*
* @param join if the method should wait for the thread to finish before returning.
*/
public void shutdown(boolean join) {
setShutdownInProgress();
nullifyWatchdog();
if (sshConnection != null) {
logger.info("Shutting down the ssh connection.");
try {
sshConnection.disconnect();
} catch (Exception ex) {
logger.warn("Error when disconnecting sshConnection.", ex);
}
}
if (join) {
try {
this.join();
} catch (InterruptedException ex) {
logger.warn("Got interrupted while waiting for shutdown.", ex);
}
}
}
/**
* Notifies all listeners of a Gerrit connection event.
*
* @param event the event.
*/
public void notifyListeners(GerritConnectionEvent event) {
for (ConnectionListener listener : listeners) {
try {
switch(event) {
case GERRIT_CONNECTION_ESTABLISHED:
listener.connectionEstablished();
break;
case GERRIT_CONNECTION_DOWN:
listener.connectionDown();
break;
default:
break;
}
} catch (Exception ex) {
logger.error("ConnectionListener threw Exception. ", ex);
}
}
}
/**
* Notifies all ConnectionListeners that the connection is down.
*/
protected void notifyConnectionDown() {
connected = false;
notifyListeners(GerritConnectionEvent.GERRIT_CONNECTION_DOWN);
}
/**
* Notifies all ConnectionListeners that the connection is established.
*/
protected void notifyConnectionEstablished() {
connected = true;
notifyListeners(GerritConnectionEvent.GERRIT_CONNECTION_ESTABLISHED);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy