rt/launcher/src/main/java/org/apidesign/bck2brwsr/launcher/Bck2BrwsrLauncher.java
author Jaroslav Tulach <jaroslav.tulach@apidesign.org>
Wed, 17 Apr 2013 17:04:40 +0200
branchfx
changeset 1004 04efef2a9c1e
parent 969 df08556c5c7c
permissions -rw-r--r--
Rather than piggybacking on first alert call, use the fact that the server and FX Web View are in the same VM and notify the view that bck2brwsr.js is about to be served from the server.
     1 /**
     2  * Back 2 Browser Bytecode Translator
     3  * Copyright (C) 2012 Jaroslav Tulach <jaroslav.tulach@apidesign.org>
     4  *
     5  * This program is free software: you can redistribute it and/or modify
     6  * it under the terms of the GNU General Public License as published by
     7  * the Free Software Foundation, version 2 of the License.
     8  *
     9  * This program is distributed in the hope that it will be useful,
    10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12  * GNU General Public License for more details.
    13  *
    14  * You should have received a copy of the GNU General Public License
    15  * along with this program. Look for COPYING file in the top folder.
    16  * If not, see http://opensource.org/licenses/GPL-2.0.
    17  */
    18 package org.apidesign.bck2brwsr.launcher;
    19 
    20 import java.io.Closeable;
    21 import java.io.File;
    22 import java.io.IOException;
    23 import java.io.InputStream;
    24 import java.io.InterruptedIOException;
    25 import java.io.OutputStream;
    26 import java.io.UnsupportedEncodingException;
    27 import java.io.Writer;
    28 import java.net.URI;
    29 import java.net.URISyntaxException;
    30 import java.net.URL;
    31 import java.util.ArrayList;
    32 import java.util.Arrays;
    33 import java.util.Enumeration;
    34 import java.util.LinkedHashSet;
    35 import java.util.List;
    36 import java.util.Set;
    37 import java.util.concurrent.BlockingQueue;
    38 import java.util.concurrent.CountDownLatch;
    39 import java.util.concurrent.LinkedBlockingQueue;
    40 import java.util.concurrent.TimeUnit;
    41 import java.util.logging.Level;
    42 import java.util.logging.Logger;
    43 import org.apidesign.bck2brwsr.launcher.InvocationContext.Resource;
    44 import org.apidesign.vm4brwsr.Bck2Brwsr;
    45 import org.glassfish.grizzly.PortRange;
    46 import org.glassfish.grizzly.http.server.HttpHandler;
    47 import org.glassfish.grizzly.http.server.HttpServer;
    48 import org.glassfish.grizzly.http.server.NetworkListener;
    49 import org.glassfish.grizzly.http.server.Request;
    50 import org.glassfish.grizzly.http.server.Response;
    51 import org.glassfish.grizzly.http.server.ServerConfiguration;
    52 import org.glassfish.grizzly.http.util.HttpStatus;
    53 
    54 /**
    55  * Lightweight server to launch Bck2Brwsr applications and tests.
    56  * Supports execution in native browser as well as Java's internal 
    57  * execution engine.
    58  */
    59 class Bck2BrwsrLauncher extends Launcher implements Closeable {
    60     private static final Logger LOG = Logger.getLogger(Bck2BrwsrLauncher.class.getName());
    61     private static final InvocationContext END = new InvocationContext(null, null, null);
    62     private final Set<ClassLoader> loaders = new LinkedHashSet<>();
    63     private final BlockingQueue<InvocationContext> methods = new LinkedBlockingQueue<>();
    64     private long timeOut;
    65     private final Res resources = new Res();
    66     private final String cmd;
    67     private Object[] brwsr;
    68     private HttpServer server;
    69     private CountDownLatch wait;
    70     
    71     public Bck2BrwsrLauncher(String cmd) {
    72         this.cmd = cmd;
    73     }
    74     
    75     @Override
    76     InvocationContext runMethod(InvocationContext c) throws IOException {
    77         loaders.add(c.clazz.getClassLoader());
    78         methods.add(c);
    79         try {
    80             c.await(timeOut);
    81         } catch (InterruptedException ex) {
    82             throw new IOException(ex);
    83         }
    84         return c;
    85     }
    86     
    87     public void setTimeout(long ms) {
    88         timeOut = ms;
    89     }
    90     
    91     public void addClassLoader(ClassLoader url) {
    92         this.loaders.add(url);
    93     }
    94     
    95     ClassLoader[] loaders() {
    96         return loaders.toArray(new ClassLoader[loaders.size()]);
    97     }
    98 
    99     public void showURL(String startpage) throws IOException {
   100         if (!startpage.startsWith("/")) {
   101             startpage = "/" + startpage;
   102         }
   103         HttpServer s = initServer(".", true);
   104         int last = startpage.lastIndexOf('/');
   105         String prefix = startpage.substring(0, last);
   106         String simpleName = startpage.substring(last);
   107         s.getServerConfiguration().addHttpHandler(new SubTree(resources, prefix), "/");
   108         try {
   109             launchServerAndBrwsr(s, simpleName);
   110         } catch (URISyntaxException | InterruptedException ex) {
   111             throw new IOException(ex);
   112         }
   113     }
   114 
   115     void showDirectory(File dir, String startpage) throws IOException {
   116         if (!startpage.startsWith("/")) {
   117             startpage = "/" + startpage;
   118         }
   119         HttpServer s = initServer(dir.getPath(), false);
   120         try {
   121             launchServerAndBrwsr(s, startpage);
   122         } catch (URISyntaxException | InterruptedException ex) {
   123             throw new IOException(ex);
   124         }
   125     }
   126 
   127     @Override
   128     public void initialize() throws IOException {
   129         try {
   130             executeInBrowser();
   131         } catch (InterruptedException ex) {
   132             final InterruptedIOException iio = new InterruptedIOException(ex.getMessage());
   133             iio.initCause(ex);
   134             throw iio;
   135         } catch (Exception ex) {
   136             if (ex instanceof IOException) {
   137                 throw (IOException)ex;
   138             }
   139             if (ex instanceof RuntimeException) {
   140                 throw (RuntimeException)ex;
   141             }
   142             throw new IOException(ex);
   143         }
   144     }
   145     
   146     private HttpServer initServer(String path, boolean addClasses) throws IOException {
   147         HttpServer s = HttpServer.createSimpleServer(path, new PortRange(8080, 65535));
   148 
   149         final ServerConfiguration conf = s.getServerConfiguration();
   150         if (addClasses) {
   151             conf.addHttpHandler(new VM(), "/bck2brwsr.js");
   152             conf.addHttpHandler(new Classes(resources), "/classes/");
   153         }
   154         return s;
   155     }
   156     
   157     private void executeInBrowser() throws InterruptedException, URISyntaxException, IOException {
   158         wait = new CountDownLatch(1);
   159         server = initServer(".", true);
   160         final ServerConfiguration conf = server.getServerConfiguration();
   161         
   162         class DynamicResourceHandler extends HttpHandler {
   163             private final InvocationContext ic;
   164             public DynamicResourceHandler(InvocationContext ic) {
   165                 if (ic == null || ic.resources.isEmpty()) {
   166                     throw new NullPointerException();
   167                 }
   168                 this.ic = ic;
   169                 for (Resource r : ic.resources) {
   170                     conf.addHttpHandler(this, r.httpPath);
   171                 }
   172             }
   173 
   174             public void close() {
   175                 conf.removeHttpHandler(this);
   176             }
   177             
   178             @Override
   179             public void service(Request request, Response response) throws Exception {
   180                 for (Resource r : ic.resources) {
   181                     if (r.httpPath.equals(request.getRequestURI())) {
   182                         LOG.log(Level.INFO, "Serving HttpResource for {0}", request.getRequestURI());
   183                         response.setContentType(r.httpType);
   184                         r.httpContent.reset();
   185                         String[] params = null;
   186                         if (r.parameters.length != 0) {
   187                             params = new String[r.parameters.length];
   188                             for (int i = 0; i < r.parameters.length; i++) {
   189                                 params[i] = request.getParameter(r.parameters[i]);
   190                             }
   191                         }
   192                         
   193                         copyStream(r.httpContent, response.getOutputStream(), null, params);
   194                     }
   195                 }
   196             }
   197         }
   198         
   199         conf.addHttpHandler(new Page(resources, 
   200             "org/apidesign/bck2brwsr/launcher/harness.xhtml"
   201         ), "/execute");
   202         
   203         conf.addHttpHandler(new HttpHandler() {
   204             int cnt;
   205             List<InvocationContext> cases = new ArrayList<>();
   206             DynamicResourceHandler prev;
   207             @Override
   208             public void service(Request request, Response response) throws Exception {
   209                 String id = request.getParameter("request");
   210                 String value = request.getParameter("result");
   211                 if (value != null && value.indexOf((char)0xC5) != -1) {
   212                     value = toUTF8(value);
   213                 }
   214                 
   215                 
   216                 InvocationContext mi = null;
   217                 int caseNmbr = -1;
   218                 
   219                 if (id != null && value != null) {
   220                     LOG.log(Level.INFO, "Received result for case {0} = {1}", new Object[]{id, value});
   221                     value = decodeURL(value);
   222                     int indx = Integer.parseInt(id);
   223                     cases.get(indx).result(value, null);
   224                     if (++indx < cases.size()) {
   225                         mi = cases.get(indx);
   226                         LOG.log(Level.INFO, "Re-executing case {0}", indx);
   227                         caseNmbr = indx;
   228                     }
   229                 } else {
   230                     if (!cases.isEmpty()) {
   231                         LOG.info("Re-executing test cases");
   232                         mi = cases.get(0);
   233                         caseNmbr = 0;
   234                     }
   235                 }
   236                 
   237                 if (prev != null) {
   238                     prev.close();
   239                     prev = null;
   240                 }
   241                 
   242                 if (mi == null) {
   243                     mi = methods.take();
   244                     caseNmbr = cnt++;
   245                 }
   246                 if (mi == END) {
   247                     response.getWriter().write("");
   248                     wait.countDown();
   249                     cnt = 0;
   250                     LOG.log(Level.INFO, "End of data reached. Exiting.");
   251                     return;
   252                 }
   253                 
   254                 if (!mi.resources.isEmpty()) {
   255                     prev = new DynamicResourceHandler(mi);
   256                 }
   257                 
   258                 cases.add(mi);
   259                 final String cn = mi.clazz.getName();
   260                 final String mn = mi.methodName;
   261                 LOG.log(Level.INFO, "Request for {0} case. Sending {1}.{2}", new Object[]{caseNmbr, cn, mn});
   262                 response.getWriter().write("{"
   263                     + "className: '" + cn + "', "
   264                     + "methodName: '" + mn + "', "
   265                     + "request: " + caseNmbr
   266                 );
   267                 if (mi.html != null) {
   268                     response.getWriter().write(", html: '");
   269                     response.getWriter().write(encodeJSON(mi.html));
   270                     response.getWriter().write("'");
   271                 }
   272                 response.getWriter().write("}");
   273             }
   274         }, "/data");
   275 
   276         this.brwsr = launchServerAndBrwsr(server, "/execute");
   277     }
   278     
   279     private static String encodeJSON(String in) {
   280         StringBuilder sb = new StringBuilder();
   281         for (int i = 0; i < in.length(); i++) {
   282             char ch = in.charAt(i);
   283             if (ch < 32 || ch == '\'' || ch == '"') {
   284                 sb.append("\\u");
   285                 String hs = "0000" + Integer.toHexString(ch);
   286                 hs = hs.substring(hs.length() - 4);
   287                 sb.append(hs);
   288             } else {
   289                 sb.append(ch);
   290             }
   291         }
   292         return sb.toString();
   293     }
   294     
   295     @Override
   296     public void shutdown() throws IOException {
   297         methods.offer(END);
   298         for (;;) {
   299             int prev = methods.size();
   300             try {
   301                 if (wait != null && wait.await(timeOut, TimeUnit.MILLISECONDS)) {
   302                     break;
   303                 }
   304             } catch (InterruptedException ex) {
   305                 throw new IOException(ex);
   306             }
   307             if (prev == methods.size()) {
   308                 LOG.log(
   309                     Level.WARNING, 
   310                     "Timeout and no test has been executed meanwhile (at {0}). Giving up.", 
   311                     methods.size()
   312                 );
   313                 break;
   314             }
   315             LOG.log(Level.INFO, 
   316                 "Timeout, but tests got from {0} to {1}. Trying again.", 
   317                 new Object[]{prev, methods.size()}
   318             );
   319         }
   320         stopServerAndBrwsr(server, brwsr);
   321     }
   322     
   323     static void copyStream(InputStream is, OutputStream os, String baseURL, String... params) throws IOException {
   324         for (;;) {
   325             int ch = is.read();
   326             if (ch == -1) {
   327                 break;
   328             }
   329             if (ch == '$' && params.length > 0) {
   330                 int cnt = is.read() - '0';
   331                 if (baseURL != null && cnt == 'U' - '0') {
   332                     os.write(baseURL.getBytes("UTF-8"));
   333                 } else {
   334                     if (cnt >= 0 && cnt < params.length) {
   335                         os.write(params[cnt].getBytes("UTF-8"));
   336                     } else {
   337                         os.write('$');
   338                         os.write(cnt + '0');
   339                     }
   340                 }
   341             } else {
   342                 os.write(ch);
   343             }
   344         }
   345     }
   346 
   347     private Object[] launchServerAndBrwsr(HttpServer server, final String page) throws IOException, URISyntaxException, InterruptedException {
   348         server.start();
   349         NetworkListener listener = server.getListeners().iterator().next();
   350         int port = listener.getPort();
   351         
   352         URI uri = new URI("http://localhost:" + port + page);
   353         return showBrwsr(uri);
   354     }
   355     private static String toUTF8(String value) throws UnsupportedEncodingException {
   356         byte[] arr = new byte[value.length()];
   357         for (int i = 0; i < arr.length; i++) {
   358             arr[i] = (byte)value.charAt(i);
   359         }
   360         return new String(arr, "UTF-8");
   361     }
   362     
   363     private static String decodeURL(String s) {
   364         for (;;) {
   365             int pos = s.indexOf('%');
   366             if (pos == -1) {
   367                 return s;
   368             }
   369             int i = Integer.parseInt(s.substring(pos + 1, pos + 2), 16);
   370             s = s.substring(0, pos) + (char)i + s.substring(pos + 2);
   371         }
   372     }
   373     
   374     private void stopServerAndBrwsr(HttpServer server, Object[] brwsr) throws IOException {
   375         if (brwsr == null) {
   376             return;
   377         }
   378         Process process = (Process)brwsr[0];
   379         
   380         server.stop();
   381         InputStream stdout = process.getInputStream();
   382         InputStream stderr = process.getErrorStream();
   383         drain("StdOut", stdout);
   384         drain("StdErr", stderr);
   385         process.destroy();
   386         int res;
   387         try {
   388             res = process.waitFor();
   389         } catch (InterruptedException ex) {
   390             throw new IOException(ex);
   391         }
   392         LOG.log(Level.INFO, "Exit code: {0}", res);
   393 
   394         deleteTree((File)brwsr[1]);
   395     }
   396     
   397     private static void drain(String name, InputStream is) throws IOException {
   398         int av = is.available();
   399         if (av > 0) {
   400             StringBuilder sb = new StringBuilder();
   401             sb.append("v== ").append(name).append(" ==v\n");
   402             while (av-- > 0) {
   403                 sb.append((char)is.read());
   404             }
   405             sb.append("\n^== ").append(name).append(" ==^");
   406             LOG.log(Level.INFO, sb.toString());
   407         }
   408     }
   409 
   410     private void deleteTree(File file) {
   411         if (file == null) {
   412             return;
   413         }
   414         File[] arr = file.listFiles();
   415         if (arr != null) {
   416             for (File s : arr) {
   417                 deleteTree(s);
   418             }
   419         }
   420         file.delete();
   421     }
   422 
   423     @Override
   424     public void close() throws IOException {
   425         shutdown();
   426     }
   427 
   428     protected Object[] showBrwsr(URI uri) throws IOException {
   429         LOG.log(Level.INFO, "Showing {0}", uri);
   430         if (cmd == null) {
   431             try {
   432                 LOG.log(Level.INFO, "Trying Desktop.browse on {0} {2} by {1}", new Object[] {
   433                     System.getProperty("java.vm.name"),
   434                     System.getProperty("java.vm.vendor"),
   435                     System.getProperty("java.vm.version"),
   436                 });
   437                 java.awt.Desktop.getDesktop().browse(uri);
   438                 LOG.log(Level.INFO, "Desktop.browse successfully finished");
   439                 return null;
   440             } catch (UnsupportedOperationException ex) {
   441                 LOG.log(Level.INFO, "Desktop.browse not supported: {0}", ex.getMessage());
   442                 LOG.log(Level.FINE, null, ex);
   443             }
   444         }
   445         {
   446             String cmdName = cmd == null ? "xdg-open" : cmd;
   447             String[] cmdArr = { 
   448                 cmdName, uri.toString()
   449             };
   450             LOG.log(Level.INFO, "Launching {0}", Arrays.toString(cmdArr));
   451             final Process process = Runtime.getRuntime().exec(cmdArr);
   452             return new Object[] { process, null };
   453         }
   454     }
   455 
   456     void generateBck2BrwsrJS(StringBuilder sb, Bck2Brwsr.Resources loader) throws IOException {
   457         Bck2Brwsr.generate(sb, loader);
   458         sb.append(
   459             "(function WrapperVM(global) {"
   460             + "  function ldCls(res) {\n"
   461             + "    var request = new XMLHttpRequest();\n"
   462             + "    request.open('GET', '/classes/' + res, false);\n"
   463             + "    request.send();\n"
   464             + "    if (request.status !== 200) return null;\n"
   465             + "    var arr = eval('(' + request.responseText + ')');\n"
   466             + "    return arr;\n"
   467             + "  }\n"
   468             + "  var prevvm = global.bck2brwsr;\n"
   469             + "  global.bck2brwsr = function() {\n"
   470             + "    var args = Array.prototype.slice.apply(arguments);\n"
   471             + "    args.unshift(ldCls);\n"
   472             + "    return prevvm.apply(null, args);\n"
   473             + "  };\n"
   474             + "})(this);\n");
   475     }
   476 
   477     private class Res implements Bck2Brwsr.Resources {
   478         @Override
   479         public InputStream get(String resource) throws IOException {
   480             for (ClassLoader l : loaders) {
   481                 URL u = null;
   482                 Enumeration<URL> en = l.getResources(resource);
   483                 while (en.hasMoreElements()) {
   484                     u = en.nextElement();
   485                 }
   486                 if (u != null) {
   487                     return u.openStream();
   488                 }
   489             }
   490             throw new IOException("Can't find " + resource);
   491         }
   492     }
   493 
   494     private static class Page extends HttpHandler {
   495         final String resource;
   496         private final String[] args;
   497         private final Res res;
   498         
   499         public Page(Res res, String resource, String... args) {
   500             this.res = res;
   501             this.resource = resource;
   502             this.args = args.length == 0 ? new String[] { "$0" } : args;
   503         }
   504 
   505         @Override
   506         public void service(Request request, Response response) throws Exception {
   507             String r = computePage(request);
   508             if (r.startsWith("/")) {
   509                 r = r.substring(1);
   510             }
   511             String[] replace = {};
   512             if (r.endsWith(".html")) {
   513                 response.setContentType("text/html");
   514                 LOG.info("Content type text/html");
   515                 replace = args;
   516             }
   517             if (r.endsWith(".xhtml")) {
   518                 response.setContentType("application/xhtml+xml");
   519                 LOG.info("Content type application/xhtml+xml");
   520                 replace = args;
   521             }
   522             OutputStream os = response.getOutputStream();
   523             try (InputStream is = res.get(r)) {
   524                 copyStream(is, os, request.getRequestURL().toString(), replace);
   525             } catch (IOException ex) {
   526                 response.setDetailMessage(ex.getLocalizedMessage());
   527                 response.setError();
   528                 response.setStatus(404);
   529             }
   530         }
   531 
   532         protected String computePage(Request request) {
   533             String r = resource;
   534             if (r == null) {
   535                 r = request.getHttpHandlerPath();
   536             }
   537             return r;
   538         }
   539     }
   540     
   541     private static class SubTree extends Page {
   542 
   543         public SubTree(Res res, String resource, String... args) {
   544             super(res, resource, args);
   545         }
   546 
   547         @Override
   548         protected String computePage(Request request) {
   549             return resource + request.getHttpHandlerPath();
   550         }
   551         
   552         
   553     }
   554 
   555     private class VM extends HttpHandler {
   556         @Override
   557         public void service(Request request, Response response) throws Exception {
   558             response.setCharacterEncoding("UTF-8");
   559             response.setContentType("text/javascript");
   560             StringBuilder sb = new StringBuilder();
   561             generateBck2BrwsrJS(sb, Bck2BrwsrLauncher.this.resources);
   562             response.getWriter().write(sb.toString());
   563         }
   564     }
   565 
   566     private static class Classes extends HttpHandler {
   567         private final Res loader;
   568 
   569         public Classes(Res loader) {
   570             this.loader = loader;
   571         }
   572 
   573         @Override
   574         public void service(Request request, Response response) throws Exception {
   575             String res = request.getHttpHandlerPath();
   576             if (res.startsWith("/")) {
   577                 res = res.substring(1);
   578             }
   579             try (InputStream is = loader.get(res)) {
   580                 response.setContentType("text/javascript");
   581                 Writer w = response.getWriter();
   582                 w.append("[");
   583                 for (int i = 0;; i++) {
   584                     int b = is.read();
   585                     if (b == -1) {
   586                         break;
   587                     }
   588                     if (i > 0) {
   589                         w.append(", ");
   590                     }
   591                     if (i % 20 == 0) {
   592                         w.write("\n");
   593                     }
   594                     if (b > 127) {
   595                         b = b - 256;
   596                     }
   597                     w.append(Integer.toString(b));
   598                 }
   599                 w.append("\n]");
   600             } catch (IOException ex) {
   601                 response.setStatus(HttpStatus.NOT_FOUND_404);
   602                 response.setError();
   603                 response.setDetailMessage(ex.getMessage());
   604             }
   605         }
   606     }
   607 }