diff --git a/.classpath b/.classpath index 4c11341e1..8485df73d 100644 --- a/.classpath +++ b/.classpath @@ -20,7 +20,7 @@ - - + + diff --git a/build.xml b/build.xml index f0084fa30..0733bcbd5 100644 --- a/build.xml +++ b/build.xml @@ -6,7 +6,7 @@ - + @@ -57,7 +57,7 @@ - + @@ -72,6 +72,12 @@ dest="${lib.dir}/somns-deps-dev.jar" /> + + + + connection; - - public BinaryWebSocketHandler(final InetSocketAddress address) { - super(address, NUM_THREADS); - connection = new CompletableFuture<>(); - } - - @Override - public void onClose(final WebSocket conn, final int arg1, final String arg2, final boolean arg3) { - // no-op - } - - @Override - public void onError(final WebSocket conn, final Exception ex) { - WebDebugger.log("error:"); - ex.printStackTrace(); - } - - @Override - public void onMessage(final WebSocket conn, final String msg) { - // no-op - } - - @Override - public void onOpen(final WebSocket conn, final ClientHandshake handshake) { - connection.complete(conn); - } - - public CompletableFuture getConnection() { - return connection; - } -} diff --git a/src/tools/debugger/FrontendConnector.java b/src/tools/debugger/FrontendConnector.java index ce33b06b5..b1c3dedec 100644 --- a/src/tools/debugger/FrontendConnector.java +++ b/src/tools/debugger/FrontendConnector.java @@ -1,6 +1,7 @@ package tools.debugger; import java.io.IOException; +import java.net.BindException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.util.ArrayList; @@ -9,7 +10,7 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; +import java.util.function.Function; import org.java_websocket.WebSocket; @@ -27,6 +28,8 @@ import tools.Tagging; import tools.TraceData; import tools.concurrency.ActorExecutionTrace; +import tools.debugger.WebSocketHandler.MessageHandler; +import tools.debugger.WebSocketHandler.TraceHandler; import tools.debugger.entities.ActivityType; import tools.debugger.entities.BreakpointType; import tools.debugger.entities.EntityType; @@ -64,8 +67,8 @@ public class FrontendConnector { /** * Receives requests from the client. */ - private final WebSocketHandler receiver; - private final BinaryWebSocketHandler binaryHandler; + private final MessageHandler receiver; + private final TraceHandler binaryHandler; /** * Sends requests to the client. @@ -80,9 +83,10 @@ public class FrontendConnector { private CompletableFuture clientConnected; private final Gson gson; - private static final int MESSAGE_PORT = 7977; - private static final int BINARY_PORT = 7978; - private static final int DEBUGGER_PORT = 8888; + private static final int MESSAGE_PORT = 7977; + private static final int BINARY_PORT = 7978; + private static final int DEBUGGER_PORT = 8888; + private static final int EPHEMERAL_PORT = 0; private final ArrayList notReady = new ArrayList<>(); // TODO rename: toBeSend @@ -98,15 +102,16 @@ public FrontendConnector(final Breakpoints breakpoints, try { log("[DEBUGGER] Initialize HTTP and WebSocket Server for Debugger"); - receiver = initializeWebSocket(MESSAGE_PORT, clientConnected); - log("[DEBUGGER] Started WebSocket Server"); - - binaryHandler = new BinaryWebSocketHandler(new InetSocketAddress(BINARY_PORT)); - binaryHandler.start(); - - contentServer = initializeHttpServer(DEBUGGER_PORT); + receiver = initializeWebSocket(MESSAGE_PORT, port -> new MessageHandler(port, this, gson)); + binaryHandler = initializeWebSocket(BINARY_PORT, port -> new TraceHandler(port)); + log("[DEBUGGER] Started WebSocket Servers"); + log("[DEBUGGER] Message Handler: " + receiver.getPort()); + log("[DEBUGGER] Trace Handler: " + binaryHandler.getPort()); + + contentServer = initializeHttpServer(DEBUGGER_PORT, + receiver.getPort(), binaryHandler.getPort()); log("[DEBUGGER] Started HTTP Server"); - log("[DEBUGGER] URL: http://localhost:" + DEBUGGER_PORT + "/index.html"); + log("[DEBUGGER] URL: http://localhost:" + contentServer.getAddress().getPort() + "/index.html"); } catch (IOException e) { log("Failed starting WebSocket and/or HTTP Server"); throw new RuntimeException(e); @@ -119,23 +124,49 @@ public Breakpoints getBreakpoints() { return breakpoints; } - private WebSocketHandler initializeWebSocket(final int port, - final Future clientConnected) { - InetSocketAddress address = new InetSocketAddress(port); - WebSocketHandler server = new WebSocketHandler(address, this, gson); + private T tryInitializingWebSocket(final T server) throws Throwable { server.start(); + try { + server.awaitStartup(); + } catch (ExecutionException e) { + throw e.getCause(); + } return server; } - private HttpServer initializeHttpServer(final int port) throws IOException { + private T initializeWebSocket(final int port, final Function ctor) { + try { + return tryInitializingWebSocket(ctor.apply(port)); + } catch (BindException e) { + try { + return tryInitializingWebSocket(ctor.apply(EPHEMERAL_PORT)); + } catch (Throwable e1) { + throw new RuntimeException(e); + } + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + private HttpServer tryInitializingHttpServer(final int port, + final int debuggerPort, final int tracePort) throws IOException { InetSocketAddress address = new InetSocketAddress(port); HttpServer httpServer = HttpServer.create(address, 0); - httpServer.createContext("/", new WebResourceHandler()); + httpServer.createContext("/", new WebResourceHandler(debuggerPort, tracePort)); httpServer.setExecutor(null); httpServer.start(); return httpServer; } + private HttpServer initializeHttpServer(final int port, + final int debuggerPort, final int tracePort) throws IOException { + try { + return tryInitializingHttpServer(port, debuggerPort, tracePort); + } catch (BindException e) { + return tryInitializingHttpServer(EPHEMERAL_PORT, debuggerPort, tracePort); + } + } + private void ensureConnectionIsAvailable() { assert receiver != null; assert sender != null; @@ -278,24 +309,25 @@ static void log(final String str) { } public void completeConnection(final WebSocket conn) { + Runtime.getRuntime().addShutdownHook(new Thread(() -> closeAllSockets())); + clientConnected.complete(conn); send(InitializationResponse.create(EntityType.values(), ActivityType.values(), BreakpointType.values(), SteppingType.values())); } - public void shutdown() { - int delaySec = 5; - contentServer.stop(delaySec); + private void closeAllSockets() { + final int delay = 0; + contentServer.stop(delay); sender.close(); if (binarySender != null) { binarySender.close(); } try { - int delayMsec = 1000; - receiver.stop(delayMsec); + receiver.stop(delay); if (binarySender != null) { - binaryHandler.stop(delayMsec); + binaryHandler.stop(delay); } } catch (InterruptedException e) { } } diff --git a/src/tools/debugger/WebDebugger.java b/src/tools/debugger/WebDebugger.java index c0fdf2666..851020473 100644 --- a/src/tools/debugger/WebDebugger.java +++ b/src/tools/debugger/WebDebugger.java @@ -155,7 +155,7 @@ public static void log(final String str) { @Override protected void onDispose(final Env env) { - connector.shutdown(); + /* NOOP: we close sockets with a VM shutdown hook */ } @Override diff --git a/src/tools/debugger/WebResourceHandler.java b/src/tools/debugger/WebResourceHandler.java index 399539199..797bbc61b 100644 --- a/src/tools/debugger/WebResourceHandler.java +++ b/src/tools/debugger/WebResourceHandler.java @@ -5,6 +5,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.charset.Charset; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; @@ -12,6 +13,13 @@ import som.vm.NotYetImplementedException; class WebResourceHandler implements HttpHandler { + private final int debuggerPort; + private final int tracePort; + + WebResourceHandler(final int debuggerPort, final int tracePort) { + this.debuggerPort = debuggerPort; + this.tracePort = tracePort; + } @Override public void handle(final HttpExchange exchange) throws IOException { @@ -36,10 +44,17 @@ public void handle(final HttpExchange exchange) throws IOException { } exchange.sendResponseHeaders(200, f.length()); copy(f, exchange.getResponseBody()); + exchange.close(); return; } switch (requestedFile) { + case "/ports.json": + String jsonPorts = "{\"dbgPort\":" + debuggerPort + ",\"tracePort\":" + tracePort + "\"}"; + exchange.sendResponseHeaders(200, jsonPorts.length()); + exchange.getResponseBody().write(jsonPorts.getBytes(Charset.forName("UTF-8"))); + exchange.close(); + return; case "/favicon.ico": exchange.sendResponseHeaders(404, 0); exchange.close(); diff --git a/src/tools/debugger/WebSocketHandler.java b/src/tools/debugger/WebSocketHandler.java index 99bf50cf4..582db420f 100644 --- a/src/tools/debugger/WebSocketHandler.java +++ b/src/tools/debugger/WebSocketHandler.java @@ -1,6 +1,9 @@ package tools.debugger; +import java.net.BindException; import java.net.InetSocketAddress; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import org.java_websocket.WebSocket; import org.java_websocket.handshake.ClientHandshake; @@ -12,17 +15,27 @@ import tools.debugger.message.Message.IncommingMessage; -public class WebSocketHandler extends WebSocketServer { +public abstract class WebSocketHandler extends WebSocketServer { private static final int NUM_THREADS = 1; - private final FrontendConnector connector; - private final Gson gson; + private final CompletableFuture connectionPort; - WebSocketHandler(final InetSocketAddress address, - final FrontendConnector connector, final Gson gson) { - super(address, NUM_THREADS); - this.connector = connector; - this.gson = gson; + WebSocketHandler(final int port) { + super(new InetSocketAddress(port), NUM_THREADS); + this.connectionPort = new CompletableFuture<>(); + } + + @Override + public void onStart() { + connectionPort.complete(getPort()); + } + + public int awaitStartup() throws ExecutionException { + while (true) { + try { + return connectionPort.get(); + } catch (InterruptedException e) { /* Retry on interrupt. */ } + } } @Override @@ -34,20 +47,57 @@ public void onClose(final WebSocket conn, final int code, final String reason, WebDebugger.log("onClose: code=" + code + " " + reason); } - @Override - public void onMessage(final WebSocket conn, final String message) { - try { - IncommingMessage respond = gson.fromJson(message, IncommingMessage.class); - respond.process(connector, conn); - } catch (Throwable ex) { - VM.errorPrint("Error while processing msg:" + message); - ex.printStackTrace(); - } - } - @Override public void onError(final WebSocket conn, final Exception ex) { + if (ex instanceof BindException) { + connectionPort.completeExceptionally(ex); + return; + } WebDebugger.log("error:"); ex.printStackTrace(); } + + public static class MessageHandler extends WebSocketHandler { + private final FrontendConnector connector; + private final Gson gson; + + public MessageHandler(final int port, final FrontendConnector connector, + final Gson gson) { + super(port); + this.connector = connector; + this.gson = gson; + } + + @Override + public void onMessage(final WebSocket conn, final String message) { + try { + IncommingMessage respond = gson.fromJson(message, IncommingMessage.class); + respond.process(connector, conn); + } catch (Throwable ex) { + VM.errorPrint("Error while processing msg:" + message); + ex.printStackTrace(); + } + } + } + + public static class TraceHandler extends WebSocketHandler { + private final CompletableFuture connection; + + public TraceHandler(final int port) { + super(port); + connection = new CompletableFuture<>(); + } + + @Override + public void onOpen(final WebSocket conn, final ClientHandshake handshake) { + connection.complete(conn); + } + + @Override + public void onMessage(final WebSocket conn, final String message) { } + + public CompletableFuture getConnection() { + return connection; + } + } } diff --git a/tools/kompos/package.json b/tools/kompos/package.json index b1c8ca5e9..cb65de0b0 100644 --- a/tools/kompos/package.json +++ b/tools/kompos/package.json @@ -46,6 +46,6 @@ "compile": "node ./node_modules/typescript/bin/tsc && npm run copylibs", "lint": "node_modules/tslint/bin/tslint -c tslint.json --project tsconfig.json", "watch": "node ./node_modules/typescript/bin/tsc -w", - "test": "node ./node_modules/mocha/bin/mocha -r node-define -t 5000 -u bdd ./out/tests/" + "test": "node ./node_modules/mocha/bin/mocha -r node-define -t 10000 -u bdd ./out/tests/" } } diff --git a/tools/kompos/src/vm-connection.ts b/tools/kompos/src/vm-connection.ts index 8aca06077..bb75fb20d 100644 --- a/tools/kompos/src/vm-connection.ts +++ b/tools/kompos/src/vm-connection.ts @@ -6,8 +6,6 @@ import * as WebSocket from "ws"; import {Controller} from "./controller"; import {Activity, Message, Respond, BreakpointData} from "./messages"; -const DBG_PORT = 7977; -const TRACE_PORT = 7978; const LOCAL_WS_URL = "ws://localhost"; /** @@ -35,11 +33,11 @@ export class VmConnection { return this.socket !== null && this.socket.readyState === WebSocket.OPEN; } - private connectTraceDataSocket() { + private connectTraceDataSocket(tracePort: number) { if (!this.useTraceData) { return; } console.assert(this.traceDataSocket === null || this.traceDataSocket.readyState === WebSocket.CLOSED); - this.traceDataSocket = new WebSocket(LOCAL_WS_URL + ":" + TRACE_PORT); + this.traceDataSocket = new WebSocket(LOCAL_WS_URL + ":" + tracePort); ( this.traceDataSocket).binaryType = "arraybuffer"; // workaround, typescript doesn't recognize this property const controller = this.controller; @@ -55,8 +53,15 @@ export class VmConnection { } public connect() { + $.getJSON("ports.json", data => { + console.log(data); + this.connectWebSockets(data.dbgPort, data.tracePort); + }); + } + + protected connectWebSockets(dbgPort: number, tracePort: number) { console.assert(this.socket === null || this.socket.readyState === WebSocket.CLOSED); - this.socket = new WebSocket(LOCAL_WS_URL + ":" + DBG_PORT); + this.socket = new WebSocket(LOCAL_WS_URL + ":" + dbgPort); const ctrl = this.controller; this.socket.onopen = () => { @@ -109,7 +114,7 @@ export class VmConnection { } }; - this.connectTraceDataSocket(); + this.connectTraceDataSocket(tracePort); } public disconnect() { diff --git a/tools/kompos/tests/basic-protocol.ts b/tools/kompos/tests/basic-protocol.ts index e0aa77c49..f75c263d7 100644 --- a/tools/kompos/tests/basic-protocol.ts +++ b/tools/kompos/tests/basic-protocol.ts @@ -60,7 +60,8 @@ describe("Basic Protocol", function() { conn = new TestConnection(); const ctrl = new ControllerWithInitialBreakpoints([], conn); let firstSourceCaptured = false; - sourceP = new Promise((resolve, _reject) => { + sourceP = new Promise((resolve, reject) => { + conn.fullyConnected.catch(reject); ctrl.onReceivedSource = (msg: SourceMessage) => { if (firstSourceCaptured) { return; }; firstSourceCaptured = true; @@ -255,7 +256,7 @@ describe("Basic Protocol", function() { before("Start SOMns and Connect", () => { conn = new TestConnection(); - ctrl = new HandleStoppedAndGetStackTrace([desc.breakpoint], conn); + ctrl = new HandleStoppedAndGetStackTrace([desc.breakpoint], conn, conn.fullyConnected); }); after(closeConnectionAfterSuite); @@ -276,7 +277,7 @@ describe("Basic Protocol", function() { const breakpoint = createSectionBreakpointData(PING_PONG_URI, 23, 14, 3, BT.MSG_SENDER, true); conn = new TestConnection(); - ctrl = new HandleStoppedAndGetStackTrace([breakpoint], conn, 4); + ctrl = new HandleStoppedAndGetStackTrace([breakpoint], conn, conn.fullyConnected, 4); }); after(closeConnectionAfterSuite); diff --git a/tools/kompos/tests/csp.ts b/tools/kompos/tests/csp.ts index 23776dce6..bfd420499 100644 --- a/tools/kompos/tests/csp.ts +++ b/tools/kompos/tests/csp.ts @@ -21,7 +21,7 @@ describe("Setting CSP Breakpoints", () => { const breakpoint = createSectionBreakpointData(CSP_URI, 13, 12, 4, BreakpointType.CHANNEL_BEFORE_RCV, true); conn = new TestConnection(["halt"], null, CSP_FILE); - ctrl = new HandleStoppedAndGetStackTrace([breakpoint], conn); + ctrl = new HandleStoppedAndGetStackTrace([breakpoint], conn, conn.fullyConnected); }); after(closeConnectionAfterSuite); @@ -39,7 +39,7 @@ describe("Setting CSP Breakpoints", () => { const breakpoint = createSectionBreakpointData(CSP_URI, 13, 12, 4, BreakpointType.CHANNEL_AFTER_SEND, true); conn = new TestConnection(["halt"], null, CSP_FILE); - ctrl = new HandleStoppedAndGetStackTrace([breakpoint], conn); + ctrl = new HandleStoppedAndGetStackTrace([breakpoint], conn, conn.fullyConnected); }); after(closeConnectionAfterSuite); @@ -56,7 +56,7 @@ describe("Setting CSP Breakpoints", () => { const breakpoint = createSectionBreakpointData(CSP_URI, 12, 13, 12, BreakpointType.CHANNEL_BEFORE_SEND, true); conn = new TestConnection(["halt"], null, CSP_FILE); - ctrl = new HandleStoppedAndGetStackTrace([breakpoint], conn); + ctrl = new HandleStoppedAndGetStackTrace([breakpoint], conn, conn.fullyConnected); }); after(closeConnectionAfterSuite); @@ -74,7 +74,7 @@ describe("Setting CSP Breakpoints", () => { const breakpoint = createSectionBreakpointData(CSP_URI, 12, 13, 12, BreakpointType.CHANNEL_AFTER_RCV, true); conn = new TestConnection(["halt"], null, CSP_FILE); - ctrl = new HandleStoppedAndGetStackTrace([breakpoint], conn); + ctrl = new HandleStoppedAndGetStackTrace([breakpoint], conn, conn.fullyConnected); }); after(closeConnectionAfterSuite); diff --git a/tools/kompos/tests/som-integration.ts b/tools/kompos/tests/som-integration.ts index 55c92bb6e..95f251a36 100644 --- a/tools/kompos/tests/som-integration.ts +++ b/tools/kompos/tests/som-integration.ts @@ -55,7 +55,7 @@ describe("Language Debugger Integration", function() { describe("execute `1 halt` and get suspended event", () => { before("Start SOMns and Connect", () => { conn = new TestConnection(["halt"]); - ctrl = new HandleStoppedAndGetStackTrace([], conn); + ctrl = new HandleStoppedAndGetStackTrace([], conn, conn.fullyConnected); }); after(closeConnectionAfterSuite); diff --git a/tools/kompos/tests/test-setup.ts b/tools/kompos/tests/test-setup.ts index f868ebc15..80074d481 100644 --- a/tools/kompos/tests/test-setup.ts +++ b/tools/kompos/tests/test-setup.ts @@ -78,24 +78,59 @@ export class TestConnection extends VmConnection { private initConnection(): Promise { const promise = new Promise((resolve, reject) => { + const msgPortRe = /.*Message Handler:\s+(\d+)/m; + const tracePortRe = /.*Trace Handler:\s+(\d+)/m; this.connectionResolver = resolve; let connecting = false; + let errOut = ""; + let msgPort = 0; + let tracePort = 0; + + this.somProc.on("exit", (code, signal) => { + if (code !== 0) { + this.somProc.stderr.on("close", () => { + this.somProc.on("exit", (_code, _signal) => { + reject(new Error("Process exited with code: " + code + " Signal: " + signal + " StdErr: " + errOut)); + }); + }); + } + }); - if (PRINT_SOM_OUTPUT) { - this.somProc.stderr.on("data", (data) => { console.error(data.toString()); }); - } + this.somProc.stderr.on("data", data => { + const dataStr = data.toString(); + if (PRINT_SOM_OUTPUT) { + console.error(dataStr); + } + errOut += dataStr; + }); this.somProc.stdout.on("data", (data) => { const dataStr = data.toString(); if (PRINT_SOM_OUTPUT) { console.log(dataStr); } + + let m = dataStr.match(msgPortRe); + if (m) { + msgPort = parseInt(m[1]); + } + m = dataStr.match(tracePortRe); + if (m) { + tracePort = parseInt(m[1]); + } + if (dataStr.includes("Started HTTP Server") && !connecting) { connecting = true; - this.connect(); + console.assert(msgPort > 0 && tracePort > 0); + this.connectWebSockets(msgPort, tracePort); } if (dataStr.includes("Failed starting WebSocket and/or HTTP Server")) { - reject(new Error("SOMns failed to starting WebSocket and/or HTTP Server")); + this.somProc.stderr.on("close", () => { + this.somProc.on("exit", (_code, _signal) => { + reject(new Error("SOMns failed to starting WebSocket and/or HTTP Server. StdOut: " + dataStr + " StdErr: " + errOut)); + }); + }); + this.somProc.kill(); } }); }); @@ -146,23 +181,25 @@ export class HandleStoppedAndGetStackTrace extends ControllerWithInitialBreakpoi public readonly stoppedActivities: Activity[]; constructor(initialBreakpoints: BreakpointData[], vmConnection: VmConnection, - numOps: number = 1) { + connectionP: Promise, numOps: number = 1) { super(initialBreakpoints, vmConnection); this.numOps = numOps; this.numStopped = 0; this.stoppedActivities = []; - this.stackP = new Promise((resolve, _reject) => { + this.stackP = new Promise((resolve, reject) => { this.resolveStackP = resolve; + connectionP.catch(reject); }); if (numOps > 1) { this.resolveStackPs = []; this.stackPs = []; for (let i = 1; i < numOps; i += 1) { - this.stackPs.push(new Promise((resolve, _reject) => { + this.stackPs.push(new Promise((resolve, reject) => { this.resolveStackPs.push(resolve); + connectionP.catch(reject); })); } }