1 | /* |
2 | * Copyright (C) 2017 Apple Inc. All rights reserved. |
3 | * Copyright (C) 2017 Yusuke Suzuki <utatane.tea@gmail.com> |
4 | * |
5 | * Redistribution and use in source and binary forms, with or without |
6 | * modification, are permitted provided that the following conditions |
7 | * are met: |
8 | * 1. Redistributions of source code must retain the above copyright |
9 | * notice, this list of conditions and the following disclaimer. |
10 | * 2. Redistributions in binary form must reproduce the above copyright |
11 | * notice, this list of conditions and the following disclaimer in the |
12 | * documentation and/or other materials provided with the distribution. |
13 | * |
14 | * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
15 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
16 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
17 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
18 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
19 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
20 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
21 | * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
22 | * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
23 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
24 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
25 | */ |
26 | |
27 | #include "config.h" |
28 | #include "RejectedPromiseTracker.h" |
29 | |
30 | #include "EventNames.h" |
31 | #include "EventTarget.h" |
32 | #include "JSDOMGlobalObject.h" |
33 | #include "JSDOMPromise.h" |
34 | #include "PromiseRejectionEvent.h" |
35 | #include "ScriptExecutionContext.h" |
36 | #include <JavaScriptCore/Exception.h> |
37 | #include <JavaScriptCore/HeapInlines.h> |
38 | #include <JavaScriptCore/JSCJSValueInlines.h> |
39 | #include <JavaScriptCore/JSGlobalObject.h> |
40 | #include <JavaScriptCore/JSPromise.h> |
41 | #include <JavaScriptCore/ScriptCallStack.h> |
42 | #include <JavaScriptCore/ScriptCallStackFactory.h> |
43 | #include <JavaScriptCore/Strong.h> |
44 | #include <JavaScriptCore/StrongInlines.h> |
45 | #include <JavaScriptCore/Weak.h> |
46 | #include <JavaScriptCore/WeakGCMapInlines.h> |
47 | #include <JavaScriptCore/WeakInlines.h> |
48 | |
49 | namespace WebCore { |
50 | using namespace JSC; |
51 | using namespace Inspector; |
52 | |
53 | class UnhandledPromise { |
54 | WTF_MAKE_NONCOPYABLE(UnhandledPromise); |
55 | public: |
56 | UnhandledPromise(JSDOMGlobalObject& globalObject, JSPromise& promise, RefPtr<ScriptCallStack>&& stack) |
57 | : m_promise(DOMPromise::create(globalObject, promise)) |
58 | , m_stack(WTFMove(stack)) |
59 | { |
60 | } |
61 | |
62 | UnhandledPromise(UnhandledPromise&&) = default; |
63 | |
64 | ScriptCallStack* callStack() |
65 | { |
66 | return m_stack.get(); |
67 | } |
68 | |
69 | DOMPromise& promise() |
70 | { |
71 | return m_promise.get(); |
72 | } |
73 | |
74 | private: |
75 | Ref<DOMPromise> m_promise; |
76 | RefPtr<ScriptCallStack> m_stack; |
77 | }; |
78 | |
79 | |
80 | RejectedPromiseTracker::RejectedPromiseTracker(ScriptExecutionContext& context, JSC::VM& vm) |
81 | : m_context(context) |
82 | , m_outstandingRejectedPromises(vm) |
83 | { |
84 | } |
85 | |
86 | RejectedPromiseTracker::~RejectedPromiseTracker() = default; |
87 | |
88 | static RefPtr<ScriptCallStack> createScriptCallStackFromReason(ExecState& state, JSValue reason) |
89 | { |
90 | VM& vm = state.vm(); |
91 | |
92 | // Always capture a stack from the exception if this rejection was an exception. |
93 | if (auto* exception = vm.lastException()) { |
94 | if (exception->value() == reason) |
95 | return createScriptCallStackFromException(&state, exception); |
96 | } |
97 | |
98 | // Otherwise, only capture a stack if a debugger is open. |
99 | if (state.lexicalGlobalObject()->debugger()) |
100 | return createScriptCallStack(&state); |
101 | |
102 | return nullptr; |
103 | } |
104 | |
105 | void RejectedPromiseTracker::promiseRejected(ExecState& state, JSDOMGlobalObject& globalObject, JSPromise& promise) |
106 | { |
107 | // https://html.spec.whatwg.org/multipage/webappapis.html#the-hostpromiserejectiontracker-implementation |
108 | |
109 | JSValue reason = promise.result(state.vm()); |
110 | m_aboutToBeNotifiedRejectedPromises.append(UnhandledPromise { globalObject, promise, createScriptCallStackFromReason(state, reason) }); |
111 | } |
112 | |
113 | void RejectedPromiseTracker::promiseHandled(ExecState&, JSDOMGlobalObject& globalObject, JSPromise& promise) |
114 | { |
115 | // https://html.spec.whatwg.org/multipage/webappapis.html#the-hostpromiserejectiontracker-implementation |
116 | |
117 | bool removed = m_aboutToBeNotifiedRejectedPromises.removeFirstMatching([&] (UnhandledPromise& unhandledPromise) { |
118 | auto& domPromise = unhandledPromise.promise(); |
119 | if (domPromise.isSuspended()) |
120 | return false; |
121 | return domPromise.promise() == &promise; |
122 | }); |
123 | if (removed) |
124 | return; |
125 | |
126 | if (!m_outstandingRejectedPromises.remove(&promise)) |
127 | return; |
128 | |
129 | m_context.postTask([this, rejectedPromise = DOMPromise::create(globalObject, promise)] (ScriptExecutionContext&) mutable { |
130 | reportRejectionHandled(WTFMove(rejectedPromise)); |
131 | }); |
132 | } |
133 | |
134 | void RejectedPromiseTracker::processQueueSoon() |
135 | { |
136 | // https://html.spec.whatwg.org/multipage/webappapis.html#notify-about-rejected-promises |
137 | |
138 | if (m_aboutToBeNotifiedRejectedPromises.isEmpty()) |
139 | return; |
140 | |
141 | Vector<UnhandledPromise> items = WTFMove(m_aboutToBeNotifiedRejectedPromises); |
142 | m_context.postTask([this, items = WTFMove(items)] (ScriptExecutionContext&) mutable { |
143 | reportUnhandledRejections(WTFMove(items)); |
144 | }); |
145 | } |
146 | |
147 | void RejectedPromiseTracker::reportUnhandledRejections(Vector<UnhandledPromise>&& unhandledPromises) |
148 | { |
149 | // https://html.spec.whatwg.org/multipage/webappapis.html#unhandled-promise-rejections |
150 | |
151 | VM& vm = m_context.vm(); |
152 | JSC::JSLockHolder lock(vm); |
153 | |
154 | for (auto& unhandledPromise : unhandledPromises) { |
155 | auto& domPromise = unhandledPromise.promise(); |
156 | if (domPromise.isSuspended()) |
157 | continue; |
158 | auto& state = *domPromise.globalObject()->globalExec(); |
159 | auto& promise = *domPromise.promise(); |
160 | |
161 | if (promise.isHandled(vm)) |
162 | continue; |
163 | |
164 | PromiseRejectionEvent::Init initializer; |
165 | initializer.cancelable = true; |
166 | initializer.promise = &domPromise; |
167 | initializer.reason = promise.result(vm); |
168 | |
169 | auto event = PromiseRejectionEvent::create(eventNames().unhandledrejectionEvent, initializer); |
170 | auto target = m_context.errorEventTarget(); |
171 | target->dispatchEvent(event); |
172 | |
173 | if (!event->defaultPrevented()) |
174 | m_context.reportUnhandledPromiseRejection(state, promise, unhandledPromise.callStack()); |
175 | |
176 | if (!promise.isHandled(vm)) |
177 | m_outstandingRejectedPromises.set(&promise, &promise); |
178 | } |
179 | } |
180 | |
181 | void RejectedPromiseTracker::reportRejectionHandled(Ref<DOMPromise>&& rejectedPromise) |
182 | { |
183 | // https://html.spec.whatwg.org/multipage/webappapis.html#the-hostpromiserejectiontracker-implementation |
184 | |
185 | VM& vm = m_context.vm(); |
186 | JSC::JSLockHolder lock(vm); |
187 | |
188 | if (rejectedPromise->isSuspended()) |
189 | return; |
190 | |
191 | auto& promise = *rejectedPromise->promise(); |
192 | |
193 | PromiseRejectionEvent::Init initializer; |
194 | initializer.promise = rejectedPromise.ptr(); |
195 | initializer.reason = promise.result(vm); |
196 | |
197 | auto event = PromiseRejectionEvent::create(eventNames().rejectionhandledEvent, initializer); |
198 | auto target = m_context.errorEventTarget(); |
199 | target->dispatchEvent(event); |
200 | } |
201 | |
202 | } // namespace WebCore |
203 | |