diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/Resource.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/Resource.java index 04ce935c0..40839dcd3 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/Resource.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/Resource.java @@ -4,7 +4,7 @@ package dan200.computercraft.core.apis.http; -import dan200.computercraft.core.util.GlobalCleaner; +import com.google.errorprone.annotations.OverridingMethodsMustInvokeSuper; import dan200.computercraft.core.util.IoUtil; import io.netty.channel.ChannelFuture; import org.jspecify.annotations.Nullable; @@ -64,21 +64,13 @@ public abstract class Resource> implements Closeable { * Note, this may be called multiple times, and so should be thread-safe and * avoid any major side effects. */ + @OverridingMethodsMustInvokeSuper protected void dispose() { @SuppressWarnings("unchecked") var thisT = (T) this; limiter.release(thisT); } - /** - * Register an object which, when garbage collected, will delete this object. - * - * @param object The object to reference to - */ - protected final void registerCleanable(Object object) { - GlobalCleaner.register(object, this::dispose); - } - @Override public final void close() { tryClose(); diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java index 3020403f3..26e0c6118 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java @@ -12,6 +12,7 @@ import dan200.computercraft.core.apis.http.*; import dan200.computercraft.core.apis.http.options.Options; import dan200.computercraft.core.metrics.Metrics; import dan200.computercraft.core.util.AtomicHelpers; +import dan200.computercraft.core.util.GlobalCleaner; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; @@ -30,12 +31,14 @@ import java.nio.ByteBuffer; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Future; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** * Provides functionality to verify and connect to a remote websocket. */ -public class Websocket extends Resource implements WebsocketClient { +public final class Websocket extends Resource implements WebsocketClient { private static final Logger LOG = LoggerFactory.getLogger(Websocket.class); /** @@ -44,8 +47,14 @@ public class Websocket extends Resource implements WebsocketClient { */ public static final int MAX_MESSAGE_SIZE = 1 << 30; + /** + * The timeout after sending a {@link CloseWebSocketFrame} after which the channel should be force-closed. + */ + private static final long CLOSE_TIMEOUT = 5; + private @Nullable Future executorFuture; private @Nullable ChannelFuture channelFuture; + private @Nullable ScheduledFuture closeFuture; private final IAPIEnvironment environment; private final NetworkUtils network; @@ -56,6 +65,7 @@ public class Websocket extends Resource implements WebsocketClient { private final AtomicInteger inFlight = new AtomicInteger(0); private final GenericFutureListener> onSend = f -> inFlight.decrementAndGet(); + private boolean isClosing = false; public Websocket(ResourceGroup limiter, IAPIEnvironment environment, NetworkUtils network, String address, URI uri, HttpHeaders headers, int timeout) { super(limiter); @@ -102,15 +112,15 @@ public class Websocket extends Resource implements WebsocketClient { ); }) .addListener(c -> { - if (!c.isSuccess()) failure(NetworkUtils.toFriendlyError(c.cause())); + if (!c.isSuccess()) handshakeFailure(NetworkUtils.toFriendlyError(c.cause())); }); // Do an additional check for cancellation checkClosed(); } catch (HTTPRequestException e) { - failure(NetworkUtils.toFriendlyError(e)); + handshakeFailure(NetworkUtils.toFriendlyError(e)); } catch (Exception e) { - failure(NetworkUtils.toFriendlyError(e)); + handshakeFailure(NetworkUtils.toFriendlyError(e)); LOG.error(Logging.HTTP_ERROR, "Error in websocket", e); } } @@ -125,17 +135,17 @@ public class Websocket extends Resource implements WebsocketClient { var handle = new WebsocketHandle(environment, address, this, headers, options); + GlobalCleaner.register(handle, () -> close(1001, "")); environment().queueEvent(SUCCESS_EVENT, address, handle); - registerCleanable(handle); checkClosed(); } - void failure(String message) { + void handshakeFailure(String message) { if (tryClose()) environment.queueEvent(FAILURE_EVENT, address, message); } - void close(int status, String reason) { + void serverClose(int status, String reason) { if (tryClose()) { environment.queueEvent(CLOSE_EVENT, address, Strings.isNullOrEmpty(reason) ? null : reason, @@ -149,6 +159,7 @@ public class Websocket extends Resource implements WebsocketClient { executorFuture = closeFuture(executorFuture); channelFuture = closeChannel(channelFuture); + closeFuture = closeFuture(closeFuture); } IAPIEnvironment environment() { @@ -177,7 +188,7 @@ public class Websocket extends Resource implements WebsocketClient { private void sendMessage(WebSocketFrame frame, long size) throws LuaException { var channel = channel(); - if (channel == null) return; + if (channel == null || isClosing) throw new LuaException(CLOSED_ERROR); // Grow the number of in-flight requests, aborting if we've hit the limit. This is then decremented when the // promise finishes. @@ -188,4 +199,22 @@ public class Websocket extends Resource implements WebsocketClient { environment.observe(Metrics.WEBSOCKET_OUTGOING, size); channel.writeAndFlush(frame).addListener(onSend); } + + @Override + public void close(int status, String reason) { + // Close is idempotent, so should not fail if the socket is already closed. + var channel = channel(); + if (channel == null || isClosing) return; + + isClosing = true; + + var packet = new CloseWebSocketFrame(status, reason); + environment.observe(Metrics.WEBSOCKET_OUTGOING, packet.content().readableBytes()); + channel.writeAndFlush(packet); + + // Schedule a callback to force-close the websocket if the server does not respond. This reimplements + // WebSocketProtocolHandler.applyCloseSentTimeout — unfortunately our CustomWebSocketHandshaker means we cannot + // specify a custom forceCloseTimeoutMillis. + closeFuture = channel.eventLoop().schedule(() -> serverClose(1006, ""), CLOSE_TIMEOUT, TimeUnit.SECONDS); + } } diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketClient.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketClient.java index 55c2a3e3a..7aa94a96d 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketClient.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketClient.java @@ -6,8 +6,8 @@ package dan200.computercraft.core.apis.http.websocket; import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.core.apis.http.HTTPRequestException; +import io.netty.handler.codec.http.websocketx.WebSocketCloseStatus; -import java.io.Closeable; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; @@ -17,12 +17,17 @@ import java.nio.ByteBuffer; *

* {@link WebsocketHandle} wraps this into a Lua-compatible interface. */ -public interface WebsocketClient extends Closeable { +public interface WebsocketClient { String SUCCESS_EVENT = "websocket_success"; String FAILURE_EVENT = "websocket_failure"; String CLOSE_EVENT = "websocket_closed"; String MESSAGE_EVENT = "websocket_message"; + /** + * Error message to throw when attempting to use a {@linkplain #isClosed() closed} websocket. + */ + String CLOSED_ERROR = "attempt to use a closed websocket"; + /** * Determine whether this websocket is closed. * @@ -32,9 +37,13 @@ public interface WebsocketClient extends Closeable { /** * Close this websocket. + * + * @param status The status code for this closure. Must be a {@linkplain WebSocketCloseStatus#isValidStatusCode(int) + * valid status code}. + * @param reason The reason for this closure. + * @see WebSocketCloseStatus */ - @Override - void close(); + void close(int status, String reason); /** * Send a text websocket frame. diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java index 7754f1cf7..7b03da272 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java @@ -31,7 +31,7 @@ import static dan200.computercraft.core.apis.http.websocket.WebsocketClient.MESS * @cc.module http.Websocket * @see dan200.computercraft.core.apis.HTTPAPI#websocket On how to open a websocket. */ -public class WebsocketHandle { +public final class WebsocketHandle { private static final ThreadLocal DECODER = ThreadLocal.withInitial(() -> StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPLACE)); private final IAPIEnvironment environment; @@ -64,8 +64,9 @@ public class WebsocketHandle { * @cc.changed 1.117.0 Added return value indicating why receiving the message failed. */ @LuaFunction - public final MethodResult receive(Optional timeout) throws LuaException { - checkOpen(); + public MethodResult receive(Optional timeout) throws LuaException { + if (websocket.isClosed()) throw new LuaException(WebsocketClient.CLOSED_ERROR); + var timeoutId = timeout.isPresent() ? environment.startTimer(Math.round(checkFinite(0, timeout.get()) / 0.05)) : -1; @@ -83,8 +84,8 @@ public class WebsocketHandle { * @cc.changed 1.81.0 Added argument for binary mode. */ @LuaFunction - public final void send(Coerced message, Optional binary) throws LuaException { - checkOpen(); + public void send(Coerced message, Optional binary) throws LuaException { + if (websocket.isClosed()) throw new LuaException(WebsocketClient.CLOSED_ERROR); var text = message.value(); if (options.websocketMessage() != 0 && text.remaining() > options.websocketMessage()) { @@ -108,8 +109,8 @@ public class WebsocketHandle { * along it. */ @LuaFunction - public final void close() { - websocket.close(); + public void close() { + websocket.close(1000, ""); } /** @@ -132,14 +133,10 @@ public class WebsocketHandle { * @since 1.117.0 */ @LuaFunction - public final Map getResponseHeaders() { + public Map getResponseHeaders() { return responseHeaders; } - private void checkOpen() throws LuaException { - if (websocket.isClosed()) throw new LuaException("attempt to use a closed file"); - } - private final class ReceiveCallback implements ILuaCallback { final MethodResult pull = MethodResult.pullEvent(null, this); private final IAPIEnvironment environment; diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java index 924ffebfa..1b07a90cb 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java @@ -41,7 +41,7 @@ class WebsocketHandler extends SimpleChannelInboundHandler { websocket.success(headers, options); handshakeComplete = true; } else if (evt == WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_TIMEOUT) { - websocket.failure("Timed out"); + websocket.handshakeFailure("Timed out"); } } @@ -65,7 +65,7 @@ class WebsocketHandler extends SimpleChannelInboundHandler { websocket.environment().observe(Metrics.WEBSOCKET_INCOMING, data.length); websocket.environment().queueEvent(MESSAGE_EVENT, websocket.address(), data, true); } else if (frame instanceof CloseWebSocketFrame closeFrame) { - websocket.close(closeFrame.statusCode(), closeFrame.reasonText()); + websocket.serverClose(closeFrame.statusCode(), closeFrame.reasonText()); } } @@ -77,9 +77,9 @@ class WebsocketHandler extends SimpleChannelInboundHandler { private void fail(String message) { if (handshakeComplete) { - websocket.close(-1, message); + websocket.serverClose(-1, message); } else { - websocket.failure(message); + websocket.handshakeFailure(message); } } } diff --git a/projects/core/src/test/kotlin/dan200/computercraft/core/apis/http/HttpServer.kt b/projects/core/src/test/kotlin/dan200/computercraft/core/apis/http/HttpServer.kt index ecb7df618..af8cb8e76 100644 --- a/projects/core/src/test/kotlin/dan200/computercraft/core/apis/http/HttpServer.kt +++ b/projects/core/src/test/kotlin/dan200/computercraft/core/apis/http/HttpServer.kt @@ -12,15 +12,26 @@ import io.netty.channel.nio.NioEventLoopGroup import io.netty.channel.socket.SocketChannel import io.netty.channel.socket.nio.NioServerSocketChannel import io.netty.handler.codec.http.* -import io.netty.handler.codec.http.websocketx.TextWebSocketFrame -import io.netty.handler.codec.http.websocketx.WebSocketFrame -import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler +import io.netty.handler.codec.http.websocketx.* import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler.HandshakeComplete import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler import java.net.InetSocketAddress import java.nio.charset.StandardCharsets -class HttpServer(val port: Int, private val workerGroup: EventLoopGroup, private val activeConnections: Set) { +class HttpRequest(val headers: HttpHeaders) + +private class HttpState(var lastRequest: HttpRequest? = null) + +class HttpServer private constructor( + val port: Int, + private val workerGroup: EventLoopGroup, + private val activeConnections: Set, + private val state: HttpState, +) { + /** The last HTTP request made to this server */ + val lastRequest: HttpRequest + get() = state.lastRequest ?: throw NullPointerException("No request has occurred yet") + /** Stop the server from running */ fun stop() { workerGroup.shutdownGracefully() @@ -33,10 +44,17 @@ class HttpServer(val port: Int, private val workerGroup: EventLoopGroup, private companion object { /** Runs a small HTTP server to run alongside [TestHttpApi] */ - fun runServer(run: (server: HttpServer) -> Unit) { + fun runServer(closeWebsocket: Boolean = true, run: (server: HttpServer) -> Unit) { val workerGroup: EventLoopGroup = NioEventLoopGroup(2) - val activeConnections = mutableSetOf() try { + val activeConnections = mutableSetOf() + val state = HttpState() + + val websocketConfig = WebSocketServerProtocolConfig.newBuilder() + .websocketPath("/ws") + .handleCloseFrames(closeWebsocket) + .allowExtensions(true) + .build() val ch = ServerBootstrap() .group(workerGroup) .channel(NioServerSocketChannel::class.java) @@ -47,16 +65,16 @@ class HttpServer(val port: Int, private val workerGroup: EventLoopGroup, private p.addLast(HttpServerCodec()) p.addLast(HttpContentCompressor()) p.addLast(HttpObjectAggregator(8192)) - p.addLast(HttpServerHandler()) + p.addLast(HttpServerHandler(state)) p.addLast(WebSocketServerCompressionHandler()) - p.addLast(WebSocketServerProtocolHandler("/ws", null, true)) + p.addLast(WebSocketServerProtocolHandler(websocketConfig)) p.addLast(WebSocketFrameHandler(activeConnections)) } }, ).bind(0).sync().channel() val port = (ch.localAddress() as InetSocketAddress).port try { - run(HttpServer(port, workerGroup, activeConnections)) + run(HttpServer(port, workerGroup, activeConnections, state)) } finally { ch.close().sync() } @@ -70,7 +88,7 @@ class HttpServer(val port: Int, private val workerGroup: EventLoopGroup, private /** * A HTTP handler which hosts `/` (a simple static page) and `/ws` (see [WebSocketFrameHandler]) */ -private class HttpServerHandler : SimpleChannelInboundHandler() { +private class HttpServerHandler(private val state: HttpState) : SimpleChannelInboundHandler() { companion object { private val CONTENT = "Hello, world!".toByteArray(StandardCharsets.UTF_8) } @@ -80,6 +98,7 @@ private class HttpServerHandler : SimpleChannelInboundHandler() } override fun channelRead0(ctx: ChannelHandlerContext, request: FullHttpRequest) { + state.lastRequest = HttpRequest(request.headers()) when (request.uri()) { "/", "/index.html" -> handleIndex(ctx, request) "/ws" -> handleWebsocket(ctx, request) @@ -124,12 +143,16 @@ private class HttpServerHandler : SimpleChannelInboundHandler() */ private class WebSocketFrameHandler(private val activeConnections: MutableSet) : SimpleChannelInboundHandler() { override fun channelRead0(ctx: ChannelHandlerContext, frame: WebSocketFrame) { - if (frame is TextWebSocketFrame) { - // Send the uppercase string back. - val request = frame.text() - ctx.channel().writeAndFlush(TextWebSocketFrame(request.uppercase())) - } else { - throw UnsupportedOperationException("unsupported frame type: ${frame.javaClass.name}") + when (frame) { + is TextWebSocketFrame -> { + // Send the uppercase string back. + val request = frame.text() + ctx.channel().writeAndFlush(TextWebSocketFrame(request.uppercase())) + } + + is CloseWebSocketFrame -> {} + + else -> throw UnsupportedOperationException("unsupported frame type: ${frame.javaClass.name}") } } diff --git a/projects/core/src/test/kotlin/dan200/computercraft/core/apis/http/TestHttpApi.kt b/projects/core/src/test/kotlin/dan200/computercraft/core/apis/http/TestHttpApi.kt index dc2264dd7..a75d638bc 100644 --- a/projects/core/src/test/kotlin/dan200/computercraft/core/apis/http/TestHttpApi.kt +++ b/projects/core/src/test/kotlin/dan200/computercraft/core/apis/http/TestHttpApi.kt @@ -18,14 +18,16 @@ import dan200.computercraft.core.apis.http.request.HttpResponseHandle import dan200.computercraft.core.apis.http.websocket.WebsocketHandle import dan200.computercraft.test.core.computer.LuaTaskRunner import io.netty.buffer.Unpooled +import io.netty.handler.codec.http.HttpHeaderNames import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.* +import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import java.lang.ref.Reference import java.util.* -import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds class TestHttpApi { /** Create a [HTTPAPI] which is only permitted to access the current server. */ @@ -73,8 +75,10 @@ class TestHttpApi { websocket.close() - val closeEvent = pullEventOrTimeout(500.milliseconds, "websocket_closed") - assertThat("No event was queued", closeEvent, equalTo(null)) + val closeEvent = pullEvent() + assertThat("Received a close event", closeEvent, array(equalTo("websocket_closed"), equalTo(url), nullValue(), equalTo(1000))) + + assertFalse(server.lastRequest.headers.contains(HttpHeaderNames.ORIGIN), "HTTP Headers should not contain Origin") Reference.reachabilityFence(websocket) } @@ -161,4 +165,26 @@ class TestHttpApi { } } } + + @Test + fun `Closes websocket if the server doesn't respond`() { + runServer(closeWebsocket = false) { server -> + LuaTaskRunner.runTest(timeout = 10.seconds) { + val url = "ws://127.0.0.1:${server.port}/ws" + val httpApi = addApi(createHttpApi(environment, server.port)) + assertThat("http.websocket succeeded", httpApi.websocket(ObjectArguments(url)), array(equalTo(true))) + + val connectEvent = pullEvent() + assertThat(connectEvent, array(equalTo("websocket_success"), equalTo(url), isA(WebsocketHandle::class.java))) + val websocket = connectEvent[2] as WebsocketHandle + + websocket.close() + + val closeEvent = pullEvent() + assertThat("Received a close event", closeEvent, array(equalTo("websocket_closed"), equalTo(url), nullValue(), equalTo(1006))) + + Reference.reachabilityFence(websocket) + } + } + } } diff --git a/projects/forge/build.gradle.kts b/projects/forge/build.gradle.kts index 9ce6c1f28..0d63ccf61 100644 --- a/projects/forge/build.gradle.kts +++ b/projects/forge/build.gradle.kts @@ -255,7 +255,7 @@ tasks.verifyClassesMatch { option("ModLoader", "forge") } - // We have an extra adapater between SimpleJsonResourceReloadListener.prepare and SimplePreparableReloadListener.prepare. + // We have an extra adapter between SimpleJsonResourceReloadListener.prepare and SimplePreparableReloadListener.prepare. // Not quite sure why, but doesn't cause issues. ignoredFiles.add("dan200/computercraft/impl/UpgradeManager.class") // Forge widens the inner classes of RenderType, so we end up with different InnerClasses attributes. Doesn't cause diff --git a/projects/web/src/main/java/cc/tweaked/web/http/Websocket.java b/projects/web/src/main/java/cc/tweaked/web/http/Websocket.java index 94a2aa951..2edd4ae38 100644 --- a/projects/web/src/main/java/cc/tweaked/web/http/Websocket.java +++ b/projects/web/src/main/java/cc/tweaked/web/http/Websocket.java @@ -6,6 +6,7 @@ package cc.tweaked.web.http; import cc.tweaked.web.js.Console; import com.google.common.base.Strings; +import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.core.apis.IAPIEnvironment; import dan200.computercraft.core.apis.http.Resource; import dan200.computercraft.core.apis.http.ResourceGroup; @@ -13,6 +14,7 @@ import dan200.computercraft.core.apis.http.options.Action; import dan200.computercraft.core.apis.http.options.Options; import dan200.computercraft.core.apis.http.websocket.WebsocketClient; import dan200.computercraft.core.apis.http.websocket.WebsocketHandle; +import dan200.computercraft.core.util.GlobalCleaner; import org.jspecify.annotations.Nullable; import org.teavm.jso.typedarrays.ArrayBuffer; import org.teavm.jso.typedarrays.Int8Array; @@ -32,6 +34,7 @@ final class Websocket extends Resource implements WebsocketClient { private final String address; private @Nullable WebSocket websocket; + private boolean isClosing = false; Websocket(ResourceGroup limiter, IAPIEnvironment environment, String address, URI uri) { super(limiter); @@ -48,7 +51,7 @@ final class Websocket extends Resource implements WebsocketClient { client.onOpen(e -> success(Action.ALLOW.toPartial().toOptions())); client.onError(e -> { Console.error(e); - failure("Could not connect"); + handshakeFailure("Could not connect"); }); client.onMessage(e -> { if (isClosed()) return; @@ -59,21 +62,29 @@ final class Websocket extends Resource implements WebsocketClient { environment.queueEvent("websocket_message", address, e.getDataAsString(), false); } }); - client.onClose(e -> close(e.getCode(), e.getReason())); + client.onClose(e -> serverClosed(e.getCode(), e.getReason())); } @Override - public void sendText(String message) { - if (websocket == null) return; + public void sendText(String message) throws LuaException { + if (websocket == null || isClosing) throw new LuaException(CLOSED_ERROR); websocket.send(message); } @Override - public void sendBinary(ByteBuffer message) { - if (websocket == null) return; + public void sendBinary(ByteBuffer message) throws LuaException { + if (websocket == null || isClosing) throw new LuaException(CLOSED_ERROR); websocket.send(Int8Array.fromJavaBuffer(message)); } + @Override + public void close(int status, String reason) { + if (websocket == null || isClosing) return; + + websocket.close(status, reason); + isClosing = true; + } + @Override protected void dispose() { super.dispose(); @@ -88,16 +99,16 @@ final class Websocket extends Resource implements WebsocketClient { var handle = new WebsocketHandle(environment, address, this, Map.of(), options); environment.queueEvent(SUCCESS_EVENT, address, handle); - registerCleanable(handle); + GlobalCleaner.register(handle, () -> close(1001, "")); checkClosed(); } - void failure(String message) { + private void handshakeFailure(String message) { if (tryClose()) environment.queueEvent(FAILURE_EVENT, address, message); } - void close(int status, String reason) { + private void serverClosed(int status, String reason) { if (!tryClose()) return; environment.queueEvent(CLOSE_EVENT, address, Strings.isNullOrEmpty(reason) ? null : reason, status < 0 ? null : status);