diff options
Diffstat (limited to 'ipc/testshell/XPCShellEnvironment.cpp')
-rw-r--r-- | ipc/testshell/XPCShellEnvironment.cpp | 468 |
1 files changed, 468 insertions, 0 deletions
diff --git a/ipc/testshell/XPCShellEnvironment.cpp b/ipc/testshell/XPCShellEnvironment.cpp new file mode 100644 index 0000000000..3d83b36ab4 --- /dev/null +++ b/ipc/testshell/XPCShellEnvironment.cpp @@ -0,0 +1,468 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifdef HAVE_IO_H +# include <io.h> /* for isatty() */ +#endif +#ifdef HAVE_UNISTD_H +# include <unistd.h> /* for isatty() */ +#endif + +#include "jsapi.h" +#include "js/CharacterEncoding.h" +#include "js/CompilationAndEvaluation.h" // JS::Compile{,Utf8File} +#include "js/PropertyAndElement.h" // JS_DefineFunctions, JS_DefineProperty, JS_GetProperty +#include "js/PropertySpec.h" +#include "js/RealmOptions.h" +#include "js/SourceText.h" // JS::Source{Ownership,Text} + +#include "xpcpublic.h" + +#include "XPCShellEnvironment.h" + +#include "mozilla/Utf8.h" // mozilla::Utf8Unit +#include "mozilla/dom/AutoEntryScript.h" +#include "mozilla/dom/ScriptSettings.h" + +#include "nsIPrincipal.h" +#include "nsIScriptSecurityManager.h" +#include "nsIXPConnect.h" +#include "nsServiceManagerUtils.h" + +#include "nsJSUtils.h" + +#include "BackstagePass.h" + +#include "TestShellChild.h" + +using mozilla::dom::AutoEntryScript; +using mozilla::dom::AutoJSAPI; +using mozilla::ipc::XPCShellEnvironment; +using namespace JS; + +namespace { + +static const char kDefaultRuntimeScriptFilename[] = "xpcshell.js"; + +inline XPCShellEnvironment* Environment(JS::Handle<JSObject*> global) { + AutoJSAPI jsapi; + if (!jsapi.Init(global)) { + return nullptr; + } + JSContext* cx = jsapi.cx(); + Rooted<Value> v(cx); + if (!JS_GetProperty(cx, global, "__XPCShellEnvironment", &v) || + !v.get().isDouble()) { + return nullptr; + } + return static_cast<XPCShellEnvironment*>(v.get().toPrivate()); +} + +static bool Print(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + for (unsigned i = 0; i < args.length(); i++) { + JS::Rooted<JSString*> str(cx, JS::ToString(cx, args[i])); + if (!str) return false; + JS::UniqueChars bytes = JS_EncodeStringToUTF8(cx, str); + if (!bytes) return false; + fprintf(stdout, "%s%s", i ? " " : "", bytes.get()); + fflush(stdout); + } + fputc('\n', stdout); + args.rval().setUndefined(); + return true; +} + +static bool GetLine(char* bufp, FILE* file, const char* prompt) { + char line[256]; + fputs(prompt, stdout); + fflush(stdout); + if (!fgets(line, sizeof line, file)) return false; + strcpy(bufp, line); + return true; +} + +static bool Dump(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!args.length()) return true; + + JS::Rooted<JSString*> str(cx, JS::ToString(cx, args[0])); + if (!str) return false; + JS::UniqueChars bytes = JS_EncodeStringToUTF8(cx, str); + if (!bytes) return false; + + fputs(bytes.get(), stdout); + fflush(stdout); + return true; +} + +static bool Load(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::RootedObject thisObject(cx); + if (!args.computeThis(cx, &thisObject)) return false; + if (!JS_IsGlobalObject(thisObject)) { + JS_ReportErrorASCII(cx, "Trying to load() into a non-global object"); + return false; + } + + for (unsigned i = 0; i < args.length(); i++) { + JS::Rooted<JSString*> str(cx, JS::ToString(cx, args[i])); + if (!str) { + return false; + } + JS::UniqueChars filename = JS_EncodeStringToUTF8(cx, str); + if (!filename) { + return false; + } + + JS::CompileOptions options(cx); + JS::Rooted<JSScript*> script( + cx, JS::CompileUtf8Path(cx, options, filename.get())); + if (!script) { + return false; + } + + if (!JS_ExecuteScript(cx, script)) { + return false; + } + } + args.rval().setUndefined(); + return true; +} + +static bool Quit(JSContext* cx, unsigned argc, JS::Value* vp) { + Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx)); + XPCShellEnvironment* env = Environment(global); + env->SetIsQuitting(); + + return false; +} + +static bool DumpXPC(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = CallArgsFromVp(argc, vp); + + uint16_t depth = 2; + if (args.length() > 0) { + if (!JS::ToUint16(cx, args[0], &depth)) return false; + } + + nsCOMPtr<nsIXPConnect> xpc = nsIXPConnect::XPConnect(); + if (xpc) xpc->DebugDump(int16_t(depth)); + args.rval().setUndefined(); + return true; +} + +static bool GC(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS_GC(cx); + + args.rval().setUndefined(); + return true; +} + +#ifdef JS_GC_ZEAL +static bool GCZeal(JSContext* cx, unsigned argc, JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + uint32_t zeal; + if (!ToUint32(cx, args.get(0), &zeal)) return false; + + JS_SetGCZeal(cx, uint8_t(zeal), JS_DEFAULT_ZEAL_FREQ); + return true; +} +#endif + +#ifdef ANDROID +static bool ChangeTestShellDir(JSContext* cx, unsigned argc, Value* vp) { + // This method should only be used by testing/xpcshell/head.js to change to + // the correct directory on Android Remote XPCShell tests. + // + // TODO: Bug 1801725 - Find a more ergonomic way to do this than exposing + // identical methods in XPCShellEnvironment and XPCShellImpl to chdir on + // android for Remote XPCShell tests on Android. + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1) { + JS_ReportErrorASCII(cx, "changeTestShellDir() takes one argument"); + return false; + } + + nsAutoJSCString path; + if (!path.init(cx, args[0])) { + JS_ReportErrorASCII( + cx, "changeTestShellDir(): could not convert argument 1 to string"); + return false; + } + + if (chdir(path.get())) { + JS_ReportErrorASCII(cx, "changeTestShellDir(): could not change directory"); + return false; + } + + return true; +} +#endif + +const JSFunctionSpec gGlobalFunctions[] = { + JS_FN("print", Print, 0, 0), + JS_FN("load", Load, 1, 0), + JS_FN("quit", Quit, 0, 0), + JS_FN("dumpXPC", DumpXPC, 1, 0), + JS_FN("dump", Dump, 1, 0), + JS_FN("gc", GC, 0, 0), +#ifdef JS_GC_ZEAL + JS_FN("gczeal", GCZeal, 1, 0), +#endif +#ifdef ANDROID + JS_FN("changeTestShellDir", ChangeTestShellDir, 1, 0), +#endif + JS_FS_END}; + +typedef enum JSShellErrNum { +#define MSG_DEF(name, number, count, exception, format) name = number, +#include "jsshell.msg" +#undef MSG_DEF + JSShellErr_Limit +#undef MSGDEF +} JSShellErrNum; + +} /* anonymous namespace */ + +void XPCShellEnvironment::ProcessFile(JSContext* cx, const char* filename, + FILE* file, bool forceTTY) { + XPCShellEnvironment* env = this; + + JS::Rooted<JS::Value> result(cx); + int lineno, startline; + bool ok, hitEOF; + char *bufp, buffer[4096]; + JSString* str; + + JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx)); + MOZ_ASSERT(global); + + if (forceTTY) { + file = stdin; + } else if (!isatty(fileno(file))) { + /* + * It's not interactive - just execute it. + * + * Support the UNIX #! shell hack; gobble the first line if it starts + * with '#'. + */ + int ch = fgetc(file); + if (ch == '#') { + while ((ch = fgetc(file)) != EOF) { + if (ch == '\n' || ch == '\r') break; + } + } + ungetc(ch, file); + + JS::CompileOptions options(cx); + options.setFileAndLine(filename, 1); + + JS::Rooted<JSScript*> script(cx, JS::CompileUtf8File(cx, options, file)); + if (script) { + (void)JS_ExecuteScript(cx, script, &result); + } + + return; + } + + /* It's an interactive filehandle; drop into read-eval-print loop. */ + lineno = 1; + hitEOF = false; + do { + bufp = buffer; + *bufp = '\0'; + + /* + * Accumulate lines until we get a 'compilable unit' - one that either + * generates an error (before running out of source) or that compiles + * cleanly. This should be whenever we get a complete statement that + * coincides with the end of a line. + */ + startline = lineno; + do { + if (!GetLine(bufp, file, startline == lineno ? "js> " : "")) { + hitEOF = true; + break; + } + bufp += strlen(bufp); + lineno++; + } while ( + !JS_Utf8BufferIsCompilableUnit(cx, global, buffer, strlen(buffer))); + + /* Clear any pending exception from previous failed compiles. */ + JS_ClearPendingException(cx); + + JS::CompileOptions options(cx); + options.setFileAndLine("typein", startline); + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + JS::Rooted<JSScript*> script(cx); + + if (srcBuf.init(cx, buffer, strlen(buffer), + JS::SourceOwnership::Borrowed) && + (script = JS::Compile(cx, options, srcBuf))) { + ok = JS_ExecuteScript(cx, script, &result); + if (ok && !result.isUndefined()) { + /* Suppress warnings from JS::ToString(). */ + JS::AutoSuppressWarningReporter suppressWarnings(cx); + str = JS::ToString(cx, result); + JS::UniqueChars bytes; + if (str) bytes = JS_EncodeStringToLatin1(cx, str); + + if (!!bytes) + fprintf(stdout, "%s\n", bytes.get()); + else + ok = false; + } + } + } while (!hitEOF && !env->IsQuitting()); + + fprintf(stdout, "\n"); +} + +// static +XPCShellEnvironment* XPCShellEnvironment::CreateEnvironment() { + auto* env = new XPCShellEnvironment(); + if (env && !env->Init()) { + delete env; + env = nullptr; + } + return env; +} + +XPCShellEnvironment::XPCShellEnvironment() : mQuitting(false) {} + +XPCShellEnvironment::~XPCShellEnvironment() { + if (GetGlobalObject()) { + AutoJSAPI jsapi; + if (!jsapi.Init(GetGlobalObject())) { + return; + } + JS_SetAllNonReservedSlotsToUndefined(mGlobalHolder); + mGlobalHolder.reset(); + + JS_GC(jsapi.cx()); + } +} + +bool XPCShellEnvironment::Init() { + nsresult rv; + + // unbuffer stdout so that output is in the correct order; note that stderr + // is unbuffered by default + setbuf(stdout, 0); + + AutoSafeJSContext cx; + + mGlobalHolder.init(cx); + + nsCOMPtr<nsIPrincipal> principal; + nsCOMPtr<nsIScriptSecurityManager> securityManager = + do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID, &rv); + if (NS_SUCCEEDED(rv) && securityManager) { + rv = securityManager->GetSystemPrincipal(getter_AddRefs(principal)); + if (NS_FAILED(rv)) { + fprintf(stderr, + "+++ Failed to obtain SystemPrincipal from ScriptSecurityManager " + "service.\n"); + } + } else { + fprintf(stderr, + "+++ Failed to get ScriptSecurityManager service, running without " + "principals"); + } + + auto backstagePass = MakeRefPtr<BackstagePass>(); + + JS::RealmOptions options; + options.creationOptions().setNewCompartmentInSystemZone(); + xpc::SetPrefableRealmOptions(options); + + JS::Rooted<JSObject*> globalObj(cx); + rv = xpc::InitClassesWithNewWrappedGlobal( + cx, static_cast<nsIGlobalObject*>(backstagePass), principal, 0, options, + &globalObj); + if (NS_FAILED(rv)) { + NS_ERROR("InitClassesWithNewWrappedGlobal failed!"); + return false; + } + + if (!globalObj) { + NS_ERROR("Failed to get global JSObject!"); + return false; + } + JSAutoRealm ar(cx, globalObj); + + backstagePass->SetGlobalObject(globalObj); + + JS::Rooted<Value> privateVal(cx, PrivateValue(this)); + if (!JS_DefineProperty(cx, globalObj, "__XPCShellEnvironment", privateVal, + JSPROP_READONLY | JSPROP_PERMANENT) || + !JS_DefineFunctions(cx, globalObj, gGlobalFunctions)) { + NS_ERROR("JS_DefineFunctions failed!"); + return false; + } + + mGlobalHolder = globalObj; + + FILE* runtimeScriptFile = fopen(kDefaultRuntimeScriptFilename, "r"); + if (runtimeScriptFile) { + fprintf(stdout, "[loading '%s'...]\n", kDefaultRuntimeScriptFilename); + ProcessFile(cx, kDefaultRuntimeScriptFilename, runtimeScriptFile, false); + fclose(runtimeScriptFile); + } + + return true; +} + +bool XPCShellEnvironment::EvaluateString(const nsAString& aString, + nsString* aResult) { + AutoEntryScript aes(GetGlobalObject(), + "ipc XPCShellEnvironment::EvaluateString"); + JSContext* cx = aes.cx(); + + JS::CompileOptions options(cx); + options.setFileAndLine("typein", 0); + + JS::SourceText<char16_t> srcBuf; + if (!srcBuf.init(cx, aString.BeginReading(), aString.Length(), + JS::SourceOwnership::Borrowed)) { + return false; + } + + JS::Rooted<JSScript*> script(cx, JS::Compile(cx, options, srcBuf)); + if (!script) { + return false; + } + + if (aResult) { + aResult->Truncate(); + } + + JS::Rooted<JS::Value> result(cx); + bool ok = JS_ExecuteScript(cx, script, &result); + if (ok && !result.isUndefined()) { + /* Suppress warnings from JS::ToString(). */ + JS::AutoSuppressWarningReporter suppressWarnings(cx); + JSString* str = JS::ToString(cx, result); + nsAutoJSString autoStr; + if (str) autoStr.init(cx, str); + + if (!autoStr.IsEmpty() && aResult) { + aResult->Assign(autoStr); + } + } + + return true; +} |