browser(firefox): implement RemoteDebuggingPipe (#3273)

This commit is contained in:
Dmitry Gozman 2020-08-03 13:27:09 -07:00 committed by GitHub
parent bad4005d7d
commit 1148f0b906
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 353 additions and 32 deletions

View file

@ -1,2 +1,2 @@
1146
Changed: lushnikov@chromium.org Thu 30 Jul 2020 11:44:10 PM PDT
1147
Changed: dgozman@gmail.com Mon Aug 3 13:14:27 PDT 2020

View file

@ -32,9 +32,9 @@ CommandLineHandler.prototype = {
/* nsICommandLineHandler */
handle: async function(cmdLine) {
const jugglerFlag = cmdLine.handleFlagWithParam("juggler", false);
if (!jugglerFlag || isNaN(jugglerFlag))
const jugglerPipeFlag = cmdLine.handleFlag("juggler-pipe", false);
if (!jugglerPipeFlag && (!jugglerFlag || isNaN(jugglerFlag)))
return;
const port = parseInt(jugglerFlag, 10);
const silent = cmdLine.preventDefault;
if (silent)
Services.startup.enterLastWindowClosingSurvivalArea();
@ -42,38 +42,64 @@ CommandLineHandler.prototype = {
const targetRegistry = new TargetRegistry();
new NetworkObserver(targetRegistry);
const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
const WebSocketServer = require('devtools/server/socket/websocket-server');
this._server = Cc["@mozilla.org/network/server-socket;1"].createInstance(Ci.nsIServerSocket);
this._server.initSpecialConnection(port, Ci.nsIServerSocket.KeepWhenOffline | Ci.nsIServerSocket.LoopbackOnly, 4);
const token = helper.generateId();
const loadFrameScript = () => {
Services.mm.loadFrameScript(FRAME_SCRIPT, true /* aAllowDelayedLoad */);
if (Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless) {
const styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Components.interfaces.nsIStyleSheetService);
const ioService = Cc["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
const uri = ioService.newURI('chrome://juggler/content/content/hidden-scrollbars.css', null, null);
styleSheetService.loadAndRegisterSheet(uri, styleSheetService.AGENT_SHEET);
}
};
// Force create hidden window here, otherwise its creation later closes the web socket!
Services.appShell.hiddenDOMWindow;
this._server.asyncListen({
onSocketAccepted: async(socket, transport) => {
const input = transport.openInputStream(0, 0, 0);
const output = transport.openOutputStream(0, 0, 0);
const webSocket = await WebSocketServer.accept(transport, input, output, "/" + token);
const dispatcher = new Dispatcher(webSocket);
const browserHandler = new BrowserHandler(dispatcher.rootSession(), dispatcher, targetRegistry, () => {
if (silent)
Services.startup.exitLastWindowClosingSurvivalArea();
});
dispatcher.rootSession().registerHandler('Browser', browserHandler);
}
});
Services.mm.loadFrameScript(FRAME_SCRIPT, true /* aAllowDelayedLoad */);
if (Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless) {
const styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Components.interfaces.nsIStyleSheetService);
const ioService = Cc["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
const uri = ioService.newURI('chrome://juggler/content/content/hidden-scrollbars.css', null, null);
styleSheetService.loadAndRegisterSheet(uri, styleSheetService.AGENT_SHEET);
if (jugglerFlag) {
const port = parseInt(jugglerFlag, 10);
const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
const WebSocketServer = require('devtools/server/socket/websocket-server');
this._server = Cc["@mozilla.org/network/server-socket;1"].createInstance(Ci.nsIServerSocket);
this._server.initSpecialConnection(port, Ci.nsIServerSocket.KeepWhenOffline | Ci.nsIServerSocket.LoopbackOnly, 4);
const token = helper.generateId();
this._server.asyncListen({
onSocketAccepted: async(socket, transport) => {
const input = transport.openInputStream(0, 0, 0);
const output = transport.openOutputStream(0, 0, 0);
const webSocket = await WebSocketServer.accept(transport, input, output, "/" + token);
const dispatcher = new Dispatcher(webSocket);
const browserHandler = new BrowserHandler(dispatcher.rootSession(), dispatcher, targetRegistry, () => {
if (silent)
Services.startup.exitLastWindowClosingSurvivalArea();
});
dispatcher.rootSession().registerHandler('Browser', browserHandler);
}
});
loadFrameScript();
dump(`Juggler listening on ws://127.0.0.1:${this._server.port}/${token}\n`);
} else if (jugglerPipeFlag) {
const pipe = Cc['@mozilla.org/juggler/remotedebuggingpipe;1'].getService(Ci.nsIRemoteDebuggingPipe);
const connection = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIRemoteDebuggingPipeClient]),
receiveMessage(message) {
if (this.onmessage)
this.onmessage({ data: message });
},
send(message) {
pipe.sendMessage(message);
},
};
pipe.init(connection);
const dispatcher = new Dispatcher(connection);
const browserHandler = new BrowserHandler(dispatcher.rootSession(), dispatcher, targetRegistry, () => {
if (silent)
Services.startup.exitLastWindowClosingSurvivalArea();
pipe.stop();
});
dispatcher.rootSession().registerHandler('Browser', browserHandler);
loadFrameScript();
dump(`Juggler listening to the pipe\n`);
}
dump(`Juggler listening on ws://127.0.0.1:${this._server.port}/${token}\n`);
},
QueryInterface: ChromeUtils.generateQI([ Ci.nsICommandLineHandler ]),

View file

@ -2,7 +2,7 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DIRS += ["components", "screencast"]
DIRS += ["components", "screencast", "pipe"]
JAR_MANIFESTS += ["jar.mn"]
#JS_PREFERENCE_FILES += ["prefs/marionette.js"]

View file

@ -0,0 +1,15 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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 http://mozilla.org/MPL/2.0/.
Classes = [
{
'cid': '{d69ecefe-3df7-4d11-9dc7-f604edb96da2}',
'contract_ids': ['@mozilla.org/juggler/remotedebuggingpipe;1'],
'type': 'nsIRemoteDebuggingPipe',
'constructor': 'mozilla::nsRemoteDebuggingPipe::GetSingleton',
'headers': ['/juggler/pipe/nsRemoteDebuggingPipe.h'],
},
]

View file

@ -0,0 +1,24 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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 http://mozilla.org/MPL/2.0/.
XPIDL_SOURCES += [
'nsIRemoteDebuggingPipe.idl',
]
XPIDL_MODULE = 'jugglerpipe'
SOURCES += [
'nsRemoteDebuggingPipe.cpp',
]
XPCOM_MANIFESTS += [
'components.conf',
]
LOCAL_INCLUDES += [
]
FINAL_LIBRARY = 'xul'

View file

@ -0,0 +1,19 @@
/* 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 http://mozilla.org/MPL/2.0/. */
#include "nsISupports.idl"
[scriptable, uuid(7910c231-971a-4653-abdc-a8599a986c4c)]
interface nsIRemoteDebuggingPipeClient : nsISupports
{
void receiveMessage(in AString message);
};
[scriptable, uuid(b7bfb66b-fd46-4aa2-b4ad-396177186d94)]
interface nsIRemoteDebuggingPipe : nsISupports
{
void init(in nsIRemoteDebuggingPipeClient client);
void sendMessage(in AString message);
void stop();
};

View file

@ -0,0 +1,204 @@
/* 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 http://mozilla.org/MPL/2.0/. */
#include "nsRemoteDebuggingPipe.h"
#include <cstring>
#if defined(OS_WIN)
#include <io.h>
#else
#include <stdio.h>
#include <unistd.h>
#endif
#include "mozilla/StaticPtr.h"
#include "nsISupportsPrimitives.h"
#include "nsThreadUtils.h"
namespace mozilla {
NS_IMPL_ISUPPORTS(nsRemoteDebuggingPipe, nsIRemoteDebuggingPipe)
namespace {
StaticRefPtr<nsRemoteDebuggingPipe> gPipe;
const int readFD = 3;
const int writeFD = 4;
const size_t kWritePacketSize = 1 << 16;
#if defined(OS_WIN)
HANDLE readHandle;
HANDLE writeHandle;
#endif
size_t ReadBytes(void* buffer, size_t size, bool exact_size)
{
size_t bytesRead = 0;
while (bytesRead < size) {
#if defined(OS_WIN)
DWORD sizeRead = 0;
bool hadError = !ReadFile(readHandle, static_cast<char*>(buffer) + bytesRead,
size - bytesRead, &sizeRead, nullptr);
#else
int sizeRead = read(readFD, static_cast<char*>(buffer) + bytesRead,
size - bytesRead);
if (sizeRead < 0 && errno == EINTR)
continue;
bool hadError = sizeRead <= 0;
#endif
if (hadError) {
return 0;
}
bytesRead += sizeRead;
if (!exact_size)
break;
}
return bytesRead;
}
void WriteBytes(const char* bytes, size_t size)
{
size_t totalWritten = 0;
while (totalWritten < size) {
size_t length = size - totalWritten;
if (length > kWritePacketSize)
length = kWritePacketSize;
#if defined(OS_WIN)
DWORD bytesWritten = 0;
bool hadError = !WriteFile(writeHandle, bytes + totalWritten, static_cast<DWORD>(length), &bytesWritten, nullptr);
#else
int bytesWritten = write(writeFD, bytes + totalWritten, length);
if (bytesWritten < 0 && errno == EINTR)
continue;
bool hadError = bytesWritten <= 0;
#endif
if (hadError)
return;
totalWritten += bytesWritten;
}
}
} // namespace
// static
already_AddRefed<nsIRemoteDebuggingPipe> nsRemoteDebuggingPipe::GetSingleton() {
if (!gPipe) {
gPipe = new nsRemoteDebuggingPipe();
}
return do_AddRef(gPipe);
}
nsRemoteDebuggingPipe::nsRemoteDebuggingPipe() = default;
nsRemoteDebuggingPipe::~nsRemoteDebuggingPipe() = default;
nsresult nsRemoteDebuggingPipe::Init(nsIRemoteDebuggingPipeClient* aClient) {
MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Remote debugging pipe must be used on the Main thread.");
if (mClient) {
return NS_ERROR_FAILURE;
}
mClient = aClient;
MOZ_ALWAYS_SUCCEEDS(NS_NewNamedThread("Pipe Reader", getter_AddRefs(mReaderThread)));
MOZ_ALWAYS_SUCCEEDS(NS_NewNamedThread("Pipe Writer", getter_AddRefs(mWriterThread)));
#if defined(OS_WIN)
readHandle = reinterpret_cast<HANDLE>(_get_osfhandle(readFD));
writeHandle = reinterpret_cast<HANDLE>(_get_osfhandle(writeFD));
#endif
MOZ_ALWAYS_SUCCEEDS(mReaderThread->Dispatch(NewRunnableMethod(
"nsRemoteDebuggingPipe::ReaderLoop",
this, &nsRemoteDebuggingPipe::ReaderLoop), nsIThread::DISPATCH_NORMAL));
return NS_OK;
}
nsresult nsRemoteDebuggingPipe::Stop() {
MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Remote debugging pipe must be used on the Main thread.");
if (!mClient) {
return NS_ERROR_FAILURE;
}
m_terminated = true;
mClient = nullptr;
// Cancel pending synchronous read.
#if defined(OS_WIN)
CancelIoEx(readHandle, nullptr);
CloseHandle(readHandle);
#else
shutdown(readFD, SHUT_RDWR);
#endif
mReaderThread->Shutdown();
mReaderThread = nullptr;
mWriterThread->AsyncShutdown();
mWriterThread = nullptr;
return NS_OK;
}
void nsRemoteDebuggingPipe::ReaderLoop() {
const size_t bufSize = 256 * 1024;
std::vector<char> buffer;
buffer.resize(bufSize);
std::vector<char> line;
while (!m_terminated) {
size_t size = ReadBytes(buffer.data(), bufSize, false);
if (!size)
break;
size_t start = 0;
size_t end = line.size();
line.insert(line.end(), buffer.begin(), buffer.begin() + size);
while (true) {
for (; end < line.size(); ++end) {
if (line[end] == '\0') {
break;
}
}
if (end == line.size()) {
break;
}
if (end > start) {
nsCString message;
message.Append(line.data() + start, end - start);
nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod<nsCString>(
"nsRemoteDebuggingPipe::ReceiveMessage",
this, &nsRemoteDebuggingPipe::ReceiveMessage, std::move(message));
NS_DispatchToMainThread(runnable.forget());
}
++end;
start = end;
}
if (start != 0 && start < line.size()) {
memmove(line.data(), line.data() + start, line.size() - start);
}
line.resize(line.size() - start);
}
}
void nsRemoteDebuggingPipe::ReceiveMessage(const nsCString& aMessage) {
MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Remote debugging pipe must be used on the Main thread.");
if (mClient) {
NS_ConvertUTF8toUTF16 utf16(aMessage);
mClient->ReceiveMessage(utf16);
}
}
nsresult nsRemoteDebuggingPipe::SendMessage(const nsAString& aMessage) {
MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Remote debugging pipe must be used on the Main thread.");
if (!mClient) {
return NS_ERROR_FAILURE;
}
NS_ConvertUTF16toUTF8 utf8(aMessage);
nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
"nsRemoteDebuggingPipe::SendMessage",
[message = std::move(utf8)] {
const nsCString& flat = PromiseFlatCString(message);
WriteBytes(flat.Data(), flat.Length());
WriteBytes("\0", 1);
});
MOZ_ALWAYS_SUCCEEDS(mWriterThread->Dispatch(runnable.forget(), nsIThread::DISPATCH_NORMAL));
return NS_OK;
}
} // namespace mozilla

View file

@ -0,0 +1,33 @@
/* 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 http://mozilla.org/MPL/2.0/. */
#pragma once
#include <memory>
#include "nsCOMPtr.h"
#include "nsIRemoteDebuggingPipe.h"
#include "nsThread.h"
namespace mozilla {
class nsRemoteDebuggingPipe final : public nsIRemoteDebuggingPipe {
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSIREMOTEDEBUGGINGPIPE
static already_AddRefed<nsIRemoteDebuggingPipe> GetSingleton();
nsRemoteDebuggingPipe();
private:
void ReaderLoop();
void ReceiveMessage(const nsCString& aMessage);
~nsRemoteDebuggingPipe();
RefPtr<nsIRemoteDebuggingPipeClient> mClient;
nsCOMPtr<nsIThread> mReaderThread;
nsCOMPtr<nsIThread> mWriterThread;
std::atomic<bool> m_terminated { false };
};
} // namespace mozilla