@@ -22,14 +22,13 @@ package org.sonar.process; | |||
import java.io.IOException; | |||
import java.net.Inet4Address; | |||
import java.net.InetAddress; | |||
import java.net.InetSocketAddress; | |||
import java.net.NetworkInterface; | |||
import java.net.ServerSocket; | |||
import java.net.SocketException; | |||
import java.net.UnknownHostException; | |||
import java.util.HashSet; | |||
import java.util.Set; | |||
import java.util.stream.Collectors; | |||
import org.apache.commons.io.IOUtils; | |||
import org.apache.commons.lang.ArrayUtils; | |||
import static java.lang.String.format; | |||
import static java.util.Collections.list; | |||
@@ -37,14 +36,15 @@ import static org.apache.commons.lang.StringUtils.isBlank; | |||
public final class NetworkUtils { | |||
private static final RandomPortFinder RANDOM_PORT_FINDER = new RandomPortFinder(); | |||
private static final Set<Integer> ALREADY_ALLOCATED = new HashSet<>(); | |||
private static final int MAX_TRIES = 50; | |||
private NetworkUtils() { | |||
// only statics | |||
// prevent instantiation | |||
} | |||
public static int freePort() { | |||
return RANDOM_PORT_FINDER.getNextAvailablePort(); | |||
public static int getNextAvailablePort(InetAddress address) { | |||
return getNextAvailablePort(address, PortAllocator.INSTANCE); | |||
} | |||
/** | |||
@@ -64,18 +64,15 @@ public final class NetworkUtils { | |||
try { | |||
ips = list(NetworkInterface.getNetworkInterfaces()).stream() | |||
.flatMap(netif -> | |||
list(netif.getInetAddresses()).stream() | |||
.filter(inetAddress -> | |||
// Removing IPv6 for the time being | |||
inetAddress instanceof Inet4Address && | |||
// Removing loopback addresses, useless for identifying a server | |||
!inetAddress.isLoopbackAddress() && | |||
// Removing interfaces without IPs | |||
!isBlank(inetAddress.getHostAddress()) | |||
) | |||
.map(InetAddress::getHostAddress) | |||
) | |||
.flatMap(netif -> list(netif.getInetAddresses()).stream() | |||
.filter(inetAddress -> | |||
// Removing IPv6 for the time being | |||
inetAddress instanceof Inet4Address && | |||
// Removing loopback addresses, useless for identifying a server | |||
!inetAddress.isLoopbackAddress() && | |||
// Removing interfaces without IPs | |||
!isBlank(inetAddress.getHostAddress())) | |||
.map(InetAddress::getHostAddress)) | |||
.filter(p -> !isBlank(p)) | |||
.collect(Collectors.joining(",")); | |||
} catch (SocketException e) { | |||
@@ -85,41 +82,35 @@ public final class NetworkUtils { | |||
return format("%s (%s)", hostname, ips); | |||
} | |||
static class RandomPortFinder { | |||
private static final int MAX_TRY = 10; | |||
// Firefox blocks some reserved ports : http://www-archive.mozilla.org/projects/netlib/PortBanning.html | |||
private static final int[] BLOCKED_PORTS = {2049, 4045, 6000}; | |||
public int getNextAvailablePort() { | |||
for (int i = 0; i < MAX_TRY; i++) { | |||
try { | |||
int port = getRandomUnusedPort(); | |||
if (isValidPort(port)) { | |||
return port; | |||
} | |||
} catch (Exception e) { | |||
throw new IllegalStateException("Can't find an open network port", e); | |||
} | |||
/** | |||
* Warning - the allocated ports are kept in memory and are never clean-up. Besides the memory consumption, | |||
* that means that ports already allocated are never freed. As a consequence | |||
* no more than ~64512 calls to this method are allowed. | |||
*/ | |||
static int getNextAvailablePort(InetAddress address, PortAllocator portAllocator) { | |||
for (int i = 0; i < MAX_TRIES; i++) { | |||
int port = portAllocator.getAvailable(address); | |||
if (isValidPort(port)) { | |||
ALREADY_ALLOCATED.add(port); | |||
return port; | |||
} | |||
throw new IllegalStateException("Can't find an open network port"); | |||
} | |||
throw new IllegalStateException("Fail to find an available port on " + address); | |||
} | |||
public int getRandomUnusedPort() throws IOException { | |||
ServerSocket socket = null; | |||
try { | |||
socket = new ServerSocket(); | |||
socket.bind(new InetSocketAddress("localhost", 0)); | |||
private static boolean isValidPort(int port) { | |||
return port > 1023 && !ALREADY_ALLOCATED.contains(port); | |||
} | |||
static class PortAllocator { | |||
private static final PortAllocator INSTANCE = new PortAllocator(); | |||
int getAvailable(InetAddress address) { | |||
try (ServerSocket socket = new ServerSocket(0, 50, address)) { | |||
return socket.getLocalPort(); | |||
} catch (IOException e) { | |||
throw new IllegalStateException("Can not find a free network port", e); | |||
} finally { | |||
IOUtils.closeQuietly(socket); | |||
throw new IllegalStateException("Fail to find an available port on " + address, e); | |||
} | |||
} | |||
public static boolean isValidPort(int port) { | |||
return port > 1023 && !ArrayUtils.contains(BLOCKED_PORTS, port); | |||
} | |||
} | |||
} |
@@ -19,7 +19,8 @@ | |||
*/ | |||
package org.sonar.process; | |||
import java.util.HashMap; | |||
import java.net.InetAddress; | |||
import java.net.UnknownHostException; | |||
import java.util.Map; | |||
import java.util.Properties; | |||
@@ -94,23 +95,25 @@ public class ProcessProperties { | |||
props.setDefault(entry.getKey().toString(), entry.getValue().toString()); | |||
} | |||
// init ports | |||
for (Map.Entry<String, Integer> entry : defaultPorts().entrySet()) { | |||
String key = entry.getKey(); | |||
int port = props.valueAsInt(key, -1); | |||
if (port == -1) { | |||
// default port | |||
props.set(key, String.valueOf((int) entry.getValue())); | |||
} else if (port == 0) { | |||
// pick one available port | |||
props.set(key, String.valueOf(NetworkUtils.freePort())); | |||
fixPortIfZero(props, SEARCH_HOST, SEARCH_PORT); | |||
} | |||
private static void fixPortIfZero(Props props, String addressPropertyKey, String portPropertyKey) { | |||
String port = props.value(portPropertyKey); | |||
if ("0".equals(port)) { | |||
String address = props.nonNullValue(addressPropertyKey); | |||
try { | |||
props.set(portPropertyKey, String.valueOf(NetworkUtils.getNextAvailablePort(InetAddress.getByName(address)))); | |||
} catch (UnknownHostException e) { | |||
throw new IllegalStateException("Cannot resolve address [" + address + "] set by property [" + addressPropertyKey + "]", e); | |||
} | |||
} | |||
} | |||
public static Properties defaults() { | |||
Properties defaults = new Properties(); | |||
defaults.put(SEARCH_HOST, "127.0.0.1"); | |||
defaults.put(SEARCH_HOST, InetAddress.getLoopbackAddress().getHostAddress()); | |||
defaults.put(SEARCH_PORT, "9001"); | |||
defaults.put(SEARCH_JAVA_OPTS, "-Xmx1G -Xms256m -Xss256k -Djna.nosys=true " + | |||
"-XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly " + | |||
"-XX:+HeapDumpOnOutOfMemoryError"); | |||
@@ -144,10 +147,4 @@ public class ProcessProperties { | |||
return defaults; | |||
} | |||
private static Map<String, Integer> defaultPorts() { | |||
Map<String, Integer> defaults = new HashMap<>(); | |||
defaults.put(SEARCH_PORT, 9001); | |||
return defaults; | |||
} | |||
} |
@@ -19,61 +19,64 @@ | |||
*/ | |||
package org.sonar.process; | |||
import java.io.IOException; | |||
import java.net.InetAddress; | |||
import java.util.HashSet; | |||
import java.util.Set; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonar.process.NetworkUtils.RandomPortFinder; | |||
import org.junit.rules.ExpectedException; | |||
import static java.net.InetAddress.getLoopbackAddress; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.mockito.Mockito.doReturn; | |||
import static org.mockito.Mockito.doThrow; | |||
import static org.mockito.Mockito.spy; | |||
import static org.mockito.Matchers.any; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
import static org.sonar.process.NetworkUtils.getNextAvailablePort; | |||
public class NetworkUtilsTest { | |||
@Rule | |||
public ExpectedException expectedException = ExpectedException.none(); | |||
@Test | |||
public void shouldGetAvailablePortWithoutLockingHost() { | |||
for (int i = 0; i < 1000; i++) { | |||
/* | |||
* The Well Known Ports are those from 0 through 1023. | |||
* DCCP Well Known ports SHOULD NOT be used without IANA registration. | |||
*/ | |||
assertThat(NetworkUtils.freePort()).isGreaterThan(1023); | |||
public void getNextAvailablePort_never_returns_the_same_port_in_current_jvm() { | |||
Set<Integer> ports = new HashSet<>(); | |||
for (int i = 0; i < 100; i++) { | |||
int port = getNextAvailablePort(getLoopbackAddress()); | |||
assertThat(port).isGreaterThan(1_023); | |||
ports.add(port); | |||
} | |||
assertThat(ports).hasSize(100); | |||
} | |||
@Test | |||
public void shouldGetRandomPort() { | |||
assertThat(NetworkUtils.freePort()).isNotEqualTo(NetworkUtils.freePort()); | |||
} | |||
public void getNextAvailablePort_retries_to_get_available_port_when_port_has_already_been_allocated() { | |||
NetworkUtils.PortAllocator portAllocator = mock(NetworkUtils.PortAllocator.class); | |||
when(portAllocator.getAvailable(any(InetAddress.class))).thenReturn(9_000, 9_000, 9_000, 9_100); | |||
@Test | |||
public void shouldNotBeValidPorts() { | |||
assertThat(RandomPortFinder.isValidPort(0)).isFalse();// <=1023 | |||
assertThat(RandomPortFinder.isValidPort(50)).isFalse();// <=1023 | |||
assertThat(RandomPortFinder.isValidPort(1023)).isFalse();// <=1023 | |||
assertThat(RandomPortFinder.isValidPort(2049)).isFalse();// NFS | |||
assertThat(RandomPortFinder.isValidPort(4045)).isFalse();// lockd | |||
InetAddress address = getLoopbackAddress(); | |||
assertThat(getNextAvailablePort(address, portAllocator)).isEqualTo(9_000); | |||
assertThat(getNextAvailablePort(address, portAllocator)).isEqualTo(9_100); | |||
} | |||
@Test | |||
public void shouldBeValidPorts() { | |||
assertThat(RandomPortFinder.isValidPort(1059)).isTrue(); | |||
} | |||
public void getNextAvailablePort_does_not_return_special_ports() { | |||
NetworkUtils.PortAllocator portAllocator = mock(NetworkUtils.PortAllocator.class); | |||
when(portAllocator.getAvailable(any(InetAddress.class))).thenReturn(900, 903, 1_059); | |||
@Test(expected = IllegalStateException.class) | |||
public void shouldFailWhenNoValidPortIsAvailable() throws IOException { | |||
RandomPortFinder randomPortFinder = spy(new RandomPortFinder()); | |||
doReturn(0).when(randomPortFinder).getRandomUnusedPort(); | |||
randomPortFinder.getNextAvailablePort(); | |||
// the two first ports are banned because < 1023, so 1_059 is returned | |||
assertThat(getNextAvailablePort(getLoopbackAddress(), portAllocator)).isEqualTo(1_059); | |||
} | |||
@Test(expected = IllegalStateException.class) | |||
public void shouldFailWhenItsNotPossibleToOpenASocket() throws IOException { | |||
RandomPortFinder randomPortFinder = spy(new RandomPortFinder()); | |||
doThrow(new IOException("Not possible")).when(randomPortFinder).getRandomUnusedPort(); | |||
@Test | |||
public void getNextAvailablePort_throws_ISE_if_too_many_attempts() { | |||
NetworkUtils.PortAllocator portAllocator = mock(NetworkUtils.PortAllocator.class); | |||
when(portAllocator.getAvailable(any(InetAddress.class))).thenReturn(900); | |||
expectedException.expect(IllegalStateException.class); | |||
expectedException.expectMessage("Fail to find an available port on "); | |||
randomPortFinder.getNextAvailablePort(); | |||
getNextAvailablePort(getLoopbackAddress(), portAllocator); | |||
} | |||
@Test |
@@ -19,6 +19,7 @@ | |||
*/ | |||
package org.sonar.process; | |||
import java.net.InetAddress; | |||
import java.util.Properties; | |||
import org.junit.Test; | |||
import org.sonar.test.TestUtils; | |||
@@ -28,7 +29,7 @@ import static org.assertj.core.api.Assertions.assertThat; | |||
public class ProcessPropertiesTest { | |||
@Test | |||
public void init_defaults() { | |||
public void completeDefaults_adds_default_values() { | |||
Props props = new Props(new Properties()); | |||
ProcessProperties.completeDefaults(props); | |||
@@ -37,7 +38,7 @@ public class ProcessPropertiesTest { | |||
} | |||
@Test | |||
public void do_not_override_existing_properties() { | |||
public void completeDefaults_does_not_override_existing_properties() { | |||
Properties p = new Properties(); | |||
p.setProperty("sonar.jdbc.username", "angela"); | |||
Props props = new Props(p); | |||
@@ -47,7 +48,20 @@ public class ProcessPropertiesTest { | |||
} | |||
@Test | |||
public void use_random_port_if_zero() { | |||
public void completeDefaults_set_default_elasticsearch_port_and_bind_address() throws Exception{ | |||
Properties p = new Properties(); | |||
Props props = new Props(p); | |||
ProcessProperties.completeDefaults(props); | |||
String address = props.value("sonar.search.host"); | |||
assertThat(address).isNotEmpty(); | |||
assertThat(InetAddress.getByName(address).isLoopbackAddress()).isTrue(); | |||
assertThat(props.valueAsInt("sonar.search.port")).isEqualTo(9001); | |||
} | |||
@Test | |||
public void completeDefaults_sets_the_port_of_elasticsearch_if_value_is_zero() { | |||
Properties p = new Properties(); | |||
p.setProperty("sonar.search.port", "0"); | |||
Props props = new Props(p); |
@@ -54,7 +54,7 @@ public class SearchServerTest { | |||
@Rule | |||
public TemporaryFolder temp = new TemporaryFolder(); | |||
private int port = NetworkUtils.freePort(); | |||
private int port = NetworkUtils.getNextAvailablePort(InetAddress.getLoopbackAddress()); | |||
private Client client; | |||
private SearchServer underTest; | |||
@@ -21,7 +21,7 @@ package org.sonar.server.app; | |||
import java.io.File; | |||
import java.net.ConnectException; | |||
import java.net.Inet4Address; | |||
import java.net.InetAddress; | |||
import java.net.URL; | |||
import java.util.Properties; | |||
import org.apache.commons.io.FileUtils; | |||
@@ -54,17 +54,17 @@ public class EmbeddedTomcatTest { | |||
props.set("sonar.path.logs", temp.newFolder().getAbsolutePath()); | |||
// start server on a random port | |||
int httpPort = NetworkUtils.freePort(); | |||
int ajpPort = NetworkUtils.freePort(); | |||
InetAddress address = InetAddress.getLoopbackAddress(); | |||
int httpPort = NetworkUtils.getNextAvailablePort(address); | |||
props.set("sonar.web.host", address.getHostAddress()); | |||
props.set("sonar.web.port", String.valueOf(httpPort)); | |||
props.set("sonar.ajp.port", String.valueOf(ajpPort)); | |||
EmbeddedTomcat tomcat = new EmbeddedTomcat(props); | |||
assertThat(tomcat.getStatus()).isEqualTo(EmbeddedTomcat.Status.DOWN); | |||
tomcat.start(); | |||
assertThat(tomcat.getStatus()).isEqualTo(EmbeddedTomcat.Status.UP); | |||
// check that http connector accepts requests | |||
URL url = new URL("http://" + Inet4Address.getLocalHost().getHostAddress() + ":" + httpPort); | |||
URL url = new URL("http://" + address.getHostAddress() + ":" + httpPort); | |||
url.openConnection().connect(); | |||
// stop server |
@@ -38,16 +38,16 @@ public class EsServerHolder { | |||
private static EsServerHolder HOLDER = null; | |||
private final String clusterName; | |||
private final InetAddress address; | |||
private final int port; | |||
private final String hostName; | |||
private final File homeDir; | |||
private final SearchServer server; | |||
private EsServerHolder(SearchServer server, String clusterName, int port, String hostName, File homeDir) { | |||
private EsServerHolder(SearchServer server, String clusterName, InetAddress address, int port, File homeDir) { | |||
this.server = server; | |||
this.clusterName = clusterName; | |||
this.address = address; | |||
this.port = port; | |||
this.hostName = hostName; | |||
this.homeDir = homeDir; | |||
} | |||
@@ -59,8 +59,8 @@ public class EsServerHolder { | |||
return port; | |||
} | |||
public String getHostName() { | |||
return hostName; | |||
public InetAddress getAddress() { | |||
return address; | |||
} | |||
public SearchServer getServer() { | |||
@@ -98,18 +98,18 @@ public class EsServerHolder { | |||
homeDir.mkdir(); | |||
String clusterName = "testCluster"; | |||
String hostName = "127.0.0.1"; | |||
int port = NetworkUtils.freePort(); | |||
InetAddress address = InetAddress.getLoopbackAddress(); | |||
int port = NetworkUtils.getNextAvailablePort(address); | |||
Properties properties = new Properties(); | |||
properties.setProperty(ProcessProperties.CLUSTER_NAME, clusterName); | |||
properties.setProperty(ProcessProperties.SEARCH_PORT, String.valueOf(port)); | |||
properties.setProperty(ProcessProperties.SEARCH_HOST, hostName); | |||
properties.setProperty(ProcessProperties.SEARCH_HOST, address.getHostAddress()); | |||
properties.setProperty(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath()); | |||
properties.setProperty(ProcessEntryPoint.PROPERTY_SHARED_PATH, homeDir.getAbsolutePath()); | |||
SearchServer server = new SearchServer(new Props(properties)); | |||
server.start(); | |||
HOLDER = new EsServerHolder(server, clusterName, port, hostName, homeDir); | |||
HOLDER = new EsServerHolder(server, clusterName, address, port, homeDir); | |||
} | |||
HOLDER.reset(); | |||
return HOLDER; |
@@ -113,7 +113,7 @@ public class EmbeddedDatabaseTest { | |||
@Test | |||
public void start_ignores_URL_to_create_database_and_uses_default_username_and_password_when_then_are_not_set() throws IOException { | |||
int port = NetworkUtils.freePort(); | |||
int port = NetworkUtils.getNextAvailablePort(InetAddress.getLoopbackAddress()); | |||
settings | |||
.setProperty(PATH_DATA, temporaryFolder.newFolder().getAbsolutePath()) | |||
.setProperty(PROP_URL, "jdbc url") | |||
@@ -126,7 +126,7 @@ public class EmbeddedDatabaseTest { | |||
@Test | |||
public void start_creates_db_and_adds_tcp_listener() throws IOException { | |||
int port = NetworkUtils.freePort(); | |||
int port = NetworkUtils.getNextAvailablePort(InetAddress.getLoopbackAddress()); | |||
settings | |||
.setProperty(PATH_DATA, temporaryFolder.newFolder().getAbsolutePath()) | |||
.setProperty(PROP_URL, "jdbc url") | |||
@@ -144,7 +144,7 @@ public class EmbeddedDatabaseTest { | |||
@Test | |||
public void start_supports_in_memory_H2_JDBC_URL() throws IOException { | |||
int port = NetworkUtils.freePort(); | |||
int port = NetworkUtils.getNextAvailablePort(InetAddress.getLoopbackAddress()); | |||
settings | |||
.setProperty(PATH_DATA, temporaryFolder.newFolder().getAbsolutePath()) | |||
.setProperty(PROP_URL, "jdbc:h2:mem:sonar") |
@@ -105,7 +105,7 @@ public class ServerTester extends ExternalResource { | |||
esServerHolder = EsServerHolder.get(); | |||
properties.setProperty(ProcessProperties.CLUSTER_NAME, esServerHolder.getClusterName()); | |||
properties.setProperty(ProcessProperties.SEARCH_PORT, String.valueOf(esServerHolder.getPort())); | |||
properties.setProperty(ProcessProperties.SEARCH_HOST, String.valueOf(esServerHolder.getHostName())); | |||
properties.setProperty(ProcessProperties.SEARCH_HOST, esServerHolder.getAddress().getHostAddress()); | |||
properties.setProperty(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath()); | |||
properties.setProperty(ProcessProperties.PATH_DATA, new File(homeDir, "data").getAbsolutePath()); | |||
File temporaryFolderIn = createTemporaryFolderIn(); |
@@ -213,9 +213,9 @@ | |||
#sonar.search.port=9001 | |||
# Elasticsearch host. The search server will bind this address and the search client will connect to it. | |||
# Default is 127.0.0.1. | |||
# Default is loopback address. | |||
# As a security precaution, should NOT be set to a publicly available address. | |||
#sonar.search.host=127.0.0.1 | |||
#sonar.search.host= | |||
#-------------------------------------------------------------------------------------------------- |