Ladybird+LibWebView: Add mechanism to get Mach task port for helpers

On macOS, it's not trivial to get a Mach task port for your children.
This implementation registers the chrome process as a well-known
service with launchd based on its pid, and lets each child process
send over a reference to its mach_task_self() back to the chrome.

We'll need this Mach task port right to get process statistics.
This commit is contained in:
Andrew Kaster 2024-04-04 14:10:08 -06:00 committed by Andrew Kaster
parent 77f18cf062
commit 8c5e64e686
Notes: sideshowbarker 2024-07-17 01:46:00 +09:00
14 changed files with 305 additions and 8 deletions

View file

@ -298,6 +298,10 @@
# cmakedefine01 LZW_DEBUG
#endif
#ifndef MACH_PORT_DEBUG
# cmakedefine01 MACH_PORT_DEBUG
#endif
#ifndef MALLOC_DEBUG
# cmakedefine01 MALLOC_DEBUG
#endif

View file

@ -173,6 +173,11 @@ else()
add_library(ladybird STATIC ${SOURCES})
endif()
if (APPLE)
target_sources(ladybird PRIVATE MachPortServer.cpp)
target_link_libraries(ladybird PRIVATE LibThreading)
endif()
target_sources(ladybird PUBLIC FILE_SET ladybird TYPE HEADERS
BASE_DIRS ${SERENITY_SOURCE_DIR}
FILES ${LADYBIRD_HEADERS}

View file

@ -0,0 +1,90 @@
/*
* Copyright (c) 2024, Andrew Kaster <akaster@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "MachPortServer.h"
#include <AK/Debug.h>
#include <LibWebView/Platform/ProcessStatisticsMach.h>
namespace Ladybird {
MachPortServer::MachPortServer()
: m_thread(Threading::Thread::construct([this]() -> intptr_t { thread_loop(); return 0; }, "MachPortServer"sv))
, m_server_port_name(ByteString::formatted("org.SerenityOS.Ladybird.helper.{}", getpid()))
{
if (auto err = allocate_server_port(); err.is_error())
dbgln("Failed to allocate server port: {}", err.error());
else
start();
}
MachPortServer::~MachPortServer()
{
stop();
}
void MachPortServer::start()
{
m_thread->start();
}
void MachPortServer::stop()
{
// FIXME: We should join instead (after storing should_stop = false) when we have a way to interrupt the thread's mach_msg call
m_thread->detach();
m_should_stop.store(true, MemoryOrder::memory_order_release);
}
bool MachPortServer::is_initialized()
{
return MACH_PORT_VALID(m_server_port_recv_right.port()) && MACH_PORT_VALID(m_server_port_send_right.port());
}
ErrorOr<void> MachPortServer::allocate_server_port()
{
m_server_port_recv_right = TRY(Core::MachPort::create_with_right(Core::MachPort::PortRight::Receive));
m_server_port_send_right = TRY(m_server_port_recv_right.insert_right(Core::MachPort::MessageRight::MakeSend));
TRY(m_server_port_recv_right.register_with_bootstrap_server(m_server_port_name));
dbgln_if(MACH_PORT_DEBUG, "Success! we created and attached mach port {:x} to bootstrap server with name {}", m_server_port_recv_right.port(), m_server_port_name);
return {};
}
void MachPortServer::thread_loop()
{
while (!m_should_stop.load(MemoryOrder::memory_order_acquire)) {
WebView::ParentPortMessage message {};
// Get the pid of the child from the audit trailer so we can associate the port w/it
mach_msg_options_t const options = MACH_RCV_MSG | MACH_RCV_TRAILER_TYPE(MACH_RCV_TRAILER_AUDIT) | MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AUDIT);
// FIXME: How can we interrupt this call during application shutdown?
auto const ret = mach_msg(&message.header, options, 0, sizeof(message), m_server_port_recv_right.port(), MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
if (ret != KERN_SUCCESS) {
dbgln("mach_msg failed: {}", mach_error_string(ret));
break;
}
if (message.header.msgh_id != WebView::SELF_TASK_PORT_MESSAGE_ID) {
dbgln("Received message with id {}, ignoring", message.header.msgh_id);
continue;
}
if (MACH_MSGH_BITS_LOCAL(message.header.msgh_bits) != MACH_MSG_TYPE_MOVE_SEND) {
dbgln("Received message with invalid local port rights {}, ignoring", MACH_MSGH_BITS_LOCAL(message.header.msgh_bits));
continue;
}
auto pid = static_cast<pid_t>(message.trailer.msgh_audit.val[5]);
auto child_port = Core::MachPort::adopt_right(message.port_descriptor.name, Core::MachPort::PortRight::Send);
dbgln_if(MACH_PORT_DEBUG, "Received child port {:x} from pid {}", child_port.port(), pid);
if (on_receive_child_mach_port)
on_receive_child_mach_port(pid, move(child_port));
}
}
}

49
Ladybird/MachPortServer.h Normal file
View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2024, Andrew Kaster <akaster@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Platform.h>
#if !defined(AK_OS_MACH)
# error "This file is only for Mach kernel-based OS's"
#endif
#include <AK/Atomic.h>
#include <AK/String.h>
#include <LibCore/MachPort.h>
#include <LibThreading/Thread.h>
namespace Ladybird {
class MachPortServer {
public:
MachPortServer();
~MachPortServer();
void start();
void stop();
bool is_initialized();
Function<void(pid_t, Core::MachPort)> on_receive_child_mach_port;
ByteString const& server_port_name() const { return m_server_port_name; }
private:
void thread_loop();
ErrorOr<void> allocate_server_port();
NonnullRefPtr<Threading::Thread> m_thread;
ByteString const m_server_port_name;
Core::MachPort m_server_port_recv_right;
Core::MachPort m_server_port_send_right;
Atomic<bool> m_should_stop { false };
};
}

View file

@ -114,6 +114,7 @@ set(LOOKUPSERVER_DEBUG ON)
set(LOOPBACK_DEBUG ON)
set(LZMA_DEBUG ON)
set(LZW_DEBUG ON)
set(MACH_PORT_DEBUG ON)
set(MALLOC_DEBUG ON)
set(MARKDOWN_DEBUG ON)
set(MATROSKA_DEBUG ON)

View file

@ -302,6 +302,7 @@ write_cmake_config("ak_debug_gen") {
"LOOKUPSERVER_DEBUG=",
"LZMA_DEBUG=",
"LZW_DEBUG=",
"MACH_PORT_DEBUG=",
"MALLOC_DEBUG=",
"MARKDOWN_DEBUG=",
"MATROSKA_DEBUG=",

View file

@ -145,7 +145,10 @@ executable("ladybird_executable") {
]
}
if (current_os != "mac") {
if (current_os == "mac") {
sources += [ "MachPortServer.cpp" ]
deps += [ "//Userland/Libraries/LibThreading" ]
} else {
data_deps += [
":ladybird_copy_config_resources",
":ladybird_copy_emoji",

View file

@ -154,6 +154,8 @@ shared_library("LibWebView") {
]
} else if (current_os == "linux") {
sources += [ "Platform/ProcessStatisticsLinux.cpp" ]
} else if (current_os == "mac") {
sources += [ "Platform/ProcessStatisticsMach.cpp" ]
} else {
sources += [ "Platform/ProcessStatisticsNoop.cpp" ]
}

View file

@ -31,6 +31,10 @@ elseif(LINUX AND NOT ANDROID)
list(APPEND SOURCES
Platform/ProcessStatisticsLinux.cpp
)
elseif(APPLE)
list(APPEND SOURCES
Platform/ProcessStatisticsMach.cpp
)
else()
list(APPEND SOURCES
Platform/ProcessStatisticsNoop.cpp

View file

@ -8,6 +8,10 @@
#include <AK/Types.h>
#if defined(AK_OS_MACH)
# include <LibCore/MachPort.h>
#endif
namespace WebView {
enum class ProcessType {
@ -20,12 +24,28 @@ enum class ProcessType {
};
struct ProcessInfo {
ProcessType type;
ProcessInfo(ProcessType type, pid_t pid)
: type(type)
, pid(pid)
{
}
ProcessType type = ProcessType::WebContent;
pid_t pid;
u64 memory_usage_bytes = 0;
float cpu_percent = 0.0f;
u64 time_spent_in_process = 0;
#if defined(AK_OS_MACH)
Core::MachPort child_task_port;
ProcessInfo(pid_t pid, Core::MachPort&& port)
: pid(pid)
, child_task_port(move(port))
{
}
#endif
};
}

View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2024, Andrew Kaster <akaster@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Platform.h>
#if !defined(AK_OS_MACH)
# error "This file is only available on Mach platforms"
#endif
#include <AK/ByteString.h>
#include <LibCore/MachPort.h>
#include <LibWebView/Platform/ProcessStatisticsMach.h>
namespace WebView {
ErrorOr<void> update_process_statistics(ProcessStatistics&)
{
return {};
}
void register_with_mach_server(ByteString const& server_name)
{
auto server_port_or_error = Core::MachPort::look_up_from_bootstrap_server(server_name);
if (server_port_or_error.is_error()) {
dbgln("Failed to lookup server port: {}", server_port_or_error.error());
return;
}
auto server_port = server_port_or_error.release_value();
// Send our own task port to the server so they can query statistics about us
ChildPortMessage message {};
message.header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, MACH_MSGH_BITS_ZERO) | MACH_MSGH_BITS_COMPLEX;
message.header.msgh_size = sizeof(message);
message.header.msgh_remote_port = server_port.port();
message.header.msgh_local_port = MACH_PORT_NULL;
message.header.msgh_id = SELF_TASK_PORT_MESSAGE_ID;
message.body.msgh_descriptor_count = 1;
message.port_descriptor.name = mach_task_self();
message.port_descriptor.disposition = MACH_MSG_TYPE_COPY_SEND;
message.port_descriptor.type = MACH_MSG_PORT_DESCRIPTOR;
mach_msg_timeout_t const timeout = 100; // milliseconds
auto const send_result = mach_msg(&message.header, MACH_SEND_MSG | MACH_SEND_TIMEOUT, message.header.msgh_size, 0, MACH_PORT_NULL, timeout, MACH_PORT_NULL);
if (send_result != KERN_SUCCESS) {
dbgln("Failed to send message to server: {}", mach_error_string(send_result));
return;
}
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2024, Andrew Kaster <akaster@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Platform.h>
#if !defined(AK_OS_MACH)
# error "This file is only available on Mach platforms"
#endif
#include <LibWebView/Platform/ProcessStatistics.h>
#include <mach/mach.h>
namespace WebView {
struct ChildPortMessage {
mach_msg_header_t header;
mach_msg_body_t body;
mach_msg_port_descriptor_t port_descriptor;
};
struct ParentPortMessage {
mach_msg_header_t header;
mach_msg_body_t body;
mach_msg_port_descriptor_t port_descriptor;
mach_msg_audit_trailer_t trailer; // for the child's pid
};
static constexpr mach_msg_id_t SELF_TASK_PORT_MESSAGE_ID = 0x1234CAFE;
void register_with_mach_server(ByteString const& server_name);
}

View file

@ -84,12 +84,31 @@ void ProcessManager::initialize()
void ProcessManager::add_process(ProcessType type, pid_t pid)
{
Threading::MutexLocker locker { m_lock };
dbgln("ProcessManager::add_process({}, {})", process_name_from_type(type), pid);
m_statistics.processes.append({ type, pid, 0, 0 });
if (auto existing_process = m_statistics.processes.find_if([&](auto& info) { return info.pid == pid; }); !existing_process.is_end()) {
existing_process->type = type;
return;
}
m_statistics.processes.append({ type, pid });
}
#if defined(AK_OS_MACH)
void ProcessManager::add_process(pid_t pid, Core::MachPort&& port)
{
Threading::MutexLocker locker { m_lock };
dbgln("ProcessManager::add_process({}, {:p})", pid, port.port());
if (auto existing_process = m_statistics.processes.find_if([&](auto& info) { return info.pid == pid; }); !existing_process.is_end()) {
existing_process->child_task_port = move(port);
return;
}
m_statistics.processes.append({ pid, move(port) });
}
#endif
void ProcessManager::remove_process(pid_t pid)
{
Threading::MutexLocker locker { m_lock };
m_statistics.processes.remove_first_matching([&](auto& info) {
if (info.pid == pid) {
dbgln("ProcessManager: Remove process {} ({})", process_name_from_type(info.type), pid);
@ -113,13 +132,15 @@ void ProcessManager::update_all_processes()
}
}
Threading::MutexLocker locker { m_lock };
(void)update_process_statistics(m_statistics);
}
String ProcessManager::generate_html()
{
Threading::MutexLocker locker { m_lock };
StringBuilder builder;
auto processes = m_statistics.processes;
auto const& processes = m_statistics.processes;
builder.append(R"(
<html>
@ -170,7 +191,7 @@ String ProcessManager::generate_html()
<tbody>
)"sv);
for (auto& process : processes) {
for (auto const& process : processes) {
builder.append("<tr>"sv);
builder.append("<td>"sv);
builder.append(WebView::process_name_from_type(process.type));

View file

@ -4,14 +4,15 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Types.h>
#include <AK/Vector.h>
#include <LibCore/EventReceiver.h>
#include <LibThreading/Mutex.h>
#include <LibWebView/Forward.h>
#include <LibWebView/Platform/ProcessStatistics.h>
#pragma once
namespace WebView {
ProcessType process_type_from_name(StringView);
@ -25,8 +26,12 @@ public:
void add_process(WebView::ProcessType, pid_t);
void remove_process(pid_t);
#if defined(AK_OS_MACH)
void add_process(pid_t, Core::MachPort&&);
#endif
void update_all_processes();
Vector<ProcessInfo> processes() const { return m_statistics.processes; }
Vector<ProcessInfo> const& processes() const { return m_statistics.processes; }
String generate_html();
@ -35,6 +40,7 @@ private:
~ProcessManager();
ProcessStatistics m_statistics;
Threading::Mutex m_lock;
};
}