/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
/*
 * Copyright (c) 2008  litl, LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */

#include <config.h>

#include <signal.h>  // for sigaction, SIGUSR1, sa_handler
#include <stdint.h>
#include <stdio.h>      // for FILE, fclose, size_t
#include <string.h>     // for memset

#ifdef HAVE_UNISTD_H
#    include <unistd.h>  // for getpid
#elif defined (_WIN32)
#    include <process.h>
#endif

#include <new>
#include <string>       // for u16string
#include <type_traits>  // for remove_reference<>::type
#include <unordered_map>
#include <utility>  // for move
#include <vector>

#include <gio/gio.h>
#include <girepository.h>
#include <glib-object.h>
#include <glib.h>

#ifdef G_OS_WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif

#include <js/AllocPolicy.h>  // for SystemAllocPolicy
#include <js/CallArgs.h>     // for UndefinedHandleValue
#include <js/CompilationAndEvaluation.h>
#include <js/CompileOptions.h>
#include <js/GCAPI.h>               // for JS_GC, JS_AddExtraGCRootsTr...
#include <js/GCHashTable.h>         // for WeakCache
#include <js/GCVector.h>            // for RootedVector
#include <js/Promise.h>             // for JobQueue::SavedJobQueue
#include <js/PropertyDescriptor.h>  // for JSPROP_PERMANENT, JSPROP_RE...
#include <js/RootingAPI.h>
#include <js/SourceText.h>
#include <js/TracingAPI.h>
#include <js/TypeDecls.h>
#include <js/UniquePtr.h>
#include <js/Utility.h>  // for DeletePolicy
#include <js/Value.h>
#include <js/ValueArray.h>
#include <jsapi.h>        // for JS_IsExceptionPending, ...
#include <jsfriendapi.h>  // for DumpHeap, IgnoreNurseryObjects
#include <mozilla/UniquePtr.h>

#include "gi/object.h"
#include "gi/private.h"
#include "gi/repo.h"
#include "gjs/atoms.h"
#include "gjs/byteArray.h"
#include "gjs/context-private.h"
#include "gjs/context.h"
#include "gjs/engine.h"
#include "gjs/error-types.h"
#include "gjs/global.h"
#include "gjs/importer.h"
#include "gjs/jsapi-util.h"
#include "gjs/mem.h"
#include "gjs/native.h"
#include "gjs/profiler-private.h"
#include "gjs/profiler.h"
#include "modules/modules.h"
#include "util/log.h"

static void     gjs_context_dispose           (GObject               *object);
static void     gjs_context_finalize          (GObject               *object);
static void     gjs_context_constructed       (GObject               *object);
static void     gjs_context_get_property      (GObject               *object,
                                                  guint                  prop_id,
                                                  GValue                *value,
                                                  GParamSpec            *pspec);
static void     gjs_context_set_property      (GObject               *object,
                                                  guint                  prop_id,
                                                  const GValue          *value,
                                                  GParamSpec            *pspec);

void GjsContextPrivate::EnvironmentPreparer::invoke(JS::HandleObject scope,
                                                    Closure& closure) {
    g_assert(!JS_IsExceptionPending(m_cx));

    JSAutoRealm ar(m_cx, scope);
    if (!closure(m_cx))
        gjs_log_exception(m_cx);
}

struct _GjsContext {
    GObject parent;
};

struct _GjsContextClass {
    GObjectClass parent;
};

G_DEFINE_TYPE_WITH_PRIVATE(GjsContext, gjs_context, G_TYPE_OBJECT);

GjsContextPrivate* GjsContextPrivate::from_object(GObject* js_context) {
    g_return_val_if_fail(GJS_IS_CONTEXT(js_context), nullptr);
    return static_cast<GjsContextPrivate*>(
        gjs_context_get_instance_private(GJS_CONTEXT(js_context)));
}

GjsContextPrivate* GjsContextPrivate::from_object(GjsContext* js_context) {
    g_return_val_if_fail(GJS_IS_CONTEXT(js_context), nullptr);
    return static_cast<GjsContextPrivate*>(
        gjs_context_get_instance_private(js_context));
}

GjsContextPrivate* GjsContextPrivate::from_current_context() {
    return from_object(gjs_context_get_current());
}

enum {
    PROP_0,
    PROP_SEARCH_PATH,
    PROP_PROGRAM_NAME,
    PROP_PROFILER_ENABLED,
    PROP_PROFILER_SIGUSR2,
};

static GMutex contexts_lock;
static GList *all_contexts = NULL;

static GjsAutoChar dump_heap_output;
static unsigned dump_heap_idle_id = 0;

#ifdef G_OS_UNIX
/* Currently heap dumping is only supported on UNIX platforms! */
static void
gjs_context_dump_heaps(void)
{
    static unsigned counter = 0;

    gjs_memory_report("signal handler", false);

    /* dump to sequential files to allow easier comparisons */
    GjsAutoChar filename = g_strdup_printf("%s.%jd.%u", dump_heap_output.get(),
                                           intmax_t(getpid()), counter);
    ++counter;

    FILE *fp = fopen(filename, "w");
    if (!fp)
        return;

    for (GList *l = all_contexts; l; l = g_list_next(l)) {
        auto* gjs = static_cast<GjsContextPrivate*>(l->data);
        js::DumpHeap(gjs->context(), fp, js::IgnoreNurseryObjects);
    }

    fclose(fp);
}

static gboolean dump_heap_idle(void*) {
    dump_heap_idle_id = 0;

    gjs_context_dump_heaps();

    return false;
}

static void dump_heap_signal_handler(int signum [[maybe_unused]]) {
    if (dump_heap_idle_id == 0)
        dump_heap_idle_id = g_idle_add_full(G_PRIORITY_HIGH_IDLE,
                                            dump_heap_idle, nullptr, nullptr);
}
#endif

static void
setup_dump_heap(void)
{
    static bool dump_heap_initialized = false;
    if (!dump_heap_initialized) {
        dump_heap_initialized = true;

        /* install signal handler only if environment variable is set */
        const char *heap_output = g_getenv("GJS_DEBUG_HEAP_OUTPUT");
        if (heap_output) {
#ifdef G_OS_UNIX
            struct sigaction sa;

            dump_heap_output = g_strdup(heap_output);

            memset(&sa, 0, sizeof(sa));
            sa.sa_handler = dump_heap_signal_handler;
            sigaction(SIGUSR1, &sa, nullptr);
#else
            g_message(
                "heap dump is currently only supported on UNIX platforms");
#endif
        }
    }
}

static void
gjs_context_init(GjsContext *js_context)
{
    gjs_context_make_current(js_context);
}

static void
gjs_context_class_init(GjsContextClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS (klass);
    GParamSpec *pspec;

    object_class->dispose = gjs_context_dispose;
    object_class->finalize = gjs_context_finalize;

    object_class->constructed = gjs_context_constructed;
    object_class->get_property = gjs_context_get_property;
    object_class->set_property = gjs_context_set_property;

    pspec = g_param_spec_boxed("search-path",
                               "Search path",
                               "Path where modules to import should reside",
                               G_TYPE_STRV,
                               (GParamFlags) (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));

    g_object_class_install_property(object_class,
                                    PROP_SEARCH_PATH,
                                    pspec);
    g_param_spec_unref(pspec);

    pspec = g_param_spec_string("program-name",
                                "Program Name",
                                "The filename of the launched JS program",
                                "",
                                (GParamFlags) (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));

    g_object_class_install_property(object_class,
                                    PROP_PROGRAM_NAME,
                                    pspec);
    g_param_spec_unref(pspec);

    /**
     * GjsContext:profiler-enabled:
     *
     * Set this property to profile any JS code run by this context. By
     * default, the profiler is started and stopped when you call
     * gjs_context_eval().
     *
     * The value of this property is superseded by the GJS_ENABLE_PROFILER
     * environment variable.
     *
     * You may only have one context with the profiler enabled at a time.
     */
    pspec = g_param_spec_boolean("profiler-enabled", "Profiler enabled",
                                 "Whether to profile JS code run by this context",
                                 FALSE,
                                 GParamFlags(G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
    g_object_class_install_property(object_class, PROP_PROFILER_ENABLED, pspec);
    g_param_spec_unref(pspec);

    /**
     * GjsContext:profiler-sigusr2:
     *
     * Set this property to install a SIGUSR2 signal handler that starts and
     * stops the profiler. This property also implies that
     * #GjsContext:profiler-enabled is set.
     */
    pspec = g_param_spec_boolean("profiler-sigusr2", "Profiler SIGUSR2",
                                 "Whether to activate the profiler on SIGUSR2",
                                 FALSE,
                                 GParamFlags(G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
    g_object_class_install_property(object_class, PROP_PROFILER_SIGUSR2, pspec);
    g_param_spec_unref(pspec);

    /* For GjsPrivate */
    {
#ifdef G_OS_WIN32
        extern HMODULE gjs_dll;
        char *basedir = g_win32_get_package_installation_directory_of_module (gjs_dll);
        char *priv_typelib_dir = g_build_filename (basedir, "lib", "gjs", "girepository-1.0", NULL);
        g_free (basedir);
#else
        char *priv_typelib_dir = g_build_filename (PKGLIBDIR, "girepository-1.0", NULL);
#endif
        g_irepository_prepend_search_path(priv_typelib_dir);
    g_free (priv_typelib_dir);
    }

    gjs_register_native_module("_byteArrayNative", gjs_define_byte_array_stuff);
    gjs_register_native_module("_gi", gjs_define_private_gi_stuff);
    gjs_register_native_module("gi", gjs_define_repo);

    gjs_register_static_modules();
}

void GjsContextPrivate::trace(JSTracer* trc, void* data) {
    auto* gjs = static_cast<GjsContextPrivate*>(data);
    JS::TraceEdge<JSObject*>(trc, &gjs->m_global, "GJS global object");
    gjs->m_atoms->trace(trc);
    gjs->m_job_queue.trace(trc);
    gjs->m_object_init_list.trace(trc);
}

void GjsContextPrivate::warn_about_unhandled_promise_rejections(void) {
    for (auto& kv : m_unhandled_rejection_stacks) {
        const char *stack = kv.second;
        g_warning("Unhandled promise rejection. To suppress this warning, add "
                  "an error handler to your promise chain with .catch() or a "
                  "try-catch block around your await expression. %s%s",
                  stack ? "Stack trace of the failed promise:\n" :
                    "Unfortunately there is no stack trace of the failed promise.",
                  stack ? stack : "");
    }
    m_unhandled_rejection_stacks.clear();
}

static void
gjs_context_dispose(GObject *object)
{
    gjs_debug(GJS_DEBUG_CONTEXT, "JS shutdown sequence");

    GjsContextPrivate* gjs = GjsContextPrivate::from_object(object);

    /* Profiler must be stopped and freed before context is shut down */
    gjs->free_profiler();

    /* Stop accepting entries in the toggle queue before running dispose
     * notifications, which causes all GjsMaybeOwned instances to unroot.
     * We don't want any objects to toggle down after that. */
    gjs_debug(GJS_DEBUG_CONTEXT, "Shutting down toggle queue");
    gjs_object_clear_toggles();
    gjs_object_shutdown_toggle_queue();

    /* Run dispose notifications next, so that anything releasing
     * references in response to this can still get garbage collected */
    gjs_debug(GJS_DEBUG_CONTEXT,
              "Notifying reference holders of GjsContext dispose");
    G_OBJECT_CLASS(gjs_context_parent_class)->dispose(object);

    gjs->dispose();
}

void GjsContextPrivate::free_profiler(void) {
    gjs_debug(GJS_DEBUG_CONTEXT, "Stopping profiler");
    if (m_profiler)
        g_clear_pointer(&m_profiler, _gjs_profiler_free);
}

void GjsContextPrivate::dispose(void) {
    if (m_cx) {
        gjs_debug(GJS_DEBUG_CONTEXT,
                  "Checking unhandled promise rejections");
        warn_about_unhandled_promise_rejections();

        gjs_debug(GJS_DEBUG_CONTEXT, "Releasing cached JS wrappers");
        m_fundamental_table->clear();
        m_gtype_table->clear();

        /* Do a full GC here before tearing down, since once we do
         * that we may not have the JS_GetPrivate() to access the
         * context
         */
        gjs_debug(GJS_DEBUG_CONTEXT, "Final triggered GC");
        JS_GC(m_cx);

        gjs_debug(GJS_DEBUG_CONTEXT, "Destroying JS context");
        m_destroying = true;

        /* Now, release all native objects, to avoid recursion between
         * the JS teardown and the C teardown.  The JSObject proxies
         * still exist, but point to NULL.
         */
        gjs_debug(GJS_DEBUG_CONTEXT, "Releasing all native objects");
        ObjectInstance::prepare_shutdown();

        gjs_debug(GJS_DEBUG_CONTEXT, "Disabling auto GC");
        if (m_auto_gc_id > 0) {
            g_source_remove(m_auto_gc_id);
            m_auto_gc_id = 0;
        }

        gjs_debug(GJS_DEBUG_CONTEXT, "Ending trace on global object");
        JS_RemoveExtraGCRootsTracer(m_cx, &GjsContextPrivate::trace, this);
        m_global = nullptr;

        gjs_debug(GJS_DEBUG_CONTEXT, "Freeing allocated resources");
        delete m_fundamental_table;
        delete m_gtype_table;
        delete m_atoms;

        /* Tear down JS */
        JS_DestroyContext(m_cx);
        m_cx = nullptr;
        // don't use g_clear_pointer() as we want the pointer intact while we
        // destroy the context in case we dump stack
        gjs_debug(GJS_DEBUG_CONTEXT, "JS context destroyed");
    }
}

GjsContextPrivate::~GjsContextPrivate(void) {
    g_clear_pointer(&m_search_path, g_strfreev);
    g_clear_pointer(&m_program_name, g_free);
}

static void
gjs_context_finalize(GObject *object)
{
    if (gjs_context_get_current() == (GjsContext*)object)
        gjs_context_make_current(NULL);

    g_mutex_lock(&contexts_lock);
    all_contexts = g_list_remove(all_contexts, object);
    g_mutex_unlock(&contexts_lock);

    GjsContextPrivate* gjs = GjsContextPrivate::from_object(object);
    gjs->~GjsContextPrivate();
    G_OBJECT_CLASS(gjs_context_parent_class)->finalize(object);
}

static void
gjs_context_constructed(GObject *object)
{
    GjsContext *js_context = GJS_CONTEXT(object);

    G_OBJECT_CLASS(gjs_context_parent_class)->constructed(object);

    GjsContextPrivate* gjs_location = GjsContextPrivate::from_object(object);
    JSContext* cx = gjs_create_js_context(gjs_location);
    if (!cx)
        g_error("Failed to create javascript context");

    new (gjs_location) GjsContextPrivate(cx, js_context);

    g_mutex_lock(&contexts_lock);
    all_contexts = g_list_prepend(all_contexts, object);
    g_mutex_unlock(&contexts_lock);

    setup_dump_heap();

    g_object_weak_ref(object, &ObjectInstance::context_dispose_notify, nullptr);
}

GjsContextPrivate::GjsContextPrivate(JSContext* cx, GjsContext* public_context)
    : m_public_context(public_context),
      m_cx(cx),
      m_environment_preparer(cx) {
    m_owner_thread = g_thread_self();

    const char *env_profiler = g_getenv("GJS_ENABLE_PROFILER");
    if (env_profiler || m_should_listen_sigusr2)
        m_should_profile = true;

    if (m_should_profile) {
        m_profiler = _gjs_profiler_new(public_context);

        if (!m_profiler) {
            m_should_profile = false;
        } else {
            if (m_should_listen_sigusr2)
                _gjs_profiler_setup_signals(m_profiler, public_context);
        }
    }

    JSRuntime* rt = JS_GetRuntime(m_cx);
    m_fundamental_table = new JS::WeakCache<FundamentalTable>(rt);
    m_gtype_table = new JS::WeakCache<GTypeTable>(rt);

    m_atoms = new GjsAtoms();

    JS::RootedObject global(
        m_cx, gjs_create_global_object(cx, GjsGlobalType::DEFAULT));

    if (!global) {
        gjs_log_exception(m_cx);
        g_error("Failed to initialize global object");
    }

    JSAutoRealm ar(m_cx, global);

    m_global = global;
    JS_AddExtraGCRootsTracer(m_cx, &GjsContextPrivate::trace, this);

    if (!m_atoms->init_atoms(m_cx)) {
        gjs_log_exception(m_cx);
        g_error("Failed to initialize global strings");
    }

    std::vector<std::string> paths;
    if (m_search_path)
        paths = {m_search_path, m_search_path + g_strv_length(m_search_path)};
    JS::RootedObject importer(m_cx, gjs_create_root_importer(m_cx, paths));
    if (!importer) {
        gjs_log_exception(cx);
        g_error("Failed to create root importer");
    }

    JS::Value v_importer = gjs_get_global_slot(global, GjsGlobalSlot::IMPORTS);
    g_assert(((void) "Someone else already created root importer",
              v_importer.isUndefined()));

    gjs_set_global_slot(global, GjsGlobalSlot::IMPORTS,
                        JS::ObjectValue(*importer));

    if (!gjs_define_global_properties(m_cx, global, GjsGlobalType::DEFAULT,
                                      "GJS", "default")) {
        gjs_log_exception(m_cx);
        g_error("Failed to define properties on global object");
    }
}

static void
gjs_context_get_property (GObject     *object,
                          guint        prop_id,
                          GValue      *value,
                          GParamSpec  *pspec)
{
    GjsContextPrivate* gjs = GjsContextPrivate::from_object(object);

    switch (prop_id) {
    case PROP_PROGRAM_NAME:
        g_value_set_string(value, gjs->program_name());
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        break;
    }
}

static void
gjs_context_set_property (GObject      *object,
                          guint         prop_id,
                          const GValue *value,
                          GParamSpec   *pspec)
{
    GjsContextPrivate* gjs = GjsContextPrivate::from_object(object);

    switch (prop_id) {
    case PROP_SEARCH_PATH:
        gjs->set_search_path(static_cast<char**>(g_value_dup_boxed(value)));
        break;
    case PROP_PROGRAM_NAME:
        gjs->set_program_name(g_value_dup_string(value));
        break;
    case PROP_PROFILER_ENABLED:
        gjs->set_should_profile(g_value_get_boolean(value));
        break;
    case PROP_PROFILER_SIGUSR2:
        gjs->set_should_listen_sigusr2(g_value_get_boolean(value));
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        break;
    }
}


GjsContext*
gjs_context_new(void)
{
    return (GjsContext*) g_object_new (GJS_TYPE_CONTEXT, NULL);
}

GjsContext*
gjs_context_new_with_search_path(char** search_path)
{
    return (GjsContext*) g_object_new (GJS_TYPE_CONTEXT,
                         "search-path", search_path,
                         NULL);
}

gboolean GjsContextPrivate::trigger_gc_if_needed(void* data) {
    auto* gjs = static_cast<GjsContextPrivate*>(data);
    gjs->m_auto_gc_id = 0;

    if (gjs->m_force_gc) {
        gjs_debug_lifecycle(GJS_DEBUG_CONTEXT, "Big Hammer hit");
        JS_GC(gjs->m_cx);
    } else {
        gjs_gc_if_needed(gjs->m_cx);
    }
    gjs->m_force_gc = false;

    return G_SOURCE_REMOVE;
}

void GjsContextPrivate::schedule_gc_internal(bool force_gc) {
    m_force_gc |= force_gc;

    if (m_auto_gc_id > 0)
        return;

    if (force_gc)
        gjs_debug_lifecycle(GJS_DEBUG_CONTEXT, "Big Hammer scheduled");

    m_auto_gc_id = g_timeout_add_seconds_full(G_PRIORITY_LOW, 10,
                                              trigger_gc_if_needed, this,
                                              nullptr);
}

/*
 * GjsContextPrivate::schedule_gc_if_needed:
 *
 * Does a minor GC immediately if the JS engine decides one is needed, but also
 * schedules a full GC in the next idle time.
 */
void GjsContextPrivate::schedule_gc_if_needed(void) {
    // We call JS_MaybeGC immediately, but defer a check for a full GC cycle
    // to an idle handler.
    JS_MaybeGC(m_cx);

    schedule_gc_internal(false);
}

void GjsContextPrivate::set_sweeping(bool value) {
    // If we have a profiler enabled, record the duration of GC sweep
    if (this->m_profiler != nullptr) {
        int64_t now = g_get_monotonic_time() * 1000L;

        if (value) {
            m_sweep_begin_time = now;
        } else {
            if (m_sweep_begin_time != 0) {
                _gjs_profiler_add_mark(this->m_profiler, m_sweep_begin_time,
                                       now - m_sweep_begin_time, "GJS", "Sweep",
                                       nullptr);
                m_sweep_begin_time = 0;
            }
        }
    }

    m_in_gc_sweep = value;
}

void GjsContextPrivate::exit(uint8_t exit_code) {
    g_assert(!m_should_exit);
    m_should_exit = true;
    m_exit_code = exit_code;
}

bool GjsContextPrivate::should_exit(uint8_t* exit_code_p) const {
    if (exit_code_p != NULL)
        *exit_code_p = m_exit_code;
    return m_should_exit;
}

void GjsContextPrivate::start_draining_job_queue(void) {
    if (!m_idle_drain_handler)
        m_idle_drain_handler = g_idle_add_full(
            G_PRIORITY_DEFAULT, drain_job_queue_idle_handler, this, nullptr);
}

void GjsContextPrivate::stop_draining_job_queue(void) {
    m_draining_job_queue = false;
    if (m_idle_drain_handler) {
        g_source_remove(m_idle_drain_handler);
        m_idle_drain_handler = 0;
    }
}

gboolean GjsContextPrivate::drain_job_queue_idle_handler(void* data) {
    auto* gjs = static_cast<GjsContextPrivate*>(data);
    gjs->runJobs(gjs->context());
    /* Uncatchable exceptions are swallowed here - no way to get a handle on
     * the main loop to exit it from this idle handler */
    g_assert(gjs->empty() && gjs->m_idle_drain_handler == 0 &&
             "GjsContextPrivate::runJobs() should have emptied queue");
    return G_SOURCE_REMOVE;
}

JSObject* GjsContextPrivate::getIncumbentGlobal(JSContext* cx) {
    // This is equivalent to SpiderMonkey's behavior.
    return JS::CurrentGlobalOrNull(cx);
}

/* See engine.cpp and JS::SetEnqueuePromiseJobCallback(). */
bool GjsContextPrivate::enqueuePromiseJob(
    JSContext* cx, JS::HandleObject promise [[maybe_unused]],
    JS::HandleObject job, JS::HandleObject allocation_site [[maybe_unused]],
    JS::HandleObject incumbent_global [[maybe_unused]]) {
    g_assert(cx == m_cx);
    g_assert(from_cx(cx) == this);

    if (m_idle_drain_handler)
        g_assert(!empty());
    else
        g_assert(empty());

    if (!m_job_queue.append(job)) {
        JS_ReportOutOfMemory(m_cx);
        return false;
    }

    start_draining_job_queue();
    return true;
}

// Override of JobQueue::runJobs(). Called by js::RunJobs(), and when execution
// of the job queue was interrupted by the debugger and is resuming.
void GjsContextPrivate::runJobs(JSContext* cx) {
    g_assert(cx == m_cx);
    g_assert(from_cx(cx) == this);
    if (!run_jobs_fallible())
        gjs_log_exception(cx);
}

/*
 * GjsContext::run_jobs_fallible:
 *
 * Drains the queue of promise callbacks that the JS engine has reported
 * finished, calling each one and logging any exceptions that it throws.
 *
 * Adapted from js::RunJobs() in SpiderMonkey's default job queue
 * implementation.
 *
 * Returns: false if one of the jobs threw an uncatchable exception;
 * otherwise true.
 */
bool GjsContextPrivate::run_jobs_fallible(void) {
    bool retval = true;

    if (m_draining_job_queue || m_should_exit)
        return true;

    m_draining_job_queue = true;  // Ignore reentrant calls

    JS::RootedObject job(m_cx);
    JS::HandleValueArray args(JS::HandleValueArray::empty());
    JS::RootedValue rval(m_cx);

    /* Execute jobs in a loop until we've reached the end of the queue.
     * Since executing a job can trigger enqueueing of additional jobs,
     * it's crucial to recheck the queue length during each iteration. */
    for (size_t ix = 0; ix < m_job_queue.length(); ix++) {
        /* A previous job might have set this flag. e.g., System.exit(). */
        if (m_should_exit)
            break;

        job = m_job_queue[ix];

        /* It's possible that job draining was interrupted prematurely,
         * leaving the queue partly processed. In that case, slots for
         * already-executed entries will contain nullptrs, which we should
         * just skip. */
        if (!job)
            continue;

        m_job_queue[ix] = nullptr;
        {
            JSAutoRealm ar(m_cx, job);
            if (!JS::Call(m_cx, JS::UndefinedHandleValue, job, args, &rval)) {
                /* Uncatchable exception - return false so that
                 * System.exit() works in the interactive shell and when
                 * exiting the interpreter. */
                if (!JS_IsExceptionPending(m_cx)) {
                    /* System.exit() is an uncatchable exception, but does not
                     * indicate a bug. Log everything else. */
                    if (!should_exit(nullptr))
                        g_critical("Promise callback terminated with uncatchable exception");
                    retval = false;
                    continue;
                }

                /* There's nowhere for the exception to go at this point */
                gjs_log_exception_uncaught(m_cx);
            }
        }
    }

    m_job_queue.clear();
    stop_draining_job_queue();
    return retval;
}

class GjsContextPrivate::SavedQueue : public JS::JobQueue::SavedJobQueue {
 private:
    GjsContextPrivate* m_gjs;
    JS::PersistentRooted<JobQueueStorage> m_queue;
    bool m_was_draining : 1;

 public:
    explicit SavedQueue(GjsContextPrivate* gjs)
        : m_gjs(gjs),
          m_queue(gjs->m_cx, std::move(gjs->m_job_queue)),
          m_was_draining(gjs->m_draining_job_queue) {
        gjs->stop_draining_job_queue();
    }

    ~SavedQueue(void) {
        m_gjs->m_job_queue = std::move(m_queue.get());
        if (m_was_draining)
            m_gjs->start_draining_job_queue();
    }
};

js::UniquePtr<JS::JobQueue::SavedJobQueue> GjsContextPrivate::saveJobQueue(
    JSContext* cx) {
    g_assert(cx == m_cx);
    g_assert(from_cx(cx) == this);

    auto saved_queue = js::MakeUnique<SavedQueue>(this);
    if (!saved_queue) {
        JS_ReportOutOfMemory(cx);
        return nullptr;
    }

    g_assert(m_job_queue.empty());
    return saved_queue;
}

void GjsContextPrivate::register_unhandled_promise_rejection(
    uint64_t id, GjsAutoChar&& stack) {
    m_unhandled_rejection_stacks[id] = std::move(stack);
}

void GjsContextPrivate::unregister_unhandled_promise_rejection(uint64_t id) {
    size_t erased = m_unhandled_rejection_stacks.erase(id);
    g_assert(((void)"Handler attached to rejected promise that wasn't "
              "previously marked as unhandled", erased == 1));
}

/**
 * gjs_context_maybe_gc:
 * @context: a #GjsContext
 * 
 * Similar to the Spidermonkey JS_MaybeGC() call which
 * heuristically looks at JS runtime memory usage and
 * may initiate a garbage collection. 
 *
 * This function always unconditionally invokes JS_MaybeGC(), but
 * additionally looks at memory usage from the system malloc()
 * when available, and if the delta has grown since the last run
 * significantly, also initiates a full JavaScript garbage
 * collection.  The idea is that since GJS is a bridge between
 * JavaScript and system libraries, and JS objects act as proxies
 * for these system memory objects, GJS consumers need a way to
 * hint to the runtime that it may be a good idea to try a
 * collection.
 *
 * A good time to call this function is when your application
 * transitions to an idle state.
 */ 
void
gjs_context_maybe_gc (GjsContext  *context)
{
    GjsContextPrivate* gjs = GjsContextPrivate::from_object(context);
    gjs_maybe_gc(gjs->context());
}

/**
 * gjs_context_gc:
 * @context: a #GjsContext
 * 
 * Initiate a full GC; may or may not block until complete.  This
 * function just calls Spidermonkey JS_GC().
 */ 
void
gjs_context_gc (GjsContext  *context)
{
    GjsContextPrivate* gjs = GjsContextPrivate::from_object(context);
    JS_GC(gjs->context());
}

/**
 * gjs_context_get_all:
 *
 * Returns a newly-allocated list containing all known instances of #GjsContext.
 * This is useful for operating on the contexts from a process-global situation
 * such as a debugger.
 *
 * Return value: (element-type GjsContext) (transfer full): Known #GjsContext instances
 */
GList*
gjs_context_get_all(void)
{
  GList *result;
  GList *iter;
  g_mutex_lock (&contexts_lock);
  result = g_list_copy(all_contexts);
  for (iter = result; iter; iter = iter->next)
    g_object_ref((GObject*)iter->data);
  g_mutex_unlock (&contexts_lock);
  return result;
}

/**
 * gjs_context_get_native_context:
 *
 * Returns a pointer to the underlying native context.  For SpiderMonkey, this
 * is a JSContext *
 */
void*
gjs_context_get_native_context (GjsContext *js_context)
{
    g_return_val_if_fail(GJS_IS_CONTEXT(js_context), NULL);
    GjsContextPrivate* gjs = GjsContextPrivate::from_object(js_context);
    return gjs->context();
}

bool
gjs_context_eval(GjsContext   *js_context,
                 const char   *script,
                 gssize        script_len,
                 const char   *filename,
                 int          *exit_status_p,
                 GError      **error)
{
    g_return_val_if_fail(GJS_IS_CONTEXT(js_context), false);

    GjsAutoUnref<GjsContext> js_context_ref(js_context, GjsAutoTakeOwnership());

    GjsContextPrivate* gjs = GjsContextPrivate::from_object(js_context);
    return gjs->eval(script, script_len, filename, exit_status_p, error);
}

bool GjsContextPrivate::eval(const char* script, ssize_t script_len,
                             const char* filename, int* exit_status_p,
                             GError** error) {
    AutoResetExit reset(this);

    bool auto_profile = m_should_profile;
    if (auto_profile &&
        (_gjs_profiler_is_running(m_profiler) || m_should_listen_sigusr2))
        auto_profile = false;

    JSAutoRealm ar(m_cx, m_global);

    if (auto_profile)
        gjs_profiler_start(m_profiler);

    JS::RootedValue retval(m_cx);
    bool ok = eval_with_scope(nullptr, script, script_len, filename, &retval);

    /* The promise job queue should be drained even on error, to finish
     * outstanding async tasks before the context is torn down. Drain after
     * uncaught exceptions have been reported since draining runs callbacks. */
    {
        JS::AutoSaveExceptionState saved_exc(m_cx);
        ok = run_jobs_fallible() && ok;
    }

    if (auto_profile)
        gjs_profiler_stop(m_profiler);

    if (!ok) {
        uint8_t code;
        if (should_exit(&code)) {
            /* exit_status_p is public API so can't be changed, but should be
             * uint8_t, not int */
            *exit_status_p = code;
            g_set_error(error, GJS_ERROR, GJS_ERROR_SYSTEM_EXIT,
                        "Exit with code %d", code);
            return false;  // Don't log anything
        }

        if (!JS_IsExceptionPending(m_cx)) {
            g_critical("Script %s terminated with an uncatchable exception",
                       filename);
            g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
                        "Script %s terminated with an uncatchable exception",
                        filename);
        } else {
            g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
                        "Script %s threw an exception", filename);
        }

        gjs_log_exception_uncaught(m_cx);
        /* No exit code from script, but we don't want to exit(0) */
        *exit_status_p = 1;
        return false;
    }

    if (exit_status_p) {
        if (retval.isInt32()) {
            int code = retval.toInt32();
            gjs_debug(GJS_DEBUG_CONTEXT,
                      "Script returned integer code %d", code);
            *exit_status_p = code;
        } else {
            /* Assume success if no integer was returned */
            *exit_status_p = 0;
        }
    }

    return true;
}

bool
gjs_context_eval_file(GjsContext    *js_context,
                      const char    *filename,
                      int           *exit_status_p,
                      GError       **error)
{
    char *script;
    size_t script_len;
    GjsAutoUnref<GFile> file = g_file_new_for_commandline_arg(filename);

    if (!g_file_load_contents(file, nullptr, &script, &script_len, nullptr,
                              error))
        return false;
    GjsAutoChar script_ref = script;

    return gjs_context_eval(js_context, script, script_len, filename,
                            exit_status_p, error);
}

/*
 * GjsContextPrivate::eval_with_scope:
 * @scope_object: an object to use as the global scope, or nullptr
 * @script: JavaScript program encoded in UTF-8
 * @script_len: length of @script, or -1 if @script is 0-terminated
 * @filename: filename to use as the origin of @script
 * @retval: location for the return value of @script
 *
 * Executes @script with a local scope so that nothing from the script leaks out
 * into the global scope.
 * If @scope_object is given, then everything that @script placed in the global
 * namespace is defined on @scope_object.
 * Otherwise, the global definitions are just discarded.
 */
bool GjsContextPrivate::eval_with_scope(JS::HandleObject scope_object,
                                        const char* script, ssize_t script_len,
                                        const char* filename,
                                        JS::MutableHandleValue retval) {
    /* log and clear exception if it's set (should not be, normally...) */
    if (JS_IsExceptionPending(m_cx)) {
        g_warning("eval_with_scope() called with a pending exception");
        return false;
    }

    JS::RootedObject eval_obj(m_cx, scope_object);
    if (!eval_obj)
        eval_obj = JS_NewPlainObject(m_cx);

    std::u16string utf16_string = gjs_utf8_script_to_utf16(script, script_len);
    // COMPAT: This could use JS::SourceText<mozilla::Utf8Unit> directly,
    // but that messes up code coverage. See bug
    // https://bugzilla.mozilla.org/show_bug.cgi?id=1404784
    JS::SourceText<char16_t> buf;
    if (!buf.init(m_cx, utf16_string.c_str(), utf16_string.size(),
                  JS::SourceOwnership::Borrowed))
        return false;

    JS::RootedObjectVector scope_chain(m_cx);
    if (!scope_chain.append(eval_obj)) {
        JS_ReportOutOfMemory(m_cx);
        return false;
    }

    JS::CompileOptions options(m_cx);
    options.setFileAndLine(filename, 1);

    if (!JS::Evaluate(m_cx, scope_chain, options, buf, retval))
        return false;

    schedule_gc_if_needed();

    if (JS_IsExceptionPending(m_cx)) {
        g_warning(
            "JS::Evaluate() returned true but exception was pending; "
            "did somebody call gjs_throw() without returning false?");
        return false;
    }

    gjs_debug(GJS_DEBUG_CONTEXT, "Script evaluation succeeded");

    return true;
}

/*
 * GjsContextPrivate::call_function:
 * @this_obj: Object to use as the 'this' for the function call
 * @func_val: Callable to call, as a JS value
 * @args: Arguments to pass to the callable
 * @rval: Location for the return value
 *
 * Use this instead of JS_CallFunctionValue(), because it schedules a GC if
 * one is needed. It's good practice to check if a GC should be run every time
 * we return from JS back into C++.
 */
bool GjsContextPrivate::call_function(JS::HandleObject this_obj,
                                      JS::HandleValue func_val,
                                      const JS::HandleValueArray& args,
                                      JS::MutableHandleValue rval) {
    if (!JS_CallFunctionValue(m_cx, this_obj, func_val, args, rval))
        return false;

    schedule_gc_if_needed();

    return true;
}

bool
gjs_context_define_string_array(GjsContext  *js_context,
                                const char    *array_name,
                                gssize         array_length,
                                const char   **array_values,
                                GError       **error)
{
    g_return_val_if_fail(GJS_IS_CONTEXT(js_context), false);
    GjsContextPrivate* gjs = GjsContextPrivate::from_object(js_context);

    JSAutoRealm ar(gjs->context(), gjs->global());

    std::vector<std::string> strings;
    if (array_values) {
        if (array_length < 0)
            array_length = g_strv_length(const_cast<char**>(array_values));
        strings = {array_values, array_values + array_length};
    }

    JS::RootedObject global_root(gjs->context(), gjs->global());
    if (!gjs_define_string_array(gjs->context(), global_root, array_name,
                                 strings, JSPROP_READONLY | JSPROP_PERMANENT)) {
        gjs_log_exception(gjs->context());
        g_set_error(error,
                    GJS_ERROR,
                    GJS_ERROR_FAILED,
                    "gjs_define_string_array() failed");
        return false;
    }

    return true;
}

static GjsContext *current_context;

GjsContext *
gjs_context_get_current (void)
{
    return current_context;
}

void
gjs_context_make_current (GjsContext *context)
{
    g_assert (context == NULL || current_context == NULL);

    current_context = context;
}

/**
 * gjs_context_get_profiler:
 * @self: the #GjsContext
 *
 * Returns the profiler's internal instance of #GjsProfiler for you to
 * customize, or %NULL if profiling is not enabled on this #GjsContext.
 *
 * Returns: (transfer none) (nullable): a #GjsProfiler
 */
GjsProfiler *
gjs_context_get_profiler(GjsContext *self)
{
    return GjsContextPrivate::from_object(self)->profiler();
}

/**
 * gjs_get_js_version:
 *
 * Returns the underlying version of the JS engine.
 *
 * Returns: a string
 */
const char *
gjs_get_js_version(void)
{
    return JS_GetImplementationVersion();
}
