Super Dev Mode: add -launcherDir flag

This provides a way to start a GWT application using Super Dev Mode
without having done a full compile of the GWT application ahead of
time.

The -launcherDir flag is optional and points to an output directory.
The code server will create a subdirectory for each module. On
startup and after each recompile, it will write out a nocache.js
file and any public resources in the module. (Note that we don't delete
files except for a nocache.js.gz file if it exists.)

For example, -launcherDir can be pointed to the expanded war
directory of a Java frontend server.

An upcoming patch will switch devmode's -superDevMode flag to use this
mechanism instead.

Change-Id: Iced1fb38d85d225c92e2d02b30e53675e1a18bfb
Review-Link: https://gwt-review.googlesource.com/#/c/9455/
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/Options.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/Options.java
index dc03310..882c179 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/Options.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/Options.java
@@ -60,6 +60,7 @@
   private boolean noPrecompile = false;
   private boolean isCompileTest = false;
   private File workDir;
+  private File launcherDir;
   private final List<String> moduleNames = new ArrayList<String>();
   private boolean allowMissingSourceDir = false;
   private final List<File> sourcePath = new ArrayList<File>();
@@ -189,6 +190,15 @@
   }
 
   /**
+   * A directory where each module's files for launching Super Dev Mode should be written,
+   * or null if not supplied.
+   * (For example, nocache.js and public resource files will go here.)
+   */
+  File getLauncherDir() {
+    return launcherDir;
+  }
+
+  /**
    * The names of the module that will be compiled (along with all its dependencies).
    */
   List<String> getModuleNames() {
@@ -300,6 +310,7 @@
       registerHandler(new SourceFlag());
       registerHandler(new StrictResourcesFlag());
       registerHandler(new WorkDirFlag());
+      registerHandler(new LauncherDir());
       registerHandler(new ArgHandlerIncrementalCompile(new OptionIncrementalCompile() {
         @Override
         public boolean isIncrementalCompileEnabled() {
@@ -625,6 +636,48 @@
     }
   }
 
+  private class LauncherDir extends ArgHandler {
+
+    @Override
+    public String getTag() {
+      return "-launcherDir";
+    }
+
+    @Override
+    public String[] getTags() {
+      // add an alias since in DevMode this was "-war"
+      return new String[] {getTag(), "-war"};
+    }
+
+    @Override
+    public String[] getTagArgs() {
+      return new String[0];
+    }
+
+    @Override
+    public String getPurpose() {
+      return "An output directory where files for launching Super Dev Mode will be written. "
+          + "(Optional.)";
+    }
+
+    @Override
+    public int handle(String[] args, int startIndex) {
+      if (startIndex + 1 >= args.length) {
+        System.err.println(getTag() + " should be followed by the name of a directory");
+        return -1;
+      }
+
+      File candidate = new File(args[startIndex + 1]);
+      if (!candidate.isDirectory()) {
+        System.err.println("not a directory: " + candidate);
+        return -1;
+      }
+
+      launcherDir = candidate;
+      return 1;
+    }
+  }
+
   private class ModuleNameArgument extends ArgHandlerExtra {
 
     @Override
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java
index 53f9c30..331b9a8 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java
@@ -51,6 +51,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.net.URL;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -196,11 +197,11 @@
     module.getCompilationState(compileLogger, compilerContext);
 
     setUpCompileDir(compileDir, module, compileLogger);
+    writeLauncherFiles(module, compileLogger);
 
     outputModuleName.set(module.getName());
     lastBuild.set(compileDir);
 
-
     long elapsedTime = System.currentTimeMillis() - startTime;
     compileLogger.log(TreeLogger.Type.INFO, "Module setup completed in " + elapsedTime + " ms");
 
@@ -224,18 +225,7 @@
           compileLogger.log(Type.WARN, "cannot create directory: " + outputDir);
         }
       }
-
-      // Copy the public resources to the output.
-      ResourceOracleImpl publicResources = module.getPublicResourceOracle();
-      for (String pathName : publicResources.getPathNames()) {
-        File file = new File(outputDir, pathName);
-        File parent = file.getParentFile();
-        if (!parent.isDirectory() && !parent.mkdirs()) {
-          compileLogger.log(Type.ERROR, "cannot create directory: " + parent);
-          throw new UnableToCompleteException();
-        }
-        Files.asByteSink(file).writeFrom(publicResources.getResourceAsStream(pathName));
-      }
+      writePublicResources(outputDir, module, compileLogger);
 
       // Create a "module_name.nocache.js" that calculates the permutation and forces a recompile.
       String nocacheJs = generateModuleRecompileJs(module, compileLogger);
@@ -251,6 +241,21 @@
     }
   }
 
+  private static void writePublicResources(File moduleOutputDir, ModuleDef module,
+      TreeLogger compileLogger) throws UnableToCompleteException, IOException {
+    // Copy the public resources to the output.
+    ResourceOracleImpl publicResources = module.getPublicResourceOracle();
+    for (String pathName : publicResources.getPathNames()) {
+      File file = new File(moduleOutputDir, pathName);
+      File parent = file.getParentFile();
+      if (!parent.isDirectory() && !parent.mkdirs()) {
+        compileLogger.log(Type.ERROR, "cannot create directory: " + parent);
+        throw new UnableToCompleteException();
+      }
+      Files.asByteSink(file).writeFrom(publicResources.getResourceAsStream(pathName));
+    }
+  }
+
   /**
    * Generates the nocache.js file to use when precompile is not on.
    */
@@ -333,6 +338,7 @@
       String moduleName = outputModuleName.get();
       writeRecompileNoCacheJs(new File(publishedCompileDir.getWarDir(), moduleName), moduleName,
           recompileJs, compileLogger);
+      writeLauncherFiles(module, compileLogger);
     } else {
       // always recompile after an error
       lastBuildInput = null;
@@ -355,6 +361,61 @@
   }
 
   /**
+   * Updates files for launching Super Dev Mode.
+   */
+  private void writeLauncherFiles(ModuleDef module, TreeLogger compileLogger)
+      throws UnableToCompleteException {
+
+    File launcherDir = options.getLauncherDir();
+    if (launcherDir == null) {
+      return; // not turned on
+    }
+
+    File moduleDir = new File(launcherDir + "/" + module.getName());
+    if (!moduleDir.isDirectory()) {
+      if (!moduleDir.mkdirs()) {
+        compileLogger.log(Type.ERROR, "Can't create launcher dir for module: " + moduleDir);
+        throw new UnableToCompleteException();
+      }
+    }
+
+    try {
+      String stub = generateStubNocacheJs(module.getName());
+
+      final File noCacheJs = new File(moduleDir, module.getName() + ".nocache.js");
+      Files.write(stub, noCacheJs, Charsets.UTF_8);
+
+      // Remove gz file so it doesn't get used instead.
+      // (We may be writing to an existing war directory.)
+      final File noCacheJsGz = new File(noCacheJs.getPath() + ".gz");
+      if (noCacheJsGz.exists()) {
+        if (!noCacheJsGz.delete()) {
+          compileLogger.log(Type.ERROR, "cannot delete file: " + noCacheJsGz);
+          throw new UnableToCompleteException();
+        }
+      }
+
+      writePublicResources(moduleDir, module, compileLogger);
+
+    } catch (IOException e) {
+      compileLogger.log(Type.ERROR, "Can't update launcher dir", e);
+      throw new UnableToCompleteException();
+    }
+  }
+
+  /**
+   * Returns the contents of a nocache.js file that will compile and then run a GWT application
+   * using Super Dev Mode.
+   */
+  private String generateStubNocacheJs(String outputModuleName) throws IOException {
+    URL url = Resources.getResource(Recompiler.class, "stub.nocache.js");
+    final String template = Resources.toString(url, Charsets.UTF_8);
+    return template
+        .replace("__MODULE_NAME__", outputModuleName)
+        .replace("__SUPERDEV_PORT__", String.valueOf(options.getPort()));
+  }
+
+  /**
    * Returns the log from the last compile. (It may be a failed build.)
    */
   File getLastLog() {