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
Notes: github-actions[bot] 2026-03-23 17:52:42 +00:00
24 changed files with 353 additions and 194 deletions

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());
};