/*******************************************************************************
* Copyright (c) 2012-2015 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.api.core.util;
import org.eclipse.che.commons.lang.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.ServerSocket;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* Helps to find free ports.
* Usage:
* <pre>
* CustomPortService portService = ...
* int free = portService.acquire();
* if (free < 0) {
* // No free ports.
* } else {
* try {
* // Do something.
* } finally {
* portService.release(free);
* }
* }
* </pre>
* <p/>
* Note: It is important to release port when it is not needed any more, otherwise it will be not possible to reuse ports.
*
* @author andrew00x
* @see #MIN_PORT
* @see #MAX_PORT
*/
@Singleton
public class CustomPortService {
/** Name of configuration parameter that sets min port number. */
public static final String MIN_PORT = "sys.resources.min_port";
/** Name of configuration parameter that sets max port number. */
public static final String MAX_PORT = "sys.resources.max_port";
private static final Logger LOG = LoggerFactory.getLogger(CustomPortService.class);
private final Random rnd;
private final ConcurrentMap<Integer, Boolean> portsInUse;
private final Pair<Integer, Integer> range;
@Inject
public CustomPortService(@Named(MIN_PORT) int minPort, @Named(MAX_PORT) int maxPort) {
this(Pair.of(minPort, maxPort));
}
public CustomPortService(Pair<Integer, Integer> range) {
if (range.first < 0 || range.second > 65535) {
throw new IllegalArgumentException(String.format("Invalid port range: [%d:%d]", range.first, range.second));
}
this.range = range;
rnd = new Random();
portsInUse = new ConcurrentHashMap<>();
}
/**
* This service stores allocated ports in internal storage to avoid checking ports that already in use. After calling this method
* storage is cleared. For next port allocation this service will iterates through range of configured ports until finds free port.
* It may be expensive since checking port means trying to open {@link ServerSocket} and {@link DatagramSocket} on each port in the
* range.
*
* @see #MIN_PORT
* @see #MAX_PORT
*/
public void reset() {
portsInUse.clear();
}
/**
* Returns range of ports that service uses for lookup free port. Modifications to the returned {@code Pair} will not affect the
* internal {@code Pair}.
*/
public Pair<Integer, Integer> getRange() {
return Pair.of(range.first, range.second);
}
/**
* Get free port from the whole range of possible ports.
*
* @return free port or {@code -1} if there is no free port
*/
public int acquire() {
return doAcquire(range.first, range.second);
}
/**
* Get free port from the specified range. Specified range may not be wider than configured range otherwise IllegalArgumentException is
* thrown. Configured range may be checked with method {@link #getRange()}.
*
* @return free port or {@code -1} if there is no free port
* @throws IllegalArgumentException
* if {@code min > range.first} or if {@code min > range.second}
* @see #getRange()
* @see #MIN_PORT
* @see #MAX_PORT
*/
public int acquire(int min, int max) {
if (min < range.first) {
throw new IllegalArgumentException(String.format("Min port value may not be less than %d", range.first));
}
if (max > range.second) {
throw new IllegalArgumentException(String.format("Max port value may not be greater than %d", range.second));
}
return doAcquire(min, max);
}
public void release(int port) {
if (port != -1) {
portsInUse.remove(port);
}
LOG.debug("Release port {}", port);
}
private int doAcquire(int min, int max) {
// Use this for getting ports for web applications but unfortunately get issue with browser cache.
// If different applications reuse the same port sometimes user can see previous application.
// Make number of port in 'more random' way instead of checking from min to max until find free port.
final int m = min + rnd.nextInt((max - min) + 1);
final boolean ev = (m % 2) == 0;
int port;
if (ev) {
port = lookupForward(m, max);
if (port < 0) {
port = lookupBackward(m, min);
}
} else {
port = lookupBackward(m, min);
if (port < 0) {
port = lookupForward(m, max);
}
}
return port;
}
private int lookupForward(int min, int max) {
for (int port = min; port <= max; port++) {
if (checkPort(port)) {
return port;
}
}
return -1;
}
private int lookupBackward(int max, int min) {
for (int port = max; port >= min; port--) {
if (checkPort(port)) {
return port;
}
}
return -1;
}
private boolean checkPort(int port) {
if (portsInUse.putIfAbsent(port, Boolean.TRUE) == null) {
ServerSocket ss = null;
DatagramSocket ds = null;
try {
ss = new ServerSocket(port);
ds = new DatagramSocket(port);
LOG.debug("Acquire port {}", port);
return true;
} catch (IOException ignored) {
portsInUse.remove(port);
} finally {
if (ds != null) {
ds.close();
}
if (ss != null) {
try {
ss.close();
} catch (IOException ignored) {
}
}
}
}
return false;
}
}