context.cpp 29.9 KB
Newer Older
1
/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
2
/*
Havoc Pennington's avatar
Havoc Pennington committed
3
 * Copyright (c) 2008  litl, LLC
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 *
 * 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>

26
#include <array>
27
#include <unordered_map>
28

29
30
#include <gio/gio.h>

31
#include "context-private.h"
Philip Chimento's avatar
Philip Chimento committed
32
#include "engine.h"
33
#include "global.h"
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
34
35
#include "importer.h"
#include "jsapi-util.h"
36
#include "jsapi-wrapper.h"
Johan Dahlin's avatar
Johan Dahlin committed
37
#include "native.h"
38
#include "profiler-private.h"
Johan Dahlin's avatar
Johan Dahlin committed
39
#include "byteArray.h"
40
#include "gi/object.h"
41
#include "gi/repo.h"
Colin Walters's avatar
Colin Walters committed
42

43
44
#include <modules/modules.h>

Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
45
#include <util/log.h>
46
#include <util/glib.h>
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
47
48
#include <util/error.h>

Chun-wei Fan's avatar
Chun-wei Fan committed
49
50
51
52
53
#ifdef G_OS_WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif

Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
54
55
56
57
#include <string.h>

static void     gjs_context_dispose           (GObject               *object);
static void     gjs_context_finalize          (GObject               *object);
58
static void     gjs_context_constructed       (GObject               *object);
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
59
60
61
62
63
64
65
66
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);
67
68
69

using JobQueue = JS::GCVector<JSObject *, 0, js::SystemAllocPolicy>;

Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
70
71
72
73
struct _GjsContext {
    GObject parent;

    JSContext *context;
74
    JS::Heap<JSObject*> global;
75
    GThread *owner_thread;
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
76

77
    char *program_name;
78

Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
79
    char **search_path;
80

Philip Chimento's avatar
Philip Chimento committed
81
    bool destroying;
Philip Chimento's avatar
Philip Chimento committed
82
    bool in_gc_sweep;
83

84
85
86
    bool should_exit;
    uint8_t exit_code;

87
88
    guint    auto_gc_id;

89
    std::array<JS::PersistentRootedId*, GJS_STRING_LAST> const_strings;
90
91
92
93

    JS::PersistentRooted<JobQueue> *job_queue;
    unsigned idle_drain_handler;
    bool draining_job_queue;
94
95

    std::unordered_map<uint64_t, GjsAutoChar> unhandled_rejection_stacks;
Philip Chimento's avatar
Philip Chimento committed
96
97
98
99

    GjsProfiler *profiler;
    bool should_profile : 1;
    bool should_listen_sigusr2 : 1;
100
101
102
103
104
105
106
107
};

/* Keep this consistent with GjsConstString */
static const char *const_strings[] = {
    "constructor", "prototype", "length",
    "imports", "__parentModule__", "__init__", "searchPath",
    "__gjsKeepAlive", "__gjsPrivateNS",
    "gi", "versions", "overrides",
108
    "_init", "_instance_init", "_new_internal", "new",
109
110
    "message", "code", "stack", "fileName", "lineNumber", "columnNumber",
    "name", "x", "y", "width", "height", "__modulePath__"
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
111
112
};

113
114
G_STATIC_ASSERT(G_N_ELEMENTS(const_strings) == GJS_STRING_LAST);

Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
115
116
117
118
struct _GjsContextClass {
    GObjectClass parent;
};

119
120
121
122
123
/* Temporary workaround for https://bugzilla.gnome.org/show_bug.cgi?id=793175 */
#if __GNUC__ >= 8
_Pragma("GCC diagnostic push")
_Pragma("GCC diagnostic ignored \"-Wcast-function-type\"")
#endif
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
124
G_DEFINE_TYPE(GjsContext, gjs_context, G_TYPE_OBJECT);
125
126
127
#if __GNUC__ >= 8
_Pragma("GCC diagnostic pop")
#endif
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
128
129
130
131

enum {
    PROP_0,
    PROP_SEARCH_PATH,
132
    PROP_PROGRAM_NAME,
Philip Chimento's avatar
Philip Chimento committed
133
134
    PROP_PROFILER_ENABLED,
    PROP_PROFILER_SIGUSR2,
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
135
136
};

137
static GMutex contexts_lock;
138
139
static GList *all_contexts = NULL;

Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
140
141
142
static void
gjs_context_init(GjsContext *js_context)
{
143
    gjs_context_make_current(js_context);
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
144
145
146
147
148
149
150
151
152
153
154
}

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;

155
    object_class->constructed = gjs_context_constructed;
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
156
157
158
    object_class->get_property = gjs_context_get_property;
    object_class->set_property = gjs_context_set_property;

159
160
161
162
    pspec = g_param_spec_boxed("search-path",
                               "Search path",
                               "Path where modules to import should reside",
                               G_TYPE_STRV,
163
                               (GParamFlags) (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
164
165
166
167

    g_object_class_install_property(object_class,
                                    PROP_SEARCH_PATH,
                                    pspec);
Philip Chimento's avatar
Philip Chimento committed
168
    g_param_spec_unref(pspec);
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
169

170
171
172
173
    pspec = g_param_spec_string("program-name",
                                "Program Name",
                                "The filename of the launched JS program",
                                "",
174
                                (GParamFlags) (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
175
176
177
178

    g_object_class_install_property(object_class,
                                    PROP_PROGRAM_NAME,
                                    pspec);
Philip Chimento's avatar
Philip Chimento committed
179
    g_param_spec_unref(pspec);
180

Philip Chimento's avatar
Philip Chimento committed
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
    /**
     * 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);

214
215
    /* For GjsPrivate */
    {
Chun-wei Fan's avatar
Chun-wei Fan committed
216
217
218
219
220
221
#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", "girepository-1.0", NULL);
        g_free (basedir);
#else
222
        char *priv_typelib_dir = g_build_filename (PKGLIBDIR, "girepository-1.0", NULL);
Chun-wei Fan's avatar
Chun-wei Fan committed
223
#endif
224
        g_irepository_prepend_search_path(priv_typelib_dir);
225
    g_free (priv_typelib_dir);
226
227
    }

228
229
    gjs_register_native_module("byteArray", gjs_define_byte_array_stuff);
    gjs_register_native_module("_gi", gjs_define_private_gi_stuff);
230
    gjs_register_native_module("gi", gjs_define_repo);
231
232

    gjs_register_static_modules();
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
233
234
}

235
236
237
238
static void
gjs_context_tracer(JSTracer *trc, void *data)
{
    GjsContext *gjs_context = reinterpret_cast<GjsContext *>(data);
239
    JS::TraceEdge<JSObject *>(trc, &gjs_context->global, "GJS global object");
240
241
}

242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
static void
warn_about_unhandled_promise_rejections(GjsContext *gjs_context)
{
    for (auto& kv : gjs_context->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 : "");
    }
    gjs_context->unhandled_rejection_stacks.clear();
}

Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
257
258
259
260
261
262
263
static void
gjs_context_dispose(GObject *object)
{
    GjsContext *js_context;

    js_context = GJS_CONTEXT(object);

Philip Chimento's avatar
Philip Chimento committed
264
265
266
267
    /* Profiler must be stopped and freed before context is shut down */
    if (js_context->profiler)
        g_clear_pointer(&js_context->profiler, _gjs_profiler_free);

268
269
270
271
    /* Run dispose notifications first, so that anything releasing
     * references in response to this can still get garbage collected */
    G_OBJECT_CLASS(gjs_context_parent_class)->dispose(object);

Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
272
273
274
    if (js_context->context != NULL) {

        gjs_debug(GJS_DEBUG_CONTEXT,
275
                  "Destroying JS context");
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
276

277
278
        warn_about_unhandled_promise_rejections(js_context);

279
        JS_BeginRequest(js_context->context);
280

281
282
283
284
        /* Do a full GC here before tearing down, since once we do
         * that we may not have the JS_GetPrivate() to access the
         * context
         */
285
        JS_GC(js_context->context);
286
        JS_EndRequest(js_context->context);
287

Philip Chimento's avatar
Philip Chimento committed
288
        js_context->destroying = true;
289
290
291
292
293

        /* 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.
         */
294
        gjs_object_prepare_shutdown();
295

296
297
298
299
300
        if (js_context->auto_gc_id > 0) {
            g_source_remove (js_context->auto_gc_id);
            js_context->auto_gc_id = 0;
        }

301
        JS_RemoveExtraGCRootsTracer(js_context->context, gjs_context_tracer,
302
                                    js_context);
303
        js_context->global = NULL;
304

305
306
307
        for (auto& root : js_context->const_strings)
            delete root;

308
309
        delete js_context->job_queue;

310
        /* Tear down JS */
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
        JS_DestroyContext(js_context->context);
        js_context->context = NULL;
    }
}

static void
gjs_context_finalize(GObject *object)
{
    GjsContext *js_context;

    js_context = GJS_CONTEXT(object);

    if (js_context->search_path != NULL) {
        g_strfreev(js_context->search_path);
        js_context->search_path = NULL;
    }
327

Sam Spilsbury's avatar
Sam Spilsbury committed
328
329
330
331
332
    if (js_context->program_name != NULL) {
        g_free(js_context->program_name);
        js_context->program_name = NULL;
    }

333
334
335
    if (gjs_context_get_current() == (GjsContext*)object)
        gjs_context_make_current(NULL);

336
    g_mutex_lock(&contexts_lock);
337
    all_contexts = g_list_remove(all_contexts, object);
338
    g_mutex_unlock(&contexts_lock);
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
339

340
    js_context->global.~Heap();
341
    js_context->const_strings.~array();
342
    js_context->unhandled_rejection_stacks.~unordered_map();
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
343
344
345
    G_OBJECT_CLASS(gjs_context_parent_class)->finalize(object);
}

346
347
static void
gjs_context_constructed(GObject *object)
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
348
{
349
    GjsContext *js_context = GJS_CONTEXT(object);
350
    int i;
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
351

352
    G_OBJECT_CLASS(gjs_context_parent_class)->constructed(object);
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
353

354
    js_context->owner_thread = g_thread_self();
355

Philip Chimento's avatar
Philip Chimento committed
356
357
    JSContext *cx = gjs_create_js_context(js_context);
    if (!cx)
Giovanni Campagna's avatar
Giovanni Campagna committed
358
        g_error("Failed to create javascript context");
Philip Chimento's avatar
Philip Chimento committed
359
    js_context->context = cx;
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
360

Philip Chimento's avatar
Philip Chimento committed
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
    const char *env_profiler = g_getenv("GJS_ENABLE_PROFILER");
    if (env_profiler || js_context->should_listen_sigusr2)
        js_context->should_profile = true;

    if (js_context->should_profile) {
        js_context->profiler = _gjs_profiler_new(js_context);

        if (!js_context->profiler) {
            js_context->should_profile = false;
        } else {
            if (js_context->should_listen_sigusr2)
                _gjs_profiler_setup_signals(js_context->profiler, js_context);
        }
    }

376
    new (&js_context->unhandled_rejection_stacks) std::unordered_map<uint64_t, GjsAutoChar>;
377
    new (&js_context->const_strings) std::array<JS::PersistentRootedId*, GJS_STRING_LAST>;
378
    for (i = 0; i < GJS_STRING_LAST; i++) {
Philip Chimento's avatar
Philip Chimento committed
379
380
        js_context->const_strings[i] = new JS::PersistentRootedId(cx,
            gjs_intern_string_to_id(cx, const_strings[i]));
381
    }
382

383
384
385
386
    js_context->job_queue = new JS::PersistentRooted<JobQueue>(cx);
    if (!js_context->job_queue)
        g_error("Failed to initialize promise job queue");

Philip Chimento's avatar
Philip Chimento committed
387
    JS_BeginRequest(cx);
388

Philip Chimento's avatar
Philip Chimento committed
389
    JS::RootedObject global(cx, gjs_create_global_object(cx));
390
391
392
393
    if (!global) {
        gjs_log_exception(js_context->context);
        g_error("Failed to initialize global object");
    }
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
394

Philip Chimento's avatar
Philip Chimento committed
395
    JSAutoCompartment ac(cx, global);
396

397
    new (&js_context->global) JS::Heap<JSObject *>(global);
Philip Chimento's avatar
Philip Chimento committed
398
    JS_AddExtraGCRootsTracer(cx, gjs_context_tracer, js_context);
399

Philip Chimento's avatar
Philip Chimento committed
400
401
    JS::RootedObject importer(cx, gjs_create_root_importer(cx,
        js_context->search_path ? js_context->search_path : nullptr));
402
    if (!importer)
Giovanni Campagna's avatar
Giovanni Campagna committed
403
        g_error("Failed to create root importer");
404

Philip Chimento's avatar
Philip Chimento committed
405
    JS::Value v_importer = gjs_get_global_slot(cx, GJS_GLOBAL_SLOT_IMPORTS);
406
407
408
    g_assert(((void) "Someone else already created root importer",
              v_importer.isUndefined()));

Philip Chimento's avatar
Philip Chimento committed
409
    gjs_set_global_slot(cx, GJS_GLOBAL_SLOT_IMPORTS, JS::ObjectValue(*importer));
410

411
    if (!gjs_define_global_properties(cx, global, "default")) {
Philip Chimento's avatar
Philip Chimento committed
412
        gjs_log_exception(cx);
413
414
        g_error("Failed to define properties on global object");
    }
415

Philip Chimento's avatar
Philip Chimento committed
416
    JS_EndRequest(cx);
417

418
    g_mutex_lock (&contexts_lock);
419
    all_contexts = g_list_prepend(all_contexts, object);
420
    g_mutex_unlock (&contexts_lock);
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
421
422
423
424
425
426
427
428
429
430
431
432
433
}

static void
gjs_context_get_property (GObject     *object,
                          guint        prop_id,
                          GValue      *value,
                          GParamSpec  *pspec)
{
    GjsContext *js_context;

    js_context = GJS_CONTEXT (object);

    switch (prop_id) {
434
435
436
    case PROP_PROGRAM_NAME:
        g_value_set_string(value, js_context->program_name);
        break;
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
    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)
{
    GjsContext *js_context;

    js_context = GJS_CONTEXT (object);

    switch (prop_id) {
    case PROP_SEARCH_PATH:
455
        js_context->search_path = (char**) g_value_dup_boxed(value);
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
456
        break;
457
458
459
    case PROP_PROGRAM_NAME:
        js_context->program_name = g_value_dup_string(value);
        break;
Philip Chimento's avatar
Philip Chimento committed
460
461
462
463
464
465
    case PROP_PROFILER_ENABLED:
        js_context->should_profile = g_value_get_boolean(value);
        break;
    case PROP_PROFILER_SIGUSR2:
        js_context->should_listen_sigusr2 = g_value_get_boolean(value);
        break;
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
466
467
468
469
470
471
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        break;
    }
}

472

Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
473
474
475
GjsContext*
gjs_context_new(void)
{
476
    return (GjsContext*) g_object_new (GJS_TYPE_CONTEXT, NULL);
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
477
478
479
480
481
}

GjsContext*
gjs_context_new_with_search_path(char** search_path)
{
482
    return (GjsContext*) g_object_new (GJS_TYPE_CONTEXT,
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
483
484
485
486
                         "search-path", search_path,
                         NULL);
}

Philip Chimento's avatar
Philip Chimento committed
487
bool
488
489
490
491
492
_gjs_context_destroying (GjsContext *context)
{
    return context->destroying;
}

493
494
495
496
497
498
static gboolean
trigger_gc_if_needed (gpointer user_data)
{
    GjsContext *js_context = GJS_CONTEXT(user_data);
    js_context->auto_gc_id = 0;
    gjs_gc_if_needed(js_context->context);
Philip Chimento's avatar
Philip Chimento committed
499
    return G_SOURCE_REMOVE;
500
501
502
503
504
505
506
507
508
509
510
511
512
}

void
_gjs_context_schedule_gc_if_needed (GjsContext *js_context)
{
    if (js_context->auto_gc_id > 0)
        return;

    js_context->auto_gc_id = g_idle_add_full(G_PRIORITY_LOW,
                                             trigger_gc_if_needed,
                                             js_context, NULL);
}

513
514
515
516
517
518
519
520
521
void
_gjs_context_exit(GjsContext *js_context,
                  uint8_t     exit_code)
{
    g_assert(!js_context->should_exit);
    js_context->should_exit = true;
    js_context->exit_code = exit_code;
}

522
523
524
bool
_gjs_context_should_exit(GjsContext *js_context,
                         uint8_t    *exit_code_p)
525
526
527
528
529
530
{
    if (exit_code_p != NULL)
        *exit_code_p = js_context->exit_code;
    return js_context->should_exit;
}

531
532
533
534
535
536
537
static void
context_reset_exit(GjsContext *js_context)
{
    js_context->should_exit = false;
    js_context->exit_code = 0;
}

538
539
540
bool
_gjs_context_get_is_owner_thread(GjsContext *js_context)
{
541
    return js_context->owner_thread == g_thread_self();
542
543
}

Philip Chimento's avatar
Philip Chimento committed
544
545
546
547
548
549
550
551
552
553
554
555
556
557
void
_gjs_context_set_sweeping(GjsContext *js_context,
                          bool        sweeping)
{
    js_context->in_gc_sweep = sweeping;
}

bool
_gjs_context_is_sweeping(JSContext *cx)
{
    auto js_context = static_cast<GjsContext *>(JS_GetContextPrivate(cx));
    return js_context->in_gc_sweep;
}

558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
static gboolean
drain_job_queue_idle_handler(void *data)
{
    auto gjs_context = static_cast<GjsContext *>(data);
    _gjs_context_run_jobs(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(((void) "_gjs_context_run_jobs() should have emptied queue",
              gjs_context->idle_drain_handler == 0));
    return G_SOURCE_REMOVE;
}

/* See engine.cpp and JS::SetEnqueuePromiseJobCallback(). */
bool
_gjs_context_enqueue_job(GjsContext      *gjs_context,
                         JS::HandleObject job)
{
    if (gjs_context->idle_drain_handler)
        g_assert(gjs_context->job_queue->length() > 0);
    else
        g_assert(gjs_context->job_queue->length() == 0);

    if (!gjs_context->job_queue->append(job))
        return false;
    if (!gjs_context->idle_drain_handler)
        gjs_context->idle_drain_handler =
            g_idle_add(drain_job_queue_idle_handler, gjs_context);

    return true;
}

/**
 * _gjs_context_run_jobs:
 * @gjs_context: The #GjsContext instance
 *
 * 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
_gjs_context_run_jobs(GjsContext *gjs_context)
{
    bool retval = true;
    g_assert(gjs_context->job_queue);

    if (gjs_context->draining_job_queue || gjs_context->should_exit)
        return true;

    auto cx = static_cast<JSContext *>(gjs_context_get_native_context(gjs_context));
    JSAutoRequest ar(cx);

    gjs_context->draining_job_queue = true;  /* Ignore reentrant calls */

    JS::RootedObject job(cx);
    JS::HandleValueArray args(JS::HandleValueArray::empty());
    JS::RootedValue rval(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 < gjs_context->job_queue->length(); ix++) {
        /* A previous job might have set this flag. e.g., System.exit(). */
        if (gjs_context->should_exit)
            break;

        job = gjs_context->job_queue->get()[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;

        gjs_context->job_queue->get()[ix] = nullptr;
        {
            JSAutoCompartment ac(cx, job);
            if (!JS::Call(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(cx)) {
645
646
647
648
                    /* System.exit() is an uncatchable exception, but does not
                     * indicate a bug. Log everything else. */
                    if (!_gjs_context_should_exit(gjs_context, nullptr))
                        g_critical("Promise callback terminated with uncatchable exception");
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
                    retval = false;
                    continue;
                }

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

    gjs_context->draining_job_queue = false;
    gjs_context->job_queue->clear();
    if (gjs_context->idle_drain_handler) {
        g_source_remove(gjs_context->idle_drain_handler);
        gjs_context->idle_drain_handler = 0;
    }
    return retval;
}

668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
void
_gjs_context_register_unhandled_promise_rejection(GjsContext   *gjs_context,
                                                  uint64_t      id,
                                                  GjsAutoChar&& stack)
{
    gjs_context->unhandled_rejection_stacks[id] = std::move(stack);
}

void
_gjs_context_unregister_unhandled_promise_rejection(GjsContext *gjs_context,
                                                    uint64_t    id)
{
    size_t erased = gjs_context->unhandled_rejection_stacks.erase(id);
    g_assert(((void)"Handler attached to rejected promise that wasn't "
              "previously marked as unhandled", erased == 1));
}

685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
/**
 * 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)
{
    gjs_maybe_gc(context->context);
}

712
713
714
715
716
717
718
719
720
721
/**
 * 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)
{
722
    JS_GC(context->context);
723
724
}

725
726
/**
 * gjs_context_get_all:
727
 *
728
729
730
 * 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.
731
 *
732
733
734
735
736
737
738
 * Return value: (element-type GjsContext) (transfer full): Known #GjsContext instances
 */
GList*
gjs_context_get_all(void)
{
  GList *result;
  GList *iter;
739
  g_mutex_lock (&contexts_lock);
740
741
742
  result = g_list_copy(all_contexts);
  for (iter = result; iter; iter = iter->next)
    g_object_ref((GObject*)iter->data);
743
  g_mutex_unlock (&contexts_lock);
744
745
746
  return result;
}

747
748
749
750
751
752
753
754
/**
 * 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)
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
755
{
756
    g_return_val_if_fail(GJS_IS_CONTEXT(js_context), NULL);
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
757
758
759
    return js_context->context;
}

Philip Chimento's avatar
Philip Chimento committed
760
bool
761
gjs_context_eval(GjsContext   *js_context,
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
762
763
764
765
766
767
                 const char   *script,
                 gssize        script_len,
                 const char   *filename,
                 int          *exit_status_p,
                 GError      **error)
{
Philip Chimento's avatar
Philip Chimento committed
768
    bool ret = false;
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
769

Philip Chimento's avatar
Philip Chimento committed
770
771
772
773
    bool auto_profile = js_context->should_profile;
    if (auto_profile && (_gjs_profiler_is_running(js_context->profiler) ||
                         js_context->should_listen_sigusr2))
        auto_profile = false;
774

Jasper St. Pierre's avatar
Jasper St. Pierre committed
775
    JSAutoCompartment ac(js_context->context, js_context->global);
776
    JSAutoRequest ar(js_context->context);
Jasper St. Pierre's avatar
Jasper St. Pierre committed
777

Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
778
779
    g_object_ref(G_OBJECT(js_context));

Philip Chimento's avatar
Philip Chimento committed
780
781
782
    if (auto_profile)
        gjs_profiler_start(js_context->profiler);

783
    JS::RootedValue retval(js_context->context);
784
785
786
787
788
789
790
791
    bool ok = gjs_eval_with_scope(js_context->context, 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. */
    ok = _gjs_context_run_jobs(js_context) && ok;

Philip Chimento's avatar
Philip Chimento committed
792
793
    if (auto_profile)
        gjs_profiler_stop(js_context->profiler);
794

795
    if (!ok) {
796
        uint8_t code;
797
        if (_gjs_context_should_exit(js_context, &code)) {
798
799
800
801
802
803
804
            /* 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);
            goto out;  /* Don't log anything */
        }
805

806
        if (!JS_IsExceptionPending(js_context->context)) {
807
808
            g_critical("Script %s terminated with an uncatchable exception",
                       filename);
809
810
811
812
813
814
815
816
            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);
        }

817
        gjs_log_exception(js_context->context);
818
819
        /* No exit code from script, but we don't want to exit(0) */
        *exit_status_p = 1;
820
        goto out;
821
    }
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
822

823
    if (exit_status_p) {
824
        if (retval.isInt32()) {
825
826
827
828
            int code = retval.toInt32();
            gjs_debug(GJS_DEBUG_CONTEXT,
                      "Script returned integer code %d", code);
            *exit_status_p = code;
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
829
830
831
832
833
834
        } else {
            /* Assume success if no integer was returned */
            *exit_status_p = 0;
        }
    }

Philip Chimento's avatar
Philip Chimento committed
835
    ret = true;
836

837
 out:
838
    g_object_unref(G_OBJECT(js_context));
839
    context_reset_exit(js_context);
840
    return ret;
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
841
842
}

Philip Chimento's avatar
Philip Chimento committed
843
bool
844
gjs_context_eval_file(GjsContext    *js_context,
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
845
846
847
848
                      const char    *filename,
                      int           *exit_status_p,
                      GError       **error)
{
849
850
851
    char *script;
    size_t script_len;
    GjsAutoUnref<GFile> file = g_file_new_for_commandline_arg(filename);
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
852

853
854
855
856
    if (!g_file_load_contents(file, nullptr, &script, &script_len, nullptr,
                              error))
        return false;
    GjsAutoChar script_ref = script;
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
857

858
859
    return gjs_context_eval(js_context, script, script_len, filename,
                            exit_status_p, error);
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
860
861
}

Philip Chimento's avatar
Philip Chimento committed
862
bool
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
863
864
865
866
867
868
gjs_context_define_string_array(GjsContext  *js_context,
                                const char    *array_name,
                                gssize         array_length,
                                const char   **array_values,
                                GError       **error)
{
Tim Lunn's avatar
Tim Lunn committed
869
    JSAutoCompartment ac(js_context->context, js_context->global);
870
871
    JSAutoRequest ar(js_context->context);

Philip Chimento's avatar
Philip Chimento committed
872
    JS::RootedObject global_root(js_context->context, js_context->global);
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
873
    if (!gjs_define_string_array(js_context->context,
Philip Chimento's avatar
Philip Chimento committed
874
                                 global_root,
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
875
876
                                 array_name, array_length, array_values,
                                 JSPROP_READONLY | JSPROP_PERMANENT)) {
Giovanni Campagna's avatar
Giovanni Campagna committed
877
878
879
880
881
        gjs_log_exception(js_context->context);
        g_set_error(error,
                    GJS_ERROR,
                    GJS_ERROR_FAILED,
                    "gjs_define_string_array() failed");
Philip Chimento's avatar
Philip Chimento committed
882
        return false;
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
883
884
    }

Philip Chimento's avatar
Philip Chimento committed
885
    return true;
Lucas Almeida Rocha's avatar
Lucas Almeida Rocha committed
886
}
887

888
889
static GjsContext *current_context;

890
GjsContext *
891
gjs_context_get_current (void)
892
893
894
895
896
{
    return current_context;
}

void
897
gjs_context_make_current (GjsContext *context)
898
{
899
    g_assert (context == NULL || current_context == NULL);
900

901
    current_context = context;
902
}
903

904
905
906
907
/* It's OK to return JS::HandleId here, to avoid an extra root, with the
 * caveat that you should not use this value after the GjsContext has
 * been destroyed. */
JS::HandleId
908
909
910
911
gjs_context_get_const_string(JSContext      *context,
                             GjsConstString  name)
{
    GjsContext *gjs_context = (GjsContext *) JS_GetContextPrivate(context);
912
    return *gjs_context->const_strings[name];
913
914
}

915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
/**
 * gjs_get_import_global:
 * @context: a #JSContext
 *
 * Gets the "import global" for the context's runtime. The import
 * global object is the global object for the context. It is used
 * as the root object for the scope of modules loaded by GJS in this
 * runtime, and should also be used as the globals 'obj' argument passed
 * to JS_InitClass() and the parent argument passed to JS_ConstructObject()
 * when creating a native classes that are shared between all contexts using
 * the runtime. (The standard JS classes are not shared, but we share
 * classes such as GObject proxy classes since objects of these classes can
 * easily migrate between contexts and having different classes depending
 * on the context where they were first accessed would be confusing.)
 *
 * Return value: the "import global" for the context's
 *  runtime. Will never return %NULL while GJS has an active context
 *  for the runtime.
 */
JSObject*
gjs_get_import_global(JSContext *context)
{
    GjsContext *gjs_context = (GjsContext *) JS_GetContextPrivate(context);
    return gjs_context->global;
}
Philip Chimento's avatar
Philip Chimento committed
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954

/**
 * 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 self->profiler;
}