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
49namespace WebCore {
50using namespace JSC;
51using namespace Inspector;
52
53class UnhandledPromise {
54 WTF_MAKE_NONCOPYABLE(UnhandledPromise);
55public:
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
74private:
75 Ref<DOMPromise> m_promise;
76 RefPtr<ScriptCallStack> m_stack;
77};
78
79
80RejectedPromiseTracker::RejectedPromiseTracker(ScriptExecutionContext& context, JSC::VM& vm)
81 : m_context(context)
82 , m_outstandingRejectedPromises(vm)
83{
84}
85
86RejectedPromiseTracker::~RejectedPromiseTracker() = default;
87
88static 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
105void 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
113void 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
134void 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
147void 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
181void 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