XPCShellEnvironment.cpp (12889B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: 3 * This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #ifdef HAVE_IO_H 8 # include <io.h> /* for isatty() */ 9 #endif 10 #ifdef HAVE_UNISTD_H 11 # include <unistd.h> /* for isatty() */ 12 #endif 13 14 #include "jsapi.h" 15 #include "js/CharacterEncoding.h" 16 #include "js/CompilationAndEvaluation.h" // JS::Compile{,Utf8File} 17 #include "js/PropertyAndElement.h" // JS_DefineFunctions, JS_DefineProperty, JS_GetProperty 18 #include "js/PropertySpec.h" 19 #include "js/RealmOptions.h" 20 #include "js/SourceText.h" // JS::Source{Ownership,Text} 21 22 #include "xpcpublic.h" 23 24 #include "XPCShellEnvironment.h" 25 26 #include "mozilla/Utf8.h" // mozilla::Utf8Unit 27 #include "mozilla/dom/AutoEntryScript.h" 28 #include "mozilla/dom/ScriptSettings.h" 29 30 #include "nsIPrincipal.h" 31 #include "nsIScriptSecurityManager.h" 32 #include "nsIXPConnect.h" 33 #include "nsServiceManagerUtils.h" 34 35 #include "nsJSUtils.h" 36 37 #include "SystemGlobal.h" 38 39 #include "TestShellChild.h" 40 41 using mozilla::dom::AutoEntryScript; 42 using mozilla::dom::AutoJSAPI; 43 using mozilla::ipc::XPCShellEnvironment; 44 using namespace JS; 45 46 namespace { 47 48 static const char kDefaultRuntimeScriptFilename[] = "xpcshell.js"; 49 50 inline XPCShellEnvironment* Environment(JS::Handle<JSObject*> global) { 51 AutoJSAPI jsapi; 52 if (!jsapi.Init(global)) { 53 return nullptr; 54 } 55 JSContext* cx = jsapi.cx(); 56 Rooted<Value> v(cx); 57 if (!JS_GetProperty(cx, global, "__XPCShellEnvironment", &v) || 58 !v.get().isDouble()) { 59 return nullptr; 60 } 61 return static_cast<XPCShellEnvironment*>(v.get().toPrivate()); 62 } 63 64 static bool Print(JSContext* cx, unsigned argc, JS::Value* vp) { 65 JS::CallArgs args = JS::CallArgsFromVp(argc, vp); 66 67 for (unsigned i = 0; i < args.length(); i++) { 68 JS::Rooted<JSString*> str(cx, JS::ToString(cx, args[i])); 69 if (!str) return false; 70 JS::UniqueChars bytes = JS_EncodeStringToUTF8(cx, str); 71 if (!bytes) return false; 72 fprintf(stdout, "%s%s", i ? " " : "", bytes.get()); 73 fflush(stdout); 74 } 75 fputc('\n', stdout); 76 args.rval().setUndefined(); 77 return true; 78 } 79 80 static bool GetLine(char* bufp, FILE* file, const char* prompt) { 81 char line[256]; 82 fputs(prompt, stdout); 83 fflush(stdout); 84 if (!fgets(line, sizeof line, file)) return false; 85 strcpy(bufp, line); 86 return true; 87 } 88 89 static bool Dump(JSContext* cx, unsigned argc, JS::Value* vp) { 90 JS::CallArgs args = JS::CallArgsFromVp(argc, vp); 91 92 if (!args.length()) return true; 93 94 JS::Rooted<JSString*> str(cx, JS::ToString(cx, args[0])); 95 if (!str) return false; 96 JS::UniqueChars bytes = JS_EncodeStringToUTF8(cx, str); 97 if (!bytes) return false; 98 99 fputs(bytes.get(), stdout); 100 fflush(stdout); 101 return true; 102 } 103 104 static bool Load(JSContext* cx, unsigned argc, JS::Value* vp) { 105 JS::CallArgs args = JS::CallArgsFromVp(argc, vp); 106 107 JS::RootedObject thisObject(cx); 108 if (!args.computeThis(cx, &thisObject)) return false; 109 if (!JS_IsGlobalObject(thisObject)) { 110 JS_ReportErrorASCII(cx, "Trying to load() into a non-global object"); 111 return false; 112 } 113 114 for (unsigned i = 0; i < args.length(); i++) { 115 JS::Rooted<JSString*> str(cx, JS::ToString(cx, args[i])); 116 if (!str) { 117 return false; 118 } 119 JS::UniqueChars filename = JS_EncodeStringToUTF8(cx, str); 120 if (!filename) { 121 return false; 122 } 123 124 JS::CompileOptions options(cx); 125 JS::Rooted<JSScript*> script( 126 cx, JS::CompileUtf8Path(cx, options, filename.get())); 127 if (!script) { 128 return false; 129 } 130 131 if (!JS_ExecuteScript(cx, script)) { 132 return false; 133 } 134 } 135 args.rval().setUndefined(); 136 return true; 137 } 138 139 static bool Quit(JSContext* cx, unsigned argc, JS::Value* vp) { 140 Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx)); 141 XPCShellEnvironment* env = Environment(global); 142 env->SetIsQuitting(); 143 144 return false; 145 } 146 147 static bool DumpXPC(JSContext* cx, unsigned argc, JS::Value* vp) { 148 JS::CallArgs args = CallArgsFromVp(argc, vp); 149 150 uint16_t depth = 2; 151 if (args.length() > 0) { 152 if (!JS::ToUint16(cx, args[0], &depth)) return false; 153 } 154 155 nsCOMPtr<nsIXPConnect> xpc = nsIXPConnect::XPConnect(); 156 if (xpc) xpc->DebugDump(int16_t(depth)); 157 args.rval().setUndefined(); 158 return true; 159 } 160 161 static bool GC(JSContext* cx, unsigned argc, JS::Value* vp) { 162 JS::CallArgs args = JS::CallArgsFromVp(argc, vp); 163 164 JS_GC(cx); 165 166 args.rval().setUndefined(); 167 return true; 168 } 169 170 #ifdef JS_GC_ZEAL 171 static bool GCZeal(JSContext* cx, unsigned argc, JS::Value* vp) { 172 CallArgs args = CallArgsFromVp(argc, vp); 173 174 uint32_t zeal; 175 if (!ToUint32(cx, args.get(0), &zeal)) return false; 176 177 JS::SetGCZeal(cx, uint8_t(zeal), JS::ShellDefaultGCZealFrequency); 178 return true; 179 } 180 #endif 181 182 #ifdef ANDROID 183 static bool ChangeTestShellDir(JSContext* cx, unsigned argc, Value* vp) { 184 // This method should only be used by testing/xpcshell/head.js to change to 185 // the correct directory on Android Remote XPCShell tests. 186 // 187 // TODO: Bug 1801725 - Find a more ergonomic way to do this than exposing 188 // identical methods in XPCShellEnvironment and XPCShellImpl to chdir on 189 // android for Remote XPCShell tests on Android. 190 CallArgs args = CallArgsFromVp(argc, vp); 191 192 if (args.length() != 1) { 193 JS_ReportErrorASCII(cx, "changeTestShellDir() takes one argument"); 194 return false; 195 } 196 197 nsAutoJSCString path; 198 if (!path.init(cx, args[0])) { 199 JS_ReportErrorASCII( 200 cx, "changeTestShellDir(): could not convert argument 1 to string"); 201 return false; 202 } 203 204 if (chdir(path.get())) { 205 JS_ReportErrorASCII(cx, "changeTestShellDir(): could not change directory"); 206 return false; 207 } 208 209 return true; 210 } 211 #endif 212 213 const JSFunctionSpec gGlobalFunctions[] = { 214 JS_FN("print", Print, 0, 0), 215 JS_FN("load", Load, 1, 0), 216 JS_FN("quit", Quit, 0, 0), 217 JS_FN("dumpXPC", DumpXPC, 1, 0), 218 JS_FN("dump", Dump, 1, 0), 219 JS_FN("gc", GC, 0, 0), 220 #ifdef JS_GC_ZEAL 221 JS_FN("gczeal", GCZeal, 1, 0), 222 #endif 223 #ifdef ANDROID 224 JS_FN("changeTestShellDir", ChangeTestShellDir, 1, 0), 225 #endif 226 JS_FS_END}; 227 228 typedef enum JSShellErrNum { 229 #define MSG_DEF(name, number, count, exception, format) name = number, 230 #include "jsshell.msg" 231 #undef MSG_DEF 232 JSShellErr_Limit 233 #undef MSGDEF 234 } JSShellErrNum; 235 236 } /* anonymous namespace */ 237 238 void XPCShellEnvironment::ProcessFile(JSContext* cx, const char* filename, 239 FILE* file, bool forceTTY) { 240 XPCShellEnvironment* env = this; 241 242 JS::Rooted<JS::Value> result(cx); 243 int lineno, startline; 244 bool ok, hitEOF; 245 char *bufp, buffer[4096]; 246 JSString* str; 247 248 JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx)); 249 MOZ_ASSERT(global); 250 251 if (forceTTY) { 252 file = stdin; 253 } else if (!isatty(fileno(file))) { 254 /* 255 * It's not interactive - just execute it. 256 * 257 * Support the UNIX #! shell hack; gobble the first line if it starts 258 * with '#'. 259 */ 260 int ch = fgetc(file); 261 if (ch == '#') { 262 while ((ch = fgetc(file)) != EOF) { 263 if (ch == '\n' || ch == '\r') break; 264 } 265 } 266 ungetc(ch, file); 267 268 JS::CompileOptions options(cx); 269 options.setFileAndLine(filename, 1); 270 271 JS::Rooted<JSScript*> script(cx, JS::CompileUtf8File(cx, options, file)); 272 if (script) { 273 (void)JS_ExecuteScript(cx, script, &result); 274 } 275 276 return; 277 } 278 279 /* It's an interactive filehandle; drop into read-eval-print loop. */ 280 lineno = 1; 281 hitEOF = false; 282 do { 283 bufp = buffer; 284 *bufp = '\0'; 285 286 /* 287 * Accumulate lines until we get a 'compilable unit' - one that either 288 * generates an error (before running out of source) or that compiles 289 * cleanly. This should be whenever we get a complete statement that 290 * coincides with the end of a line. 291 */ 292 startline = lineno; 293 do { 294 if (!GetLine(bufp, file, startline == lineno ? "js> " : "")) { 295 hitEOF = true; 296 break; 297 } 298 bufp += strlen(bufp); 299 lineno++; 300 } while ( 301 !JS_Utf8BufferIsCompilableUnit(cx, global, buffer, strlen(buffer))); 302 303 /* Clear any pending exception from previous failed compiles. */ 304 JS_ClearPendingException(cx); 305 306 JS::CompileOptions options(cx); 307 options.setFileAndLine("typein", startline); 308 309 JS::SourceText<mozilla::Utf8Unit> srcBuf; 310 JS::Rooted<JSScript*> script(cx); 311 312 if (srcBuf.init(cx, buffer, strlen(buffer), 313 JS::SourceOwnership::Borrowed) && 314 (script = JS::Compile(cx, options, srcBuf))) { 315 ok = JS_ExecuteScript(cx, script, &result); 316 if (ok && !result.isUndefined()) { 317 /* Suppress warnings from JS::ToString(). */ 318 JS::AutoSuppressWarningReporter suppressWarnings(cx); 319 str = JS::ToString(cx, result); 320 JS::UniqueChars bytes; 321 if (str) bytes = JS_EncodeStringToLatin1(cx, str); 322 323 if (!!bytes) 324 fprintf(stdout, "%s\n", bytes.get()); 325 else 326 ok = false; 327 } 328 } 329 } while (!hitEOF && !env->IsQuitting()); 330 331 fprintf(stdout, "\n"); 332 } 333 334 // static 335 XPCShellEnvironment* XPCShellEnvironment::CreateEnvironment() { 336 auto* env = new XPCShellEnvironment(); 337 if (env && !env->Init()) { 338 delete env; 339 env = nullptr; 340 } 341 return env; 342 } 343 344 XPCShellEnvironment::XPCShellEnvironment() : mQuitting(false) {} 345 346 XPCShellEnvironment::~XPCShellEnvironment() { 347 if (GetGlobalObject()) { 348 AutoJSAPI jsapi; 349 if (!jsapi.Init(GetGlobalObject())) { 350 return; 351 } 352 JS_SetAllNonReservedSlotsToUndefined(mGlobalHolder); 353 mGlobalHolder.reset(); 354 355 JS_GC(jsapi.cx()); 356 } 357 } 358 359 bool XPCShellEnvironment::Init() { 360 nsresult rv; 361 362 // unbuffer stdout so that output is in the correct order; note that stderr 363 // is unbuffered by default 364 setbuf(stdout, 0); 365 366 AutoSafeJSContext cx; 367 368 mGlobalHolder.init(cx); 369 370 nsCOMPtr<nsIPrincipal> principal; 371 nsCOMPtr<nsIScriptSecurityManager> securityManager = 372 do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID, &rv); 373 if (NS_SUCCEEDED(rv) && securityManager) { 374 rv = securityManager->GetSystemPrincipal(getter_AddRefs(principal)); 375 if (NS_FAILED(rv)) { 376 fprintf(stderr, 377 "+++ Failed to obtain SystemPrincipal from ScriptSecurityManager " 378 "service.\n"); 379 } 380 } else { 381 fprintf(stderr, 382 "+++ Failed to get ScriptSecurityManager service, running without " 383 "principals"); 384 } 385 386 auto systemGlobal = MakeRefPtr<SystemGlobal>(); 387 388 JS::RealmOptions options; 389 options.creationOptions().setNewCompartmentInSystemZone(); 390 xpc::SetPrefableRealmOptions(options); 391 392 JS::Rooted<JSObject*> globalObj(cx); 393 rv = xpc::InitClassesWithNewWrappedGlobal( 394 cx, static_cast<nsIGlobalObject*>(systemGlobal), principal, 0, options, 395 &globalObj); 396 if (NS_FAILED(rv)) { 397 NS_ERROR("InitClassesWithNewWrappedGlobal failed!"); 398 return false; 399 } 400 401 if (!globalObj) { 402 NS_ERROR("Failed to get global JSObject!"); 403 return false; 404 } 405 JSAutoRealm ar(cx, globalObj); 406 407 systemGlobal->SetGlobalObject(globalObj); 408 409 JS::Rooted<Value> privateVal(cx, PrivateValue(this)); 410 if (!JS_DefineProperty(cx, globalObj, "__XPCShellEnvironment", privateVal, 411 JSPROP_READONLY | JSPROP_PERMANENT) || 412 !JS_DefineFunctions(cx, globalObj, gGlobalFunctions)) { 413 NS_ERROR("JS_DefineFunctions failed!"); 414 return false; 415 } 416 417 mGlobalHolder = globalObj; 418 419 FILE* runtimeScriptFile = fopen(kDefaultRuntimeScriptFilename, "r"); 420 if (runtimeScriptFile) { 421 fprintf(stdout, "[loading '%s'...]\n", kDefaultRuntimeScriptFilename); 422 ProcessFile(cx, kDefaultRuntimeScriptFilename, runtimeScriptFile, false); 423 fclose(runtimeScriptFile); 424 } 425 426 return true; 427 } 428 429 bool XPCShellEnvironment::EvaluateString(const nsAString& aString, 430 nsString* aResult) { 431 AutoEntryScript aes(GetGlobalObject(), 432 "ipc XPCShellEnvironment::EvaluateString"); 433 JSContext* cx = aes.cx(); 434 435 JS::CompileOptions options(cx); 436 options.setFileAndLine("typein", 0); 437 438 JS::SourceText<char16_t> srcBuf; 439 if (!srcBuf.init(cx, aString.BeginReading(), aString.Length(), 440 JS::SourceOwnership::Borrowed)) { 441 return false; 442 } 443 444 JS::Rooted<JSScript*> script(cx, JS::Compile(cx, options, srcBuf)); 445 if (!script) { 446 return false; 447 } 448 449 if (aResult) { 450 aResult->Truncate(); 451 } 452 453 JS::Rooted<JS::Value> result(cx); 454 bool ok = JS_ExecuteScript(cx, script, &result); 455 if (ok && !result.isUndefined()) { 456 /* Suppress warnings from JS::ToString(). */ 457 JS::AutoSuppressWarningReporter suppressWarnings(cx); 458 JSString* str = JS::ToString(cx, result); 459 nsAutoJSString autoStr; 460 if (str) autoStr.init(cx, str); 461 462 if (!autoStr.IsEmpty() && aResult) { 463 aResult->Assign(autoStr); 464 } 465 } 466 467 return true; 468 }