RemoteInspector.mm   [plain text]


/*
 * Copyright (C) 2013 Apple Inc. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#import "config.h"
#import "RemoteInspector.h"

#if ENABLE(REMOTE_INSPECTOR)

#import "InitializeThreading.h"
#import "RemoteInspectorConstants.h"
#import "RemoteInspectorDebuggable.h"
#import "RemoteInspectorDebuggableConnection.h"
#import <Foundation/Foundation.h>
#import <notify.h>
#import <wtf/Assertions.h>
#import <wtf/NeverDestroyed.h>
#import <wtf/text/WTFString.h>
#import <xpc/xpc.h>

#if PLATFORM(IOS)
#import <wtf/ios/WebCoreThread.h>
#endif

namespace Inspector {

static void dispatchAsyncOnQueueSafeForAnyDebuggable(void (^block)())
{
#if PLATFORM(IOS)
    if (WebCoreWebThreadIsEnabled && WebCoreWebThreadIsEnabled()) {
        WebCoreWebThreadRun(block);
        return;
    }
#endif

    dispatch_async(dispatch_get_main_queue(), block);
}

bool RemoteInspector::startEnabled = true;

void RemoteInspector::startDisabled()
{
    RemoteInspector::startEnabled = false;
}

RemoteInspector& RemoteInspector::shared()
{
    static NeverDestroyed<RemoteInspector> shared;

    static dispatch_once_t once;
    dispatch_once(&once, ^{
        JSC::initializeThreading();
        if (RemoteInspector::startEnabled)
            shared.get().start();
    });

    return shared;
}

RemoteInspector::RemoteInspector()
    : m_xpcQueue(dispatch_queue_create("com.apple.JavaScriptCore.remote-inspector-xpc", DISPATCH_QUEUE_SERIAL))
    , m_nextAvailableIdentifier(1)
    , m_notifyToken(0)
    , m_enabled(false)
    , m_hasActiveDebugSession(false)
    , m_pushScheduled(false)
    , m_parentProcessIdentifier(0)
    , m_shouldSendParentProcessInformation(false)
{
}

unsigned RemoteInspector::nextAvailableIdentifier()
{
    unsigned nextValidIdentifier;
    do {
        nextValidIdentifier = m_nextAvailableIdentifier++;
    } while (!nextValidIdentifier || nextValidIdentifier == std::numeric_limits<unsigned>::max() || m_debuggableMap.contains(nextValidIdentifier));
    return nextValidIdentifier;
}

void RemoteInspector::registerDebuggable(RemoteInspectorDebuggable* debuggable)
{
    std::lock_guard<std::mutex> lock(m_mutex);

    unsigned identifier = nextAvailableIdentifier();
    debuggable->setIdentifier(identifier);

    auto result = m_debuggableMap.set(identifier, std::make_pair(debuggable, debuggable->info()));
    ASSERT_UNUSED(result, result.isNewEntry);

    if (debuggable->remoteDebuggingAllowed())
        pushListingSoon();
}

void RemoteInspector::unregisterDebuggable(RemoteInspectorDebuggable* debuggable)
{
    std::lock_guard<std::mutex> lock(m_mutex);

    unsigned identifier = debuggable->identifier();
    if (!identifier)
        return;

    bool wasRemoved = m_debuggableMap.remove(identifier);
    ASSERT_UNUSED(wasRemoved, wasRemoved);

    if (RefPtr<RemoteInspectorDebuggableConnection> connection = m_connectionMap.take(identifier))
        connection->closeFromDebuggable();

    if (debuggable->remoteDebuggingAllowed())
        pushListingSoon();
}

void RemoteInspector::updateDebuggable(RemoteInspectorDebuggable* debuggable)
{
    std::lock_guard<std::mutex> lock(m_mutex);

    unsigned identifier = debuggable->identifier();
    if (!identifier)
        return;

    auto result = m_debuggableMap.set(identifier, std::make_pair(debuggable, debuggable->info()));
    ASSERT_UNUSED(result, !result.isNewEntry);

    pushListingSoon();
}

void RemoteInspector::sendMessageToRemoteFrontend(unsigned identifier, const String& message)
{
    std::lock_guard<std::mutex> lock(m_mutex);

    if (!m_xpcConnection)
        return;

    RefPtr<RemoteInspectorDebuggableConnection> connection = m_connectionMap.get(identifier);
    if (!connection)
        return;

    NSDictionary *userInfo = @{
        WIRRawDataKey: [static_cast<NSString *>(message) dataUsingEncoding:NSUTF8StringEncoding],
        WIRConnectionIdentifierKey: connection->connectionIdentifier(),
        WIRDestinationKey: connection->destination()
    };

    m_xpcConnection->sendMessage(WIRRawDataMessage, userInfo);
}

void RemoteInspector::setupFailed(unsigned identifier)
{
    std::lock_guard<std::mutex> lock(m_mutex);

    m_connectionMap.remove(identifier);

    updateHasActiveDebugSession();

    pushListingSoon();
}

void RemoteInspector::start()
{
    std::lock_guard<std::mutex> lock(m_mutex);

    if (m_enabled)
        return;

    m_enabled = true;

    notify_register_dispatch(WIRServiceAvailableNotification, &m_notifyToken, m_xpcQueue, ^(int) {
        RemoteInspector::shared().setupXPCConnectionIfNeeded();
    });

    notify_post(WIRServiceAvailabilityCheckNotification);
}

void RemoteInspector::stop()
{
    std::lock_guard<std::mutex> lock(m_mutex);

    stopInternal(StopSource::API);
}

void RemoteInspector::stopInternal(StopSource source)
{
    if (!m_enabled)
        return;

    m_enabled = false;

    m_pushScheduled = false;

    for (auto it = m_connectionMap.begin(), end = m_connectionMap.end(); it != end; ++it)
        it->value->close();
    m_connectionMap.clear();

    updateHasActiveDebugSession();

    if (m_xpcConnection) {
        switch (source) {
        case StopSource::API:
            m_xpcConnection->close();
            break;
        case StopSource::XPCMessage:
            m_xpcConnection->closeFromMessage();
            break;
        }

        m_xpcConnection = nullptr;
    }

    notify_cancel(m_notifyToken);
}

void RemoteInspector::setupXPCConnectionIfNeeded()
{
    std::lock_guard<std::mutex> lock(m_mutex);

    if (m_xpcConnection)
        return;

    xpc_connection_t connection = xpc_connection_create_mach_service(WIRXPCMachPortName, m_xpcQueue, 0);
    if (!connection)
        return;

    m_xpcConnection = adoptRef(new RemoteInspectorXPCConnection(connection, m_xpcQueue, this));
    m_xpcConnection->sendMessage(@"syn", nil); // Send a simple message to initialize the XPC connection.
    xpc_release(connection);

    pushListingSoon();
}

#pragma mark - Proxy Application Information

void RemoteInspector::setParentProcessInformation(pid_t pid, RetainPtr<CFDataRef> auditData)
{
    std::lock_guard<std::mutex> lock(m_mutex);

    if (m_parentProcessIdentifier || m_parentProcessAuditData)
        return;

    m_parentProcessIdentifier = pid;
    m_parentProcessAuditData = auditData;

    if (m_shouldSendParentProcessInformation)
        receivedProxyApplicationSetupMessage(nil);
}

#pragma mark - RemoteInspectorXPCConnection::Client

void RemoteInspector::xpcConnectionReceivedMessage(RemoteInspectorXPCConnection*, NSString *messageName, NSDictionary *userInfo)
{
    std::lock_guard<std::mutex> lock(m_mutex);

    if ([messageName isEqualToString:WIRPermissionDenied]) {
        stopInternal(StopSource::XPCMessage);
        return;
    }

    if ([messageName isEqualToString:WIRSocketDataMessage])
        receivedDataMessage(userInfo);
    else if ([messageName isEqualToString:WIRSocketSetupMessage])
        receivedSetupMessage(userInfo);
    else if ([messageName isEqualToString:WIRWebPageCloseMessage])
        receivedDidCloseMessage(userInfo);
    else if ([messageName isEqualToString:WIRApplicationGetListingMessage])
        receivedGetListingMessage(userInfo);
    else if ([messageName isEqualToString:WIRIndicateMessage])
        receivedIndicateMessage(userInfo);
    else if ([messageName isEqualToString:WIRProxyApplicationSetupMessage])
        receivedProxyApplicationSetupMessage(userInfo);
    else if ([messageName isEqualToString:WIRConnectionDiedMessage])
        receivedConnectionDiedMessage(userInfo);
    else
        NSLog(@"Unrecognized RemoteInspector XPC Message: %@", messageName);
}

void RemoteInspector::xpcConnectionFailed(RemoteInspectorXPCConnection* connection)
{
    std::lock_guard<std::mutex> lock(m_mutex);

    ASSERT(connection == m_xpcConnection);
    if (connection != m_xpcConnection)
        return;

    m_pushScheduled = false;

    for (auto it = m_connectionMap.begin(), end = m_connectionMap.end(); it != end; ++it)
        it->value->close();
    m_connectionMap.clear();

    updateHasActiveDebugSession();

    // The connection will close itself.
    m_xpcConnection = nullptr;
}

void RemoteInspector::xpcConnectionUnhandledMessage(RemoteInspectorXPCConnection*, xpc_object_t)
{
    // Intentionally ignored.
}

#pragma mark - Listings

NSDictionary *RemoteInspector::listingForDebuggable(const RemoteInspectorDebuggableInfo& debuggableInfo) const
{
    NSMutableDictionary *debuggableDetails = [NSMutableDictionary dictionary];

    [debuggableDetails setObject:@(debuggableInfo.identifier) forKey:WIRPageIdentifierKey];

    switch (debuggableInfo.type) {
    case RemoteInspectorDebuggable::JavaScript: {
        NSString *name = debuggableInfo.name;
        [debuggableDetails setObject:name forKey:WIRTitleKey];
        [debuggableDetails setObject:WIRTypeJavaScript forKey:WIRTypeKey];
        break;
    }
    case RemoteInspectorDebuggable::Web: {
        NSString *url = debuggableInfo.url;
        NSString *title = debuggableInfo.name;
        [debuggableDetails setObject:url forKey:WIRURLKey];
        [debuggableDetails setObject:title forKey:WIRTitleKey];
        [debuggableDetails setObject:WIRTypeWeb forKey:WIRTypeKey];
        break;
    }
    default:
        ASSERT_NOT_REACHED();
        break;
    }

    if (RefPtr<RemoteInspectorDebuggableConnection> connection = m_connectionMap.get(debuggableInfo.identifier))
        [debuggableDetails setObject:connection->connectionIdentifier() forKey:WIRConnectionIdentifierKey];

    if (debuggableInfo.hasLocalDebugger)
        [debuggableDetails setObject:@YES forKey:WIRHasLocalDebuggerKey];

    return debuggableDetails;
}

void RemoteInspector::pushListingNow()
{
    ASSERT(m_xpcConnection);
    if (!m_xpcConnection)
        return;

    m_pushScheduled = false;

    RetainPtr<NSMutableDictionary> response = adoptNS([[NSMutableDictionary alloc] init]);
    for (auto it = m_debuggableMap.begin(), end = m_debuggableMap.end(); it != end; ++it) {
        const RemoteInspectorDebuggableInfo& debuggableInfo = it->value.second;
        if (debuggableInfo.remoteDebuggingAllowed) {
            NSDictionary *details = listingForDebuggable(debuggableInfo);
            [response setObject:details forKey:[NSString stringWithFormat:@"%u", debuggableInfo.identifier]];
        }
    }

    RetainPtr<NSMutableDictionary> outgoing = adoptNS([[NSMutableDictionary alloc] init]);
    [outgoing setObject:response.get() forKey:WIRListingKey];

    m_xpcConnection->sendMessage(WIRListingMessage, outgoing.get());
}

void RemoteInspector::pushListingSoon()
{
    if (!m_xpcConnection)
        return;

    if (m_pushScheduled)
        return;

    m_pushScheduled = true;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.2 * NSEC_PER_SEC), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        std::lock_guard<std::mutex> lock(m_mutex);
        if (m_pushScheduled)
            pushListingNow();
    });
}

#pragma mark - Active Debugger Sessions

void RemoteInspector::updateHasActiveDebugSession()
{
    bool hasActiveDebuggerSession = !m_connectionMap.isEmpty();
    if (hasActiveDebuggerSession == m_hasActiveDebugSession)
        return;

    m_hasActiveDebugSession = hasActiveDebuggerSession;

    // FIXME: Expose some way to access this state in an embedder.
    // Legacy iOS WebKit 1 had a notification. This will need to be smarter with WebKit2.
}

#pragma mark - Received XPC Messages

void RemoteInspector::receivedSetupMessage(NSDictionary *userInfo)
{
    NSNumber *pageId = [userInfo objectForKey:WIRPageIdentifierKey];
    if (!pageId)
        return;

    NSString *connectionIdentifier = [userInfo objectForKey:WIRConnectionIdentifierKey];
    if (!connectionIdentifier)
        return;

    NSString *sender = [userInfo objectForKey:WIRSenderKey];
    if (!sender)
        return;

    unsigned identifier = [pageId unsignedIntValue];
    if (m_connectionMap.contains(identifier))
        return;

    auto it = m_debuggableMap.find(identifier);
    if (it == m_debuggableMap.end())
        return;

    // Attempt to create a connection. This may fail if the page already has an inspector or if it disallows inspection.
    RemoteInspectorDebuggable* debuggable = it->value.first;
    RemoteInspectorDebuggableInfo debuggableInfo = it->value.second;
    RefPtr<RemoteInspectorDebuggableConnection> connection = adoptRef(new RemoteInspectorDebuggableConnection(debuggable, connectionIdentifier, sender, debuggableInfo.type));
    if (!connection->setup()) {
        connection->close();
        return;
    }

    m_connectionMap.set(identifier, connection.release());

    updateHasActiveDebugSession();

    pushListingSoon();
}

void RemoteInspector::receivedDataMessage(NSDictionary *userInfo)
{
    NSNumber *pageId = [userInfo objectForKey:WIRPageIdentifierKey];
    if (!pageId)
        return;

    unsigned pageIdentifier = [pageId unsignedIntValue];
    RefPtr<RemoteInspectorDebuggableConnection> connection = m_connectionMap.get(pageIdentifier);
    if (!connection)
        return;

    NSData *data = [userInfo objectForKey:WIRSocketDataKey];
    RetainPtr<NSString> message = adoptNS([[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
    connection->sendMessageToBackend(message.get());
}

void RemoteInspector::receivedDidCloseMessage(NSDictionary *userInfo)
{
    NSNumber *pageId = [userInfo objectForKey:WIRPageIdentifierKey];
    if (!pageId)
        return;

    NSString *connectionIdentifier = [userInfo objectForKey:WIRConnectionIdentifierKey];
    if (!connectionIdentifier)
        return;

    unsigned identifier = [pageId unsignedIntValue];
    RefPtr<RemoteInspectorDebuggableConnection> connection = m_connectionMap.get(identifier);
    if (!connection)
        return;

    if (![connectionIdentifier isEqualToString:connection->connectionIdentifier()])
        return;

    connection->close();
    m_connectionMap.remove(identifier);

    updateHasActiveDebugSession();

    pushListingSoon();
}

void RemoteInspector::receivedGetListingMessage(NSDictionary *)
{
    pushListingNow();
}

void RemoteInspector::receivedIndicateMessage(NSDictionary *userInfo)
{
    NSNumber *pageId = [userInfo objectForKey:WIRPageIdentifierKey];
    if (!pageId)
        return;

    unsigned identifier = [pageId unsignedIntValue];
    BOOL indicateEnabled = [[userInfo objectForKey:WIRIndicateEnabledKey] boolValue];

    dispatchAsyncOnQueueSafeForAnyDebuggable(^{
        RemoteInspectorDebuggable* debuggable = nullptr;
        {
            std::lock_guard<std::mutex> lock(m_mutex);

            auto it = m_debuggableMap.find(identifier);
            if (it == m_debuggableMap.end())
                return;

            debuggable = it->value.first;
        }
        debuggable->setIndicating(indicateEnabled);
    });
}

void RemoteInspector::receivedProxyApplicationSetupMessage(NSDictionary *)
{
    ASSERT(m_xpcConnection);
    if (!m_xpcConnection)
        return;

    if (!m_parentProcessIdentifier || !m_parentProcessAuditData) {
        // We are a proxy application without parent process information.
        // Wait a bit for the information, but give up after a second.
        m_shouldSendParentProcessInformation = true;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            std::lock_guard<std::mutex> lock(m_mutex);
            if (m_shouldSendParentProcessInformation)
                stopInternal(StopSource::XPCMessage);
        });
        return;
    }

    m_shouldSendParentProcessInformation = false;

    m_xpcConnection->sendMessage(WIRProxyApplicationSetupResponseMessage, @{
        WIRProxyApplicationParentPIDKey: @(m_parentProcessIdentifier),
        WIRProxyApplicationParentAuditDataKey: (NSData *)m_parentProcessAuditData.get(),
    });
}

void RemoteInspector::receivedConnectionDiedMessage(NSDictionary *userInfo)
{
    NSString *connectionIdentifier = [userInfo objectForKey:WIRConnectionIdentifierKey];
    if (!connectionIdentifier)
        return;

    auto it = m_connectionMap.begin();
    auto end = m_connectionMap.end();
    for (; it != end; ++it) {
        if ([connectionIdentifier isEqualToString:it->value->connectionIdentifier()])
            break;
    }

    if (it == end)
        return;

    RefPtr<RemoteInspectorDebuggableConnection> connection = it->value;
    connection->close();
    m_connectionMap.remove(it);

    updateHasActiveDebugSession();
}

} // namespace Inspector

#endif // ENABLE(REMOTE_INSPECTOR)