Everywhere: Remove dynamic Mach bootstrap registration on macOS

Registering multiple Mach port names with the bootstrap server at
runtime is not how macOS expects it to be used — the bootstrap server
is meant for static services, and the only reason we used it originally
was so child processes could reach back to the UI process.

Remove bootstrap_transport_over_socket(), which had both sides register
dynamic names with the bootstrap server and exchange them over a socket.
Instead, WebDriver and BrowserProcess connections now go through
MachPortServer instances directly. When a non-child process contacts a
MachPortServer, the server creates a port pair on demand (detected via
sysctl ppid check) and returns the local half immediately. This keeps
bootstrap server usage limited to the one original case: child processes
looking up their parent's MachPortServer.

WebDriver Session now runs its own MachPortServer per session.
--webdriver-content-path becomes --webdriver-mach-server-name on macOS.
Spare WebContent launches are skipped when a WebDriver session is active
to avoid bootstrap races.
This commit is contained in:
Aliaksandr Kalenik 2026-03-22 19:59:08 +01:00 committed by Alexander Kalenik
parent 4ea4d63008
commit c6d740ea41
24 changed files with 353 additions and 194 deletions

View file

@ -5,39 +5,18 @@
*/
#include <AK/Assertions.h>
#include <AK/ByteBuffer.h>
#include <AK/ByteString.h>
#include <AK/Optional.h>
#include <LibCore/MachPort.h>
#include <LibCore/Platform/MachMessageTypes.h>
#include <LibCore/System.h>
#include <LibIPC/TransportBootstrapMach.h>
#include <mach/mach.h>
#include <sys/sysctl.h>
namespace IPC {
static ErrorOr<void> write_exact(int fd, ReadonlyBytes bytes)
{
size_t total_written = 0;
while (total_written < bytes.size()) {
auto nwritten = TRY(Core::System::write(fd, bytes.slice(total_written)));
VERIFY(nwritten > 0);
total_written += nwritten;
}
return {};
}
static ErrorOr<void> read_exact(int fd, Bytes bytes)
{
size_t total_read = 0;
while (total_read < bytes.size()) {
auto nread = TRY(Core::System::read(fd, bytes.slice(total_read)));
VERIFY(nread > 0);
total_read += nread;
}
return {};
}
ErrorOr<TransportBootstrapMachPorts> bootstrap_transport_from_server_port(Core::MachPort const& server_port)
{
auto reply_port = TRY(Core::MachPort::create_with_right(Core::MachPort::PortRight::Receive));
@ -84,42 +63,6 @@ ErrorOr<TransportBootstrapMachPorts> bootstrap_transport_from_mach_server(String
return bootstrap_transport_from_server_port(server_port);
}
ErrorOr<TransportBootstrapMachPorts> bootstrap_transport_over_socket(Core::LocalSocket& socket)
{
auto our_receive_right = TRY(Core::MachPort::create_with_right(Core::MachPort::PortRight::Receive));
auto our_name = ByteString::formatted("org.ladybird.ipc.{}.{}", Core::System::getpid(), our_receive_right.port());
TRY(our_receive_right.register_with_bootstrap_server(our_name));
auto socket_fd = socket.fd().value();
TRY(socket.set_blocking(true));
auto const name_bytes = our_name.bytes();
u32 const name_length = static_cast<u32>(name_bytes.size());
TRY(write_exact(socket_fd, ReadonlyBytes { reinterpret_cast<u8 const*>(&name_length), sizeof(name_length) }));
TRY(write_exact(socket_fd, name_bytes));
u32 peer_name_length = 0;
TRY(read_exact(socket_fd, Bytes { reinterpret_cast<u8*>(&peer_name_length), sizeof(peer_name_length) }));
VERIFY(peer_name_length > 0);
VERIFY(peer_name_length <= 256);
auto peer_name_buffer = TRY(ByteBuffer::create_uninitialized(peer_name_length));
TRY(read_exact(socket_fd, peer_name_buffer.bytes()));
auto peer_name = ByteString { peer_name_buffer.bytes() };
auto peer_send_right = TRY(Core::MachPort::look_up_from_bootstrap_server(peer_name));
u8 ack = 1;
TRY(write_exact(socket_fd, { &ack, 1 }));
TRY(read_exact(socket_fd, { &ack, 1 }));
VERIFY(ack == 1);
return TransportBootstrapMachPorts {
.receive_right = move(our_receive_right),
.send_right = move(peer_send_right),
};
}
void TransportBootstrapMachServer::send_transport_ports_to_child(Core::MachPort reply_port, TransportBootstrapMachPorts ports)
{
Core::Platform::MessageWithIPCChannelPorts message {};
@ -141,17 +84,57 @@ void TransportBootstrapMachServer::send_transport_ports_to_child(Core::MachPort
VERIFY(ret == KERN_SUCCESS);
}
static bool process_is_child_of_us(pid_t pid)
{
int mib[4] = {};
struct kinfo_proc info = {};
size_t size = sizeof(info);
mib[0] = CTL_KERN;
mib[1] = KERN_PROC;
mib[2] = KERN_PROC_PID;
mib[3] = pid;
if (sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, nullptr, 0) < 0)
return false;
if (size == 0)
return false;
return info.kp_eproc.e_ppid == Core::System::getpid();
}
ErrorOr<TransportBootstrapMachPorts> TransportBootstrapMachServer::create_on_demand_local_transport(Core::MachPort reply_port)
{
auto local_receive_right = TRY(Core::MachPort::create_with_right(Core::MachPort::PortRight::Receive));
auto local_send_right = TRY(local_receive_right.insert_right(Core::MachPort::MessageRight::MakeSend));
auto remote_receive_right = TRY(Core::MachPort::create_with_right(Core::MachPort::PortRight::Receive));
auto remote_send_right = TRY(remote_receive_right.insert_right(Core::MachPort::MessageRight::MakeSend));
send_transport_ports_to_child(move(reply_port), TransportBootstrapMachPorts {
.receive_right = move(remote_receive_right),
.send_right = move(local_send_right),
});
return TransportBootstrapMachPorts {
.receive_right = move(local_receive_right),
.send_right = move(remote_send_right),
};
}
void TransportBootstrapMachServer::register_pending_transport(pid_t pid, TransportBootstrapMachPorts ports)
{
m_pending_bootstrap_mutex.lock();
auto pending = m_pending_bootstrap.take(pid);
if (!pending.has_value()) {
m_pending_bootstrap.set(pid, WaitingForPorts { move(ports) });
m_pending_bootstrap_mutex.unlock();
return;
Optional<PendingBootstrapState> pending;
{
Threading::MutexLocker locker(m_pending_bootstrap_mutex);
pending = m_pending_bootstrap.take(pid);
if (!pending.has_value()) {
m_pending_bootstrap.set(pid, WaitingForPorts { move(ports) });
return;
}
}
m_pending_bootstrap_mutex.unlock();
pending.release_value().visit(
[&](WaitingForPorts&) {
VERIFY_NOT_REACHED();
@ -161,22 +144,29 @@ void TransportBootstrapMachServer::register_pending_transport(pid_t pid, Transpo
});
}
void TransportBootstrapMachServer::register_reply_port(pid_t pid, Core::MachPort reply_port)
ErrorOr<TransportBootstrapMachServer::RegisterReplyPortResult> TransportBootstrapMachServer::register_reply_port(pid_t pid, Core::MachPort reply_port)
{
m_pending_bootstrap_mutex.lock();
auto pending = m_pending_bootstrap.take(pid);
if (!pending.has_value()) {
m_pending_bootstrap.set(pid, WaitingForReplyPort { move(reply_port) });
m_pending_bootstrap_mutex.unlock();
return;
Optional<PendingBootstrapState> pending;
{
Threading::MutexLocker locker(m_pending_bootstrap_mutex);
pending = m_pending_bootstrap.take(pid);
if (!pending.has_value()) {
if (process_is_child_of_us(pid)) {
m_pending_bootstrap.set(pid, WaitingForReplyPort { move(reply_port) });
return WaitingForChildTransport {};
}
}
}
m_pending_bootstrap_mutex.unlock();
pending.release_value().visit(
[&](WaitingForPorts& waiting) {
if (!pending.has_value())
return OnDemandTransport { .ports = TRY(create_on_demand_local_transport(move(reply_port))) };
return pending.release_value().visit(
[&](WaitingForPorts& waiting) -> ErrorOr<RegisterReplyPortResult> {
send_transport_ports_to_child(move(reply_port), move(waiting.ports));
return WaitingForChildTransport {};
},
[&](WaitingForReplyPort&) {
[&](WaitingForReplyPort&) -> ErrorOr<RegisterReplyPortResult> {
VERIFY_NOT_REACHED();
});
}

View file

@ -16,7 +16,6 @@
#endif
#include <LibCore/MachPort.h>
#include <LibCore/Socket.h>
#include <LibThreading/Mutex.h>
namespace IPC {
@ -28,7 +27,6 @@ struct TransportBootstrapMachPorts {
ErrorOr<TransportBootstrapMachPorts> bootstrap_transport_from_mach_server(StringView server_name);
ErrorOr<TransportBootstrapMachPorts> bootstrap_transport_from_server_port(Core::MachPort const& server_port);
ErrorOr<TransportBootstrapMachPorts> bootstrap_transport_over_socket(Core::LocalSocket&);
class TransportBootstrapMachServer {
AK_MAKE_NONCOPYABLE(TransportBootstrapMachServer);
@ -36,8 +34,15 @@ class TransportBootstrapMachServer {
public:
TransportBootstrapMachServer() = default;
struct WaitingForChildTransport {
};
struct OnDemandTransport {
TransportBootstrapMachPorts ports;
};
using RegisterReplyPortResult = Variant<WaitingForChildTransport, OnDemandTransport>;
void register_pending_transport(pid_t, TransportBootstrapMachPorts);
void register_reply_port(pid_t, Core::MachPort reply_port);
ErrorOr<RegisterReplyPortResult> register_reply_port(pid_t, Core::MachPort reply_port);
private:
struct WaitingForPorts {
@ -49,6 +54,7 @@ private:
using PendingBootstrapState = Variant<WaitingForPorts, WaitingForReplyPort>;
static void send_transport_ports_to_child(Core::MachPort reply_port, TransportBootstrapMachPorts ports);
static ErrorOr<TransportBootstrapMachPorts> create_on_demand_local_transport(Core::MachPort reply_port);
Threading::Mutex m_pending_bootstrap_mutex;
HashMap<pid_t, PendingBootstrapState> m_pending_bootstrap;

View file

@ -7,9 +7,7 @@
#include <AK/NonnullOwnPtr.h>
#include <LibCore/MachPort.h>
#include <LibCore/Notifier.h>
#include <LibCore/Socket.h>
#include <LibCore/System.h>
#include <LibIPC/TransportBootstrapMach.h>
#include <LibIPC/TransportMachPort.h>
#include <LibThreading/Thread.h>
@ -48,12 +46,6 @@ static Attachment attachment_from_descriptor(mach_msg_port_descriptor_t const& d
}
}
ErrorOr<NonnullOwnPtr<TransportMachPort>> TransportMachPort::from_socket(NonnullOwnPtr<Core::LocalSocket> socket)
{
auto ports = TRY(bootstrap_transport_over_socket(*socket));
return make<TransportMachPort>(move(ports.receive_right), move(ports.send_right));
}
ErrorOr<TransportMachPort::Paired> TransportMachPort::create_paired()
{
auto port_a_recv = TRY(Core::MachPort::create_with_right(Core::MachPort::PortRight::Receive));

View file

@ -37,11 +37,6 @@ public:
};
static ErrorOr<Paired> create_paired();
// Bootstrap a Mach port connection over an existing Unix socket.
// Used when the initial connection is socket-based (e.g., WebDriver).
// Both sides must call this concurrently on the same socket.
static ErrorOr<NonnullOwnPtr<TransportMachPort>> from_socket(NonnullOwnPtr<Core::LocalSocket> socket);
TransportMachPort(Core::MachPort receive_right, Core::MachPort send_right);
~TransportMachPort();

View file

@ -27,6 +27,7 @@
#include <LibWebView/WebContentClient.h>
#if defined(AK_OS_MACOS)
# include <LibIPC/Transport.h>
# include <LibIPC/TransportBootstrapMach.h>
# include <LibWebView/MachPortServer.h>
#endif
@ -88,12 +89,31 @@ ErrorOr<void> Application::initialize(Main::Arguments const& arguments)
#endif
#if defined(AK_OS_MACOS)
m_mach_port_server = make<MachPortServer>();
m_mach_port_server = make<MachPortServer>(mach_server_name_for_process("Ladybird"sv, Core::System::getpid()));
set_mach_server_name(m_mach_port_server->server_port_name());
m_mach_port_server->on_receive_child_mach_port = [this](MachPortServer::ChildMachPortRegistration registration) {
set_process_mach_port(registration.pid, move(registration.child_port));
m_transport_bootstrap_server.register_reply_port(registration.pid, move(registration.reply_port));
auto registration_result = m_transport_bootstrap_server.register_reply_port(registration.pid, move(registration.reply_port));
if (registration_result.is_error()) {
dbgln("Failed to bootstrap Mach transport for pid {}: {}", registration.pid, registration_result.error());
return;
}
registration_result.release_value().visit(
[](IPC::TransportBootstrapMachServer::WaitingForChildTransport) {
},
[this](IPC::TransportBootstrapMachServer::OnDemandTransport& transport) {
if (!m_on_browser_process_transport)
return;
VERIFY(m_event_loop);
m_event_loop->deferred_invoke([this, transport = move(transport.ports)]() mutable {
if (!m_on_browser_process_transport)
return;
m_on_browser_process_transport(make<IPC::Transport>(move(transport.receive_right), move(transport.send_right)));
});
});
};
m_mach_port_server->on_receive_backing_stores = [](MachPortServer::BackingStoresMessage message) {
if (auto view = WebContentClient::view_for_pid_and_page_id(message.pid, message.page_id); view.has_value())
@ -114,7 +134,7 @@ ErrorOr<void> Application::initialize(Main::Arguments const& arguments)
Optional<u16> devtools_port;
Optional<StringView> debug_process;
Optional<StringView> profile_process;
Optional<StringView> webdriver_content_ipc_path;
Optional<StringView> webdriver_endpoint;
Optional<StringView> user_agent_preset;
Optional<StringView> dns_server_address;
Optional<StringView> default_time_zone;
@ -173,7 +193,11 @@ ErrorOr<void> Application::initialize(Main::Arguments const& arguments)
args_parser.add_option(disable_sql_database, "Disable SQL database", "disable-sql-database");
args_parser.add_option(debug_process, "Wait for a debugger to attach to the given process name (WebContent, RequestServer, etc.)", "debug-process", 0, "process-name");
args_parser.add_option(profile_process, "Enable callgrind profiling of the given process name (WebContent, RequestServer, etc.)", "profile-process", 0, "process-name");
args_parser.add_option(webdriver_content_ipc_path, "Path to WebDriver IPC for WebContent", "webdriver-content-path", 0, "path", Core::ArgsParser::OptionHideMode::CommandLineAndMarkdown);
#if defined(AK_OS_MACOS)
args_parser.add_option(webdriver_endpoint, "Mach server name for WebDriver IPC", "webdriver-mach-server-name", 0, "name", Core::ArgsParser::OptionHideMode::CommandLineAndMarkdown);
#else
args_parser.add_option(webdriver_endpoint, "Path to WebDriver IPC for WebContent", "webdriver-content-path", 0, "path", Core::ArgsParser::OptionHideMode::CommandLineAndMarkdown);
#endif
args_parser.add_option(enable_test_mode, "Enable test mode", "test-mode");
args_parser.add_option(log_all_js_exceptions, "Log all JavaScript exceptions", "log-all-js-exceptions");
args_parser.add_option(disable_site_isolation, "Disable site isolation", "disable-site-isolation");
@ -269,8 +293,8 @@ ErrorOr<void> Application::initialize(Main::Arguments const& arguments)
if (window_height.has_value())
m_browser_options.window_height = *window_height;
if (webdriver_content_ipc_path.has_value())
m_browser_options.webdriver_content_ipc_path = *webdriver_content_ipc_path;
if (webdriver_endpoint.has_value())
m_browser_options.webdriver_endpoint = *webdriver_endpoint;
auto http_disk_cache_mode = HTTPDiskCacheMode::Enabled;
if (disable_http_disk_cache)
@ -357,6 +381,11 @@ ErrorOr<NonnullRefPtr<WebContentClient>> Application::launch_web_content_process
void Application::launch_spare_web_content_process()
{
// Spare WebContent processes inherit the active WebDriver endpoint, but they are not part of the
// session and can race browser shutdown while bootstrapping.
if (browser_options().webdriver_endpoint.has_value())
return;
// Disable spare processes when debugging WebContent. Otherwise, it breaks running `gdb attach -p $(pidof WebContent)`.
if (browser_options().debug_helper_process == ProcessType::WebContent)
return;
@ -571,7 +600,7 @@ ErrorOr<int> Application::execute()
view = HeadlessWebView::create(move(theme), { m_browser_options.window_width, m_browser_options.window_height });
if (!m_browser_options.webdriver_content_ipc_path.has_value()) {
if (!m_browser_options.webdriver_endpoint.has_value()) {
if (m_browser_options.urls.size() != 1)
return Error::from_string_literal("Headless mode currently only supports exactly one URL");
@ -614,6 +643,13 @@ void Application::set_process_mach_port(pid_t pid, Core::MachPort&& port)
}
#endif
#if defined(AK_OS_MACOS)
void Application::set_browser_process_transport_handler(Function<void(NonnullOwnPtr<IPC::Transport>)> handler)
{
m_on_browser_process_transport = move(handler);
}
#endif
Optional<Process&> Application::find_process(pid_t pid)
{
return m_process_manager->find_process(pid);

View file

@ -7,6 +7,7 @@
#pragma once
#include <AK/ByteString.h>
#include <AK/Function.h>
#include <AK/LexicalPath.h>
#include <AK/Optional.h>
#include <LibCore/EventLoop.h>
@ -14,6 +15,7 @@
#include <LibDatabase/Forward.h>
#include <LibDevTools/DevToolsDelegate.h>
#include <LibDevTools/Forward.h>
#include <LibIPC/Forward.h>
#include <LibImageDecoderClient/Client.h>
#include <LibMain/Main.h>
#include <LibRequests/Forward.h>
@ -64,6 +66,7 @@ public:
static ProcessManager& process_manager() { return *the().m_process_manager; }
#if defined(AK_OS_MACOS)
static IPC::TransportBootstrapMachServer& transport_bootstrap_server() { return the().m_transport_bootstrap_server; }
void set_browser_process_transport_handler(Function<void(NonnullOwnPtr<IPC::Transport>)> handler);
#endif
ErrorOr<NonnullRefPtr<WebContentClient>> launch_web_content_process(ViewImplementation&);
@ -270,6 +273,7 @@ private:
#if defined(AK_OS_MACOS)
OwnPtr<MachPortServer> m_mach_port_server;
IPC::TransportBootstrapMachServer m_transport_bootstrap_server;
Function<void(NonnullOwnPtr<IPC::Transport>)> m_on_browser_process_transport;
#endif
OwnPtr<DevTools::DevToolsServer> m_devtools;

View file

@ -10,9 +10,13 @@
#include <LibCore/System.h>
#include <LibIPC/ConnectionToServer.h>
#include <LibIPC/Transport.h>
#if defined(AK_OS_MACOS)
# include <LibIPC/TransportBootstrapMach.h>
#endif
#include <LibWebView/Application.h>
#include <LibWebView/BrowserProcess.h>
#include <LibWebView/URL.h>
#include <LibWebView/Utilities.h>
namespace WebView {
@ -33,11 +37,19 @@ ErrorOr<BrowserProcess::ProcessDisposition> BrowserProcess::connect(Vector<ByteS
auto [socket_path, pid_path] = TRY(Process::paths_for_process(process_name));
if (auto pid = TRY(Process::get_process_pid(process_name, pid_path)); pid.has_value()) {
#if defined(AK_OS_MACOS)
TRY(connect_as_client(*pid, raw_urls, new_window));
#else
TRY(connect_as_client(socket_path, raw_urls, new_window));
#endif
return ProcessDisposition::ExitProcess;
}
#if defined(AK_OS_MACOS)
TRY(connect_as_server());
#else
TRY(connect_as_server(socket_path));
#endif
m_pid_path = pid_path;
m_pid_file = TRY(Core::File::open(pid_path, Core::File::OpenMode::Write));
@ -46,6 +58,26 @@ ErrorOr<BrowserProcess::ProcessDisposition> BrowserProcess::connect(Vector<ByteS
return ProcessDisposition::ContinueMainProcess;
}
#if defined(AK_OS_MACOS)
ErrorOr<void> BrowserProcess::connect_as_client(pid_t pid, Vector<ByteString> const& raw_urls, NewWindow new_window)
{
auto transport_ports = TRY(IPC::bootstrap_transport_from_mach_server(mach_server_name_for_process("Ladybird"sv, pid)));
auto client = UIProcessClient::construct(make<IPC::Transport>(move(transport_ports.receive_right), move(transport_ports.send_right)));
switch (new_window) {
case NewWindow::Yes:
if (!client->send_sync_but_allow_failure<Messages::UIProcessServer::CreateNewWindow>(raw_urls))
dbgln("Failed to send CreateNewWindow message to UIProcess");
return {};
case NewWindow::No:
if (!client->send_sync_but_allow_failure<Messages::UIProcessServer::CreateNewTab>(raw_urls))
dbgln("Failed to send CreateNewTab message to UIProcess");
return {};
}
VERIFY_NOT_REACHED();
}
#else
ErrorOr<void> BrowserProcess::connect_as_client(ByteString const& socket_path, Vector<ByteString> const& raw_urls, NewWindow new_window)
{
auto socket = TRY(Core::LocalSocket::connect(socket_path));
@ -65,7 +97,17 @@ ErrorOr<void> BrowserProcess::connect_as_client(ByteString const& socket_path, V
VERIFY_NOT_REACHED();
}
#endif
#if defined(AK_OS_MACOS)
ErrorOr<void> BrowserProcess::connect_as_server()
{
Application::the().set_browser_process_transport_handler([this](auto transport) {
accept_transport(move(transport));
});
return {};
}
#else
ErrorOr<void> BrowserProcess::connect_as_server(ByteString const& socket_path)
{
auto socket_fd = TRY(Process::create_ipc_socket(socket_path));
@ -85,6 +127,7 @@ ErrorOr<void> BrowserProcess::connect_as_server(ByteString const& socket_path)
return {};
}
#endif
void BrowserProcess::accept_transport(NonnullOwnPtr<IPC::Transport> transport)
{
@ -102,6 +145,10 @@ void BrowserProcess::accept_transport(NonnullOwnPtr<IPC::Transport> transport)
BrowserProcess::~BrowserProcess()
{
#if defined(AK_OS_MACOS)
Application::the().set_browser_process_transport_handler({});
#endif
if (m_pid_file) {
MUST(m_pid_file->truncate(0));
#if defined(AK_OS_WINDOWS)

View file

@ -60,8 +60,13 @@ public:
private:
void accept_transport(NonnullOwnPtr<IPC::Transport>);
#if defined(AK_OS_MACOS)
ErrorOr<void> connect_as_client(pid_t pid, Vector<ByteString> const& raw_urls, NewWindow new_window);
ErrorOr<void> connect_as_server();
#else
ErrorOr<void> connect_as_client(ByteString const& socket_path, Vector<ByteString> const& raw_urls, NewWindow new_window);
ErrorOr<void> connect_as_server(ByteString const& socket_path);
#endif
RefPtr<Core::LocalServer> m_local_server;
OwnPtr<Core::File> m_pid_file;

View file

@ -11,9 +11,9 @@
namespace WebView {
MachPortServer::MachPortServer()
MachPortServer::MachPortServer(ByteString server_port_name)
: m_thread(Threading::Thread::construct("MachPortServer"sv, [this]() -> intptr_t { thread_loop(); return 0; }))
, m_server_port_name(ByteString::formatted("org.ladybird.Ladybird.helper.{}", getpid()))
, m_server_port_name(move(server_port_name))
{
if (auto err = allocate_server_port(); err.is_error())
dbgln("Failed to allocate server port: {}", err.error());

View file

@ -22,7 +22,7 @@ namespace WebView {
class WEBVIEW_API MachPortServer {
public:
MachPortServer();
explicit MachPortServer(ByteString server_port_name);
~MachPortServer();
void start();

View file

@ -87,7 +87,7 @@ struct BrowserOptions {
DisableSQLDatabase disable_sql_database { DisableSQLDatabase::No };
Optional<ProcessType> debug_helper_process {};
Optional<ProcessType> profile_helper_process {};
Optional<ByteString> webdriver_content_ipc_path {};
Optional<ByteString> webdriver_endpoint {};
Optional<DNSSettings> dns_settings {};
Optional<u16> devtools_port;
EnableContentFilter enable_content_filter { EnableContentFilter::Yes };

View file

@ -45,6 +45,11 @@ void set_mach_server_name(ByteString name)
s_mach_server_name = move(name);
}
ByteString mach_server_name_for_process(StringView process_name, pid_t pid)
{
return ByteString::formatted("org.ladybird.{}.helper.{}", process_name, pid);
}
static ErrorOr<ByteString> application_directory()
{
if (s_ladybird_binary_path.has_value())

View file

@ -10,6 +10,7 @@
#include <AK/ByteString.h>
#include <AK/Error.h>
#include <AK/String.h>
#include <AK/Types.h>
#include <AK/Vector.h>
#include <LibWebView/Forward.h>
@ -22,6 +23,7 @@ WEBVIEW_API ErrorOr<Vector<ByteString>> get_paths_for_helper_process(StringView
WEBVIEW_API extern ByteString s_ladybird_resource_root;
WEBVIEW_API Optional<ByteString const&> mach_server_name();
WEBVIEW_API void set_mach_server_name(ByteString name);
WEBVIEW_API ByteString mach_server_name_for_process(StringView process_name, pid_t pid);
WEBVIEW_API ErrorOr<void> handle_attached_debugger();

View file

@ -650,8 +650,8 @@ void ViewImplementation::initialize_client(CreateNewClient create_new_client)
client().async_set_system_visibility_state(m_client_state.page_index, m_system_visibility_state);
client().async_set_document_cookie_version_buffer(m_client_state.page_index, m_document_cookie_version_buffer);
if (auto webdriver_content_ipc_path = Application::browser_options().webdriver_content_ipc_path; webdriver_content_ipc_path.has_value())
client().async_connect_to_webdriver(m_client_state.page_index, *webdriver_content_ipc_path);
if (auto webdriver_endpoint = Application::browser_options().webdriver_endpoint; webdriver_endpoint.has_value())
client().async_connect_to_webdriver(m_client_state.page_index, *webdriver_endpoint);
Application::the().apply_view_options({}, *this);

View file

@ -122,11 +122,11 @@ void ConnectionFromClient::set_window_handle(u64 page_id, String handle)
page->page().top_level_traversable()->set_window_handle(move(handle));
}
void ConnectionFromClient::connect_to_webdriver(u64 page_id, ByteString webdriver_ipc_path)
void ConnectionFromClient::connect_to_webdriver(u64 page_id, ByteString webdriver_endpoint)
{
if (auto page = this->page(page_id); page.has_value()) {
// FIXME: Propagate this error back to the browser.
if (auto result = page->connect_to_webdriver(webdriver_ipc_path); result.is_error())
if (auto result = page->connect_to_webdriver(webdriver_endpoint); result.is_error())
dbgln("Unable to connect to the WebDriver process: {}", result.error());
}
}

View file

@ -62,7 +62,7 @@ private:
virtual void close_server() override;
virtual Messages::WebContentServer::GetWindowHandleResponse get_window_handle(u64 page_id) override;
virtual void set_window_handle(u64 page_id, String handle) override;
virtual void connect_to_webdriver(u64 page_id, ByteString webdriver_ipc_path) override;
virtual void connect_to_webdriver(u64 page_id, ByteString webdriver_endpoint) override;
virtual void connect_to_web_ui(u64 page_id, IPC::TransportHandle handle) override;
virtual void connect_to_request_server(IPC::TransportHandle handle) override;
virtual void connect_to_image_decoder(IPC::TransportHandle handle) override;

View file

@ -790,10 +790,10 @@ void PageClient::page_did_take_screenshot(Gfx::ShareableBitmap const& screenshot
client().async_did_take_screenshot(m_id, screenshot);
}
ErrorOr<void> PageClient::connect_to_webdriver(ByteString const& webdriver_ipc_path)
ErrorOr<void> PageClient::connect_to_webdriver(ByteString const& webdriver_endpoint)
{
VERIFY(!m_webdriver);
m_webdriver = TRY(WebDriverConnection::connect(*this, webdriver_ipc_path));
m_webdriver = TRY(WebDriverConnection::connect(*this, webdriver_endpoint));
return {};
}

View file

@ -45,7 +45,7 @@ public:
virtual Web::Page& page() override { return *m_page; }
virtual Web::Page const& page() const override { return *m_page; }
ErrorOr<void> connect_to_webdriver(ByteString const& webdriver_ipc_path);
ErrorOr<void> connect_to_webdriver(ByteString const& webdriver_endpoint);
ErrorOr<void> connect_to_web_ui(IPC::TransportHandle);
virtual Queue<Web::QueuedInputEvent>& input_event_queue() override;

View file

@ -28,7 +28,7 @@ endpoint WebContentServer
get_window_handle(u64 page_id) => (String handle)
set_window_handle(u64 page_id, String handle) =|
connect_to_webdriver(u64 page_id, ByteString webdriver_ipc_path) =|
connect_to_webdriver(u64 page_id, ByteString webdriver_endpoint) =|
connect_to_web_ui(u64 page_id, IPC::TransportHandle handle) =|
connect_to_request_server(IPC::TransportHandle handle) =|
connect_to_image_decoder(IPC::TransportHandle handle) =|

View file

@ -14,7 +14,11 @@
#include <AK/Time.h>
#include <AK/Vector.h>
#include <LibCore/File.h>
#include <LibCore/Socket.h>
#if !defined(AK_OS_MACOS)
# include <LibCore/Socket.h>
#else
# include <LibIPC/TransportBootstrapMach.h>
#endif
#include <LibHTTP/Cookie/Cookie.h>
#include <LibHTTP/Cookie/ParsedCookie.h>
#include <LibIPC/Transport.h>
@ -194,16 +198,24 @@ static bool fire_an_event(FlyString const& name, Optional<Web::DOM::Element&> ta
return target->dispatch_event(event);
}
ErrorOr<NonnullRefPtr<WebDriverConnection>> WebDriverConnection::connect(Web::PageClient& page_client, ByteString const& webdriver_ipc_path)
ErrorOr<NonnullRefPtr<WebDriverConnection>> WebDriverConnection::connect(Web::PageClient& page_client, ByteString const& webdriver_endpoint)
{
dbgln_if(WEBDRIVER_DEBUG, "Trying to connect to {}", webdriver_ipc_path);
auto socket = TRY(Core::LocalSocket::connect(webdriver_ipc_path));
dbgln_if(WEBDRIVER_DEBUG, "Trying to connect to {}", webdriver_endpoint);
#if defined(AK_OS_MACOS)
auto transport_ports = TRY(IPC::bootstrap_transport_from_mach_server(webdriver_endpoint));
#else
auto socket = TRY(Core::LocalSocket::connect(webdriver_endpoint));
#endif
// Allow pop-ups, or otherwise /window/new won't be able to open a new tab.
page_client.page().set_should_block_pop_ups(false);
dbgln_if(WEBDRIVER_DEBUG, "Connected to WebDriver");
#if defined(AK_OS_MACOS)
auto transport = make<IPC::Transport>(move(transport_ports.receive_right), move(transport_ports.send_right));
#else
auto transport = TRY(IPC::Transport::from_socket(move(socket)));
#endif
auto connection = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) WebDriverConnection(move(transport), page_client)));
connection->async_did_set_window_handle(page_client.page().top_level_traversable()->window_handle());
return connection;

View file

@ -34,7 +34,7 @@ class WebDriverConnection final
C_OBJECT_ABSTRACT(WebDriverConnection)
public:
static ErrorOr<NonnullRefPtr<WebDriverConnection>> connect(Web::PageClient& page_client, ByteString const& webdriver_ipc_path);
static ErrorOr<NonnullRefPtr<WebDriverConnection>> connect(Web::PageClient& page_client, ByteString const& webdriver_endpoint);
virtual ~WebDriverConnection() = default;
void visit_edges(JS::Cell::Visitor&);

View file

@ -10,9 +10,14 @@
#include <AK/HashMap.h>
#include <AK/JsonObject.h>
#include <LibCore/LocalServer.h>
#include <LibCore/Socket.h>
#include <LibCore/StandardPaths.h>
#if !defined(AK_OS_MACOS)
# include <LibCore/LocalServer.h>
# include <LibCore/Socket.h>
# include <LibCore/StandardPaths.h>
#else
# include <LibIPC/TransportBootstrapMach.h>
# include <LibWebView/Utilities.h>
#endif
#include <LibCore/System.h>
#include <LibIPC/Transport.h>
#include <LibWeb/Crypto/Crypto.h>
@ -123,6 +128,7 @@ Session::Session(NonnullRefPtr<Client> client, JsonObject const& capabilities, S
, m_options(capabilities)
, m_session_id(move(session_id))
, m_session_flags(flags)
, m_event_loop(Core::EventLoop::current_weak())
{
}
@ -192,15 +198,67 @@ void Session::close()
if (m_browser_process.has_value())
MUST(Core::System::kill(m_browser_process->pid(), SIGTERM));
if (m_web_content_socket_path.has_value()) {
MUST(Core::System::unlink(*m_web_content_socket_path));
m_web_content_socket_path = {};
}
#if defined(AK_OS_MACOS)
m_web_content_mach_port_server = nullptr;
#else
if (!m_web_content_endpoint.is_empty())
MUST(Core::System::unlink(m_web_content_endpoint));
#endif
m_web_content_endpoint = {};
// 5. If an error has occurred in any of the steps above, return the error, otherwise return success with data null.
}
ErrorOr<NonnullRefPtr<Core::LocalServer>> Session::create_server(NonnullRefPtr<ServerPromise> promise)
ErrorOr<void> Session::accept_web_content_transport(NonnullOwnPtr<IPC::Transport> transport, NonnullRefPtr<ServerPromise> promise)
{
auto web_content_connection = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) WebContentConnection(move(transport))));
dbgln("WebDriver is connected to WebContent");
auto connection_id = m_next_pending_connection_id++;
// Publish the connection before the initial did_set_window_handle message can race in.
m_pending_connections.set(connection_id, web_content_connection);
web_content_connection->on_close = [this, promise, connection_id]() {
if (m_pending_connections.remove(connection_id)) {
dbgln_if(WEBDRIVER_DEBUG, "Pending connection {} closed before sending its handle", connection_id);
promise->reject(Error::from_string_literal("Window was closed before sending its handle"));
}
};
web_content_connection->on_did_set_window_handle = [this, promise, connection_id](String window_handle) {
auto maybe_pending_connection = m_pending_connections.take(connection_id);
if (!maybe_pending_connection.has_value())
return;
auto pending_connection = maybe_pending_connection.value();
pending_connection->on_did_set_window_handle = nullptr;
dbgln_if(WEBDRIVER_DEBUG, "Window {} registered with WebDriver.", window_handle);
pending_connection->on_close = [this, window_handle]() {
dbgln_if(WEBDRIVER_DEBUG, "Window {} was closed remotely.", window_handle);
m_windows.remove(window_handle);
if (m_windows.is_empty())
close();
};
pending_connection->async_set_page_load_strategy(m_page_load_strategy);
pending_connection->async_set_strict_file_interactability(m_strict_file_interactiblity);
pending_connection->async_set_user_prompt_handler(Web::WebDriver::user_prompt_handler());
if (m_timeouts_configuration.has_value())
pending_connection->async_set_timeouts(*m_timeouts_configuration);
m_windows.set(window_handle, Session::Window { window_handle, move(pending_connection) });
if (m_current_window_handle.is_empty())
m_current_window_handle = window_handle;
promise->resolve({});
};
return {};
}
ErrorOr<void> Session::create_server(NonnullRefPtr<ServerPromise> promise)
{
#if defined(AK_OS_WINDOWS)
static_assert(IsSame<IPC::Transport, IPC::TransportSocketWindows>, "Need to handle other IPC transports here");
@ -210,12 +268,44 @@ ErrorOr<NonnullRefPtr<Core::LocalServer>> Session::create_server(NonnullRefPtr<S
static_assert(IsSame<IPC::Transport, IPC::TransportSocket>, "Need to handle other IPC transports here");
#endif
dbgln("Listening for WebDriver connection on {}", *m_web_content_socket_path);
dbgln("Listening for WebDriver connection on {}", m_web_content_endpoint);
(void)Core::System::unlink(*m_web_content_socket_path);
#if defined(AK_OS_MACOS)
m_web_content_mach_port_server = make<WebView::MachPortServer>(m_web_content_endpoint);
if (!m_web_content_mach_port_server->is_initialized())
return Error::from_string_literal("Failed to initialize Mach port server for WebDriver");
m_web_content_mach_port_server->on_receive_child_mach_port = [this, promise](auto registration) {
auto registration_result = m_transport_bootstrap_server.register_reply_port(registration.pid, move(registration.reply_port));
if (registration_result.is_error()) {
auto event_loop = m_event_loop->take();
VERIFY(event_loop);
event_loop->deferred_invoke([promise, error = registration_result.release_error()]() mutable {
promise->resolve(move(error));
});
return;
}
registration_result.release_value().visit(
[](IPC::TransportBootstrapMachServer::WaitingForChildTransport) {
VERIFY_NOT_REACHED();
},
[this, promise](IPC::TransportBootstrapMachServer::OnDemandTransport& transport) {
auto event_loop = m_event_loop->take();
VERIFY(event_loop);
event_loop->deferred_invoke([this, promise, transport = move(transport.ports)]() mutable {
if (auto result = accept_web_content_transport(make<IPC::Transport>(move(transport.receive_right), move(transport.send_right)), promise); result.is_error())
promise->resolve(result.release_error());
});
});
};
return {};
#else
(void)Core::System::unlink(m_web_content_endpoint);
auto server = Core::LocalServer::construct();
server->listen(*m_web_content_socket_path);
server->listen(m_web_content_endpoint);
server->on_accept = [this, promise](auto client_socket) {
auto maybe_transport = IPC::Transport::from_socket(move(client_socket));
@ -223,73 +313,31 @@ ErrorOr<NonnullRefPtr<Core::LocalServer>> Session::create_server(NonnullRefPtr<S
promise->resolve(maybe_transport.release_error());
return;
}
auto maybe_connection = adopt_nonnull_ref_or_enomem(new (nothrow) WebContentConnection(maybe_transport.release_value()));
if (maybe_connection.is_error()) {
promise->resolve(maybe_connection.release_error());
return;
}
dbgln("WebDriver is connected to WebContent socket");
auto web_content_connection = maybe_connection.release_value();
auto connection_id = m_next_pending_connection_id++;
web_content_connection->on_close = [this, promise, connection_id]() {
if (m_pending_connections.remove(connection_id)) {
dbgln_if(WEBDRIVER_DEBUG, "Pending connection {} closed before sending its handle", connection_id);
promise->reject(Error::from_string_literal("Window was closed before sending its handle"));
}
};
web_content_connection->on_did_set_window_handle = [this, promise, connection_id](String window_handle) {
auto maybe_pending_connection = m_pending_connections.take(connection_id);
if (!maybe_pending_connection.has_value())
return;
auto pending_connection = maybe_pending_connection.value();
pending_connection->on_did_set_window_handle = nullptr;
dbgln_if(WEBDRIVER_DEBUG, "Window {} registered with WebDriver.", window_handle);
pending_connection->on_close = [this, window_handle]() {
dbgln_if(WEBDRIVER_DEBUG, "Window {} was closed remotely.", window_handle);
m_windows.remove(window_handle);
if (m_windows.is_empty())
close();
};
pending_connection->async_set_page_load_strategy(m_page_load_strategy);
pending_connection->async_set_strict_file_interactability(m_strict_file_interactiblity);
pending_connection->async_set_user_prompt_handler(Web::WebDriver::user_prompt_handler());
if (m_timeouts_configuration.has_value())
pending_connection->async_set_timeouts(*m_timeouts_configuration);
m_windows.set(window_handle, Session::Window { window_handle, move(pending_connection) });
if (m_current_window_handle.is_empty())
m_current_window_handle = window_handle;
promise->resolve({});
};
m_pending_connections.set(connection_id, move(web_content_connection));
if (auto result = accept_web_content_transport(maybe_transport.release_value(), promise); result.is_error())
promise->resolve(result.release_error());
};
server->on_accept_error = [promise](auto error) {
promise->resolve(move(error));
};
return server;
m_web_content_server = server;
return {};
#endif
}
ErrorOr<void> Session::start(LaunchBrowserCallback const& launch_browser_callback)
{
auto promise = ServerPromise::construct();
m_web_content_socket_path = ByteString::formatted("{}/webdriver/session_{}_{}", TRY(Core::StandardPaths::runtime_directory()), Core::System::getpid(), m_session_id);
m_web_content_server = TRY(create_server(promise));
#if defined(AK_OS_MACOS)
m_web_content_endpoint = ByteString::formatted("{}.{}", WebView::mach_server_name_for_process("WebDriver"sv, Core::System::getpid()), m_session_id);
#else
m_web_content_endpoint = ByteString::formatted("{}/webdriver/session_{}_{}", TRY(Core::StandardPaths::runtime_directory()), Core::System::getpid(), m_session_id);
#endif
TRY(create_server(promise));
m_browser_process = TRY(launch_browser_callback(*m_web_content_socket_path, m_options.headless));
m_browser_process = TRY(launch_browser_callback(m_web_content_endpoint, m_options.headless));
// FIXME: Allow this to be more asynchronous. For now, this at least allows us to propagate
// errors received while accepting the Browser and WebContent sockets.

View file

@ -10,11 +10,18 @@
#include <AK/Error.h>
#include <AK/JsonValue.h>
#include <AK/NonnullRefPtr.h>
#include <AK/RefCounted.h>
#include <AK/RefPtr.h>
#include <AK/ScopeGuard.h>
#include <AK/String.h>
#include <LibCore/EventLoop.h>
#if !defined(AK_OS_MACOS)
# include <LibCore/LocalServer.h>
#else
# include <LibIPC/TransportBootstrapMach.h>
# include <LibWebView/MachPortServer.h>
#endif
#include <LibCore/Process.h>
#include <LibCore/Promise.h>
#include <LibWeb/WebDriver/Capabilities.h>
@ -85,10 +92,11 @@ public:
private:
Session(NonnullRefPtr<Client> client, JsonObject const& capabilities, String session_id, Web::WebDriver::SessionFlags flags);
ErrorOr<void> start(LaunchBrowserCallback const&);
using ServerPromise = Core::Promise<ErrorOr<void>>;
ErrorOr<NonnullRefPtr<Core::LocalServer>> create_server(NonnullRefPtr<ServerPromise> promise);
ErrorOr<void> start(LaunchBrowserCallback const&);
ErrorOr<void> accept_web_content_transport(NonnullOwnPtr<IPC::Transport>, NonnullRefPtr<ServerPromise> promise);
ErrorOr<void> create_server(NonnullRefPtr<ServerPromise> promise);
NonnullRefPtr<Client> m_client;
Web::WebDriver::LadybirdOptions m_options;
@ -102,10 +110,16 @@ private:
HashMap<u64, NonnullRefPtr<WebContentConnection>> m_pending_connections;
u64 m_next_pending_connection_id { 0 };
Optional<ByteString> m_web_content_socket_path;
ByteString m_web_content_endpoint;
Optional<Core::Process> m_browser_process;
NonnullRefPtr<Core::WeakEventLoopReference> m_event_loop;
#if defined(AK_OS_MACOS)
OwnPtr<WebView::MachPortServer> m_web_content_mach_port_server;
IPC::TransportBootstrapMachServer m_transport_bootstrap_server;
#else
RefPtr<Core::LocalServer> m_web_content_server;
#endif
Web::WebDriver::PageLoadStrategy m_page_load_strategy { Web::WebDriver::PageLoadStrategy::Normal };
Optional<JsonValue> m_timeouts_configuration;

View file

@ -33,12 +33,15 @@ static ErrorOr<Core::Process> launch_process(StringView application, ReadonlySpa
return result;
}
static Vector<ByteString> create_arguments(ByteString const& socket_path, bool headless, bool expose_experimental_interfaces, bool force_cpu_painting, Optional<StringView> debug_process, Optional<StringView> default_time_zone)
static Vector<ByteString> create_arguments(ByteString const& webdriver_endpoint, bool headless, bool expose_experimental_interfaces, bool force_cpu_painting, Optional<StringView> debug_process, Optional<StringView> default_time_zone)
{
Vector<ByteString> arguments {
"--webdriver-content-path"sv,
socket_path,
};
Vector<ByteString> arguments;
#if defined(AK_OS_MACOS)
arguments.append("--webdriver-mach-server-name"sv);
#else
arguments.append("--webdriver-content-path"sv);
#endif
arguments.append(webdriver_endpoint);
Vector<ByteString> certificate_args;
for (auto const& certificate : certificates) {
@ -131,8 +134,8 @@ ErrorOr<int> ladybird_main(Main::Arguments arguments)
return;
}
auto launch_browser_callback = [&](ByteString const& socket_path, bool headless) {
auto arguments = create_arguments(socket_path, headless, expose_experimental_interfaces, force_cpu_painting, debug_process, default_time_zone);
auto launch_browser_callback = [&](ByteString const& webdriver_endpoint, bool headless) {
auto arguments = create_arguments(webdriver_endpoint, headless, expose_experimental_interfaces, force_cpu_painting, debug_process, default_time_zone);
return launch_process("Ladybird"sv, arguments.span());
};