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 339733 58b542930c53df069d9a742fffc3781c08be07ee
parent 339732 c3470e4402220b4496a831037d53e13e1419604c
child 339734 b9d035d4674401765b22c76280c13f9cef2e4e28
push id12803
push userjbeich@FreeBSD.org
push dateSun, 13 Mar 2016 09:48:54 +0000
reviewersterrence
bugs944164
milestone48.0a1
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