+ * The Restarter should be {@link #initialize(String[]) initialized} early to ensure that + * classes are loaded multiple times. Mostly the {@link RestartApplicationListener} can be + * relied upon to perform initialization, however, you may need to call + * {@link #initialize(String[])} directly if your SpringApplication arguments are not + * identical to your main method arguments. + *
+ * By default, applications running in an IDE (i.e. those not packaged as "fat jars") will
+ * automatically detect URLs that can change. It's also possible to manually configure
+ * URLs or class file updates for remote restart scenarios.
+ *
+ * @author Phillip Webb
+ * @since 1.3.0
+ * @see RestartApplicationListener
+ * @see #initialize(String[])
+ * @see #getInstance()
+ * @see #restart()
+ */
+public class Restarter {
+
+ private static Restarter instance;
+
+ private Log logger = new DeferredLog();
+
+ private final boolean forceReferenceCleanup;
+
+ private URL[] initialUrls;
+
+ private final String mainClassName;
+
+ private final ClassLoader applicationClassLoader;
+
+ private final String[] args;
+
+ private final UncaughtExceptionHandler exceptionHandler;
+
+ private final Set
+ * Requests should be made using HTTP GET or POST (depending if there is a payload), with
+ * any payload contained in the body. The following response codes can be returned from
+ * the server:
+ *
+ *
+ * Requests and responses that contain payloads include a {@code x-seq} header that
+ * contains a running sequence number (used to ensure data is applied in the correct
+ * order). The first request containing a payload should have a {@code x-seq} value of
+ * {@code 1}.
+ *
+ * @author Phillip Webb
+ * @since 1.3.0
+ * @see org.springframework.boot.developertools.tunnel.client.HttpTunnelConnection
+ */
+public class HttpTunnelServer {
+
+ private static final int SECONDS = 1000;
+
+ private static final int DEFAULT_LONG_POLL_TIMEOUT = 10 * SECONDS;
+
+ private static final long DEFAULT_DISCONNECT_TIMEOUT = 30 * SECONDS;
+
+ private static final MediaType DISCONNECT_MEDIA_TYPE = new MediaType("application",
+ "x-disconnect");
+
+ private static final Log logger = LogFactory.getLog(HttpTunnelServer.class);
+
+ private final TargetServerConnection serverConnection;
+
+ private int longPollTimeout = DEFAULT_LONG_POLL_TIMEOUT;
+
+ private long disconnectTimeout = DEFAULT_DISCONNECT_TIMEOUT;
+
+ private volatile ServerThread serverThread;
+
+ /**
+ * Creates a new {@link HttpTunnelServer} instance.
+ * @param serverConnection the connection to the target server
+ */
+ public HttpTunnelServer(TargetServerConnection serverConnection) {
+ Assert.notNull(serverConnection, "ServerConnection must not be null");
+ this.serverConnection = serverConnection;
+ }
+
+ /**
+ * Handle an incoming HTTP connection.
+ * @param request the HTTP request
+ * @param response the HTTP response
+ * @throws IOException
+ */
+ public void handle(ServerHttpRequest request, ServerHttpResponse response)
+ throws IOException {
+ handle(new HttpConnection(request, response));
+ }
+
+ /**
+ * Handle an incoming HTTP connection.
+ * @param httpConnection the HTTP connection
+ * @throws IOException
+ */
+ protected void handle(HttpConnection httpConnection) throws IOException {
+ try {
+ getServerThread().handleIncomingHttp(httpConnection);
+ httpConnection.waitForResponse();
+ }
+ catch (ConnectException ex) {
+ httpConnection.respond(HttpStatus.GONE);
+ }
+ }
+
+ /**
+ * Returns the active server thread, creating and starting it if necessary.
+ * @return the {@code ServerThread} (never {@code null})
+ * @throws IOException
+ */
+ protected ServerThread getServerThread() throws IOException {
+ synchronized (this) {
+ if (this.serverThread == null) {
+ ByteChannel channel = this.serverConnection.open(this.longPollTimeout);
+ this.serverThread = new ServerThread(channel);
+ this.serverThread.start();
+ }
+ return this.serverThread;
+ }
+ }
+
+ /**
+ * Called when the server thread exits.
+ */
+ void clearServerThread() {
+ synchronized (this) {
+ this.serverThread = null;
+ }
+ }
+
+ /**
+ * Set the long poll timeout for the server.
+ * @param longPollTimeout the long poll timeout in milliseconds
+ */
+ public void setLongPollTimeout(int longPollTimeout) {
+ Assert.isTrue(longPollTimeout > 0, "LongPollTimeout must be a positive value");
+ this.longPollTimeout = longPollTimeout;
+ }
+
+ /**
+ * Set the maximum amount of time to wait for a client before closing the connection.
+ * @param disconnectTimeout the disconnect timeout in milliseconds
+ */
+ public void setDisconnectTimeout(long disconnectTimeout) {
+ Assert.isTrue(disconnectTimeout > 0, "DisconnectTimeout must be a positive value");
+ this.disconnectTimeout = disconnectTimeout;
+ }
+
+ /**
+ * The main server thread used to transfer tunnel traffic.
+ */
+ protected class ServerThread extends Thread {
+
+ private final ByteChannel targetServer;
+
+ private final Deque
+ * [ CLIENT ] [ SERVER ]
+ * | (a) Initial empty request |
+ * |------------------------------}|
+ * | (b) Data I |
+ * --}|------------------------------}|---}
+ * | Response I (a) |
+ * {--|<------------------------------|{---
+ * | |
+ * | (c) Data II |
+ * --}|------------------------------}|---}
+ * | Response II (b) |
+ * {--|{------------------------------|{---
+ * . .
+ * . .
+ *
+ *
+ * Each incoming request is held open to be used to carry the next available response. The
+ * server will hold at most two connections open at any given time.
+ *
+ *
+ *
+ *
+ * Status
+ * Meaning
+ *
+ *
+ * 200 (OK)
+ * Data payload response.
+ *
+ *
+ * 204 (No Content)
+ * The long poll has timed out and the client should start a new request.
+ *
+ *
+ * 429 (Too many requests)
+ * There are already enough connections open, this one can be dropped.
+ *
+ *
+ * 410 (Gone)
+ * The target server has disconnected.
+ *