Super Dev Mode: skip directory if unable to create it

Also rename AppSpace => OutboxDir

Bug: issue 8619
Change-Id: Id937cadb4251f90beed25202816fd095a7a11be7
Review-Link: https://gwt-review.googlesource.com/#/c/9610/
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/AppSpace.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/AppSpace.java
deleted file mode 100644
index bd4eccf..0000000
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/AppSpace.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright 2011 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.google.gwt.dev.codeserver;
-
-import com.google.gwt.dev.util.Util;
-
-import java.io.File;
-import java.io.IOException;
-
-/**
- * The directory tree where Super Dev Mode keeps all the files for one GWT app.
- * Each time we recompile the app, we create a new subdirectory (see {@link CompileDir}).
- * In addition, there are some files that are shared between recompiles, such as
- * the unit cache.
- */
-class AppSpace {
-
-  static final String COMPILE_DIR_PREFIX = "compile-";
-  private final File root;
-
-  /**
-   * @see #create
-   */
-  private AppSpace(File root) {
-    this.root = root;
-  }
-
-  File getSpeedTracerLogFile() {
-    return new File(root, "speedtracer.html");
-  }
-
-  File getUnitCacheDir() {
-    return new File(root, "gwt-unitcache");
-  }
-
-  File getCompileDir(int compileId) {
-    return new File(root, COMPILE_DIR_PREFIX + compileId);
-  }
-
-  /**
-   * Creates an app directory, doing any cleanup needed.
-   * @param dir the directory to use. It need not exist, but
-   * the parent dir should exist.
-   */
-  static AppSpace create(File dir) throws IOException {
-    if (!dir.exists() && !dir.mkdir()) {
-      throw new IOException("can't create app directory: " + dir);
-    }
-
-    // clean up existing subdirectories
-    for (File candidate : dir.listFiles()) {
-      if (candidate.getName().startsWith(COMPILE_DIR_PREFIX)) {
-        System.err.println("deleting: " + candidate);
-        Util.recursiveDelete(candidate, false);
-      }
-    }
-
-    return new AppSpace(dir);
-  }
-}
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/CodeServer.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/CodeServer.java
index c83ce74..aeb7ebb 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/CodeServer.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/CodeServer.java
@@ -144,9 +144,9 @@
     int nextOutboxId = 1;
     OutboxTable outboxes = new OutboxTable();
     for (String moduleName : options.getModuleNames()) {
-      AppSpace appSpace = AppSpace.create(new File(workDir, moduleName));
+      OutboxDir outboxDir = OutboxDir.create(new File(workDir, moduleName), logger);
 
-      Recompiler recompiler = new Recompiler(appSpace, launcherDir, moduleName, options);
+      Recompiler recompiler = new Recompiler(outboxDir, launcherDir, moduleName, options);
 
       // The id should be treated as an opaque string since we will change it again.
       // TODO: change outbox id to include binding properties.
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/CompileDir.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/CompileDir.java
index 25ec834..6509cc0 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/CompileDir.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/CompileDir.java
@@ -31,7 +31,7 @@
  * the app, we will create a new, empty CompileDir. This way, a failed compile doesn't
  * modify the last good compile.
  *
- * <p>The CompileDir gets created within the appropriate {@link AppSpace} for the app
+ * <p>The CompileDir gets created within the appropriate {@link OutboxDir} for the app
  * being compiled.
  */
 public class CompileDir {
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/OutboxDir.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/OutboxDir.java
new file mode 100644
index 0000000..ee15477
--- /dev/null
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/OutboxDir.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.gwt.dev.codeserver;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.TreeLogger.Type;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.dev.util.Util;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * The directory tree where Super Dev Mode keeps all the files for one GWT app.
+ * Each time we recompile the app, we create a new subdirectory (see {@link CompileDir}).
+ * In addition, there are some files that are shared between recompiles, such as
+ * the unit cache.
+ */
+class OutboxDir {
+  private static final String COMPILE_DIR_PREFIX = "compile-";
+  private static final int MAX_CREATE_DIRECTORY_RETRIES = 50;
+
+  private final File root;
+  private int nextCompileId = 1;
+
+  /**
+   * @see #create
+   */
+  private OutboxDir(File root) {
+    this.root = root;
+  }
+
+  File getSpeedTracerLogFile() {
+    return new File(root, "speedtracer.html");
+  }
+
+  File getUnitCacheDir() {
+    return new File(root, "gwt-unitcache");
+  }
+
+  /**
+   * Creates a fresh, empty compile directory.
+   */
+  CompileDir makeCompileDir(TreeLogger logger)
+      throws UnableToCompleteException {
+
+    for (int i = 0; i < MAX_CREATE_DIRECTORY_RETRIES; i++) {
+      int candidateId = nextCompileId++;
+      File candidate = new File(root, COMPILE_DIR_PREFIX + candidateId);
+      try {
+        return CompileDir.create(candidate, logger);
+      } catch (UnableToCompleteException e) {
+        // try again
+      }
+    }
+
+    logger.log(Type.ERROR, "Gave up trying to create a compile directory.");
+    throw new UnableToCompleteException();
+  }
+
+  /**
+   * Creates an outbox directory, doing any cleanup needed.
+   * @param dir the directory to use. It need not exist, but
+   * the parent dir should exist.
+   */
+  static OutboxDir create(File dir, TreeLogger logger) throws IOException {
+    if (!dir.isDirectory() && !dir.mkdir()) {
+      throw new IOException("can't create app directory: " + dir);
+    }
+
+    File[] children = dir.listFiles();
+    if (children == null) {
+      throw new IOException("unable to list files in " + dir);
+    }
+
+    // Try to clean up existing subdirectories.
+    // (This is not guaranteed to delete all directories on Windows if a directory is locked.)
+    for (File candidate : children) {
+      if (candidate.getName().startsWith(COMPILE_DIR_PREFIX)) {
+        Util.recursiveDelete(candidate, false);
+        if (candidate.exists()) {
+          logger.log(Type.WARN, "unable to delete '" + candidate + "' (skipped)");
+        }
+      }
+    }
+
+    return new OutboxDir(dir);
+  }
+}
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 7093603..4f90ad0 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java
@@ -61,7 +61,7 @@
  */
 class Recompiler {
 
-  private final AppSpace appSpace;
+  private final OutboxDir outboxDir;
   private final LauncherDir launcherDir;
   private final String inputModuleName;
 
@@ -84,8 +84,8 @@
   private CompilerContext compilerContext;
   private Options options;
 
-  Recompiler(AppSpace appSpace, LauncherDir launcherDir, String inputModuleName, Options options) {
-    this.appSpace = appSpace;
+  Recompiler(OutboxDir outboxDir, LauncherDir launcherDir, String inputModuleName, Options options) {
+    this.outboxDir = outboxDir;
     this.launcherDir = launcherDir;
     this.inputModuleName = inputModuleName;
     this.options = options;
@@ -148,15 +148,15 @@
       System.setProperty("java.awt.headless", "true");
       if (System.getProperty("gwt.speedtracerlog") == null) {
         System.setProperty("gwt.speedtracerlog",
-            appSpace.getSpeedTracerLogFile().getAbsolutePath());
+            outboxDir.getSpeedTracerLogFile().getAbsolutePath());
       }
       compilerContext = compilerContextBuilder.unitCache(
-          UnitCacheSingleton.get(job.getLogger(), appSpace.getUnitCacheDir())).build();
+          UnitCacheSingleton.get(job.getLogger(), outboxDir.getUnitCacheDir())).build();
     }
 
     long startTime = System.currentTimeMillis();
     int compileId = ++compilesDone;
-    CompileDir compileDir = makeCompileDir(compileId, job.getLogger());
+    CompileDir compileDir = outboxDir.makeCompileDir(job.getLogger());
     TreeLogger compileLogger = makeCompileLogger(compileDir, job.getLogger());
 
     job.onStarted(compileId, compileDir);
@@ -182,7 +182,7 @@
       throws UnableToCompleteException {
 
     long startTime = System.currentTimeMillis();
-    CompileDir compileDir = makeCompileDir(++compilesDone, logger);
+    CompileDir compileDir = outboxDir.makeCompileDir(logger);
     TreeLogger compileLogger = makeCompileLogger(compileDir, logger);
 
     ModuleDef module = loadModule(Sets.<String>newHashSet(), compileLogger);
@@ -619,11 +619,6 @@
     }
   }
 
-  private CompileDir makeCompileDir(int compileId, TreeLogger logger)
-      throws UnableToCompleteException {
-    return CompileDir.create(appSpace.getCompileDir(compileId), logger);
-  }
-
   /**
    * Summarizes the inputs to a GWT compile. (Immutable.)
    * Two summaries should be equal if the compiler's inputs are equal (with high probability).
diff --git a/dev/codeserver/javatests/com/google/gwt/dev/codeserver/RecompilerTest.java b/dev/codeserver/javatests/com/google/gwt/dev/codeserver/RecompilerTest.java
index 2e6aba9..9cc7656 100644
--- a/dev/codeserver/javatests/com/google/gwt/dev/codeserver/RecompilerTest.java
+++ b/dev/codeserver/javatests/com/google/gwt/dev/codeserver/RecompilerTest.java
@@ -113,8 +113,9 @@
         fooResource);
     writeResourcesTo(originalResources, sourcePath);
 
-    Recompiler recompiler = new Recompiler(AppSpace.create(Files.createTempDir()), null,
-        "com.foo.SimpleModule", options);
+    Recompiler recompiler =
+        new Recompiler(OutboxDir.create(Files.createTempDir(), logger), null,
+            "com.foo.SimpleModule", options);
     Outbox outbox = new Outbox("Transactional Cache", recompiler, options, logger);
     OutboxTable outboxes = new OutboxTable();
     outboxes.addOutbox(outbox);