Bug 944164 - Implement proper redirection with ref counted underlying files, r=terrence
authorSteve Fink <sfink@mozilla.com>
Wed, 10 Jun 2015 11:21:03 -0700
changeset 288361 58b542930c53df069d9a742fffc3781c08be07ee
parent 288360 c3470e4402220b4496a831037d53e13e1419604c
child 288362 b9d035d4674401765b22c76280c13f9cef2e4e28
push id73403
push usersfink@mozilla.com
push dateSat, 12 Mar 2016 01:16:32 +0000
treeherdermozilla-inbound@58b542930c53 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersterrence
bugs944164
milestone48.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 944164 - Implement proper redirection with ref counted underlying files, r=terrence MozReview-Commit-ID: KVjZ0WXl9E6
js/src/shell/OSObject.cpp
js/src/shell/OSObject.h
js/src/shell/js.cpp
js/src/shell/jsshell.h
--- a/js/src/shell/OSObject.cpp
+++ b/js/src/shell/OSObject.cpp
@@ -37,16 +37,21 @@
 
 #ifdef XP_WIN
 # define PATH_MAX (MAX_PATH > _MAX_DIR ? MAX_PATH : _MAX_DIR)
 # define getcwd _getcwd
 #else
 # include <libgen.h>
 #endif
 
+using js::shell::RCFile;
+
+static RCFile** gErrFilePtr = nullptr;
+static RCFile** gOutFilePtr = nullptr;
+
 namespace js {
 namespace shell {
 
 #ifdef XP_WIN
 const char PathSeparator = '\\';
 #else
 const char PathSeparator = '/';
 #endif
@@ -301,63 +306,224 @@ osfile_writeTypedArrayToFile(JSContext* 
         JS_ReportError(cx, "can't write %s", filename.ptr());
         return false;
     }
 
     args.rval().setUndefined();
     return true;
 }
 
-static bool
-Redirect(JSContext* cx, FILE* fp, HandleString relFilename)
+/* static */ RCFile*
+RCFile::create(JSContext* cx, const char* filename, const char* mode)
+{
+    FILE* fp = fopen(filename, mode);
+    if (!fp)
+        return nullptr;
+
+    RCFile* file = cx->new_<RCFile>(fp);
+    if (!file) {
+        fclose(fp);
+        return nullptr;
+    }
+
+    return file;
+}
+
+void
+RCFile::close()
+{
+    if (fp)
+        fclose(fp);
+    fp = nullptr;
+}
+
+bool
+RCFile::release()
+{
+    if (--numRefs)
+        return false;
+    this->close();
+    return true;
+}
+
+class FileObject : public JSObject {
+    enum : uint32_t {
+        FILE_SLOT = 0,
+        NUM_SLOTS
+    };
+
+  public:
+    static const js::Class class_;
+
+    static FileObject* create(JSContext* cx, RCFile* file) {
+        JSObject* obj = js::NewObjectWithClassProto(cx, &class_, nullptr);
+        if (!obj)
+            return nullptr;
+
+        FileObject* fileObj = &obj->as<FileObject>();
+        fileObj->setRCFile(file);
+        file->acquire();
+        return fileObj;
+    }
+
+    static void finalize(FreeOp* fop, JSObject* obj) {
+        FileObject* fileObj = &obj->as<FileObject>();
+        RCFile* file = fileObj->rcFile();
+        if (file->release()) {
+            fileObj->setRCFile(nullptr);
+            fop->delete_(file);
+        }
+    }
+
+    bool isOpen() {
+        RCFile* file = rcFile();
+        return file && file->isOpen();
+    }
+
+    void close() {
+        if (!isOpen())
+            return;
+        rcFile()->close();
+    }
+
+    RCFile* rcFile() {
+        return reinterpret_cast<RCFile*>(js::GetReservedSlot(this, FILE_SLOT).toPrivate());
+    }
+
+  private:
+
+    void setRCFile(RCFile* file) {
+        js::SetReservedSlot(this, FILE_SLOT, PrivateValue(file));
+    }
+};
+
+const js::Class FileObject::class_ = {
+    "File",
+    JSCLASS_HAS_RESERVED_SLOTS(FileObject::NUM_SLOTS),
+    nullptr,               /* addProperty */
+    nullptr,               /* delProperty */
+    nullptr,               /* getProperty */
+    nullptr,               /* setProperty */
+    nullptr,               /* enumerate */
+    nullptr,               /* resolve */
+    nullptr,               /* mayResolve */
+    FileObject::finalize,  /* finalize */
+    nullptr,               /* call */
+    nullptr,               /* hasInstance */
+    nullptr,               /* construct */
+    nullptr                /* trace */
+};
+
+static FileObject*
+redirect(JSContext* cx, HandleString relFilename, RCFile** globalFile)
 {
     RootedString filename(cx, ResolvePath(cx, relFilename, RootRelative));
     if (!filename)
-        return false;
+        return nullptr;
     JSAutoByteString filenameABS(cx, filename);
     if (!filenameABS)
-        return false;
-    if (freopen(filenameABS.ptr(), "wb", fp) == nullptr) {
+        return nullptr;
+    RCFile* file = RCFile::create(cx, filenameABS.ptr(), "wb");
+    if (!file) {
         JS_ReportError(cx, "cannot redirect to %s: %s", filenameABS.ptr(), strerror(errno));
+        return nullptr;
+    }
+
+    // Grant the global gOutFile ownership of the new file, release ownership
+    // of its old file, and return a FileObject owning the old file.
+    file->acquire(); // Global owner of new file
+
+    FileObject* fileObj = FileObject::create(cx, *globalFile); // Newly created owner of old file
+    if (!fileObj) {
+        file->release();
+        return nullptr;
+    }
+
+    (*globalFile)->release(); // Release (global) ownership of old file.
+    *globalFile = file;
+
+    return fileObj;
+}
+
+static bool
+Redirect(JSContext* cx, const CallArgs& args, RCFile** outFile)
+{
+    if (args.length() > 1) {
+        JS_ReportErrorNumber(cx, js::shell::my_GetErrorMessage, nullptr, JSSMSG_INVALID_ARGS, "redirect");
         return false;
     }
+
+    RCFile* oldFile = *outFile;
+    RootedObject oldFileObj(cx, FileObject::create(cx, oldFile));
+    if (!oldFileObj)
+        return false;
+
+    if (args.get(0).isUndefined()) {
+        args.rval().setObject(*oldFileObj);
+        return true;
+    }
+
+    if (args[0].isObject()) {
+        RootedObject fileObj(cx, js::CheckedUnwrap(&args[0].toObject()));
+        if (!fileObj)
+            return false;
+
+        if (fileObj->is<FileObject>()) {
+            // Passed in a FileObject. Create a FileObject for the previous
+            // global file, and set the global file to the passed-in one.
+            *outFile = fileObj->as<FileObject>().rcFile();
+            (*outFile)->acquire();
+            oldFile->release();
+
+            args.rval().setObject(*oldFileObj);
+            return true;
+        }
+    }
+
+    RootedString filename(cx, JS::ToString(cx, args[0]));
+    if (!filename)
+        return false;
+
+    if (!redirect(cx, filename, outFile))
+        return false;
+
+    args.rval().setObject(*oldFileObj);
     return true;
 }
 
 static bool
-osfile_redirect(JSContext* cx, unsigned argc, Value* vp)
-{
+osfile_redirectOutput(JSContext* cx, unsigned argc, Value* vp) {
+    CallArgs args = CallArgsFromVp(argc, vp);
+    return Redirect(cx, args, gOutFilePtr);
+}
+
+static bool
+osfile_redirectError(JSContext* cx, unsigned argc, Value* vp) {
+    CallArgs args = CallArgsFromVp(argc, vp);
+    return Redirect(cx, args, gErrFilePtr);
+}
+
+static bool
+osfile_close(JSContext* cx, unsigned argc, Value* vp) {
     CallArgs args = CallArgsFromVp(argc, vp);
 
-    if (args.length() < 1 || args.length() > 2) {
-        JS_ReportErrorNumber(cx, my_GetErrorMessage, nullptr, JSSMSG_INVALID_ARGS, "redirect");
+    Rooted<FileObject*> fileObj(cx);
+    if (args.get(0).isObject()) {
+        JSObject *obj = js::CheckedUnwrap(&args[0].toObject());
+        if (obj->is<FileObject>())
+            fileObj = &obj->as<FileObject>();
+    }
+
+    if (!fileObj) {
+        JS_ReportErrorNumber(cx, js::shell::my_GetErrorMessage, nullptr,
+                             JSSMSG_INVALID_ARGS, "close");
         return false;
     }
 
-    if (args[0].isString() || args[0].isNull()) {
-        RootedString stdoutPath(cx);
-        if (!args[0].isNull()) {
-            stdoutPath = args[0].toString();
-            if (!stdoutPath)
-                return false;
-        }
-        if (!Redirect(cx, stdout, stdoutPath))
-            return false;
-    }
-
-    if (args.length() > 1 && (args[1].isString() || args[1].isNull())) {
-        RootedString stderrPath(cx);
-        if (!args[1].isNull()) {
-            stderrPath = args[1].toString();
-            if (!stderrPath)
-                return false;
-        }
-        if (!Redirect(cx, stderr, stderrPath))
-            return false;
-    }
+    fileObj->close();
 
     args.rval().setUndefined();
     return true;
 }
 
 static const JSFunctionSpecWithHelp osfile_functions[] = {
     JS_FN_HELP("readFile", osfile_readFile, 1, 0,
 "readFile(filename, [\"binary\"])",
@@ -372,21 +538,29 @@ static const JSFunctionSpecWithHelp osfi
     JS_FS_HELP_END
 };
 
 static const JSFunctionSpecWithHelp osfile_unsafe_functions[] = {
     JS_FN_HELP("writeTypedArrayToFile", osfile_writeTypedArrayToFile, 2, 0,
 "writeTypedArrayToFile(filename, data)",
 "  Write the contents of a typed array to the named file."),
 
-    JS_FN_HELP("redirect", osfile_redirect, 2, 0,
-"redirect(stdoutFilename[, stderrFilename])",
-"  Redirect stdout and/or stderr to the named file. Pass undefined to avoid\n"
-"   redirecting, or null to discard the output. Filenames are relative to the\n"
-"   current working directory."),
+    JS_FN_HELP("redirect", osfile_redirectOutput, 1, 0,
+"redirect([path-or-object])",
+"  Redirect print() output to the named file.\n"
+"   Return an opaque object representing the previous destination, which\n"
+"   may be passed into redirect() later to restore the output."),
+
+    JS_FN_HELP("redirectErr", osfile_redirectError, 1, 0,
+"redirectErr([path-or-object])",
+"  Same as redirect(), but for printErr"),
+
+    JS_FN_HELP("close", osfile_close, 1, 0,
+"close(object)",
+"  Close the file returned by an earlier redirect call."),
 
     JS_FS_HELP_END
 };
 
 static bool
 ospath_isAbsolute(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
@@ -727,17 +901,19 @@ static const JSFunctionSpecWithHelp os_f
 "  The return value is an object containing a 'pid' field, if a process was waitable\n"
 "  and an 'exitStatus' field if a pid exited."),
 #endif
 
     JS_FS_HELP_END
 };
 
 bool
-DefineOS(JSContext* cx, HandleObject global, bool fuzzingSafe)
+DefineOS(JSContext* cx, HandleObject global,
+         bool fuzzingSafe,
+         RCFile** shellOut, RCFile** shellErr)
 {
     RootedObject obj(cx, JS_NewPlainObject(cx));
     if (!obj || !JS_DefineProperty(cx, global, "os", obj, 0))
         return false;
 
     if (!fuzzingSafe) {
         if (!JS_DefineFunctionsWithHelp(cx, obj, os_functions))
             return false;
@@ -766,28 +942,32 @@ DefineOS(JSContext* cx, HandleObject glo
         !GenerateInterfaceHelp(cx, ospath, "os.path"))
     {
         return false;
     }
 
     if (!GenerateInterfaceHelp(cx, obj, "os"))
         return false;
 
+    gOutFilePtr = shellOut;
+    gErrFilePtr = shellErr;
+
     // For backwards compatibility, expose various os.file.* functions as
     // direct methods on the global.
     RootedValue val(cx);
 
     struct {
         const char* src;
         const char* dst;
     } osfile_exports[] = {
         { "readFile", "read" },
         { "readFile", "snarf" },
         { "readRelativeToScript", "readRelativeToScript" },
-        { "redirect", "redirect" }
+        { "redirect", "redirect" },
+        { "redirectErr", "redirectErr" }
     };
 
     for (auto pair : osfile_exports) {
         if (!JS_GetProperty(cx, osfile, pair.src, &val))
             return false;
         if (val.isObject()) {
             RootedObject function(cx, &val.toObject());
             if (!JS_DefineProperty(cx, global, pair.dst, function, 0))
--- a/js/src/shell/OSObject.h
+++ b/js/src/shell/OSObject.h
@@ -9,19 +9,23 @@
 #ifndef shell_OSObject_h
 #define shell_OSObject_h
 
 #include "jsapi.h"
 
 namespace js {
 namespace shell {
 
+struct RCFile;
+
 /* Define an os object on the given global object. */
 bool
-DefineOS(JSContext* cx, JS::HandleObject global, bool fuzzingSafe);
+DefineOS(JSContext* cx, JS::HandleObject global,
+         bool fuzzingSafe,
+         RCFile** shellOut, RCFile** shellErr);
 
 enum PathResolutionMode {
     RootRelative,
     ScriptRelative
 };
 
 JSString*
 ResolvePath(JSContext* cx, JS::HandleString filenameStr, PathResolutionMode resolveMode);
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -178,18 +178,18 @@ static bool enableNativeRegExp = false;
 static bool enableUnboxedArrays = false;
 static bool enableSharedMemory = SHARED_MEMORY_DEFAULT;
 #ifdef JS_GC_ZEAL
 static char gZealStr[128];
 #endif
 static bool printTiming = false;
 static const char* jsCacheDir = nullptr;
 static const char* jsCacheAsmJSPath = nullptr;
-static FILE* gErrFile = nullptr;
-static FILE* gOutFile = nullptr;
+static RCFile* gErrFile = nullptr;
+static RCFile* gOutFile = nullptr;
 static bool reportWarnings = true;
 static bool compileOnly = false;
 static bool fuzzingSafe = false;
 static bool disableOOMFunctions = false;
 static const char* moduleLoadPath = ".";
 
 #ifdef DEBUG
 static bool dumpEntrainedVariables = false;
@@ -344,19 +344,19 @@ GetLine(FILE* file, const char * prompt)
             return nullptr;
         if (linep[0] != '\0')
             add_history(linep);
         return linep;
     }
 #endif
 
     size_t len = 0;
-    if (*prompt != '\0') {
-        fprintf(gOutFile, "%s", prompt);
-        fflush(gOutFile);
+    if (*prompt != '\0' && gOutFile->isOpen()) {
+        fprintf(gOutFile->fp, "%s", prompt);
+        fflush(gOutFile->fp);
     }
 
     size_t size = 80;
     char* buffer = static_cast<char*>(malloc(size));
     if (!buffer)
         return nullptr;
 
     char* current = buffer;
@@ -613,51 +613,51 @@ RunModule(JSContext* cx, const char* fil
     if (!JS_CallFunction(cx, loaderObj, importFun, args, &value)) {
         sr->exitCode = EXITCODE_RUNTIME_ERROR;
         return;
     }
 }
 
 static bool
 EvalAndPrint(JSContext* cx, const char* bytes, size_t length,
-             int lineno, bool compileOnly, FILE* out)
+             int lineno, bool compileOnly)
 {
     // Eval.
     JS::CompileOptions options(cx);
     options.setIntroductionType("js shell interactive")
            .setUTF8(true)
            .setIsRunOnce(true)
            .setFileAndLine("typein", lineno);
     RootedScript script(cx);
     if (!JS::Compile(cx, options, bytes, length, &script))
         return false;
     if (compileOnly)
         return true;
     RootedValue result(cx);
     if (!JS_ExecuteScript(cx, script, &result))
         return false;
 
-    if (!result.isUndefined()) {
+    if (!result.isUndefined() && gOutFile->isOpen()) {
         // Print.
         RootedString str(cx);
         str = JS_ValueToSource(cx, result);
         if (!str)
             return false;
 
         char* utf8chars = JS_EncodeStringToUTF8(cx, str);
         if (!utf8chars)
             return false;
-        fprintf(out, "%s\n", utf8chars);
+        fprintf(gOutFile->fp, "%s\n", utf8chars);
         JS_free(cx, utf8chars);
     }
     return true;
 }
 
 static void
-ReadEvalPrintLoop(JSContext* cx, FILE* in, FILE* out, bool compileOnly)
+ReadEvalPrintLoop(JSContext* cx, FILE* in, bool compileOnly)
 {
     ShellRuntime* sr = GetShellRuntime(cx);
     int lineno = 1;
     bool hitEOF = false;
 
     do {
         /*
          * Accumulate lines until we get a 'compilable unit' - one that either
@@ -692,36 +692,35 @@ ReadEvalPrintLoop(JSContext* cx, FILE* i
                 hitEOF = true;
                 break;
             }
         } while (!JS_BufferIsCompilableUnit(cx, cx->global(), buffer.begin(), buffer.length()));
 
         if (hitEOF && buffer.empty())
             break;
 
-        if (!EvalAndPrint(cx, buffer.begin(), buffer.length(), startline, compileOnly,
-                          out))
-        {
+        if (!EvalAndPrint(cx, buffer.begin(), buffer.length(), startline, compileOnly)) {
             // Catch the error, report it, and keep going.
             JS_ReportPendingException(cx);
         }
         // If a let or const fail to initialize they will remain in an unusable
         // without further intervention. This call cleans up the global scope,
         // setting uninitialized lexicals to undefined so that they may still
         // be used. This behavior is _only_ acceptable in the context of the repl.
-        if (JS::ForceLexicalInitialization(cx, globalLexical)) {
+        if (JS::ForceLexicalInitialization(cx, globalLexical) && gErrFile->isOpen()) {
             fputs("Warning: According to the standard, after the above exception,\n"
                   "Warning: the global bindings should be permanently uninitialized.\n"
                   "Warning: We have non-standard-ly initialized them to `undefined`"
                   "for you.\nWarning: This nicety only happens in the JS shell.\n",
                   stderr);
         }
     } while (!hitEOF && !sr->quitting);
 
-    fprintf(out, "\n");
+    if (gOutFile->isOpen())
+        fprintf(gOutFile->fp, "\n");
 }
 
 enum FileKind
 {
     FileScript,
     FileModule
 };
 
@@ -745,17 +744,17 @@ Process(JSContext* cx, const char* filen
         // It's not interactive - just execute it.
         if (kind == FileScript)
             RunFile(cx, filename, file, compileOnly);
         else
             RunModule(cx, filename, file, compileOnly);
     } else {
         // It's an interactive filehandle; drop into read-eval-print loop.
         MOZ_ASSERT(kind == FileScript);
-        ReadEvalPrintLoop(cx, file, gOutFile, compileOnly);
+        ReadEvalPrintLoop(cx, file, compileOnly);
     }
 }
 
 static bool
 Version(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     JSVersion origVersion = JS_GetVersion(cx);
@@ -1643,56 +1642,66 @@ ReadLine(JSContext* cx, unsigned argc, V
 }
 
 static bool
 PutStr(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     if (args.length() != 0) {
+        if (!gOutFile->isOpen()) {
+            JS_ReportError(cx, "output file is closed");
+            return false;
+        }
+
         RootedString str(cx, JS::ToString(cx, args[0]));
         if (!str)
             return false;
         char* bytes = JS_EncodeStringToUTF8(cx, str);
         if (!bytes)
             return false;
-        fputs(bytes, gOutFile);
+        fputs(bytes, gOutFile->fp);
         JS_free(cx, bytes);
-        fflush(gOutFile);
+        fflush(gOutFile->fp);
     }
 
     args.rval().setUndefined();
     return true;
 }
 
 static bool
 Now(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     double now = PRMJ_Now() / double(PRMJ_USEC_PER_MSEC);
     args.rval().setDouble(now);
     return true;
 }
 
 static bool
-PrintInternal(JSContext* cx, const CallArgs& args, FILE* file)
-{
+PrintInternal(JSContext* cx, const CallArgs& args, RCFile* file)
+{
+    if (!file->isOpen()) {
+        JS_ReportError(cx, "output file is closed");
+        return false;
+    }
+
     for (unsigned i = 0; i < args.length(); i++) {
         RootedString str(cx, JS::ToString(cx, args[i]));
         if (!str)
             return false;
         char* bytes = JS_EncodeStringToUTF8(cx, str);
         if (!bytes)
             return false;
-        fprintf(file, "%s%s", i ? " " : "", bytes);
+        fprintf(file->fp, "%s%s", i ? " " : "", bytes);
         JS_free(cx, bytes);
     }
 
-    fputc('\n', file);
-    fflush(file);
+    fputc('\n', file->fp);
+    fflush(file->fp);
 
     args.rval().setUndefined();
     return true;
 }
 
 static bool
 Print(JSContext* cx, unsigned argc, Value* vp)
 {
@@ -1771,18 +1780,18 @@ StopTimingMutator(JSContext* cx, unsigne
     }
 
     double mutator_ms, gc_ms;
     if (!cx->runtime()->gc.stats.stopTimingMutator(mutator_ms, gc_ms)) {
         JS_ReportError(cx, "stopTimingMutator called when not timing the mutator");
         return false;
     }
     double total_ms = mutator_ms + gc_ms;
-    if (total_ms > 0) {
-        fprintf(gOutFile, "Mutator: %.3fms (%.1f%%), GC: %.3fms (%.1f%%)\n",
+    if (total_ms > 0 && gOutFile->isOpen()) {
+        fprintf(gOutFile->fp, "Mutator: %.3fms (%.1f%%), GC: %.3fms (%.1f%%)\n",
                 mutator_ms, mutator_ms / total_ms * 100.0, gc_ms, gc_ms / total_ms * 100.0);
     }
 
     args.rval().setUndefined();
     return true;
 }
 
 static const char*
@@ -2315,32 +2324,43 @@ DisassembleToString(JSContext* cx, unsig
     args.rval().setString(str);
     return true;
 }
 
 static bool
 Disassemble(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
+
+    if (!gOutFile->isOpen()) {
+        JS_ReportError(cx, "output file is closed");
+        return false;
+    }
+
     Sprinter sprinter(cx);
     if (!sprinter.init())
         return false;
     if (!DisassembleToSprinter(cx, args.length(), vp, &sprinter))
         return false;
 
-    fprintf(stdout, "%s\n", sprinter.string());
+    fprintf(gOutFile->fp, "%s\n", sprinter.string());
     args.rval().setUndefined();
     return true;
 }
 
 static bool
 DisassFile(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
+    if (!gOutFile->isOpen()) {
+        JS_ReportError(cx, "output file is closed");
+        return false;
+    }
+
     /* Support extra options at the start, just like Disassemble. */
     DisassembleOptionParser p(args.length(), args.array());
     if (!p.parse(cx))
         return false;
 
     if (!p.argc) {
         args.rval().setUndefined();
         return true;
@@ -2367,29 +2387,34 @@ DisassFile(JSContext* cx, unsigned argc,
             return false;
     }
 
     Sprinter sprinter(cx);
     if (!sprinter.init())
         return false;
     bool ok = DisassembleScript(cx, script, nullptr, p.lines, p.recursive, p.sourceNotes, &sprinter);
     if (ok)
-        fprintf(stdout, "%s\n", sprinter.string());
+        fprintf(gOutFile->fp, "%s\n", sprinter.string());
     if (!ok)
         return false;
 
     args.rval().setUndefined();
     return true;
 }
 
 static bool
 DisassWithSrc(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
+    if (!gOutFile->isOpen()) {
+        JS_ReportError(cx, "output file is closed");
+        return false;
+    }
+
 #define LINE_BUF_LEN 512
     unsigned len, line1, line2, bupline;
     FILE* file;
     char linebuf[LINE_BUF_LEN];
     static const char sep[] = ";-------------------------";
 
     bool ok = true;
     RootedScript script(cx);
@@ -2461,32 +2486,33 @@ DisassWithSrc(JSContext* cx, unsigned ar
             len = Disassemble1(cx, script, pc, script->pcToOffset(pc), true, &sprinter);
             if (!len) {
                 ok = false;
                 goto bail;
             }
             pc += len;
         }
 
-        fprintf(stdout, "%s\n", sprinter.string());
+        fprintf(gOutFile->fp, "%s\n", sprinter.string());
 
       bail:
         fclose(file);
     }
     args.rval().setUndefined();
     return ok;
 #undef LINE_BUF_LEN
 }
 
 #endif /* DEBUG */
 
 static bool
 Intern(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
+
     JSString* str = JS::ToString(cx, args.get(0));
     if (!str)
         return false;
 
     AutoStableStringChars strChars(cx);
     if (!strChars.initTwoByte(cx, str))
         return false;
 
@@ -3319,25 +3345,30 @@ ClearLastWarning(JSContext* cx, unsigned
 }
 
 #ifdef DEBUG
 static bool
 StackDump(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
+    if (!gOutFile->isOpen()) {
+        JS_ReportError(cx, "output file is closed");
+        return false;
+    }
+
     bool showArgs = ToBoolean(args.get(0));
     bool showLocals = ToBoolean(args.get(1));
     bool showThisProps = ToBoolean(args.get(2));
 
     char* buf = JS::FormatStackDump(cx, nullptr, showArgs, showLocals, showThisProps);
     if (!buf) {
-        fputs("Failed to format JavaScript stack for dump\n", gOutFile);
+        fputs("Failed to format JavaScript stack for dump\n", gOutFile->fp);
     } else {
-        fputs(buf, gOutFile);
+        fputs(buf, gOutFile->fp);
         JS_smprintf_free(buf);
     }
 
     args.rval().setUndefined();
     return true;
 }
 #endif
 
@@ -5607,30 +5638,31 @@ DefineConsole(JSContext* cx, HandleObjec
 #undef CALLGRIND_FUNCTION_COUNT
 #undef VTUNE_FUNCTION_COUNT
 #undef EXTERNAL_FUNCTION_COUNT
 
 static bool
 PrintHelpString(JSContext* cx, Value v)
 {
     JSString* str = v.toString();
+    MOZ_ASSERT(gOutFile->isOpen());
 
     JSLinearString* linear = str->ensureLinear(cx);
     if (!linear)
         return false;
 
     JS::AutoCheckCannotGC nogc;
     if (linear->hasLatin1Chars()) {
         for (const Latin1Char* p = linear->latin1Chars(nogc); *p; p++)
-            fprintf(gOutFile, "%c", char(*p));
+            fprintf(gOutFile->fp, "%c", char(*p));
     } else {
         for (const char16_t* p = linear->twoByteChars(nogc); *p; p++)
-            fprintf(gOutFile, "%c", char(*p));
-    }
-    fprintf(gOutFile, "\n");
+            fprintf(gOutFile->fp, "%c", char(*p));
+    }
+    fprintf(gOutFile->fp, "\n");
 
     return true;
 }
 
 static bool
 PrintHelp(JSContext* cx, HandleObject obj)
 {
     RootedValue usage(cx);
@@ -5666,21 +5698,26 @@ PrintEnumeratedHelp(JSContext* cx, Handl
     }
 
     return true;
 }
 
 static bool
 Help(JSContext* cx, unsigned argc, Value* vp)
 {
+    if (!gOutFile->isOpen()) {
+        JS_ReportError(cx, "output file is closed");
+        return false;
+    }
+
     CallArgs args = CallArgsFromVp(argc, vp);
 
     RootedObject obj(cx);
     if (args.length() == 0) {
-        fprintf(gOutFile, "%s\n", JS_GetImplementationVersion());
+        fprintf(gOutFile->fp, "%s\n", JS_GetImplementationVersion());
 
         RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
         if (!PrintEnumeratedHelp(cx, global, false))
             return false;
     } else {
         for (unsigned i = 0; i < args.length(); i++) {
             if (args[i].isPrimitive()) {
                 JS_ReportError(cx, "primitive arg");
@@ -5744,16 +5781,26 @@ CreateLastWarningObject(JSContext* cx, J
     RootedValue columnVal(cx, Int32Value(report->column));
     if (!DefineProperty(cx, warningObj, cx->names().columnNumber, columnVal))
         return false;
 
     GetShellRuntime(cx)->lastWarning.setObject(*warningObj);
     return true;
 }
 
+static FILE*
+ErrorFilePointer()
+{
+    if (gErrFile->isOpen())
+        return gErrFile->fp;
+
+    fprintf(stderr, "error file is closed; falling back to stderr\n");
+    return stderr;
+}
+
 static bool
 PrintStackTrace(JSContext* cx, HandleValue exn)
 {
     if (!exn.isObject())
         return false;
 
     Maybe<JSAutoCompartment> ac;
     RootedObject exnObj(cx, &exn.toObject());
@@ -5774,46 +5821,48 @@ PrintStackTrace(JSContext* cx, HandleVal
     RootedString stackStr(cx);
     if (!BuildStackString(cx, stackObj, &stackStr, 2))
         return false;
 
     UniqueChars stack(JS_EncodeStringToUTF8(cx, stackStr));
     if (!stack)
         return false;
 
-    fputs("Stack:\n", gErrFile);
-    fputs(stack.get(), gErrFile);
+    FILE* fp = ErrorFilePointer();
+    fputs("Stack:\n", fp);
+    fputs(stack.get(), fp);
 
     return true;
 }
 
 void
 js::shell::my_ErrorReporter(JSContext* cx, const char* message, JSErrorReport* report)
 {
     ShellRuntime* sr = GetShellRuntime(cx);
+    FILE* fp = ErrorFilePointer();
 
     if (report && JSREPORT_IS_WARNING(report->flags) && sr->lastWarningEnabled) {
         JS::AutoSaveExceptionState savedExc(cx);
         if (!CreateLastWarningObject(cx, report)) {
-            fputs("Unhandled error happened while creating last warning object.\n", gOutFile);
-            fflush(gOutFile);
+            fputs("Unhandled error happened while creating last warning object.\n", fp);
+            fflush(fp);
         }
         savedExc.restore();
     }
 
     // Get exception object before printing and clearing exception.
     RootedValue exn(cx);
     if (JS_IsExceptionPending(cx))
         (void) JS_GetPendingException(cx, &exn);
 
-    sr->gotError = PrintError(cx, gErrFile, message, report, reportWarnings);
+    sr->gotError = PrintError(cx, fp, message, report, reportWarnings);
     if (!exn.isUndefined()) {
         JS::AutoSaveExceptionState savedExc(cx);
         if (!PrintStackTrace(cx, exn))
-            fputs("(Unable to print stack trace)\n", gOutFile);
+            fputs("(Unable to print stack trace)\n", fp);
         savedExc.restore();
     }
 
     if (report->exnType != JSEXN_NONE && !JSREPORT_IS_WARNING(report->flags)) {
         if (report->errorNumber == JSMSG_OUT_OF_MEMORY)
             sr->exitCode = EXITCODE_OUT_OF_MEMORY;
         else
             sr->exitCode = EXITCODE_RUNTIME_ERROR;
@@ -6399,17 +6448,17 @@ NewGlobalObject(JSContext* cx, JS::Compa
 
         if (!fuzzingSafe) {
             if (!JS_DefineFunctionsWithHelp(cx, glob, fuzzing_unsafe_functions))
                 return nullptr;
             if (!DefineConsole(cx, glob))
                 return nullptr;
         }
 
-        if (!DefineOS(cx, glob, fuzzingSafe))
+        if (!DefineOS(cx, glob, fuzzingSafe, &gOutFile, &gErrFile))
             return nullptr;
 
         RootedObject performanceObj(cx, JS_NewObject(cx, nullptr));
         if (!performanceObj)
             return nullptr;
         RootedObject mozMemoryObj(cx, JS_NewObject(cx, nullptr));
         if (!mozMemoryObj)
             return nullptr;
@@ -6898,23 +6947,28 @@ Shell(JSContext* cx, OptionParser* op, c
             JS_free(cx, const_cast<char*>(jsCacheDir));
         }
     }
 
     return result;
 }
 
 static void
-MaybeOverrideOutFileFromEnv(const char* const envVar,
-                            FILE* defaultOut,
-                            FILE** outFile)
+SetOutputFile(const char* const envVar,
+              FILE* defaultOut,
+              RCFile** outFile)
 {
     const char* outPath = getenv(envVar);
-    if (!outPath || !*outPath || !(*outFile = fopen(outPath, "w"))) {
-        *outFile = defaultOut;
+    FILE* newfp;
+    if (outPath && *outPath && (newfp = fopen(outPath, "w"))) {
+        *outFile = js_new<RCFile>(newfp);
+        (*outFile)->acquire();
+    } else {
+        *outFile = js_new<RCFile>(defaultOut);
+        (*outFile)->acquire();
     }
 }
 
 /* Pretend we can always preserve wrappers for dummy DOM objects. */
 static bool
 DummyPreserveWrapperCallback(JSContext* cx, JSObject* obj)
 {
     return true;
@@ -6946,18 +7000,18 @@ main(int argc, char** argv, char** envp)
     JSRuntime* rt;
     JSContext* cx;
     int result;
 
 #ifdef HAVE_SETLOCALE
     setlocale(LC_ALL, "");
 #endif
 
-    MaybeOverrideOutFileFromEnv("JS_STDERR", stderr, &gErrFile);
-    MaybeOverrideOutFileFromEnv("JS_STDOUT", stdout, &gOutFile);
+    SetOutputFile("JS_STDERR", stderr, &gErrFile);
+    SetOutputFile("JS_STDOUT", stdout, &gOutFile);
 
     OptionParser op("Usage: {progname} [options] [[script] scriptArgs*]");
 
     op.setDescription("The SpiderMonkey shell provides a command line interface to the "
         "JavaScript engine. Code and file options provided via the command line are "
         "run left to right. If provided, the optional script argument is run after "
         "all options have been processed. Just-In-Time compilation modes may be enabled via "
         "command line options.");
--- a/js/src/shell/jsshell.h
+++ b/js/src/shell/jsshell.h
@@ -45,12 +45,30 @@ class AutoCloseFile
         bool success = true;
         if (f_ && f_ != stdin && f_ != stdout && f_ != stderr)
             success = !fclose(f_);
         f_ = nullptr;
         return success;
     }
 };
 
+// Reference counted file.
+struct RCFile {
+    FILE* fp;
+    uint32_t numRefs;
+
+    RCFile() : fp(nullptr), numRefs(0) {}
+    explicit RCFile(FILE* fp) : fp(fp), numRefs(0) {}
+
+    void acquire() { numRefs++; }
+
+    // Starts out with a ref count of zero.
+    static RCFile* create(JSContext* cx, const char* filename, const char* mode);
+
+    void close();
+    bool isOpen() const { return fp; }
+    bool release();
+};
+
 } /* namespace shell */
 } /* namespace js */
 
 #endif