diff --git a/gjs-1.71.1-promise-rejection.patch b/gjs-1.71.1-promise-rejection.patch new file mode 100644 index 0000000..7dfd077 --- /dev/null +++ b/gjs-1.71.1-promise-rejection.patch @@ -0,0 +1,258 @@ +From 3f0c615a802e196f5e14a9819a81e7c74fb6b988 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Mon, 21 Feb 2022 17:17:10 +0100 +Subject: [PATCH 1/2] context: Only warn about unhandled promises when the job + queue is flushed + +A promise rejection may be reported as not handled even though it's +going to be handled by a later added promise. In such case we were +correctly be informed about this via +unregister_unhandled_promise_rejection(), but given that since commit +4446ee7f we promptly warn about unhandled promise rejections our +unhandled rejections stack trace list was cleared earlier than we'd got +an unregistration event. + +To avoid this, let's just wait for the job queue to be fully processed, +so that we won't accidentally clear the unhandled rejections list, until +those have been fully processed. + +By doing this we are both avoiding misbehaving on the said case, and we +don't regress by keeping the exit-warns handled by #417 still valid. + +Added tests to cover both a normal rejection case and the one that was +previously leading to a crash (assertion error). + +Fixes: #466 +--- + gi/closure.cpp | 4 +- + gi/function.cpp | 6 +- + gjs/context.cpp | 6 +- + installed-tests/js/meson.build | 1 + + installed-tests/js/testPromise.js | 106 ++++++++++++++++++++++++++++++ + 5 files changed, 115 insertions(+), 8 deletions(-) + create mode 100644 installed-tests/js/testPromise.js + +diff --git a/gi/closure.cpp b/gi/closure.cpp +index 15b6a5fe7..288550a05 100644 +--- a/gi/closure.cpp ++++ b/gi/closure.cpp +@@ -183,7 +183,9 @@ bool Closure::invoke(JS::HandleObject this_obj, + JS::RootedFunction func(m_cx, m_func); + bool ok = JS::Call(m_cx, this_obj, func, args, retval); + GjsContextPrivate* gjs = GjsContextPrivate::from_cx(m_cx); +- gjs->warn_about_unhandled_promise_rejections(); ++ ++ if (gjs->should_exit(nullptr)) ++ gjs->warn_about_unhandled_promise_rejections(); + if (!ok) { + /* Exception thrown... */ + gjs_debug_closure( +diff --git a/gi/function.cpp b/gi/function.cpp +index f39dc1727..716acc8ec 100644 +--- a/gi/function.cpp ++++ b/gi/function.cpp +@@ -379,11 +379,11 @@ void GjsCallbackTrampoline::callback_closure(GIArgument** args, void* result) { + // main loop, or maybe not, but there's no way to tell, so we have + // to exit here instead of propagating the exception back to the + // original calling JS code. +- gjs->warn_about_unhandled_promise_rejections(); +- + uint8_t code; +- if (gjs->should_exit(&code)) ++ if (gjs->should_exit(&code)) { ++ gjs->warn_about_unhandled_promise_rejections(); + exit(code); ++ } + + // Some other uncatchable exception, e.g. out of memory + JSFunction* fn = callable(); +diff --git a/gjs/context.cpp b/gjs/context.cpp +index 7a6eba6e7..0ba6b250a 100644 +--- a/gjs/context.cpp ++++ b/gjs/context.cpp +@@ -1080,10 +1080,7 @@ bool GjsContextPrivate::run_jobs_fallible(GCancellable* cancellable) { + JSAutoRealm ar(m_cx, job); + gjs_debug(GJS_DEBUG_CONTEXT, "handling job %s", + gjs_debug_object(job).c_str()); +- bool ok = +- JS::Call(m_cx, JS::UndefinedHandleValue, job, args, &rval); +- warn_about_unhandled_promise_rejections(); +- if (!ok) { ++ 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. */ +@@ -1104,6 +1101,7 @@ bool GjsContextPrivate::run_jobs_fallible(GCancellable* cancellable) { + + m_draining_job_queue = false; + m_job_queue.clear(); ++ warn_about_unhandled_promise_rejections(); + JS::JobQueueIsEmpty(m_cx); + return retval; + } +diff --git a/installed-tests/js/meson.build b/installed-tests/js/meson.build +index f531fd356..567e5cc7c 100644 +--- a/installed-tests/js/meson.build ++++ b/installed-tests/js/meson.build +@@ -137,6 +137,7 @@ jasmine_tests = [ + 'Package', + 'ParamSpec', + 'Print', ++ 'Promise', + 'Regress', + 'Signals', + 'System', +diff --git a/installed-tests/js/testPromise.js b/installed-tests/js/testPromise.js +new file mode 100644 +index 000000000..df302bac2 +--- /dev/null ++++ b/installed-tests/js/testPromise.js +@@ -0,0 +1,106 @@ ++// -*- mode: js; indent-tabs-mode: nil -*- ++// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later ++// SPDX-FileContributor: Authored by: Marco Trevisan ++// SPDX-FileCopyrightText: 2022 Canonical, Ltd. ++ ++const {GLib} = imports.gi; ++ ++class SubPromise extends Promise { ++ constructor(executor) { ++ super((resolve, reject) => { ++ executor(resolve, reject); ++ }); ++ } ++} ++ ++const PromiseResult = { ++ RESOLVED: 0, ++ FAILED: 1, ++}; ++ ++describe('Promise', function () { ++ let loop; ++ ++ beforeEach(function () { ++ loop = GLib.MainLoop.new(null, false); ++ }); ++ ++ function executePromise(promise) { ++ const promiseResult = {}; ++ ++ promise.then(value => { ++ promiseResult.result = PromiseResult.RESOLVED; ++ promiseResult.value = value; ++ }).catch(e => { ++ promiseResult.result = PromiseResult.FAILED; ++ promiseResult.error = e; ++ }); ++ ++ while (promiseResult.result === undefined) ++ loop.get_context().iteration(true); ++ ++ return promiseResult; ++ } ++ ++ it('waits for all promises before handling unhandled, when handled', function () { ++ let error; ++ let resolved; ++ ++ const promise = async () => { ++ await new SubPromise(resolve => resolve('success')); ++ const rejecting = new SubPromise((_resolve, reject) => reject('got error')); ++ try { ++ await rejecting; ++ } catch (e) { ++ error = e; ++ } finally { ++ resolved = true; ++ } ++ ++ expect(resolved).toBeTrue(); ++ expect(error).toBe('got error'); ++ ++ return 'parent-returned'; ++ }; ++ ++ expect(executePromise(promise())).toEqual({ ++ result: PromiseResult.RESOLVED, ++ value: 'parent-returned', ++ }); ++ }); ++ ++ it('waits for all promises before handling unhandled, when unhandled', function () { ++ const thenHandler = jasmine.createSpy('thenHandler'); ++ ++ const promise = async () => { ++ await new Promise(resolve => resolve('success')); ++ await new Promise((_resolve, reject) => reject(new Error('got error'))); ++ return 'parent-returned'; ++ }; ++ ++ promise().then(thenHandler).catch(); ++ expect(thenHandler).not.toHaveBeenCalled(); ++ ++ GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => loop.quit()); ++ GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING, ++ 'Unhandled promise rejection.*'); ++ loop.run(); ++ GLib.test_assert_expected_messages_internal('Gjs', 'testPromise.js', 0, ++ 'warnsIfRejected'); ++ }); ++ ++ it('do not lead to high-priority IDLE starvation', function () { ++ const promise = new Promise(resolve => { ++ const id = GLib.idle_add(GLib.PRIORITY_HIGH, () => { ++ resolve(); ++ return GLib.SOURCE_REMOVE; ++ }); ++ GLib.Source.set_name_by_id(id, `Test Idle source ${id}`); ++ }); ++ ++ expect(executePromise(promise)).toEqual({ ++ result: PromiseResult.RESOLVED, ++ value: undefined, ++ }); ++ }); ++}); +-- +GitLab + + +From 865d2ae27b853848157215cc60e4888575f13200 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Tue, 22 Feb 2022 04:18:40 +0100 +Subject: [PATCH 2/2] context: Only show a critical error if unregistering an + unknown promise + +In case a promise handled is removed too early or never added we may +have a bug, but not enough critical to crash. +--- + gjs/context.cpp | 12 ++++++++---- + 1 file changed, 8 insertions(+), 4 deletions(-) + +diff --git a/gjs/context.cpp b/gjs/context.cpp +index 0ba6b250a..12eb0921c 100644 +--- a/gjs/context.cpp ++++ b/gjs/context.cpp +@@ -1150,10 +1150,14 @@ void GjsContextPrivate::register_unhandled_promise_rejection( + } + + void GjsContextPrivate::unregister_unhandled_promise_rejection(uint64_t id) { +- // Return value unused in G_DISABLE_ASSERT case +- [[maybe_unused]] 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)); ++ size_t erased = m_unhandled_rejection_stacks.erase(id); ++ if (erased != 1) { ++ g_critical("Promise %" G_GUINT64_FORMAT ++ " Handler attached to rejected promise that wasn't " ++ "previously marked as unhandled or that we wrongly reported " ++ "as unhandled", ++ id); ++ } + } + + void GjsContextPrivate::async_closure_enqueue_for_gc(Gjs::Closure* trampoline) { +-- +GitLab + diff --git a/gjs.spec b/gjs.spec index cd44661..39e78ff 100644 --- a/gjs.spec +++ b/gjs.spec @@ -4,7 +4,7 @@ Name: gjs Version: 1.71.1 -Release: 3%{?dist} +Release: 4%{?dist} Summary: Javascript Bindings for GNOME # The following files contain code from Mozilla which @@ -18,6 +18,8 @@ Source0: https://download.gnome.org/sources/%{name}/1.71/%{name}-%{versio Patch0: gjs-1.71.1-cairo-textextents-test.patch # https://gitlab.gnome.org/GNOME/gjs/-/issues/462 Patch1: gjs-1.71.1-bigint-32bit-test.patch +# https://gitlab.gnome.org/GNOME/gjs/-/merge_requests/729 +Patch2: gjs-1.71.1-promise-rejection.patch BuildRequires: gcc-c++ BuildRequires: meson @@ -101,6 +103,9 @@ sed -i -e "/'Gtk4'/d" installed-tests/js/meson.build %{_datadir}/installed-tests/ %changelog +* Wed Feb 23 2022 David King - 1.71.1-4 +- Add patch to fix AppIndicator extension crash + * Mon Feb 21 2022 David King - 1.71.1-3 - Fix 32-bit GI marshalling test