1 /* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
2 // SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
3 // SPDX-FileCopyrightText: 2008 litl, LLC
4
5 #include <config.h>
6
7 #include <signal.h> // for sigaction, SIGUSR1, sa_handler
8 #include <stdint.h>
9 #include <stdio.h> // for FILE, fclose, size_t
10 #include <string.h> // for memset
11
12 #ifdef HAVE_UNISTD_H
13 # include <unistd.h> // for getpid
14 #elif defined (_WIN32)
15 # include <process.h>
16 #endif
17
18 #ifdef DEBUG
19 # include <algorithm> // for find
20 #endif
21 #include <atomic>
22 #include <new>
23 #include <string> // for u16string
24 #include <thread> // for get_id
25 #include <unordered_map>
26 #include <utility> // for move
27 #include <vector>
28
29 #include <gio/gio.h>
30 #include <girepository.h>
31 #include <glib-object.h>
32 #include <glib.h>
33
34 #ifdef G_OS_WIN32
35 #include <windows.h>
36 #endif
37
38 #include <js/AllocPolicy.h> // for SystemAllocPolicy
39 #include <js/CallArgs.h> // for UndefinedHandleValue
40 #include <js/CharacterEncoding.h>
41 #include <js/CompilationAndEvaluation.h>
42 #include <js/CompileOptions.h>
43 #include <js/ErrorReport.h>
44 #include <js/Exception.h> // for StealPendingExceptionStack
45 #include <js/GCAPI.h> // for JS_GC, JS_AddExtraGCRootsTr...
46 #include <js/GCHashTable.h> // for WeakCache
47 #include <js/GCVector.h> // for RootedVector
48 #include <js/Id.h>
49 #include <js/Modules.h>
50 #include <js/Promise.h> // for JobQueue::SavedJobQueue
51 #include <js/PropertyDescriptor.h> // for JSPROP_PERMANENT, JSPROP_RE...
52 #include <js/RootingAPI.h>
53 #include <js/SourceText.h>
54 #include <js/TracingAPI.h>
55 #include <js/TypeDecls.h>
56 #include <js/UniquePtr.h>
57 #include <js/Value.h>
58 #include <js/ValueArray.h>
59 #include <jsapi.h> // for JS_IsExceptionPending, ...
60 #include <jsfriendapi.h> // for DumpHeap, IgnoreNurseryObjects
61 #include <mozilla/UniquePtr.h>
62
63 #include "gi/closure.h" // for Closure::Ptr, Closure
64 #include "gi/object.h"
65 #include "gi/private.h"
66 #include "gi/repo.h"
67 #include "gi/utils-inl.h"
68 #include "gjs/atoms.h"
69 #include "gjs/byteArray.h"
70 #include "gjs/context-private.h"
71 #include "gjs/context.h"
72 #include "gjs/engine.h"
73 #include "gjs/error-types.h"
74 #include "gjs/global.h"
75 #include "gjs/importer.h"
76 #include "gjs/internal.h"
77 #include "gjs/jsapi-util.h"
78 #include "gjs/mem.h"
79 #include "gjs/module.h"
80 #include "gjs/native.h"
81 #include "gjs/objectbox.h"
82 #include "gjs/profiler-private.h"
83 #include "gjs/profiler.h"
84 #include "gjs/text-encoding.h"
85 #include "modules/modules.h"
86 #include "util/log.h"
87
88 static void gjs_context_dispose (GObject *object);
89 static void gjs_context_finalize (GObject *object);
90 static void gjs_context_constructed (GObject *object);
91 static void gjs_context_get_property (GObject *object,
92 guint prop_id,
93 GValue *value,
94 GParamSpec *pspec);
95 static void gjs_context_set_property (GObject *object,
96 guint prop_id,
97 const GValue *value,
98 GParamSpec *pspec);
99
invoke(JS::HandleObject scope,Closure & closure)100 void GjsContextPrivate::EnvironmentPreparer::invoke(JS::HandleObject scope,
101 Closure& closure) {
102 g_assert(!JS_IsExceptionPending(m_cx));
103
104 JSAutoRealm ar(m_cx, scope);
105 if (!closure(m_cx))
106 gjs_log_exception(m_cx);
107 }
108
109 struct _GjsContext {
110 GObject parent;
111 };
112
113 struct _GjsContextClass {
114 GObjectClass parent;
115 };
116
117 G_DEFINE_TYPE_WITH_PRIVATE(GjsContext, gjs_context, G_TYPE_OBJECT);
118
from_object(GObject * js_context)119 GjsContextPrivate* GjsContextPrivate::from_object(GObject* js_context) {
120 g_return_val_if_fail(GJS_IS_CONTEXT(js_context), nullptr);
121 return static_cast<GjsContextPrivate*>(
122 gjs_context_get_instance_private(GJS_CONTEXT(js_context)));
123 }
124
from_object(GjsContext * js_context)125 GjsContextPrivate* GjsContextPrivate::from_object(GjsContext* js_context) {
126 g_return_val_if_fail(GJS_IS_CONTEXT(js_context), nullptr);
127 return static_cast<GjsContextPrivate*>(
128 gjs_context_get_instance_private(js_context));
129 }
130
from_current_context()131 GjsContextPrivate* GjsContextPrivate::from_current_context() {
132 return from_object(gjs_context_get_current());
133 }
134
135 enum {
136 PROP_CONTEXT_0,
137 PROP_PROGRAM_PATH,
138 PROP_SEARCH_PATH,
139 PROP_PROGRAM_NAME,
140 PROP_PROFILER_ENABLED,
141 PROP_PROFILER_SIGUSR2,
142 PROP_EXEC_AS_MODULE,
143 };
144
145 static GMutex contexts_lock;
146 static GList *all_contexts = NULL;
147
148 static GjsAutoChar dump_heap_output;
149 static unsigned dump_heap_idle_id = 0;
150
151 #ifdef G_OS_UNIX
152 // Currently heap dumping via SIGUSR1 is only supported on UNIX platforms!
153 // This can reduce performance. See note in system.cpp on System.dumpHeap().
154 static void
gjs_context_dump_heaps(void)155 gjs_context_dump_heaps(void)
156 {
157 static unsigned counter = 0;
158
159 gjs_memory_report("signal handler", false);
160
161 /* dump to sequential files to allow easier comparisons */
162 GjsAutoChar filename = g_strdup_printf("%s.%jd.%u", dump_heap_output.get(),
163 intmax_t(getpid()), counter);
164 ++counter;
165
166 FILE *fp = fopen(filename, "w");
167 if (!fp)
168 return;
169
170 for (GList *l = all_contexts; l; l = g_list_next(l)) {
171 auto* gjs = static_cast<GjsContextPrivate*>(l->data);
172 js::DumpHeap(gjs->context(), fp, js::CollectNurseryBeforeDump);
173 }
174
175 fclose(fp);
176 }
177
dump_heap_idle(void *)178 static gboolean dump_heap_idle(void*) {
179 dump_heap_idle_id = 0;
180
181 gjs_context_dump_heaps();
182
183 return false;
184 }
185
dump_heap_signal_handler(int signum)186 static void dump_heap_signal_handler(int signum [[maybe_unused]]) {
187 if (dump_heap_idle_id == 0)
188 dump_heap_idle_id = g_idle_add_full(G_PRIORITY_HIGH_IDLE,
189 dump_heap_idle, nullptr, nullptr);
190 }
191 #endif
192
193 static void
setup_dump_heap(void)194 setup_dump_heap(void)
195 {
196 static bool dump_heap_initialized = false;
197 if (!dump_heap_initialized) {
198 dump_heap_initialized = true;
199
200 /* install signal handler only if environment variable is set */
201 const char *heap_output = g_getenv("GJS_DEBUG_HEAP_OUTPUT");
202 if (heap_output) {
203 #ifdef G_OS_UNIX
204 struct sigaction sa;
205
206 dump_heap_output = g_strdup(heap_output);
207
208 memset(&sa, 0, sizeof(sa));
209 sa.sa_handler = dump_heap_signal_handler;
210 sigaction(SIGUSR1, &sa, nullptr);
211 #else
212 g_message(
213 "heap dump is currently only supported on UNIX platforms");
214 #endif
215 }
216 }
217 }
218
219 static void
gjs_context_init(GjsContext * js_context)220 gjs_context_init(GjsContext *js_context)
221 {
222 gjs_log_init();
223 gjs_context_make_current(js_context);
224 }
225
226 static void
gjs_context_class_init(GjsContextClass * klass)227 gjs_context_class_init(GjsContextClass *klass)
228 {
229 GObjectClass *object_class = G_OBJECT_CLASS (klass);
230 GParamSpec *pspec;
231
232 gjs_log_init();
233
234 object_class->dispose = gjs_context_dispose;
235 object_class->finalize = gjs_context_finalize;
236
237 object_class->constructed = gjs_context_constructed;
238 object_class->get_property = gjs_context_get_property;
239 object_class->set_property = gjs_context_set_property;
240
241 pspec = g_param_spec_boxed("search-path",
242 "Search path",
243 "Path where modules to import should reside",
244 G_TYPE_STRV,
245 (GParamFlags) (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
246
247 g_object_class_install_property(object_class,
248 PROP_SEARCH_PATH,
249 pspec);
250 g_param_spec_unref(pspec);
251
252 pspec = g_param_spec_string("program-name",
253 "Program Name",
254 "The filename of the launched JS program",
255 "",
256 (GParamFlags) (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
257
258 g_object_class_install_property(object_class,
259 PROP_PROGRAM_NAME,
260 pspec);
261 g_param_spec_unref(pspec);
262
263 pspec = g_param_spec_string(
264 "program-path", "Executed File Path",
265 "The full path of the launched file or NULL if GJS was launched from "
266 "the C API or interactive console.",
267 nullptr, (GParamFlags)(G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
268
269 g_object_class_install_property(object_class, PROP_PROGRAM_PATH, pspec);
270 g_param_spec_unref(pspec);
271
272 /**
273 * GjsContext:profiler-enabled:
274 *
275 * Set this property to profile any JS code run by this context. By
276 * default, the profiler is started and stopped when you call
277 * gjs_context_eval().
278 *
279 * The value of this property is superseded by the GJS_ENABLE_PROFILER
280 * environment variable.
281 *
282 * You may only have one context with the profiler enabled at a time.
283 */
284 pspec = g_param_spec_boolean("profiler-enabled", "Profiler enabled",
285 "Whether to profile JS code run by this context",
286 FALSE,
287 GParamFlags(G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
288 g_object_class_install_property(object_class, PROP_PROFILER_ENABLED, pspec);
289 g_param_spec_unref(pspec);
290
291 /**
292 * GjsContext:profiler-sigusr2:
293 *
294 * Set this property to install a SIGUSR2 signal handler that starts and
295 * stops the profiler. This property also implies that
296 * #GjsContext:profiler-enabled is set.
297 */
298 pspec = g_param_spec_boolean("profiler-sigusr2", "Profiler SIGUSR2",
299 "Whether to activate the profiler on SIGUSR2",
300 FALSE,
301 GParamFlags(G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
302 g_object_class_install_property(object_class, PROP_PROFILER_SIGUSR2, pspec);
303 g_param_spec_unref(pspec);
304
305 pspec = g_param_spec_boolean(
306 "exec-as-module", "Execute as module",
307 "Whether to execute the file as a module", FALSE,
308 GParamFlags(G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
309 g_object_class_install_property(object_class, PROP_EXEC_AS_MODULE, pspec);
310 g_param_spec_unref(pspec);
311
312 /* For GjsPrivate */
313 if (!g_getenv("GJS_USE_UNINSTALLED_FILES")) {
314 #ifdef G_OS_WIN32
315 extern HMODULE gjs_dll;
316 char *basedir = g_win32_get_package_installation_directory_of_module (gjs_dll);
317 char *priv_typelib_dir = g_build_filename (basedir, "lib", "gjs", "girepository-1.0", NULL);
318 g_free (basedir);
319 #else
320 char *priv_typelib_dir = g_build_filename (PKGLIBDIR, "girepository-1.0", NULL);
321 #endif
322 g_irepository_prepend_search_path(priv_typelib_dir);
323 g_free (priv_typelib_dir);
324 }
325
326 gjs_register_native_module("_byteArrayNative", gjs_define_byte_array_stuff);
327 gjs_register_native_module("_encodingNative",
328 gjs_define_text_encoding_stuff);
329 gjs_register_native_module("_gi", gjs_define_private_gi_stuff);
330 gjs_register_native_module("gi", gjs_define_repo);
331
332 gjs_register_static_modules();
333 }
334
trace(JSTracer * trc,void * data)335 void GjsContextPrivate::trace(JSTracer* trc, void* data) {
336 auto* gjs = static_cast<GjsContextPrivate*>(data);
337 JS::TraceEdge<JSObject*>(trc, &gjs->m_global, "GJS global object");
338 JS::TraceEdge<JSObject*>(trc, &gjs->m_internal_global,
339 "GJS internal global object");
340 gjs->m_atoms->trace(trc);
341 gjs->m_job_queue.trace(trc);
342 gjs->m_object_init_list.trace(trc);
343 }
344
warn_about_unhandled_promise_rejections(void)345 void GjsContextPrivate::warn_about_unhandled_promise_rejections(void) {
346 for (auto& kv : m_unhandled_rejection_stacks) {
347 const char *stack = kv.second;
348 g_warning("Unhandled promise rejection. To suppress this warning, add "
349 "an error handler to your promise chain with .catch() or a "
350 "try-catch block around your await expression. %s%s",
351 stack ? "Stack trace of the failed promise:\n" :
352 "Unfortunately there is no stack trace of the failed promise.",
353 stack ? stack : "");
354 }
355 m_unhandled_rejection_stacks.clear();
356 }
357
358 static void
gjs_context_dispose(GObject * object)359 gjs_context_dispose(GObject *object)
360 {
361 gjs_debug(GJS_DEBUG_CONTEXT, "JS shutdown sequence");
362
363 GjsContextPrivate* gjs = GjsContextPrivate::from_object(object);
364
365 g_assert(gjs->is_owner_thread() &&
366 "Gjs Context disposed from another thread");
367
368 /* Profiler must be stopped and freed before context is shut down */
369 gjs->free_profiler();
370
371 /* Stop accepting entries in the toggle queue before running dispose
372 * notifications, which causes all GjsMaybeOwned instances to unroot.
373 * We don't want any objects to toggle down after that. */
374 gjs_debug(GJS_DEBUG_CONTEXT, "Shutting down toggle queue");
375 gjs_object_clear_toggles();
376 gjs_object_shutdown_toggle_queue();
377
378 if (gjs->context())
379 ObjectInstance::context_dispose_notify(nullptr, object);
380
381 gjs_debug(GJS_DEBUG_CONTEXT,
382 "Notifying external reference holders of GjsContext dispose");
383 G_OBJECT_CLASS(gjs_context_parent_class)->dispose(object);
384
385 gjs->dispose();
386 }
387
free_profiler(void)388 void GjsContextPrivate::free_profiler(void) {
389 gjs_debug(GJS_DEBUG_CONTEXT, "Stopping profiler");
390 if (m_profiler)
391 g_clear_pointer(&m_profiler, _gjs_profiler_free);
392 }
393
register_notifier(DestroyNotify notify_func,void * data)394 void GjsContextPrivate::register_notifier(DestroyNotify notify_func,
395 void* data) {
396 m_destroy_notifications.push_back({notify_func, data});
397 }
398
unregister_notifier(DestroyNotify notify_func,void * data)399 void GjsContextPrivate::unregister_notifier(DestroyNotify notify_func,
400 void* data) {
401 auto target = std::make_pair(notify_func, data);
402 Gjs::remove_one_from_unsorted_vector(&m_destroy_notifications, target);
403 }
404
dispose(void)405 void GjsContextPrivate::dispose(void) {
406 if (m_cx) {
407 gjs_debug(GJS_DEBUG_CONTEXT,
408 "Notifying reference holders of GjsContext dispose");
409
410 for (auto const& destroy_notify : m_destroy_notifications)
411 destroy_notify.first(m_cx, destroy_notify.second);
412
413 gjs_debug(GJS_DEBUG_CONTEXT,
414 "Checking unhandled promise rejections");
415 warn_about_unhandled_promise_rejections();
416
417 gjs_debug(GJS_DEBUG_CONTEXT, "Releasing cached JS wrappers");
418 m_fundamental_table->clear();
419 m_gtype_table->clear();
420
421 /* Do a full GC here before tearing down, since once we do
422 * that we may not have the JS_GetPrivate() to access the
423 * context
424 */
425 gjs_debug(GJS_DEBUG_CONTEXT, "Final triggered GC");
426 JS_GC(m_cx, Gjs::GCReason::GJS_CONTEXT_DISPOSE);
427
428 gjs_debug(GJS_DEBUG_CONTEXT, "Destroying JS context");
429 m_destroying.store(true);
430
431 /* Now, release all native objects, to avoid recursion between
432 * the JS teardown and the C teardown. The JSObject proxies
433 * still exist, but point to NULL.
434 */
435 gjs_debug(GJS_DEBUG_CONTEXT, "Releasing all native objects");
436 ObjectInstance::prepare_shutdown();
437
438 gjs_debug(GJS_DEBUG_CONTEXT, "Disabling auto GC");
439 if (m_auto_gc_id > 0) {
440 g_source_remove(m_auto_gc_id);
441 m_auto_gc_id = 0;
442 }
443
444 gjs_debug(GJS_DEBUG_CONTEXT, "Ending trace on global object");
445 JS_RemoveExtraGCRootsTracer(m_cx, &GjsContextPrivate::trace, this);
446 m_global = nullptr;
447 m_internal_global = nullptr;
448
449 gjs_debug(GJS_DEBUG_CONTEXT, "Freeing allocated resources");
450 delete m_fundamental_table;
451 delete m_gtype_table;
452 delete m_atoms;
453
454 /* Tear down JS */
455 JS_DestroyContext(m_cx);
456 m_cx = nullptr;
457 // don't use g_clear_pointer() as we want the pointer intact while we
458 // destroy the context in case we dump stack
459 gjs_debug(GJS_DEBUG_CONTEXT, "JS context destroyed");
460 }
461 }
462
~GjsContextPrivate(void)463 GjsContextPrivate::~GjsContextPrivate(void) {
464 g_clear_pointer(&m_search_path, g_strfreev);
465 g_clear_pointer(&m_program_path, g_free);
466 g_clear_pointer(&m_program_name, g_free);
467 }
468
469 static void
gjs_context_finalize(GObject * object)470 gjs_context_finalize(GObject *object)
471 {
472 if (gjs_context_get_current() == (GjsContext*)object)
473 gjs_context_make_current(NULL);
474
475 g_mutex_lock(&contexts_lock);
476 all_contexts = g_list_remove(all_contexts, object);
477 g_mutex_unlock(&contexts_lock);
478
479 GjsContextPrivate* gjs = GjsContextPrivate::from_object(object);
480 gjs->~GjsContextPrivate();
481 G_OBJECT_CLASS(gjs_context_parent_class)->finalize(object);
482
483 g_mutex_lock(&contexts_lock);
484 if (!all_contexts)
485 gjs_log_cleanup();
486 g_mutex_unlock(&contexts_lock);
487 }
488
489 static void
gjs_context_constructed(GObject * object)490 gjs_context_constructed(GObject *object)
491 {
492 GjsContext *js_context = GJS_CONTEXT(object);
493
494 G_OBJECT_CLASS(gjs_context_parent_class)->constructed(object);
495
496 GjsContextPrivate* gjs_location = GjsContextPrivate::from_object(object);
497 JSContext* cx = gjs_create_js_context(gjs_location);
498 if (!cx)
499 g_error("Failed to create javascript context");
500
501 new (gjs_location) GjsContextPrivate(cx, js_context);
502
503 g_mutex_lock(&contexts_lock);
504 all_contexts = g_list_prepend(all_contexts, object);
505 g_mutex_unlock(&contexts_lock);
506
507 setup_dump_heap();
508 }
509
load_context_module(JSContext * cx,const char * uri,const char * debug_identifier)510 static void load_context_module(JSContext* cx, const char* uri,
511 const char* debug_identifier) {
512 JS::RootedObject loader(cx, gjs_module_load(cx, uri, uri));
513
514 if (!loader) {
515 gjs_log_exception(cx);
516 g_error("Failed to load %s module.", debug_identifier);
517 }
518
519 if (!JS::ModuleInstantiate(cx, loader)) {
520 gjs_log_exception(cx);
521 g_error("Failed to instantiate %s module.", debug_identifier);
522 }
523
524 if (!JS::ModuleEvaluate(cx, loader)) {
525 gjs_log_exception(cx);
526 g_error("Failed to evaluate %s module.", debug_identifier);
527 }
528 }
529
GjsContextPrivate(JSContext * cx,GjsContext * public_context)530 GjsContextPrivate::GjsContextPrivate(JSContext* cx, GjsContext* public_context)
531 : m_public_context(public_context),
532 m_cx(cx),
533 m_owner_thread(std::this_thread::get_id()),
534 m_environment_preparer(cx) {
535
536 JS_SetGCCallback(
537 cx,
538 [](JSContext*, JSGCStatus status, JS::GCReason reason, void* data) {
539 static_cast<GjsContextPrivate*>(data)->on_garbage_collection(
540 status, reason);
541 },
542 this);
543
544 const char *env_profiler = g_getenv("GJS_ENABLE_PROFILER");
545 if (env_profiler || m_should_listen_sigusr2)
546 m_should_profile = true;
547
548 if (m_should_profile) {
549 m_profiler = _gjs_profiler_new(public_context);
550
551 if (!m_profiler) {
552 m_should_profile = false;
553 } else {
554 if (m_should_listen_sigusr2)
555 _gjs_profiler_setup_signals(m_profiler, public_context);
556 }
557 }
558
559 JSRuntime* rt = JS_GetRuntime(m_cx);
560 m_fundamental_table = new JS::WeakCache<FundamentalTable>(rt);
561 m_gtype_table = new JS::WeakCache<GTypeTable>(rt);
562
563 m_atoms = new GjsAtoms();
564
565 if (ObjectBox::gtype() == 0)
566 g_error("Failed to initialize JSObject GType");
567
568 JS::RootedObject internal_global(
569 m_cx, gjs_create_global_object(cx, GjsGlobalType::INTERNAL));
570
571 if (!internal_global) {
572 gjs_log_exception(m_cx);
573 g_error("Failed to initialize internal global object");
574 }
575
576 JSAutoRealm ar(m_cx, internal_global);
577
578 m_internal_global = internal_global;
579 JS_AddExtraGCRootsTracer(m_cx, &GjsContextPrivate::trace, this);
580
581 if (!m_atoms->init_atoms(m_cx)) {
582 gjs_log_exception(m_cx);
583 g_error("Failed to initialize global strings");
584 }
585
586 if (!gjs_define_global_properties(m_cx, internal_global,
587 GjsGlobalType::INTERNAL,
588 "GJS internal global", "nullptr")) {
589 gjs_log_exception(m_cx);
590 g_error("Failed to define properties on internal global object");
591 }
592
593 JS::RootedObject global(
594 m_cx,
595 gjs_create_global_object(cx, GjsGlobalType::DEFAULT, internal_global));
596
597 if (!global) {
598 gjs_log_exception(m_cx);
599 g_error("Failed to initialize global object");
600 }
601
602 m_global = global;
603
604 {
605 JSAutoRealm ar(cx, global);
606
607 std::vector<std::string> paths;
608 if (m_search_path)
609 paths = {m_search_path,
610 m_search_path + g_strv_length(m_search_path)};
611 JS::RootedObject importer(m_cx, gjs_create_root_importer(m_cx, paths));
612 if (!importer) {
613 gjs_log_exception(cx);
614 g_error("Failed to create root importer");
615 }
616
617 g_assert(
618 gjs_get_global_slot(global, GjsGlobalSlot::IMPORTS).isUndefined() &&
619 "Someone else already created root importer");
620
621 gjs_set_global_slot(global, GjsGlobalSlot::IMPORTS,
622 JS::ObjectValue(*importer));
623
624 if (!gjs_define_global_properties(m_cx, global, GjsGlobalType::DEFAULT,
625 "GJS", "default")) {
626 gjs_log_exception(m_cx);
627 g_error("Failed to define properties on global object");
628 }
629 }
630
631 JS::SetModuleResolveHook(rt, gjs_module_resolve);
632 JS::SetModuleDynamicImportHook(rt, gjs_dynamic_module_resolve);
633 JS::SetModuleMetadataHook(rt, gjs_populate_module_meta);
634
635 if (!JS_DefineProperty(m_cx, internal_global, "moduleGlobalThis", global,
636 JSPROP_PERMANENT)) {
637 gjs_log_exception(m_cx);
638 g_error("Failed to define module global in internal global.");
639 }
640
641 if (!gjs_load_internal_module(cx, "internalLoader")) {
642 gjs_log_exception(cx);
643 g_error("Failed to load internal module loaders.");
644 }
645
646 load_context_module(cx,
647 "resource:///org/gnome/gjs/modules/internal/loader.js",
648 "module loader");
649
650 {
651 JSAutoRealm ar(cx, global);
652 load_context_module(
653 cx, "resource:///org/gnome/gjs/modules/esm/_bootstrap/default.js",
654 "ESM bootstrap");
655 }
656 }
657
set_args(std::vector<std::string> && args)658 void GjsContextPrivate::set_args(std::vector<std::string>&& args) {
659 m_args = args;
660 }
661
build_args_array()662 JSObject* GjsContextPrivate::build_args_array() {
663 return gjs_build_string_array(m_cx, m_args);
664 }
665
666 static void
gjs_context_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)667 gjs_context_get_property (GObject *object,
668 guint prop_id,
669 GValue *value,
670 GParamSpec *pspec)
671 {
672 GjsContextPrivate* gjs = GjsContextPrivate::from_object(object);
673
674 switch (prop_id) {
675 case PROP_PROGRAM_NAME:
676 g_value_set_string(value, gjs->program_name());
677 break;
678 case PROP_PROGRAM_PATH:
679 g_value_set_string(value, gjs->program_path());
680 break;
681 default:
682 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
683 break;
684 }
685 }
686
687 static void
gjs_context_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)688 gjs_context_set_property (GObject *object,
689 guint prop_id,
690 const GValue *value,
691 GParamSpec *pspec)
692 {
693 GjsContextPrivate* gjs = GjsContextPrivate::from_object(object);
694
695 switch (prop_id) {
696 case PROP_SEARCH_PATH:
697 gjs->set_search_path(static_cast<char**>(g_value_dup_boxed(value)));
698 break;
699 case PROP_PROGRAM_NAME:
700 gjs->set_program_name(g_value_dup_string(value));
701 break;
702 case PROP_PROGRAM_PATH:
703 gjs->set_program_path(g_value_dup_string(value));
704 break;
705 case PROP_PROFILER_ENABLED:
706 gjs->set_should_profile(g_value_get_boolean(value));
707 break;
708 case PROP_PROFILER_SIGUSR2:
709 gjs->set_should_listen_sigusr2(g_value_get_boolean(value));
710 break;
711 case PROP_EXEC_AS_MODULE:
712 gjs->set_execute_as_module(g_value_get_boolean(value));
713 break;
714 default:
715 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
716 break;
717 }
718 }
719
720
721 GjsContext*
gjs_context_new(void)722 gjs_context_new(void)
723 {
724 return (GjsContext*) g_object_new (GJS_TYPE_CONTEXT, NULL);
725 }
726
727 GjsContext*
gjs_context_new_with_search_path(char ** search_path)728 gjs_context_new_with_search_path(char** search_path)
729 {
730 return (GjsContext*) g_object_new (GJS_TYPE_CONTEXT,
731 "search-path", search_path,
732 NULL);
733 }
734
trigger_gc_if_needed(void * data)735 gboolean GjsContextPrivate::trigger_gc_if_needed(void* data) {
736 auto* gjs = static_cast<GjsContextPrivate*>(data);
737 gjs->m_auto_gc_id = 0;
738
739 if (gjs->m_force_gc) {
740 gjs_debug_lifecycle(GJS_DEBUG_CONTEXT, "Big Hammer hit");
741 JS_GC(gjs->m_cx, Gjs::GCReason::BIG_HAMMER);
742 } else {
743 gjs_gc_if_needed(gjs->m_cx);
744 }
745 gjs->m_force_gc = false;
746
747 return G_SOURCE_REMOVE;
748 }
749
schedule_gc_internal(bool force_gc)750 void GjsContextPrivate::schedule_gc_internal(bool force_gc) {
751 m_force_gc |= force_gc;
752
753 if (m_auto_gc_id > 0)
754 return;
755
756 if (force_gc)
757 gjs_debug_lifecycle(GJS_DEBUG_CONTEXT, "Big Hammer scheduled");
758
759 m_auto_gc_id = g_timeout_add_seconds_full(G_PRIORITY_LOW, 10,
760 trigger_gc_if_needed, this,
761 nullptr);
762 }
763
764 /*
765 * GjsContextPrivate::schedule_gc_if_needed:
766 *
767 * Does a minor GC immediately if the JS engine decides one is needed, but also
768 * schedules a full GC in the next idle time.
769 */
schedule_gc_if_needed(void)770 void GjsContextPrivate::schedule_gc_if_needed(void) {
771 // We call JS_MaybeGC immediately, but defer a check for a full GC cycle
772 // to an idle handler.
773 JS_MaybeGC(m_cx);
774
775 schedule_gc_internal(false);
776 }
777
on_garbage_collection(JSGCStatus status,JS::GCReason reason)778 void GjsContextPrivate::on_garbage_collection(JSGCStatus status, JS::GCReason reason) {
779 int64_t now = 0;
780 if (m_profiler)
781 now = g_get_monotonic_time() * 1000L;
782
783 switch (status) {
784 case JSGC_BEGIN:
785 m_gc_begin_time = now;
786 m_gc_reason = gjs_explain_gc_reason(reason);
787 gjs_debug_lifecycle(GJS_DEBUG_CONTEXT,
788 "Begin garbage collection because of %s",
789 m_gc_reason);
790
791 // We finalize any pending toggle refs before doing any garbage
792 // collection, so that we can collect the JS wrapper objects, and in
793 // order to minimize the chances of objects having a pending toggle
794 // up queued when they are garbage collected.
795 gjs_object_clear_toggles();
796
797 m_async_closures.clear();
798 m_async_closures.shrink_to_fit();
799 break;
800 case JSGC_END:
801 if (m_profiler && m_gc_begin_time != 0) {
802 _gjs_profiler_add_mark(m_profiler, m_gc_begin_time,
803 now - m_gc_begin_time, "GJS",
804 "Garbage collection", m_gc_reason);
805 }
806 m_gc_begin_time = 0;
807 m_gc_reason = nullptr;
808
809 m_destroy_notifications.shrink_to_fit();
810 gjs_debug_lifecycle(GJS_DEBUG_CONTEXT, "End garbage collection");
811 break;
812 default:
813 g_assert_not_reached();
814 }
815 }
816
set_finalize_status(JSFinalizeStatus status)817 void GjsContextPrivate::set_finalize_status(JSFinalizeStatus status) {
818 // Implementation note for mozjs-24:
819 //
820 // Sweeping happens in two phases, in the first phase all GC things from the
821 // allocation arenas are queued for sweeping, then the actual sweeping
822 // happens. The first phase is marked by JSFINALIZE_GROUP_START, the second
823 // one by JSFINALIZE_GROUP_END, and finally we will see
824 // JSFINALIZE_COLLECTION_END at the end of all GC. (see jsgc.cpp,
825 // BeginSweepPhase/BeginSweepingZoneGroup and SweepPhase, all called from
826 // IncrementalCollectSlice).
827 //
828 // Incremental GC muddies the waters, because BeginSweepPhase is always run
829 // to entirety, but SweepPhase can be run incrementally and mixed with JS
830 // code runs or even native code, when MaybeGC/IncrementalGC return.
831 //
832 // Luckily for us, objects are treated specially, and are not really queued
833 // for deferred incremental finalization (unless they are marked for
834 // background sweeping). Instead, they are finalized immediately during
835 // phase 1, so the following guarantees are true (and we rely on them):
836 // - phase 1 of GC will begin and end in the same JSAPI call (i.e., our
837 // callback will be called with GROUP_START and the triggering JSAPI call
838 // will not return until we see a GROUP_END)
839 // - object finalization will begin and end in the same JSAPI call
840 // - therefore, if there is a finalizer frame somewhere in the stack,
841 // GjsContextPrivate::sweeping() will return true.
842 //
843 // Comments in mozjs-24 imply that this behavior might change in the future,
844 // but it hasn't changed in mozilla-central as of 2014-02-23. In addition to
845 // that, the mozilla-central version has a huge comment in a different
846 // portion of the file, explaining why finalization of objects can't be
847 // mixed with JS code, so we can probably rely on this behavior.
848
849 int64_t now = 0;
850
851 if (m_profiler)
852 now = g_get_monotonic_time() * 1000L;
853
854 switch (status) {
855 case JSFINALIZE_GROUP_PREPARE:
856 m_in_gc_sweep = true;
857 m_sweep_begin_time = now;
858 break;
859 case JSFINALIZE_GROUP_START:
860 m_group_sweep_begin_time = now;
861 break;
862 case JSFINALIZE_GROUP_END:
863 if (m_profiler && m_group_sweep_begin_time != 0) {
864 _gjs_profiler_add_mark(m_profiler, m_group_sweep_begin_time,
865 now - m_group_sweep_begin_time, "GJS",
866 "Group sweep", nullptr);
867 }
868 m_group_sweep_begin_time = 0;
869 break;
870 case JSFINALIZE_COLLECTION_END:
871 m_in_gc_sweep = false;
872 if (m_profiler && m_sweep_begin_time != 0) {
873 _gjs_profiler_add_mark(m_profiler, m_sweep_begin_time,
874 now - m_sweep_begin_time, "GJS", "Sweep",
875 nullptr);
876 }
877 m_sweep_begin_time = 0;
878 break;
879 default:
880 g_assert_not_reached();
881 }
882 }
883
exit(uint8_t exit_code)884 void GjsContextPrivate::exit(uint8_t exit_code) {
885 g_assert(!m_should_exit);
886 m_should_exit = true;
887 m_exit_code = exit_code;
888 }
889
should_exit(uint8_t * exit_code_p) const890 bool GjsContextPrivate::should_exit(uint8_t* exit_code_p) const {
891 if (exit_code_p != NULL)
892 *exit_code_p = m_exit_code;
893 return m_should_exit;
894 }
895
start_draining_job_queue(void)896 void GjsContextPrivate::start_draining_job_queue(void) {
897 if (!m_idle_drain_handler) {
898 gjs_debug(GJS_DEBUG_CONTEXT, "Starting promise job queue handler");
899 m_idle_drain_handler = g_idle_add_full(
900 G_PRIORITY_DEFAULT, drain_job_queue_idle_handler, this, nullptr);
901 }
902 }
903
stop_draining_job_queue(void)904 void GjsContextPrivate::stop_draining_job_queue(void) {
905 m_draining_job_queue = false;
906 if (m_idle_drain_handler) {
907 gjs_debug(GJS_DEBUG_CONTEXT, "Stopping promise job queue handler");
908 g_source_remove(m_idle_drain_handler);
909 m_idle_drain_handler = 0;
910 }
911 }
912
drain_job_queue_idle_handler(void * data)913 gboolean GjsContextPrivate::drain_job_queue_idle_handler(void* data) {
914 gjs_debug(GJS_DEBUG_CONTEXT, "Promise job queue handler");
915 auto* gjs = static_cast<GjsContextPrivate*>(data);
916 gjs->runJobs(gjs->context());
917 /* Uncatchable exceptions are swallowed here - no way to get a handle on
918 * the main loop to exit it from this idle handler */
919 gjs_debug(GJS_DEBUG_CONTEXT, "Promise job queue handler finished");
920 g_assert(gjs->empty() && gjs->m_idle_drain_handler == 0 &&
921 "GjsContextPrivate::runJobs() should have emptied queue");
922 return G_SOURCE_REMOVE;
923 }
924
getIncumbentGlobal(JSContext * cx)925 JSObject* GjsContextPrivate::getIncumbentGlobal(JSContext* cx) {
926 // This is equivalent to SpiderMonkey's behavior.
927 return JS::CurrentGlobalOrNull(cx);
928 }
929
930 // See engine.cpp and JS::SetJobQueue().
enqueuePromiseJob(JSContext * cx,JS::HandleObject promise,JS::HandleObject job,JS::HandleObject allocation_site,JS::HandleObject incumbent_global)931 bool GjsContextPrivate::enqueuePromiseJob(JSContext* cx [[maybe_unused]],
932 JS::HandleObject promise,
933 JS::HandleObject job,
934 JS::HandleObject allocation_site,
935 JS::HandleObject incumbent_global
936 [[maybe_unused]]) {
937 g_assert(cx == m_cx);
938 g_assert(from_cx(cx) == this);
939
940 gjs_debug(GJS_DEBUG_CONTEXT,
941 "Enqueue job %s, promise=%s, allocation site=%s",
942 gjs_debug_object(job).c_str(), gjs_debug_object(promise).c_str(),
943 gjs_debug_object(allocation_site).c_str());
944
945 if (m_idle_drain_handler)
946 g_assert(!empty());
947 else
948 g_assert(empty());
949
950 if (!m_job_queue.append(job)) {
951 JS_ReportOutOfMemory(m_cx);
952 return false;
953 }
954
955 JS::JobQueueMayNotBeEmpty(m_cx);
956 start_draining_job_queue();
957 return true;
958 }
959
960 // Override of JobQueue::runJobs(). Called by js::RunJobs(), and when execution
961 // of the job queue was interrupted by the debugger and is resuming.
runJobs(JSContext * cx)962 void GjsContextPrivate::runJobs(JSContext* cx) {
963 g_assert(cx == m_cx);
964 g_assert(from_cx(cx) == this);
965 if (!run_jobs_fallible())
966 gjs_log_exception(cx);
967 }
968
969 /*
970 * GjsContext::run_jobs_fallible:
971 *
972 * Drains the queue of promise callbacks that the JS engine has reported
973 * finished, calling each one and logging any exceptions that it throws.
974 *
975 * Adapted from js::RunJobs() in SpiderMonkey's default job queue
976 * implementation.
977 *
978 * Returns: false if one of the jobs threw an uncatchable exception;
979 * otherwise true.
980 */
run_jobs_fallible(void)981 bool GjsContextPrivate::run_jobs_fallible(void) {
982 bool retval = true;
983
984 if (m_draining_job_queue || m_should_exit)
985 return true;
986
987 m_draining_job_queue = true; // Ignore reentrant calls
988
989 JS::RootedObject job(m_cx);
990 JS::HandleValueArray args(JS::HandleValueArray::empty());
991 JS::RootedValue rval(m_cx);
992
993 /* Execute jobs in a loop until we've reached the end of the queue.
994 * Since executing a job can trigger enqueueing of additional jobs,
995 * it's crucial to recheck the queue length during each iteration. */
996 for (size_t ix = 0; ix < m_job_queue.length(); ix++) {
997 /* A previous job might have set this flag. e.g., System.exit(). */
998 if (m_should_exit)
999 break;
1000
1001 job = m_job_queue[ix];
1002
1003 /* It's possible that job draining was interrupted prematurely,
1004 * leaving the queue partly processed. In that case, slots for
1005 * already-executed entries will contain nullptrs, which we should
1006 * just skip. */
1007 if (!job)
1008 continue;
1009
1010 m_job_queue[ix] = nullptr;
1011 {
1012 JSAutoRealm ar(m_cx, job);
1013 gjs_debug(GJS_DEBUG_CONTEXT, "handling job %s",
1014 gjs_debug_object(job).c_str());
1015 if (!JS::Call(m_cx, JS::UndefinedHandleValue, job, args, &rval)) {
1016 /* Uncatchable exception - return false so that
1017 * System.exit() works in the interactive shell and when
1018 * exiting the interpreter. */
1019 if (!JS_IsExceptionPending(m_cx)) {
1020 /* System.exit() is an uncatchable exception, but does not
1021 * indicate a bug. Log everything else. */
1022 if (!should_exit(nullptr))
1023 g_critical("Promise callback terminated with uncatchable exception");
1024 retval = false;
1025 continue;
1026 }
1027
1028 /* There's nowhere for the exception to go at this point */
1029 gjs_log_exception_uncaught(m_cx);
1030 }
1031 }
1032 }
1033
1034 m_job_queue.clear();
1035 stop_draining_job_queue();
1036 JS::JobQueueIsEmpty(m_cx);
1037 return retval;
1038 }
1039
1040 class GjsContextPrivate::SavedQueue : public JS::JobQueue::SavedJobQueue {
1041 private:
1042 GjsContextPrivate* m_gjs;
1043 JS::PersistentRooted<JobQueueStorage> m_queue;
1044 bool m_idle_was_pending : 1;
1045 bool m_was_draining : 1;
1046
1047 public:
SavedQueue(GjsContextPrivate * gjs)1048 explicit SavedQueue(GjsContextPrivate* gjs)
1049 : m_gjs(gjs),
1050 m_queue(gjs->m_cx, std::move(gjs->m_job_queue)),
1051 m_idle_was_pending(gjs->m_idle_drain_handler != 0),
1052 m_was_draining(gjs->m_draining_job_queue) {
1053 gjs_debug(GJS_DEBUG_CONTEXT, "Pausing job queue");
1054 gjs->stop_draining_job_queue();
1055 }
1056
~SavedQueue(void)1057 ~SavedQueue(void) {
1058 gjs_debug(GJS_DEBUG_CONTEXT, "Unpausing job queue");
1059 m_gjs->m_job_queue = std::move(m_queue.get());
1060 m_gjs->m_draining_job_queue = m_was_draining;
1061 if (m_idle_was_pending)
1062 m_gjs->start_draining_job_queue();
1063 }
1064 };
1065
saveJobQueue(JSContext * cx)1066 js::UniquePtr<JS::JobQueue::SavedJobQueue> GjsContextPrivate::saveJobQueue(
1067 JSContext* cx) {
1068 g_assert(cx == m_cx);
1069 g_assert(from_cx(cx) == this);
1070
1071 auto saved_queue = js::MakeUnique<SavedQueue>(this);
1072 if (!saved_queue) {
1073 JS_ReportOutOfMemory(cx);
1074 return nullptr;
1075 }
1076
1077 g_assert(m_job_queue.empty());
1078 return saved_queue;
1079 }
1080
register_unhandled_promise_rejection(uint64_t id,GjsAutoChar && stack)1081 void GjsContextPrivate::register_unhandled_promise_rejection(
1082 uint64_t id, GjsAutoChar&& stack) {
1083 m_unhandled_rejection_stacks[id] = std::move(stack);
1084 }
1085
unregister_unhandled_promise_rejection(uint64_t id)1086 void GjsContextPrivate::unregister_unhandled_promise_rejection(uint64_t id) {
1087 // Return value unused in G_DISABLE_ASSERT case
1088 [[maybe_unused]] size_t erased = m_unhandled_rejection_stacks.erase(id);
1089 g_assert(((void)"Handler attached to rejected promise that wasn't "
1090 "previously marked as unhandled", erased == 1));
1091 }
1092
async_closure_enqueue_for_gc(Gjs::Closure * trampoline)1093 void GjsContextPrivate::async_closure_enqueue_for_gc(Gjs::Closure* trampoline) {
1094 // Because we can't free the mmap'd data for a callback
1095 // while it's in use, this list keeps track of ones that
1096 // will be freed the next time gc happens
1097 g_assert(!trampoline->context() || trampoline->context() == m_cx);
1098 m_async_closures.emplace_back(trampoline);
1099 }
1100
1101 /**
1102 * gjs_context_maybe_gc:
1103 * @context: a #GjsContext
1104 *
1105 * Similar to the Spidermonkey JS_MaybeGC() call which
1106 * heuristically looks at JS runtime memory usage and
1107 * may initiate a garbage collection.
1108 *
1109 * This function always unconditionally invokes JS_MaybeGC(), but
1110 * additionally looks at memory usage from the system malloc()
1111 * when available, and if the delta has grown since the last run
1112 * significantly, also initiates a full JavaScript garbage
1113 * collection. The idea is that since GJS is a bridge between
1114 * JavaScript and system libraries, and JS objects act as proxies
1115 * for these system memory objects, GJS consumers need a way to
1116 * hint to the runtime that it may be a good idea to try a
1117 * collection.
1118 *
1119 * A good time to call this function is when your application
1120 * transitions to an idle state.
1121 */
1122 void
gjs_context_maybe_gc(GjsContext * context)1123 gjs_context_maybe_gc (GjsContext *context)
1124 {
1125 GjsContextPrivate* gjs = GjsContextPrivate::from_object(context);
1126 gjs_maybe_gc(gjs->context());
1127 }
1128
1129 /**
1130 * gjs_context_gc:
1131 * @context: a #GjsContext
1132 *
1133 * Initiate a full GC; may or may not block until complete. This
1134 * function just calls Spidermonkey JS_GC().
1135 */
1136 void
gjs_context_gc(GjsContext * context)1137 gjs_context_gc (GjsContext *context)
1138 {
1139 GjsContextPrivate* gjs = GjsContextPrivate::from_object(context);
1140 JS_GC(gjs->context(), Gjs::GCReason::GJS_API_CALL);
1141 }
1142
1143 /**
1144 * gjs_context_get_all:
1145 *
1146 * Returns a newly-allocated list containing all known instances of #GjsContext.
1147 * This is useful for operating on the contexts from a process-global situation
1148 * such as a debugger.
1149 *
1150 * Return value: (element-type GjsContext) (transfer full): Known #GjsContext instances
1151 */
1152 GList*
gjs_context_get_all(void)1153 gjs_context_get_all(void)
1154 {
1155 GList *result;
1156 GList *iter;
1157 g_mutex_lock (&contexts_lock);
1158 result = g_list_copy(all_contexts);
1159 for (iter = result; iter; iter = iter->next)
1160 g_object_ref((GObject*)iter->data);
1161 g_mutex_unlock (&contexts_lock);
1162 return result;
1163 }
1164
1165 /**
1166 * gjs_context_get_native_context:
1167 *
1168 * Returns a pointer to the underlying native context. For SpiderMonkey, this
1169 * is a JSContext *
1170 */
1171 void*
gjs_context_get_native_context(GjsContext * js_context)1172 gjs_context_get_native_context (GjsContext *js_context)
1173 {
1174 g_return_val_if_fail(GJS_IS_CONTEXT(js_context), NULL);
1175 GjsContextPrivate* gjs = GjsContextPrivate::from_object(js_context);
1176 return gjs->context();
1177 }
1178
1179 bool
gjs_context_eval(GjsContext * js_context,const char * script,gssize script_len,const char * filename,int * exit_status_p,GError ** error)1180 gjs_context_eval(GjsContext *js_context,
1181 const char *script,
1182 gssize script_len,
1183 const char *filename,
1184 int *exit_status_p,
1185 GError **error)
1186 {
1187 g_return_val_if_fail(GJS_IS_CONTEXT(js_context), false);
1188
1189 GjsAutoUnref<GjsContext> js_context_ref(js_context, GjsAutoTakeOwnership());
1190
1191 GjsContextPrivate* gjs = GjsContextPrivate::from_object(js_context);
1192 return gjs->eval(script, script_len, filename, exit_status_p, error);
1193 }
1194
gjs_context_eval_module(GjsContext * js_context,const char * identifier,uint8_t * exit_code,GError ** error)1195 bool gjs_context_eval_module(GjsContext* js_context, const char* identifier,
1196 uint8_t* exit_code, GError** error) {
1197 g_return_val_if_fail(GJS_IS_CONTEXT(js_context), false);
1198
1199 GjsAutoUnref<GjsContext> js_context_ref(js_context, GjsAutoTakeOwnership());
1200
1201 GjsContextPrivate* gjs = GjsContextPrivate::from_object(js_context);
1202 return gjs->eval_module(identifier, exit_code, error);
1203 }
1204
gjs_context_register_module(GjsContext * js_context,const char * identifier,const char * uri,GError ** error)1205 bool gjs_context_register_module(GjsContext* js_context, const char* identifier,
1206 const char* uri, GError** error) {
1207 g_return_val_if_fail(GJS_IS_CONTEXT(js_context), false);
1208
1209 GjsContextPrivate* gjs = GjsContextPrivate::from_object(js_context);
1210
1211 return gjs->register_module(identifier, uri, error);
1212 }
1213
auto_profile_enter()1214 bool GjsContextPrivate::auto_profile_enter() {
1215 bool auto_profile = m_should_profile;
1216 if (auto_profile &&
1217 (_gjs_profiler_is_running(m_profiler) || m_should_listen_sigusr2))
1218 auto_profile = false;
1219
1220 JSAutoRealm ar(m_cx, m_global);
1221
1222 if (auto_profile)
1223 gjs_profiler_start(m_profiler);
1224
1225 return auto_profile;
1226 }
1227
auto_profile_exit(bool auto_profile)1228 void GjsContextPrivate::auto_profile_exit(bool auto_profile) {
1229 if (auto_profile)
1230 gjs_profiler_stop(m_profiler);
1231 }
1232
handle_exit_code(const char * type,const char * identifier,GError ** error)1233 uint8_t GjsContextPrivate::handle_exit_code(const char* type,
1234 const char* identifier,
1235 GError** error) {
1236 uint8_t code;
1237 if (should_exit(&code)) {
1238 /* exit_status_p is public API so can't be changed, but should be
1239 * uint8_t, not int */
1240 g_set_error(error, GJS_ERROR, GJS_ERROR_SYSTEM_EXIT,
1241 "Exit with code %d", code);
1242 return code; // Don't log anything
1243 }
1244 if (!JS_IsExceptionPending(m_cx)) {
1245 g_critical("%s %s terminated with an uncatchable exception", type,
1246 identifier);
1247 g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
1248 "%s %s terminated with an uncatchable exception", type,
1249 identifier);
1250 } else {
1251 g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
1252 "%s %s threw an exception", type, identifier);
1253 }
1254
1255 gjs_log_exception_uncaught(m_cx);
1256 /* No exit code from script, but we don't want to exit(0) */
1257 return 1;
1258 }
1259
eval(const char * script,ssize_t script_len,const char * filename,int * exit_status_p,GError ** error)1260 bool GjsContextPrivate::eval(const char* script, ssize_t script_len,
1261 const char* filename, int* exit_status_p,
1262 GError** error) {
1263 AutoResetExit reset(this);
1264
1265 bool auto_profile = auto_profile_enter();
1266
1267 JSAutoRealm ar(m_cx, m_global);
1268
1269 JS::RootedValue retval(m_cx);
1270 bool ok = eval_with_scope(nullptr, script, script_len, filename, &retval);
1271
1272 /* The promise job queue should be drained even on error, to finish
1273 * outstanding async tasks before the context is torn down. Drain after
1274 * uncaught exceptions have been reported since draining runs callbacks. */
1275 {
1276 JS::AutoSaveExceptionState saved_exc(m_cx);
1277 ok = run_jobs_fallible() && ok;
1278 }
1279
1280 auto_profile_exit(auto_profile);
1281
1282 if (!ok) {
1283 *exit_status_p = handle_exit_code("Script", filename, error);
1284 return false;
1285 }
1286
1287 if (exit_status_p) {
1288 if (retval.isInt32()) {
1289 int code = retval.toInt32();
1290 gjs_debug(GJS_DEBUG_CONTEXT,
1291 "Script returned integer code %d", code);
1292 *exit_status_p = code;
1293 } else {
1294 /* Assume success if no integer was returned */
1295 *exit_status_p = 0;
1296 }
1297 }
1298
1299 return true;
1300 }
1301
eval_module(const char * identifier,uint8_t * exit_status_p,GError ** error)1302 bool GjsContextPrivate::eval_module(const char* identifier,
1303 uint8_t* exit_status_p, GError** error) {
1304 AutoResetExit reset(this);
1305
1306 bool auto_profile = auto_profile_enter();
1307
1308 JSAutoRealm ac(m_cx, m_global);
1309
1310 JS::RootedObject registry(m_cx, gjs_get_module_registry(m_global));
1311 JS::RootedId key(m_cx, gjs_intern_string_to_id(m_cx, identifier));
1312 JS::RootedObject obj(m_cx);
1313 if (!gjs_global_registry_get(m_cx, registry, key, &obj) || !obj) {
1314 g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
1315 "Cannot load module with identifier: '%s'", identifier);
1316 *exit_status_p = 1;
1317 return false;
1318 }
1319
1320 if (!JS::ModuleInstantiate(m_cx, obj)) {
1321 gjs_log_exception(m_cx);
1322 g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
1323 "Failed to resolve imports for module: '%s'", identifier);
1324 *exit_status_p = 1;
1325 return false;
1326 }
1327
1328 bool ok = true;
1329 if (!JS::ModuleEvaluate(m_cx, obj))
1330 ok = false;
1331
1332 /* The promise job queue should be drained even on error, to finish
1333 * outstanding async tasks before the context is torn down. Drain after
1334 * uncaught exceptions have been reported since draining runs callbacks.
1335 */
1336 {
1337 JS::AutoSaveExceptionState saved_exc(m_cx);
1338 ok = run_jobs_fallible() && ok;
1339 }
1340
1341 auto_profile_exit(auto_profile);
1342
1343 if (!ok) {
1344 *exit_status_p = handle_exit_code("Module", identifier, error);
1345 return false;
1346 }
1347
1348 /* Assume success if no errors were thrown or exit code set. */
1349 *exit_status_p = 0;
1350 return true;
1351 }
1352
register_module(const char * identifier,const char * uri,GError ** error)1353 bool GjsContextPrivate::register_module(const char* identifier, const char* uri,
1354 GError** error) {
1355 JSAutoRealm ar(m_cx, m_global);
1356
1357 if (gjs_module_load(m_cx, identifier, uri))
1358 return true;
1359
1360 const char* msg = "unknown";
1361 JS::ExceptionStack exn_stack(m_cx);
1362 JS::ErrorReportBuilder builder(m_cx);
1363 if (JS::StealPendingExceptionStack(m_cx, &exn_stack) &&
1364 builder.init(m_cx, exn_stack,
1365 JS::ErrorReportBuilder::WithSideEffects)) {
1366 msg = builder.toStringResult().c_str();
1367 } else {
1368 JS_ClearPendingException(m_cx);
1369 }
1370
1371 g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
1372 "Failed to parse module '%s': %s", identifier,
1373 msg ? msg : "unknown");
1374
1375 return false;
1376 }
1377
1378 bool
gjs_context_eval_file(GjsContext * js_context,const char * filename,int * exit_status_p,GError ** error)1379 gjs_context_eval_file(GjsContext *js_context,
1380 const char *filename,
1381 int *exit_status_p,
1382 GError **error)
1383 {
1384 char *script;
1385 size_t script_len;
1386 GjsAutoUnref<GFile> file = g_file_new_for_commandline_arg(filename);
1387
1388 if (!g_file_load_contents(file, nullptr, &script, &script_len, nullptr,
1389 error))
1390 return false;
1391 GjsAutoChar script_ref = script;
1392
1393 return gjs_context_eval(js_context, script, script_len, filename,
1394 exit_status_p, error);
1395 }
1396
gjs_context_eval_module_file(GjsContext * js_context,const char * filename,uint8_t * exit_status_p,GError ** error)1397 bool gjs_context_eval_module_file(GjsContext* js_context, const char* filename,
1398 uint8_t* exit_status_p, GError** error) {
1399 GjsAutoUnref<GFile> file = g_file_new_for_commandline_arg(filename);
1400 GjsAutoChar uri = g_file_get_uri(file);
1401
1402 return gjs_context_register_module(js_context, uri, uri, error) &&
1403 gjs_context_eval_module(js_context, uri, exit_status_p, error);
1404 }
1405
1406 /*
1407 * GjsContextPrivate::eval_with_scope:
1408 * @scope_object: an object to use as the global scope, or nullptr
1409 * @script: JavaScript program encoded in UTF-8
1410 * @script_len: length of @script, or -1 if @script is 0-terminated
1411 * @filename: filename to use as the origin of @script
1412 * @retval: location for the return value of @script
1413 *
1414 * Executes @script with a local scope so that nothing from the script leaks out
1415 * into the global scope.
1416 * If @scope_object is given, then everything that @script placed in the global
1417 * namespace is defined on @scope_object.
1418 * Otherwise, the global definitions are just discarded.
1419 */
eval_with_scope(JS::HandleObject scope_object,const char * script,ssize_t script_len,const char * filename,JS::MutableHandleValue retval)1420 bool GjsContextPrivate::eval_with_scope(JS::HandleObject scope_object,
1421 const char* script, ssize_t script_len,
1422 const char* filename,
1423 JS::MutableHandleValue retval) {
1424 /* log and clear exception if it's set (should not be, normally...) */
1425 if (JS_IsExceptionPending(m_cx)) {
1426 g_warning("eval_with_scope() called with a pending exception");
1427 return false;
1428 }
1429
1430 JS::RootedObject eval_obj(m_cx, scope_object);
1431 if (!eval_obj)
1432 eval_obj = JS_NewPlainObject(m_cx);
1433
1434 long items_written; // NOLINT(runtime/int) - this type required by GLib API
1435 GError* error;
1436 GjsAutoChar16 utf16_string =
1437 g_utf8_to_utf16(script, script_len,
1438 /* items_read = */ nullptr, &items_written, &error);
1439 if (!utf16_string)
1440 return gjs_throw_gerror_message(m_cx, error);
1441
1442 // COMPAT: This could use JS::SourceText<mozilla::Utf8Unit> directly,
1443 // but that messes up code coverage. See bug
1444 // https://bugzilla.mozilla.org/show_bug.cgi?id=1404784
1445 JS::SourceText<char16_t> buf;
1446 if (!buf.init(m_cx, reinterpret_cast<char16_t*>(utf16_string.get()),
1447 items_written, JS::SourceOwnership::Borrowed))
1448 return false;
1449
1450 JS::RootedObjectVector scope_chain(m_cx);
1451 if (!scope_chain.append(eval_obj)) {
1452 JS_ReportOutOfMemory(m_cx);
1453 return false;
1454 }
1455
1456 JS::CompileOptions options(m_cx);
1457 options.setFileAndLine(filename, 1);
1458
1459 GjsAutoUnref<GFile> file = g_file_new_for_commandline_arg(filename);
1460 GjsAutoChar uri = g_file_get_uri(file);
1461 JS::RootedObject priv(m_cx, gjs_script_module_build_private(m_cx, uri));
1462 if (!priv)
1463 return false;
1464
1465 options.setPrivateValue(JS::ObjectValue(*priv));
1466
1467 if (!JS::Evaluate(m_cx, scope_chain, options, buf, retval))
1468 return false;
1469
1470 schedule_gc_if_needed();
1471
1472 if (JS_IsExceptionPending(m_cx)) {
1473 g_warning(
1474 "JS::Evaluate() returned true but exception was pending; "
1475 "did somebody call gjs_throw() without returning false?");
1476 return false;
1477 }
1478
1479 gjs_debug(GJS_DEBUG_CONTEXT, "Script evaluation succeeded");
1480
1481 return true;
1482 }
1483
1484 /*
1485 * GjsContextPrivate::call_function:
1486 * @this_obj: Object to use as the 'this' for the function call
1487 * @func_val: Callable to call, as a JS value
1488 * @args: Arguments to pass to the callable
1489 * @rval: Location for the return value
1490 *
1491 * Use this instead of JS_CallFunctionValue(), because it schedules a GC if
1492 * one is needed. It's good practice to check if a GC should be run every time
1493 * we return from JS back into C++.
1494 */
call_function(JS::HandleObject this_obj,JS::HandleValue func_val,const JS::HandleValueArray & args,JS::MutableHandleValue rval)1495 bool GjsContextPrivate::call_function(JS::HandleObject this_obj,
1496 JS::HandleValue func_val,
1497 const JS::HandleValueArray& args,
1498 JS::MutableHandleValue rval) {
1499 if (!JS_CallFunctionValue(m_cx, this_obj, func_val, args, rval))
1500 return false;
1501
1502 schedule_gc_if_needed();
1503
1504 return true;
1505 }
1506
1507 bool
gjs_context_define_string_array(GjsContext * js_context,const char * array_name,gssize array_length,const char ** array_values,GError ** error)1508 gjs_context_define_string_array(GjsContext *js_context,
1509 const char *array_name,
1510 gssize array_length,
1511 const char **array_values,
1512 GError **error)
1513 {
1514 g_return_val_if_fail(GJS_IS_CONTEXT(js_context), false);
1515 GjsContextPrivate* gjs = GjsContextPrivate::from_object(js_context);
1516
1517 JSAutoRealm ar(gjs->context(), gjs->global());
1518
1519 std::vector<std::string> strings;
1520 if (array_values) {
1521 if (array_length < 0)
1522 array_length = g_strv_length(const_cast<char**>(array_values));
1523 strings = {array_values, array_values + array_length};
1524 }
1525
1526 // ARGV is a special case to preserve backwards compatibility.
1527 if (strcmp(array_name, "ARGV") == 0) {
1528 gjs->set_args(std::move(strings));
1529
1530 return true;
1531 }
1532
1533 JS::RootedObject global_root(gjs->context(), gjs->global());
1534 if (!gjs_define_string_array(gjs->context(), global_root, array_name,
1535 strings, JSPROP_READONLY | JSPROP_PERMANENT)) {
1536 gjs_log_exception(gjs->context());
1537 g_set_error(error,
1538 GJS_ERROR,
1539 GJS_ERROR_FAILED,
1540 "gjs_define_string_array() failed");
1541 return false;
1542 }
1543
1544 return true;
1545 }
1546
gjs_context_set_argv(GjsContext * js_context,ssize_t array_length,const char ** array_values)1547 void gjs_context_set_argv(GjsContext* js_context, ssize_t array_length,
1548 const char** array_values) {
1549 g_return_if_fail(GJS_IS_CONTEXT(js_context));
1550 GjsContextPrivate* gjs = GjsContextPrivate::from_object(js_context);
1551 std::vector<std::string> args(array_values, array_values + array_length);
1552 gjs->set_args(std::move(args));
1553 }
1554
1555 static GjsContext *current_context;
1556
1557 GjsContext *
gjs_context_get_current(void)1558 gjs_context_get_current (void)
1559 {
1560 return current_context;
1561 }
1562
1563 void
gjs_context_make_current(GjsContext * context)1564 gjs_context_make_current (GjsContext *context)
1565 {
1566 g_assert (context == NULL || current_context == NULL);
1567
1568 current_context = context;
1569 }
1570
1571 /**
1572 * gjs_context_get_profiler:
1573 * @self: the #GjsContext
1574 *
1575 * Returns the profiler's internal instance of #GjsProfiler for you to
1576 * customize, or %NULL if profiling is not enabled on this #GjsContext.
1577 *
1578 * Returns: (transfer none) (nullable): a #GjsProfiler
1579 */
1580 GjsProfiler *
gjs_context_get_profiler(GjsContext * self)1581 gjs_context_get_profiler(GjsContext *self)
1582 {
1583 return GjsContextPrivate::from_object(self)->profiler();
1584 }
1585
1586 /**
1587 * gjs_get_js_version:
1588 *
1589 * Returns the underlying version of the JS engine.
1590 *
1591 * Returns: a string
1592 */
1593 const char *
gjs_get_js_version(void)1594 gjs_get_js_version(void)
1595 {
1596 return JS_GetImplementationVersion();
1597 }
1598