Bug 938704 - OS.File + ES6 - WIP draft
authorDavid Rajchenbach-Teller <dteller@mozilla.com>
Sat, 08 Aug 2015 08:13:49 +0200
changeset 305009 6836f01947fb4dacf4b9a27dd5375cd8ff06f3af
parent 296070 001942e4617b2324bfa6cdfb1155581cbc3f0cc4
child 510754 4aec5a27d8f53ae0445d76c42ebf9aed0c9675e5
push id7024
push userdteller@mozilla.com
push dateMon, 02 Nov 2015 10:11:06 +0000
bugs938704
milestone44.0a1
Bug 938704 - OS.File + ES6 - WIP
toolkit/components/osfile/modules/osfile_async_front.jsm
toolkit/components/osfile/modules/osfile_async_worker.js
toolkit/components/osfile/modules/osfile_shared_front.jsm
toolkit/components/osfile/modules/osfile_unix_allthreads.jsm
toolkit/components/osfile/modules/osfile_unix_front.jsm
toolkit/components/osfile/modules/osfile_win_allthreads.jsm
toolkit/components/osfile/modules/osfile_win_front.jsm
toolkit/components/osfile/tests/xpcshell/test_removeDir.js
--- a/toolkit/components/osfile/modules/osfile_async_front.jsm
+++ b/toolkit/components/osfile/modules/osfile_async_front.jsm
@@ -1235,187 +1235,135 @@ var DirectoryIterator = function Directo
    * Open the iterator on the worker thread
    *
    * @type {Promise}
    * @resolves {*} A message accepted by the methods of DirectoryIterator
    * in the worker thread
    * @rejects {StopIteration} If all entries have already been visited
    * or the iterator has been closed.
    */
-  this.__itmsg = Scheduler.post(
+  this._itmsg = Scheduler.post(
     "new_DirectoryIterator", [Type.path.toMsg(path), options],
     path
   );
   this._isClosed = false;
 };
 DirectoryIterator.prototype = {
-  iterator: function () this,
-  __iterator__: function () this,
-
-  // Once close() is called, _itmsg should reject with a
-  // StopIteration. However, we don't want to create the promise until
-  // it's needed because it might never be used. In that case, we
-  // would get a warning on the console.
-  get _itmsg() {
-    if (!this.__itmsg) {
-      this.__itmsg = Promise.reject(StopIteration);
-    }
-    return this.__itmsg;
-  },
-
   /**
    * Determine whether the directory exists.
    *
    * @resolves {boolean}
    */
   exists: function exists() {
     return this._itmsg.then(
       function onSuccess(iterator) {
         return Scheduler.post("DirectoryIterator_prototype_exists", [iterator]);
       }
     );
   },
   /**
    * Get the next entry in the directory.
    *
-   * @return {Promise}
-   * @resolves {OS.File.Entry}
-   * @rejects {StopIteration} If all entries have already been visited.
+   * @return {Promise} // FIXME
    */
   next: function next() {
-    let self = this;
-    let promise = this._itmsg;
-
-    // Get the iterator, call _next
-    promise = promise.then(
-      function withIterator(iterator) {
-        return self._next(iterator);
-      });
-
-    return promise;
+    return this._itmsg.then(iterator => this._next(iterator));
   },
   /**
    * Get several entries at once.
    *
    * @param {number=} length If specified, the number of entries
    * to return. If unspecified, return all remaining entries.
    * @return {Promise}
    * @resolves {Array} An array containing the |length| next entries.
    */
-  nextBatch: function nextBatch(size) {
+  nextBatch: Task.async(function*() {
     if (this._isClosed) {
-      return Promise.resolve([]);
+      return [];
     }
-    let promise = this._itmsg;
-    promise = promise.then(
-      function withIterator(iterator) {
-        return Scheduler.post("DirectoryIterator_prototype_nextBatch", [iterator, size]);
-      });
-    promise = promise.then(
-      function withEntries(array) {
-        return array.map(DirectoryIterator.Entry.fromMsg);
-      });
-    return promise;
-  },
+    let iterator = yield this._itmsg;
+    let array = yield Scheduler.post("DirectoryIterator_prototype_nextBatch", [iterator, size]);
+    return array.map(DirectoryIterator.Entry.fromMsg);
+  }),
+
   /**
    * Apply a function to all elements of the directory sequentially.
    *
    * @param {Function} cb This function will be applied to all entries
    * of the directory. It receives as arguments
    *  - the OS.File.Entry corresponding to the entry;
    *  - the index of the entry in the enumeration;
    *  - the iterator itself - return |iterator.close()| to stop the loop.
    *
    * If the callback returns a promise, iteration waits until the
    * promise is resolved before proceeding.
    *
    * @return {Promise} A promise resolved once the loop has reached
    * its end.
    */
-  forEach: function forEach(cb, options) {
+  forEach: Task.async(function*(cb, options) {
     if (this._isClosed) {
-      return Promise.resolve();
+      return;
     }
-
-    let self = this;
     let position = 0;
-    let iterator;
-
-    // Grab iterator
-    let promise = this._itmsg.then(
-      function(aIterator) {
-        iterator = aIterator;
+    let iterator = yield this._itmsg;
+    while (true) {
+      let {value, done} = yield this._next(iterator);
+      if (done) {
+        return;
       }
-    );
 
-    // Then iterate
-    let loop = function loop() {
-      if (self._isClosed) {
-        return Promise.resolve();
-      }
-      return self._next(iterator).then(
-        function onSuccess(value) {
-          return Promise.resolve(cb(value, position++, self)).then(loop);
-        },
-        function onFailure(reason) {
-          if (reason == StopIteration) {
-            return;
-          }
-          throw reason;
-        }
-      );
-    };
-
-    return promise.then(loop);
-  },
+      dump(`osfile_async_front: forEach: ${value}, ${Object.keys(value)}\n`);
+      dump(`=> ${value._name}\n`);
+      yield cb(value, position++, this);
+    }
+  }),
   /**
    * Auxiliary method: fetch the next item
    *
    * @rejects {StopIteration} If all entries have already been visited
-   * or the iterator has been closed.
+   * or the iterator has been closed. // FIXME
    */
-  _next: function _next(iterator) {
+  _next: Task.async(function*(iterator) {
     if (this._isClosed) {
-      return this._itmsg;
+      return { value: undefined, done: true };
     }
-    let self = this;
-    let promise = Scheduler.post("DirectoryIterator_prototype_next", [iterator]);
-    promise = promise.then(
-      DirectoryIterator.Entry.fromMsg,
-      function onReject(reason) {
-        if (reason == StopIteration) {
-          self.close();
-          throw StopIteration;
-        }
-        throw reason;
-      });
-    return promise;
-  },
+    let {value, done} = yield Scheduler.post("DirectoryIterator_prototype_next", [iterator]);
+    if (done) {
+      this.close();
+      return { value: undefined, done: true };
+    }
+    return { value: DirectoryIterator.Entry.fromMsg(value), done: false };
+  }),
   /**
    * Close the iterator
    */
-  close: function close() {
+  close: Task.async(function*() {
     if (this._isClosed) {
-      return Promise.resolve();
+      return;
     }
     this._isClosed = true;
-    let self = this;
-    return this._itmsg.then(
-      function withIterator(iterator) {
-        // Set __itmsg to null so that the _itmsg getter returns a
-        // rejected StopIteration promise if it's ever used.
-        self.__itmsg = null;
-        return Scheduler.post("DirectoryIterator_prototype_close", [iterator]);
-      }
-    );
-  }
+    let iterator = yield this._itmsg;
+    this._itmsg = null;
+    return Scheduler.post("DirectoryIterator_prototype_close", [iterator]);
+  }),
 };
 
+/**
+ * Allow iterating with |for|
+ */
+DirectoryIterator.prototype[Symbol.iterator] = function() {
+  return { value: this.next(), done: false };
+};
+DirectoryIterator.prototype.iterator = DirectoryIterator.prototype[Symbol.iterator];
+
 DirectoryIterator.Entry = function Entry(value) {
-  return value;
+  for (let k of Object.keys(value)) {
+    this[k] = value[k];
+  }
 };
 DirectoryIterator.Entry.prototype = Object.create(SysAll.AbstractEntry.prototype);
 
 DirectoryIterator.Entry.fromMsg = function fromMsg(value) {
   return new DirectoryIterator.Entry(value);
 };
 
 File.resetWorker = function() {
--- a/toolkit/components/osfile/modules/osfile_async_worker.js
+++ b/toolkit/components/osfile/modules/osfile_async_worker.js
@@ -354,24 +354,21 @@ if (this.Components) {
        function do_flush() {
          return this.flush();
        });
    },
    // Methods of OS.File.DirectoryIterator
    DirectoryIterator_prototype_next: function next(dir) {
      return withDir(dir,
        function do_next() {
-         try {
-           return File.DirectoryIterator.Entry.toMsg(this.next());
-         } catch (x) {
-           if (x == StopIteration) {
-             OpenedDirectoryIterators.remove(dir);
-           }
-           throw x;
+         let next = this.next();
+         if (next.done) {
+           OpenedDirectoryIterators.remove(dir);
          }
+         return next;
        }, false);
    },
    DirectoryIterator_prototype_nextBatch: function nextBatch(dir, size) {
      return withDir(dir,
        function do_nextBatch() {
          let result;
          try {
            result = this.nextBatch(size);
--- a/toolkit/components/osfile/modules/osfile_shared_front.jsm
+++ b/toolkit/components/osfile/modules/osfile_shared_front.jsm
@@ -175,34 +175,28 @@ AbstractFile.openUnique = function openU
 
 /**
  * Code shared by iterators.
  */
 AbstractFile.AbstractIterator = function AbstractIterator() {
 };
 AbstractFile.AbstractIterator.prototype = {
   /**
-   * Allow iterating with |for|
-   */
-  __iterator__: function __iterator__() {
-    return this;
-  },
-  /**
    * Apply a function to all elements of the directory sequentially.
    *
    * @param {Function} cb This function will be applied to all entries
    * of the directory. It receives as arguments
    *  - the OS.File.Entry corresponding to the entry;
    *  - the index of the entry in the enumeration;
    *  - the iterator itself - calling |close| on the iterator stops
    *   the loop.
    */
   forEach: function forEach(cb) {
     let index = 0;
-    for (let entry in this) {
+    for (let entry of this) {
       cb(entry, index++, this);
     }
   },
   /**
    * Return several entries at once.
    *
    * Entries are returned in the same order as a walk with |forEach| or
    * |for(...)|.
@@ -221,16 +215,25 @@ AbstractFile.AbstractIterator.prototype 
         return array;
       }
     }
     return array;
   }
 };
 
 /**
+ * Allow iterating with |for|
+ */
+AbstractFile.AbstractIterator.prototype[Symbol.iterator] = function() {
+  return {
+    next: () => this.next()
+  }
+};
+
+/**
  * Utility function shared by implementations of |OS.File.open|:
  * extract read/write/trunc/create/existing flags from a |mode|
  * object.
  *
  * @param {*=} mode An object that may contain fields |read|,
  * |write|, |truncate|, |create|, |existing|. These fields
  * are interpreted only if true-ish.
  * @return {{read:bool, write:bool, trunc:bool, create:bool,
@@ -466,45 +469,53 @@ AbstractFile.writeAtomic =
 };
 
 /**
  * This function is used by removeDir to avoid calling lstat for each
  * files under the specified directory. External callers should not call
  * this function directly.
  */
 AbstractFile.removeRecursive = function(path, options = {}) {
+  dump(`removeRecursive 1\n`);
   let iterator = new OS.File.DirectoryIterator(path);
   if (!iterator.exists()) {
     if (!("ignoreAbsent" in options) || options.ignoreAbsent) {
       return;
     }
   }
 
+  dump(`removeRecursive 2\n`);
   try {
-    for (let entry in iterator) {
+    for (let entry of iterator) {
+      dump(`removeRecursive 3: ${entry}\n`);
       if (entry.isDir) {
         if (entry.isLink) {
           // Unlike Unix symlinks, NTFS junctions or NTFS symlinks to
           // directories are directories themselves. OS.File.remove()
           // will not work for them.
           OS.File.removeEmptyDir(entry.path, options);
+          dump(`removeRecursive 4\n`);
         } else {
           // Normal directories.
           AbstractFile.removeRecursive(entry.path, options);
+          dump(`removeRecursive 5\n`);
         }
       } else {
         // NTFS symlinks to files, Unix symlinks, or regular files.
         OS.File.remove(entry.path, options);
+        dump(`removeRecursive 6\n`);
       }
     }
+    dump(`removeRecursive 7\n`);
   } finally {
     iterator.close();
   }
-
+  dump(`removeRecursive 8\n`);
   OS.File.removeEmptyDir(path);
+  dump(`removeRecursive 9\n`);
 };
 
 /**
  * Create a directory and, optionally, its parent directories.
  *
  * @param {string} path The name of the directory.
  * @param {*=} options Additional options.
  *
--- a/toolkit/components/osfile/modules/osfile_unix_allthreads.jsm
+++ b/toolkit/components/osfile/modules/osfile_unix_allthreads.jsm
@@ -81,17 +81,17 @@ libc.declareLazy(LazyBindings, "strerror
  */
 var OSError = function OSError(operation = "unknown operation",
                                errno = ctypes.errno, path = "") {
   SharedAll.OSError.call(this, operation, path);
   this.unixErrno = errno;
 };
 OSError.prototype = Object.create(SharedAll.OSError.prototype);
 OSError.prototype.toString = function toString() {
-  return "Unix error " + this.unixErrno +
+  return "OSError: Unix error " + this.unixErrno +
     " during operation " + this.operation +
     (this.path? " on file " + this.path : "") +
     " (" + LazyBindings.strerror(this.unixErrno).readString() + ")";
 };
 OSError.prototype.toMsg = function toMsg() {
   return OSError.toMsg(this);
 };
 
@@ -279,21 +279,20 @@ exports.AbstractInfo = AbstractInfo;
 
 /**
  * Code shared by implementations of File.DirectoryIterator.Entry on Unix
  *
  * @constructor
 */
 var AbstractEntry = function AbstractEntry(isDir, isSymLink, name, path) {
   this._isDir = isDir;
-  this._isSymlLink = isSymLink;
+  this._isSymLink = isSymLink;
   this._name = name;
   this._path = path;
 };
-
 AbstractEntry.prototype = {
   /**
    * |true| if the entry is a directory, |false| otherwise
    */
   get isDir() {
     return this._isDir;
   },
   /**
--- a/toolkit/components/osfile/modules/osfile_unix_front.jsm
+++ b/toolkit/components/osfile/modules/osfile_unix_front.jsm
@@ -746,26 +746,27 @@
      File.DirectoryIterator.prototype = Object.create(exports.OS.Shared.AbstractFile.AbstractIterator.prototype);
 
      /**
       * Return the next entry in the directory, if any such entry is
       * available.
       *
       * Skip special directories "." and "..".
       *
-      * @return {File.Entry} The next entry in the directory.
-      * @throws {StopIteration} Once all files in the directory have been
-      * encountered.
+      * @return By definition of the iterator protocol, either
+      * `{value: {File.Entry}, done: false}` if there is an unvisited
+      * entry in the directory, or `{value: undefined, done: true}`,
+      * otherwise.
       */
      File.DirectoryIterator.prototype.next = function next() {
        if (!this._exists) {
          throw File.Error.noSuchFile("DirectoryIterator.prototype.next", this._path);
        }
        if (this._closed) {
-         throw StopIteration;
+        return {value: undefined, done: true};
        }
        for (let entry = UnixFile.readdir(this._dir);
             entry != null && !entry.isNull();
             entry = UnixFile.readdir(this._dir)) {
          let contents = entry.contents;
          let name = contents.d_name.readString();
          if (name == "." || name == "..") {
            continue;
@@ -778,20 +779,20 @@
            throw_on_negative("lstat", UnixFile.lstat(path, gStatDataPtr), this._path);
            isDir = (gStatData.st_mode & Const.S_IFMT) == Const.S_IFDIR;
            isSymLink = (gStatData.st_mode & Const.S_IFMT) == Const.S_IFLNK;
          } else {
            isDir = contents.d_type == Const.DT_DIR;
            isSymLink = contents.d_type == Const.DT_LNK;
          }
 
-         return new File.DirectoryIterator.Entry(isDir, isSymLink, name, this._path);
+         return {value: new File.DirectoryIterator.Entry(isDir, isSymLink, name, this._path), done: false}
        }
        this.close();
-       throw StopIteration;
+       return {value: undefined, done: true};
      };
 
      /**
       * Close the iterator and recover all resources.
       * You should call this once you have finished iterating on a directory.
       */
      File.DirectoryIterator.prototype.close = function close() {
        if (this._closed) return;
--- a/toolkit/components/osfile/modules/osfile_win_allthreads.jsm
+++ b/toolkit/components/osfile/modules/osfile_win_allthreads.jsm
@@ -104,17 +104,17 @@ OSError.prototype.toString = function to
     /* Minimum size of buffer */ 1024,
     /* Format args*/       null
   );
   if (!result) {
     buf = "additional error " +
       ctypes.winLastError +
       " while fetching system error message";
   }
-  return "Win error " + this.winLastError + " during operation "
+  return "OSError: Win error " + this.winLastError + " during operation "
     + this.operation + (this.path? " on file " + this.path : "") +
     " (" + buf.readString() + ")";
 };
 OSError.prototype.toMsg = function toMsg() {
   return OSError.toMsg(this);
 };
 
 /**
--- a/toolkit/components/osfile/modules/osfile_win_front.jsm
+++ b/toolkit/components/osfile/modules/osfile_win_front.jsm
@@ -794,32 +794,36 @@
      },
 
      /**
       * Return the next entry in the directory, if any such entry is
       * available.
       *
       * Skip special directories "." and "..".
       *
-      * @return {File.Entry} The next entry in the directory.
-      * @throws {StopIteration} Once all files in the directory have been
-      * encountered.
+      * @return By definition of the iterator protocol, either
+      * `{value: {File.Entry}, done: false}` if there is an unvisited
+      * entry in the directory, or `{value: undefined, done: true}`,
+      * otherwise.
       */
      File.DirectoryIterator.prototype.next = function next() {
          // FIXME: If we start supporting "\\?\"-prefixed paths, do not forget
          // that "." and ".." are absolutely normal file names if _path starts
          // with such prefix
          for (let entry = this._next(); entry != null; entry = this._next()) {
            let name = entry.cFileName.readString();
            if (name == "." || name == "..") {
              continue;
            }
-           return new File.DirectoryIterator.Entry(entry, this._path);
+           return {
+             value: new File.DirectoryIterator.Entry(entry, this._path),
+             done: false
+           };
          }
-         throw StopIteration;
+         return { value: undefined, done: true };
      };
 
      File.DirectoryIterator.prototype.close = function close() {
        if (this._closed) {
          return;
        }
        this._closed = true;
        if (this._handle) {
--- a/toolkit/components/osfile/tests/xpcshell/test_removeDir.js
+++ b/toolkit/components/osfile/tests/xpcshell/test_removeDir.js
@@ -13,83 +13,74 @@ do_register_cleanup(function() {
 });
 
 function run_test() {
   Services.prefs.setBoolPref("toolkit.osfile.log", true);
 
   run_next_test();
 }
 
-add_task(function() {
+add_task(function*() {
   // Set up profile. We create the directory in the profile, because the profile
   // is removed after every test run.
   do_get_profile();
 
   let file = OS.Path.join(OS.Constants.Path.profileDir, "file");
   let dir = OS.Path.join(OS.Constants.Path.profileDir, "directory");
   let file1 = OS.Path.join(dir, "file1");
   let file2 = OS.Path.join(dir, "file2");
   let subDir = OS.Path.join(dir, "subdir");
   let fileInSubDir = OS.Path.join(subDir, "file");
 
   // Sanity checking for the test
-  do_check_false((yield OS.File.exists(dir)));
+  Assert.ok(!(yield OS.File.exists(dir)), "The directory initially doesn't exist");
 
   // Remove non-existent directory
-  let exception = null;
-  try {
-    yield OS.File.removeDir(dir, {ignoreAbsent: false});
-  } catch (ex) {
-    exception = ex;
-  }
-
-  do_check_true(!!exception);
-  do_check_true(exception instanceof OS.File.Error);
+  yield Assert.rejects(OS.File.removeDir(dir, {ignoreAbsent: false}), /OSError/);
 
   // Remove non-existent directory with ignoreAbsent
   yield OS.File.removeDir(dir, {ignoreAbsent: true});
   yield OS.File.removeDir(dir);
 
   // Remove file with ignoreAbsent: false
   yield OS.File.writeAtomic(file, "content", { tmpPath: file + ".tmp" });
-  exception = null;
-  try {
-    yield OS.File.removeDir(file, {ignoreAbsent: false});
-  } catch (ex) {
-    exception = ex;
-  }
-
-  do_check_true(!!exception);
-  do_check_true(exception instanceof OS.File.Error);
+  Assert.rejects(OS.File.removeDir(file, {ignoreAbsent: false}), /OSError/);
 
   // Remove empty directory
   yield OS.File.makeDir(dir);
   yield OS.File.removeDir(dir);
-  do_check_false((yield OS.File.exists(dir)));
+  Assert.ok(!(yield OS.File.exists(dir)), "Directory has been removed");
 
   // Remove directory that contains one file
   yield OS.File.makeDir(dir);
+  do_print("Created directory");
+
   yield OS.File.writeAtomic(file1, "content", { tmpPath: file1 + ".tmp" });
+  do_print("Wrote something in the directory");
+
   yield OS.File.removeDir(dir);
-  do_check_false((yield OS.File.exists(dir)));
+  do_print("Removed directory");
+
+  Assert.ok(!(yield OS.File.exists(dir)), "Directory has been removed 2");
 
   // Remove directory that contains multiple files
   yield OS.File.makeDir(dir);
   yield OS.File.writeAtomic(file1, "content", { tmpPath: file1 + ".tmp" });
   yield OS.File.writeAtomic(file2, "content", { tmpPath: file2 + ".tmp" });
   yield OS.File.removeDir(dir);
-  do_check_false((yield OS.File.exists(dir)));
+  Assert.ok(!(yield OS.File.exists(dir)), "Directory has been removed 3");
 
   // Remove directory that contains a file and a directory
   yield OS.File.makeDir(dir);
   yield OS.File.writeAtomic(file1, "content", { tmpPath: file1 + ".tmp" });
   yield OS.File.makeDir(subDir);
   yield OS.File.writeAtomic(fileInSubDir, "content", { tmpPath: fileInSubDir + ".tmp" });
   yield OS.File.removeDir(dir);
-  do_check_false((yield OS.File.exists(dir)));
+  Assert.ok(!(yield OS.File.exists(dir)), "Directory has been removed 4");
+
 });
 
 add_task(function* test_unix_symlink() {
   // Windows does not implement OS.File.unixSymLink()
   if (OS.Constants.Win) {
     return;
   }