diff options
Diffstat (limited to 'chapter25/src/main/java/com/seaofnodes')
| -rw-r--r-- | chapter25/src/main/java/com/seaofnodes/simple/print/JSViewer.java | 76 | ||||
| -rw-r--r-- | chapter25/src/main/java/com/seaofnodes/simple/print/SimpleWebSocket.java | 96 |
2 files changed, 122 insertions, 50 deletions
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/print/JSViewer.java b/chapter25/src/main/java/com/seaofnodes/simple/print/JSViewer.java index 204b2642..156f1fe9 100644 --- a/chapter25/src/main/java/com/seaofnodes/simple/print/JSViewer.java +++ b/chapter25/src/main/java/com/seaofnodes/simple/print/JSViewer.java @@ -7,7 +7,11 @@ import com.seaofnodes.simple.util.SB; import com.seaofnodes.simple.util.Utils; import java.io.IOException; import java.nio.file.Paths; +import java.nio.file.Files; +import java.nio.file.Path; +import java.net.URI; import java.util.Collection; +import java.util.Arrays; import java.util.Stack; import static com.seaofnodes.simple.codegen.CodeGen.CODE; @@ -28,7 +32,20 @@ public class JSViewer implements AutoCloseable { JSViewer() throws Exception { // Launch server; handshake - SERVER = new SimpleWebSocket(Paths.get("docs/index.html").toUri(),12345) ; + SERVER = new SimpleWebSocket(viewerURI(),12345); + } + + // The browser application is shared by all chapters. Search upwards so + // chapter, repository-root, and linearized checkouts use the same assets. + static URI viewerURI() throws IOException { + String url = System.getProperty("simple.graph.url"); + if( url != null ) return URI.create(url); + for( Path dir = Paths.get("").toAbsolutePath(); dir != null; dir = dir.getParent() ) { + Path page = dir.resolve("graph/web/index.html"); + if( Files.isRegularFile(page) ) return page.toUri(); + } + throw new IOException("Cannot find graph/web/index.html; run inside the Simple checkout " + + "or set -Dsimple.graph.url=<viewer URL>"); } void run( ) throws Exception { @@ -137,10 +154,10 @@ public class JSViewer implements AutoCloseable { // Just the Nodes first, in a cluster no edges sb.i().p("subgraph cluster_Nodes {\n").ii(); // Magic "cluster_" in the subgraph name for( Node n : all ) { - if( n instanceof ProjNode || n instanceof CProjNode || n instanceof MemMergeNode ) + if( n instanceof ProjNode || n instanceof CProjNode ) continue; // Do not emit, rolled into MultiNode or Scope cluster already - sb.i().p(n.uniqueName()).p(" [ "); - if( n instanceof MultiNode && !(n instanceof StartNode) ) { + sb.i().p(nodeName(n)).p(" [ "); + if( n instanceof MultiNode ) { // Make a box with the MultiNode on top, and all the projections on the bottom sb. p("shape=plaintext label=<\n").ii(); sb.i().p("<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\" CELLPADDING=\"4\">\n"); @@ -150,10 +167,11 @@ public class JSViewer implements AutoCloseable { sb.i().p("<TR><TD>\n").ii(); sb.i().p("<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\">").p("\n"); sb.i().p("<TR>"); - n._outputs.sort((x,y) -> x instanceof ProjNode xp && y instanceof ProjNode yp ? (xp._idx - yp._idx) : ((x==null ? 99999 : x._nid) - (y==null ? 99999 : y._nid))); + Node[] outputs = n._outputs.asAry(); + Arrays.sort(outputs,(x,y) -> Integer.compare(idx(x),idx(y))); boolean empty_row=true; - for( Node use : n._outputs ) { - int idx = idx(n); + for( Node use : outputs ) { + int idx = idx(use); if( idx != -1 ) { cell(sb,use.glabel(),use,"p"+idx); empty_row=false; @@ -179,9 +197,9 @@ public class JSViewer implements AutoCloseable { for (Node n : all) { if (n instanceof RegionNode region) { sb.i().p("{ rank=same; "); - sb.p(region.uniqueName()).p(";"); + sb.p(nodeName(region)).p(";"); for (Node phi : region._outputs) - if (phi instanceof PhiNode) sb.p(phi.uniqueName()).p(";"); + if (phi instanceof PhiNode) sb.p(nodeName(phi)).p(";"); sb.p("}\n"); } } @@ -278,10 +296,10 @@ public class JSViewer implements AutoCloseable { // Called with an open div private static SB colorcell(SB sb, String text, Node n, boolean dark) { if( dark ) sb.p("<font color=\"white\">"); - sb.p(text); + sb.p(htmlText(text)); if( n._type!=null ) { sb.p("<br /><font point-size=\"10\">"); - n._type.gprint(sb); + sb.p(htmlText(n._type.gprint())); sb.p("</font>"); } if( dark ) sb.p("</font>"); @@ -289,7 +307,16 @@ public class JSViewer implements AutoCloseable { } - private static String makeScopeName(ScopeNode sn, int level) { return sn.uniqueName() + "_" + level; } + // DOT identity must not depend on labels (module/function names contain punctuation). + private static String nodeName(Node n) { return "n" + n._nid; } + + // glabel/gprint already use entities such as < and →. + private static String htmlText(String text) { + return text.replaceAll("&(?!#[0-9]+;|#x[0-9a-fA-F]+;|[a-zA-Z]+;)", "&") + .replace("<", "<").replace(">", ">"); + } + + private static String makeScopeName(ScopeNode sn, int level) { return nodeName(sn) + "_" + level; } private static String makePortName(String scopeName, String varName) { return scopeName + "_" + varName; } // Walk the node edges @@ -305,8 +332,7 @@ public class JSViewer implements AutoCloseable { n instanceof ProjNode || n instanceof CProjNode || // ScopeNodes are done separately - n instanceof ScopeNode || - n instanceof MemMergeNode + n instanceof ScopeNode ) continue; if( n.isDead() ) @@ -315,20 +341,20 @@ public class JSViewer implements AutoCloseable { for( int i=0; i<n.nIns(); i++ ) { Node def = n.in(i); if( def==null ) continue; - sb.i().p(n.uniqueName()).p(" -> "); + sb.i().p(nodeName(n)).p(" -> "); if( n instanceof PhiNode && def instanceof RegionNode ) { // Draw a dotted use->def edge from Phi to Region. - sb.p(def.uniqueName()); + sb.p(nodeName(def)); sb.p(" [style=dotted taillabel=").p(i).p("];\n"); } else { // Most edges land here use->def if( def instanceof CProjNode proj ) { - String mname = proj.ctrl().uniqueName(); + String mname = nodeName(proj.ctrl()); sb.p(mname).p(":p").p(proj._idx); } else if( def instanceof ProjNode proj ) { - String mname = proj.in(0).uniqueName(); + String mname = nodeName(proj.in(0)); sb.p(mname).p(":p").p(proj._idx); - } else sb.p(def.uniqueName()); + } else sb.p(nodeName(def)); // Number edges, so we can see how they track sb.p("[taillabel=").p(i); // Color the edge @@ -342,11 +368,13 @@ public class JSViewer implements AutoCloseable { // Bonus edge if hooked by parser if( (n.iskeep() || n.isUnused()) && scopeName != null ) { - sb.i().p(scopeName).p(" -> ").p(n.uniqueName()).p(" [ style=dashed color=grey];\n"); + sb.i().p(scopeName).p(" -> ").p(nodeName(n)).p(" [ style=dashed color=grey];\n"); } // Force functions above return - if( n instanceof ReturnNode ret && !ret.fun().isDead() ) - sb.i().p(ret.ctrl().uniqueName()).p(" -> ").p(ret.fun().uniqueName()).p("[style=invis]\n"); + if( n instanceof ReturnNode ret && !ret.fun().isDead() ) { + defPort(sb.i(),ret.ctrl()).p(" -> "); + defPort(sb,ret.fun()).p("[style=invis]\n"); + } } } @@ -397,7 +425,7 @@ public class JSViewer implements AutoCloseable { private static SB defPort(SB sb, Node def) { int idx = idx(def); return idx != -1 - ? sb.p(def.in(0).uniqueName()).p(":p").p(idx) - : sb.p(def.uniqueName()); + ? sb.p(nodeName(def.in(0))).p(":p").p(idx) + : sb.p(nodeName(def)); } } diff --git a/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleWebSocket.java b/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleWebSocket.java index 4077533d..d7fa8bf2 100644 --- a/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleWebSocket.java +++ b/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleWebSocket.java @@ -9,6 +9,10 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Base64; +import java.net.InetSocketAddress; +import java.nio.file.Path; +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.SimpleFileServer; // Why-Oh-Why is this needed? @@ -20,34 +24,66 @@ import java.util.Base64; class SimpleWebSocket extends ServerSocket { final InputStream _in; final OutputStream _out; + private HttpServer _http; + private Socket _client; SimpleWebSocket(URI uri, int port) throws IOException, NoSuchAlgorithmException { super(port); - // Launch client - java.awt.Desktop.getDesktop().browse(uri); - // Look for client; get in/out streams - Socket sock = accept(); - _in = sock. getInputStream(); - _out = sock.getOutputStream(); + try { + // Use an ordinary web URL; desktop dispatch of file: URLs can silently + // raise the browser without opening the page. + if( "file".equals(uri.getScheme()) ) { + Path page = Path.of(uri); + _http = SimpleFileServer.createFileServer( + new InetSocketAddress("127.0.0.1",0), page.getParent(), + SimpleFileServer.OutputLevel.NONE); + _http.start(); + uri = URI.create("http://127.0.0.1:" + _http.getAddress().getPort() + + "/" + page.getFileName()); + } + // Print the address even if the desktop silently fails to open a tab. + System.out.println("Graph viewer: " + uri); + if( Boolean.parseBoolean(System.getProperty("simple.graph.open", "true")) ) + java.awt.Desktop.getDesktop().browse(uri); + // Look for client; get in/out streams + Socket sock = accept(); + _client = sock; + _in = sock. getInputStream(); + _out = sock.getOutputStream(); - // Establish web socket with handshake; easier with a buffered reader - BufferedReader br = new BufferedReader(new InputStreamReader(_in)); - String key = null; - while( br.ready() ) { - String line = br.readLine(); - if( line.startsWith("Sec-WebSocket-Key: ") ) - key = line.substring(19); + // Establish web socket with handshake; easier with a buffered reader + BufferedReader br = new BufferedReader(new InputStreamReader(_in, StandardCharsets.US_ASCII)); + String key = null; + // TCP can deliver the request in several chunks. ready() is not an + // end-of-headers test; wait for the terminating empty line. + String line; + while( (line = br.readLine()) != null && !line.isEmpty() ) { + if( line.startsWith("Sec-WebSocket-Key: ") ) + key = line.substring(19); + } + if( key==null || line==null ) { + sock.close(); + super.close(); + throw new IOException("Incomplete WebSocket upgrade request"); + } + // Magic handshake + byte[] response = ("HTTP/1.1 101 Switching Protocols\r\n" + + "Connection: Upgrade\r\n" + + "Upgrade: websocket\r\n" + + "Sec-WebSocket-Accept: " + + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-1").digest((key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").getBytes( StandardCharsets.UTF_8 ))) + + "\r\n\r\n").getBytes( StandardCharsets.UTF_8 ); + _out.write(response,0,response.length); + _out.flush(); + } catch( IOException | NoSuchAlgorithmException | RuntimeException | Error failure ) { + if( _http != null ) _http.stop(0); + if( _client != null ) { + try { _client.close(); } catch( IOException ignored ) {} + } + try { super.close(); } catch( IOException ignored ) {} + throw failure; } - assert key!=null; - // Magic handshake - byte[] response = ("HTTP/1.1 101 Switching Protocols\r\n" - + "Connection: Upgrade\r\n" - + "Upgrade: websocket\r\n" - + "Sec-WebSocket-Accept: " - + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-1").digest((key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").getBytes( StandardCharsets.UTF_8 ))) - + "\r\n\r\n").getBytes( StandardCharsets.UTF_8 ); - _out.write(response,0,response.length); } final byte[] _ins = new byte[1024]; @@ -140,10 +176,18 @@ class SimpleWebSocket extends ServerSocket { } @Override public void close() throws IOException { - if( _out != null ) { - _out.write(0b10001000); - _out.flush(); + try { + if( _out != null ) { + _out.write(new byte[]{(byte)0x88, 0}); // Complete, empty close frame + _out.flush(); + } + } finally { + if( _http != null ) _http.stop(0); + try { + if( _client != null ) _client.close(); + } finally { + super.close(); + } } - super.close(); } } |
