DefaultSharedWorkerRepository.cpp revision 2bde8e466a4451c7319e3a072d118917957d6554
1/*
2 * Copyright (C) 2009 Google Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
7 *
8 *     * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 *     * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 *     * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31#include "config.h"
32
33#if ENABLE(SHARED_WORKERS)
34
35#include "DefaultSharedWorkerRepository.h"
36
37#include "ActiveDOMObject.h"
38#include "CrossThreadTask.h"
39#include "Document.h"
40#include "InspectorInstrumentation.h"
41#include "MessageEvent.h"
42#include "MessagePort.h"
43#include "NotImplemented.h"
44#include "PlatformString.h"
45#include "ScriptCallStack.h"
46#include "SecurityOrigin.h"
47#include "SecurityOriginHash.h"
48#include "SharedWorker.h"
49#include "SharedWorkerContext.h"
50#include "SharedWorkerRepository.h"
51#include "SharedWorkerThread.h"
52#include "WorkerLoaderProxy.h"
53#include "WorkerReportingProxy.h"
54#include "WorkerScriptLoader.h"
55#include "WorkerScriptLoaderClient.h"
56#include <wtf/HashSet.h>
57#include <wtf/Threading.h>
58
59namespace WebCore {
60
61class SharedWorkerProxy : public ThreadSafeRefCounted<SharedWorkerProxy>, public WorkerLoaderProxy, public WorkerReportingProxy {
62public:
63    static PassRefPtr<SharedWorkerProxy> create(const String& name, const KURL& url, PassRefPtr<SecurityOrigin> origin) { return adoptRef(new SharedWorkerProxy(name, url, origin)); }
64
65    void setThread(PassRefPtr<SharedWorkerThread> thread) { m_thread = thread; }
66    SharedWorkerThread* thread() { return m_thread.get(); }
67    bool isClosing() const { return m_closing; }
68    KURL url() const
69    {
70        // Don't use m_url.copy() because it isn't a threadsafe method.
71        return KURL(ParsedURLString, m_url.string().threadsafeCopy());
72    }
73
74    String name() const { return m_name.threadsafeCopy(); }
75    bool matches(const String& name, PassRefPtr<SecurityOrigin> origin, const KURL& urlToMatch) const;
76
77    // WorkerLoaderProxy
78    virtual void postTaskToLoader(PassOwnPtr<ScriptExecutionContext::Task>);
79    virtual void postTaskForModeToWorkerContext(PassOwnPtr<ScriptExecutionContext::Task>, const String&);
80
81    // WorkerReportingProxy
82    virtual void postExceptionToWorkerObject(const String& errorMessage, int lineNumber, const String& sourceURL);
83    virtual void postConsoleMessageToWorkerObject(MessageSource, MessageType, MessageLevel, const String& message, int lineNumber, const String& sourceURL);
84    virtual void workerContextClosed();
85    virtual void workerContextDestroyed();
86
87    // Updates the list of the worker's documents, per section 4.5 of the WebWorkers spec.
88    void addToWorkerDocuments(ScriptExecutionContext*);
89
90    bool isInWorkerDocuments(Document* document) { return m_workerDocuments.contains(document); }
91
92    // Removes a detached document from the list of worker's documents. May set the closing flag if this is the last document in the list.
93    void documentDetached(Document*);
94
95private:
96    SharedWorkerProxy(const String& name, const KURL&, PassRefPtr<SecurityOrigin>);
97    void close();
98
99    bool m_closing;
100    String m_name;
101    KURL m_url;
102    // The thread is freed when the proxy is destroyed, so we need to make sure that the proxy stays around until the SharedWorkerContext exits.
103    RefPtr<SharedWorkerThread> m_thread;
104    RefPtr<SecurityOrigin> m_origin;
105    HashSet<Document*> m_workerDocuments;
106    // Ensures exclusive access to the worker documents. Must not grab any other locks (such as the DefaultSharedWorkerRepository lock) while holding this one.
107    Mutex m_workerDocumentsLock;
108};
109
110SharedWorkerProxy::SharedWorkerProxy(const String& name, const KURL& url, PassRefPtr<SecurityOrigin> origin)
111    : m_closing(false)
112    , m_name(name.crossThreadString())
113    , m_url(url.copy())
114    , m_origin(origin)
115{
116    // We should be the sole owner of the SecurityOrigin, as we will free it on another thread.
117    ASSERT(m_origin->hasOneRef());
118}
119
120bool SharedWorkerProxy::matches(const String& name, PassRefPtr<SecurityOrigin> origin, const KURL& urlToMatch) const
121{
122    // If the origins don't match, or the names don't match, then this is not the proxy we are looking for.
123    if (!origin->equal(m_origin.get()))
124        return false;
125
126    // If the names are both empty, compares the URLs instead per the Web Workers spec.
127    if (name.isEmpty() && m_name.isEmpty())
128        return urlToMatch == url();
129
130    return name == m_name;
131}
132
133void SharedWorkerProxy::postTaskToLoader(PassOwnPtr<ScriptExecutionContext::Task> task)
134{
135    MutexLocker lock(m_workerDocumentsLock);
136
137    if (isClosing())
138        return;
139
140    // If we aren't closing, then we must have at least one document.
141    ASSERT(m_workerDocuments.size());
142
143    // Just pick an arbitrary active document from the HashSet and pass load requests to it.
144    // FIXME: Do we need to deal with the case where the user closes the document mid-load, via a shadow document or some other solution?
145    Document* document = *(m_workerDocuments.begin());
146    document->postTask(task);
147}
148
149void SharedWorkerProxy::postTaskForModeToWorkerContext(PassOwnPtr<ScriptExecutionContext::Task> task, const String& mode)
150{
151    if (isClosing())
152        return;
153    ASSERT(m_thread);
154    m_thread->runLoop().postTaskForMode(task, mode);
155}
156
157static void postExceptionTask(ScriptExecutionContext* context, const String& errorMessage, int lineNumber, const String& sourceURL)
158{
159    context->reportException(errorMessage, lineNumber, sourceURL, 0);
160}
161
162void SharedWorkerProxy::postExceptionToWorkerObject(const String& errorMessage, int lineNumber, const String& sourceURL)
163{
164    MutexLocker lock(m_workerDocumentsLock);
165    for (HashSet<Document*>::iterator iter = m_workerDocuments.begin(); iter != m_workerDocuments.end(); ++iter)
166        (*iter)->postTask(createCallbackTask(&postExceptionTask, errorMessage, lineNumber, sourceURL));
167}
168
169static void postConsoleMessageTask(ScriptExecutionContext* document, MessageSource source, MessageType type, MessageLevel level, const String& message, unsigned lineNumber, const String& sourceURL)
170{
171    document->addMessage(source, type, level, message, lineNumber, sourceURL, 0);
172}
173
174void SharedWorkerProxy::postConsoleMessageToWorkerObject(MessageSource source, MessageType type, MessageLevel level, const String& message, int lineNumber, const String& sourceURL)
175{
176    MutexLocker lock(m_workerDocumentsLock);
177    for (HashSet<Document*>::iterator iter = m_workerDocuments.begin(); iter != m_workerDocuments.end(); ++iter)
178        (*iter)->postTask(createCallbackTask(&postConsoleMessageTask, source, type, level, message, lineNumber, sourceURL));
179}
180
181void SharedWorkerProxy::workerContextClosed()
182{
183    if (isClosing())
184        return;
185    close();
186}
187
188void SharedWorkerProxy::workerContextDestroyed()
189{
190    // The proxy may be freed by this call, so do not reference it any further.
191    DefaultSharedWorkerRepository::instance().removeProxy(this);
192}
193
194void SharedWorkerProxy::addToWorkerDocuments(ScriptExecutionContext* context)
195{
196    // Nested workers are not yet supported, so passed-in context should always be a Document.
197    ASSERT(context->isDocument());
198    ASSERT(!isClosing());
199    MutexLocker lock(m_workerDocumentsLock);
200    Document* document = static_cast<Document*>(context);
201    m_workerDocuments.add(document);
202}
203
204void SharedWorkerProxy::documentDetached(Document* document)
205{
206    if (isClosing())
207        return;
208    // Remove the document from our set (if it's there) and if that was the last document in the set, mark the proxy as closed.
209    MutexLocker lock(m_workerDocumentsLock);
210    m_workerDocuments.remove(document);
211    if (!m_workerDocuments.size())
212        close();
213}
214
215void SharedWorkerProxy::close()
216{
217    ASSERT(!isClosing());
218    m_closing = true;
219    // Stop the worker thread - the proxy will stay around until we get workerThreadExited() notification.
220    if (m_thread)
221        m_thread->stop();
222}
223
224class SharedWorkerConnectTask : public ScriptExecutionContext::Task {
225public:
226    static PassOwnPtr<SharedWorkerConnectTask> create(PassOwnPtr<MessagePortChannel> channel)
227    {
228        return new SharedWorkerConnectTask(channel);
229    }
230
231private:
232    SharedWorkerConnectTask(PassOwnPtr<MessagePortChannel> channel)
233        : m_channel(channel)
234    {
235    }
236
237    virtual void performTask(ScriptExecutionContext* scriptContext)
238    {
239        RefPtr<MessagePort> port = MessagePort::create(*scriptContext);
240        port->entangle(m_channel.release());
241        ASSERT(scriptContext->isWorkerContext());
242        WorkerContext* workerContext = static_cast<WorkerContext*>(scriptContext);
243        // Since close() stops the thread event loop, this should not ever get called while closing.
244        ASSERT(!workerContext->isClosing());
245        ASSERT(workerContext->isSharedWorkerContext());
246        workerContext->toSharedWorkerContext()->dispatchEvent(createConnectEvent(port));
247    }
248
249    OwnPtr<MessagePortChannel> m_channel;
250};
251
252// Loads the script on behalf of a worker.
253class SharedWorkerScriptLoader : public RefCounted<SharedWorkerScriptLoader>, private WorkerScriptLoaderClient {
254public:
255    SharedWorkerScriptLoader(PassRefPtr<SharedWorker>, PassOwnPtr<MessagePortChannel>, PassRefPtr<SharedWorkerProxy>);
256    void load(const KURL&);
257
258private:
259    // WorkerScriptLoaderClient callback
260    virtual void notifyFinished();
261
262    RefPtr<SharedWorker> m_worker;
263    OwnPtr<MessagePortChannel> m_port;
264    RefPtr<SharedWorkerProxy> m_proxy;
265    OwnPtr<WorkerScriptLoader> m_scriptLoader;
266};
267
268SharedWorkerScriptLoader::SharedWorkerScriptLoader(PassRefPtr<SharedWorker> worker, PassOwnPtr<MessagePortChannel> port, PassRefPtr<SharedWorkerProxy> proxy)
269    : m_worker(worker)
270    , m_port(port)
271    , m_proxy(proxy)
272{
273}
274
275void SharedWorkerScriptLoader::load(const KURL& url)
276{
277    // Mark this object as active for the duration of the load.
278    m_scriptLoader = new WorkerScriptLoader(ResourceRequestBase::TargetIsSharedWorker);
279    m_scriptLoader->loadAsynchronously(m_worker->scriptExecutionContext(), url, DenyCrossOriginRequests, this);
280
281    // Stay alive (and keep the SharedWorker and JS wrapper alive) until the load finishes.
282    this->ref();
283    m_worker->setPendingActivity(m_worker.get());
284}
285
286void SharedWorkerScriptLoader::notifyFinished()
287{
288    // FIXME: This method is not guaranteed to be invoked if we are loading from WorkerContext (see comment for WorkerScriptLoaderClient::notifyFinished()).
289    // We need to address this before supporting nested workers.
290
291    // Hand off the just-loaded code to the repository to start up the worker thread.
292    if (m_scriptLoader->failed())
293        m_worker->dispatchEvent(Event::create(eventNames().errorEvent, false, true));
294    else {
295        InspectorInstrumentation::scriptImported(m_worker->scriptExecutionContext(), m_scriptLoader->identifier(), m_scriptLoader->script());
296        DefaultSharedWorkerRepository::instance().workerScriptLoaded(*m_proxy, m_worker->scriptExecutionContext()->userAgent(m_scriptLoader->url()), m_scriptLoader->script(), m_port.release());
297    }
298    m_worker->unsetPendingActivity(m_worker.get());
299    this->deref(); // This frees this object - must be the last action in this function.
300}
301
302DefaultSharedWorkerRepository& DefaultSharedWorkerRepository::instance()
303{
304    AtomicallyInitializedStatic(DefaultSharedWorkerRepository*, instance = new DefaultSharedWorkerRepository);
305    return *instance;
306}
307
308void DefaultSharedWorkerRepository::workerScriptLoaded(SharedWorkerProxy& proxy, const String& userAgent, const String& workerScript, PassOwnPtr<MessagePortChannel> port)
309{
310    MutexLocker lock(m_lock);
311    if (proxy.isClosing())
312        return;
313
314    // Another loader may have already started up a thread for this proxy - if so, just send a connect to the pre-existing thread.
315    if (!proxy.thread()) {
316        RefPtr<SharedWorkerThread> thread = SharedWorkerThread::create(proxy.name(), proxy.url(), userAgent, workerScript, proxy, proxy);
317        proxy.setThread(thread);
318        thread->start();
319    }
320    proxy.thread()->runLoop().postTask(SharedWorkerConnectTask::create(port));
321}
322
323bool SharedWorkerRepository::isAvailable()
324{
325    // SharedWorkers are enabled on the default WebKit platform.
326    return true;
327}
328
329void SharedWorkerRepository::connect(PassRefPtr<SharedWorker> worker, PassOwnPtr<MessagePortChannel> port, const KURL& url, const String& name, ExceptionCode& ec)
330{
331    DefaultSharedWorkerRepository::instance().connectToWorker(worker, port, url, name, ec);
332}
333
334void SharedWorkerRepository::documentDetached(Document* document)
335{
336    DefaultSharedWorkerRepository::instance().documentDetached(document);
337}
338
339bool SharedWorkerRepository::hasSharedWorkers(Document* document)
340{
341    return DefaultSharedWorkerRepository::instance().hasSharedWorkers(document);
342}
343
344bool DefaultSharedWorkerRepository::hasSharedWorkers(Document* document)
345{
346    MutexLocker lock(m_lock);
347    for (unsigned i = 0; i < m_proxies.size(); i++) {
348        if (m_proxies[i]->isInWorkerDocuments(document))
349            return true;
350    }
351    return false;
352}
353
354void DefaultSharedWorkerRepository::removeProxy(SharedWorkerProxy* proxy)
355{
356    MutexLocker lock(m_lock);
357    for (unsigned i = 0; i < m_proxies.size(); i++) {
358        if (proxy == m_proxies[i].get()) {
359            m_proxies.remove(i);
360            return;
361        }
362    }
363}
364
365void DefaultSharedWorkerRepository::documentDetached(Document* document)
366{
367    MutexLocker lock(m_lock);
368    for (unsigned i = 0; i < m_proxies.size(); i++)
369        m_proxies[i]->documentDetached(document);
370}
371
372void DefaultSharedWorkerRepository::connectToWorker(PassRefPtr<SharedWorker> worker, PassOwnPtr<MessagePortChannel> port, const KURL& url, const String& name, ExceptionCode& ec)
373{
374    MutexLocker lock(m_lock);
375    ASSERT(worker->scriptExecutionContext()->securityOrigin()->canAccess(SecurityOrigin::create(url).get()));
376    // Fetch a proxy corresponding to this SharedWorker.
377    RefPtr<SharedWorkerProxy> proxy = getProxy(name, url);
378    proxy->addToWorkerDocuments(worker->scriptExecutionContext());
379    if (proxy->url() != url) {
380        // Proxy already existed under alternate URL - return an error.
381        ec = URL_MISMATCH_ERR;
382        return;
383    }
384    // If proxy is already running, just connect to it - otherwise, kick off a loader to load the script.
385    if (proxy->thread())
386        proxy->thread()->runLoop().postTask(SharedWorkerConnectTask::create(port));
387    else {
388        RefPtr<SharedWorkerScriptLoader> loader = adoptRef(new SharedWorkerScriptLoader(worker, port, proxy.release()));
389        loader->load(url);
390    }
391}
392
393// Creates a new SharedWorkerProxy or returns an existing one from the repository. Must only be called while the repository mutex is held.
394PassRefPtr<SharedWorkerProxy> DefaultSharedWorkerRepository::getProxy(const String& name, const KURL& url)
395{
396    // Look for an existing worker, and create one if it doesn't exist.
397    // Items in the cache are freed on another thread, so do a threadsafe copy of the URL before creating the origin,
398    // to make sure no references to external strings linger.
399    RefPtr<SecurityOrigin> origin = SecurityOrigin::create(KURL(ParsedURLString, url.string().threadsafeCopy()));
400    for (unsigned i = 0; i < m_proxies.size(); i++) {
401        if (!m_proxies[i]->isClosing() && m_proxies[i]->matches(name, origin, url))
402            return m_proxies[i];
403    }
404    // Proxy is not in the repository currently - create a new one.
405    RefPtr<SharedWorkerProxy> proxy = SharedWorkerProxy::create(name, url, origin.release());
406    m_proxies.append(proxy);
407    return proxy.release();
408}
409
410DefaultSharedWorkerRepository::DefaultSharedWorkerRepository()
411{
412}
413
414DefaultSharedWorkerRepository::~DefaultSharedWorkerRepository()
415{
416}
417
418} // namespace WebCore
419
420#endif // ENABLE(SHARED_WORKERS)
421