diff options
| author | Cliff Click <cliffc@acm.org> | 2026-09-21 16:26:15 -0700 |
|---|---|---|
| committer | Cliff Click <cliffc@acm.org> | 2026-09-21 16:26:15 -0700 |
| commit | d2ffb24d0a3181d107f80df76570c04e010833ed (patch) | |
| tree | c78b75b2dcca46a4f7513e27fa9f20e43bab787c /chapter23/src/main/java/com/seaofnodes | |
| parent | 40b33cb6f8b8dc4d8551c3b522e7c5682a44540a (diff) | |
| download | Simple-d2ffb24d0a3181d107f80df76570c04e010833ed.tar.gz Simple-d2ffb24d0a3181d107f80df76570c04e010833ed.zip | |
move the graph visualizer into shared code
Diffstat (limited to 'chapter23/src/main/java/com/seaofnodes')
| -rw-r--r-- | chapter23/src/main/java/com/seaofnodes/simple/print/JSViewer.java | 18 | ||||
| -rw-r--r-- | chapter23/src/main/java/com/seaofnodes/simple/print/SimpleWebSocket.java | 96 |
2 files changed, 87 insertions, 27 deletions
diff --git a/chapter23/src/main/java/com/seaofnodes/simple/print/JSViewer.java b/chapter23/src/main/java/com/seaofnodes/simple/print/JSViewer.java index 62ff0001..f4f72a3b 100644 --- a/chapter23/src/main/java/com/seaofnodes/simple/print/JSViewer.java +++ b/chapter23/src/main/java/com/seaofnodes/simple/print/JSViewer.java @@ -7,6 +7,9 @@ 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.Stack; import static com.seaofnodes.simple.codegen.CodeGen.CODE; @@ -28,7 +31,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 { diff --git a/chapter23/src/main/java/com/seaofnodes/simple/print/SimpleWebSocket.java b/chapter23/src/main/java/com/seaofnodes/simple/print/SimpleWebSocket.java index 4077533d..d7fa8bf2 100644 --- a/chapter23/src/main/java/com/seaofnodes/simple/print/SimpleWebSocket.java +++ b/chapter23/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(); } } |
