/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ #include "pch.h" #include "strutils.h" #include "Client.h" using namespace winrt::Windows::Data::Json; using namespace winrt::Windows::Foundation; using namespace winrt::Windows::Networking; using namespace winrt::Windows::Storage::Streams; namespace winrt::servo { void DevtoolsClient::Stop() { if (mReceiving && mReceiveOp.has_value() && mReceiveOp->Status() != AsyncStatus::Completed) { mReceiveOp->Cancel(); } } void DevtoolsClient::Run() { if (mReceiving) { throw hresult_error(E_FAIL, L"Already running"); } mReceiving = true; auto socket = Sockets::StreamSocket(); auto hostname = HostName(mHostname); auto connecting = socket.ConnectAsync(hostname, mPort); connecting.Completed([=](const auto &, const auto &) { mDataReader = DataReader(socket.InputStream()); mDataWriter = DataWriter(socket.OutputStream()); mReceiveOp = {Loop()}; mReceiveOp->Completed([=](const auto &, const auto &) { mReceiveOp = {}; mDataReader->DetachStream(); mDataWriter->DetachStream(); mReceiving = false; mDelegate.OnDevtoolsDetached(); }); }); } IAsyncAction DevtoolsClient::Loop() { auto cancellation = co_await winrt::get_cancellation_token(); cancellation.callback([=] { if (mReaderOp.Status() != AsyncStatus::Completed) { mReaderOp.Cancel(); } }); while (!cancellation()) { unsigned int len = 0; while (!cancellation()) { mReaderOp = mDataReader->LoadAsync(1); co_await mReaderOp; hstring c = mDataReader->ReadString(1); if (c == L":") break; try { unsigned int digit = std::stoi(c.c_str()); len = 10 * len + digit; } catch (...) { throw hresult_error(E_FAIL, L"Can't parse message header:" + c); } if (len >= 10000) { throw hresult_error(E_FAIL, L"Message length too long"); } } if (cancellation()) { break; } hstring request = L""; mReaderOp = mDataReader->LoadAsync(len); auto bytesLoaded = co_await mReaderOp; request = request + mDataReader->ReadString(bytesLoaded); JsonObject json; if (!JsonObject::TryParse(request, json)) { throw hresult_error(E_FAIL, L"Can't parse message: " + request); } HandleMessage(json); } } void DevtoolsClient::HandleMessage(JsonObject obj) { // Basic devtools protocol implementation: // https://docs.firefox-dev.tools/backend/protocol.html if (obj.HasKey(L"from") && obj.GetNamedString(L"from") == L"root") { if (obj.HasKey(L"applicationType")) { // First message. Ask for the current tab JsonObject out; out.Insert(L"to", JsonValue::CreateStringValue(L"root")); out.Insert(L"type", JsonValue::CreateStringValue(L"getTab")); Send(out); return; } else if (obj.HasKey(L"tab")) { // Got the current tab. auto tab = obj.GetNamedObject(L"tab"); if (tab.HasKey(L"actor")) { // Attach to tab, and ask for cached messaged JsonObject msg1; msg1.Insert(L"to", tab.GetNamedValue(L"actor")); msg1.Insert(L"type", JsonValue::CreateStringValue(L"attach")); Send(msg1); JsonObject msg2; msg2.Insert(L"to", tab.GetNamedValue(L"consoleActor")); msg2.Insert(L"type", JsonValue::CreateStringValue(L"getCachedMessages")); JsonArray types; types.Append(JsonValue::CreateStringValue(L"PageError")); types.Append(JsonValue::CreateStringValue(L"ConsoleAPI")); msg2.Insert(L"messageTypes", types); Send(msg2); return; } } } else if (obj.HasKey(L"type")) { // Not from root if (obj.GetNamedString(L"type") == L"pageError") { // Got a page error HandlePageError(obj.GetNamedObject(L"pageError")); return; } else if (obj.GetNamedString(L"type") == L"consoleAPICall") { // console.* calls auto message = obj.GetNamedObject(L"message"); HandleConsoleMessage(message); return; } else if (obj.GetNamedString(L"type") == L"tabAttached") { // Ignore return; } else if (obj.GetNamedString(L"type") == L"networkEvent") { // Ignore return; } else if (obj.GetNamedString(L"type") == L"tabNavigated") { if (obj.HasKey(L"state") && obj.GetNamedString(L"state") == L"stop") { mDelegate.ClearConsole(); } return; } else if (obj.GetNamedString(L"type") == L"networkEventUpdate") { // FIXME: log if there is a non-200 HTTP response return; } } else if (obj.HasKey(L"messages")) { // Response to getCachedMessages for (auto messageValue : obj.GetNamedArray(L"messages")) { auto message = messageValue.GetObject(); if (message.GetNamedString(L"_type") == L"ConsoleAPI") { HandleConsoleMessage(message); } else if (message.GetNamedString(L"_type") == L"PageError") { HandlePageError(message); } else { HandleNonHandledMessage(message); } } return; } HandleNonHandledMessage(obj); } DevtoolsMessageLevel DevtoolsClient::ParseLevel(JsonObject message) { if (message.GetNamedBoolean(L"error", false)) { return DevtoolsMessageLevel::Error; } if (message.GetNamedBoolean(L"warning", false)) { return DevtoolsMessageLevel::Warn; } if (message.GetNamedBoolean(L"exception", false)) { return DevtoolsMessageLevel::Error; } auto level = message.GetNamedString(L"level", L""); if (level == L"warn") { return DevtoolsMessageLevel::Warn; } else if (level == L"error") { return DevtoolsMessageLevel::Error; } return DevtoolsMessageLevel::None; } hstring DevtoolsClient::ParseSource(JsonObject message) { auto source = message.GetNamedString(L"filename", L"<>"); if (message.HasKey(L"lineNumber")) { source = source + L":" + to_hstring(message.GetNamedNumber(L"lineNumber")); } if (message.HasKey(L"columnNumber")) { source = source + L":" + to_hstring(message.GetNamedNumber(L"columnNumber")); } return source; } void DevtoolsClient::HandlePageError(JsonObject message) { auto source = ParseSource(message); auto body = message.GetNamedString(L"errorMessage", L""); auto level = ParseLevel(message); mDelegate.OnDevtoolsMessage(level, source, body); } void DevtoolsClient::HandleConsoleMessage(JsonObject message) { auto source = ParseSource(message); auto level = ParseLevel(message); hstring body = L""; for (auto arg : message.GetNamedArray(L"arguments")) { body = body + arg.Stringify(); } mDelegate.OnDevtoolsMessage(level, source, body); } void DevtoolsClient::HandleNonHandledMessage(JsonObject message) { auto level = DevtoolsMessageLevel::Warn; auto body = L"Unhandled devtools message: " + message.Stringify(); mDelegate.OnDevtoolsMessage(level, L"", body); } void DevtoolsClient::SendPendingObjects() { if (mPendingObjects.empty() || mSending) { return; } mSending = true; auto obj = mPendingObjects.front(); mPendingObjects.erase(mPendingObjects.begin()); hstring msg = obj.Stringify(); hstring size = to_hstring(msg.size()); hstring request = size + L":" + msg; mDataWriter->WriteString(request); mDataWriter->StoreAsync().Completed([=](const auto &, const auto &) { mDataWriter->FlushAsync().Completed([=](const auto &, const auto &) { mSending = false; SendPendingObjects(); }); }); } void DevtoolsClient::Send(JsonObject obj) { mPendingObjects.push_back(obj); SendPendingObjects(); } } // namespace winrt::servo