blob: 1e833c4cb16580fd37290aed49b4b41d64a63aa8 [file] [log] [blame]
//===-- DarwinLogCollector.cpp ----------------------------------*- C++ -*-===//
//
// The LLVM Compiler Infrastructure
//
// This file is distributed under the University of Illinois Open Source
// License. See LICENSE.TXT for details.
//
//===----------------------------------------------------------------------===//
#include "DarwinLogCollector.h"
#include "ActivityStreamSPI.h"
#include <dlfcn.h>
#include <cinttypes>
#include <mutex>
#include <vector>
#include "DNB.h"
#include "DNBLog.h"
#include "DarwinLogTypes.h"
#include "LogFilterChain.h"
#include "LogFilterExactMatch.h"
#include "LogFilterRegex.h"
#include "LogMessageOsLog.h"
#include "MachProcess.h"
#include "RNBContext.h"
#include "RNBDefs.h"
#include "RNBRemote.h"
// Use an anonymous namespace for variables and methods that have no
// reason to leak out through the interface.
namespace {
/// Specify max depth that the activity parent-child chain will search
/// back to get the full activity chain name. If we do more than this,
/// we assume either we hit a loop or it's just too long.
static const size_t MAX_ACTIVITY_CHAIN_DEPTH = 10;
// Used to tap into and retrieve logs from target process.
// (Consumer of os_log).
static os_activity_stream_for_pid_t s_os_activity_stream_for_pid;
static os_activity_stream_resume_t s_os_activity_stream_resume;
static os_activity_stream_cancel_t s_os_activity_stream_cancel;
static os_log_copy_formatted_message_t s_os_log_copy_formatted_message;
static os_activity_stream_set_event_handler_t
s_os_activity_stream_set_event_handler;
bool LookupSPICalls() {
static std::once_flag s_once_flag;
static bool s_has_spi;
std::call_once(s_once_flag, [] {
dlopen ("/System/Library/PrivateFrameworks/LoggingSupport.framework/LoggingSupport", RTLD_NOW);
s_os_activity_stream_for_pid = (os_activity_stream_for_pid_t)dlsym(
RTLD_DEFAULT, "os_activity_stream_for_pid");
s_os_activity_stream_resume = (os_activity_stream_resume_t)dlsym(
RTLD_DEFAULT, "os_activity_stream_resume");
s_os_activity_stream_cancel = (os_activity_stream_cancel_t)dlsym(
RTLD_DEFAULT, "os_activity_stream_cancel");
s_os_log_copy_formatted_message = (os_log_copy_formatted_message_t)dlsym(
RTLD_DEFAULT, "os_log_copy_formatted_message");
s_os_activity_stream_set_event_handler =
(os_activity_stream_set_event_handler_t)dlsym(
RTLD_DEFAULT, "os_activity_stream_set_event_handler");
// We'll indicate we're all set if every function entry point
// was found.
s_has_spi = (s_os_activity_stream_for_pid != nullptr) &&
(s_os_activity_stream_resume != nullptr) &&
(s_os_activity_stream_cancel != nullptr) &&
(s_os_log_copy_formatted_message != nullptr) &&
(s_os_activity_stream_set_event_handler != nullptr);
if (s_has_spi) {
DNBLogThreadedIf(LOG_DARWIN_LOG, "Found os_log SPI calls.");
// Tell LogMessageOsLog how to format messages when search
// criteria requires it.
LogMessageOsLog::SetFormatterFunction(s_os_log_copy_formatted_message);
} else {
DNBLogThreadedIf(LOG_DARWIN_LOG, "Failed to find os_log SPI "
"calls.");
}
});
return s_has_spi;
}
using Mutex = std::mutex;
static Mutex s_collector_mutex;
static std::vector<DarwinLogCollectorSP> s_collectors;
static void TrackCollector(const DarwinLogCollectorSP &collector_sp) {
std::lock_guard<Mutex> locker(s_collector_mutex);
if (std::find(s_collectors.begin(), s_collectors.end(), collector_sp) !=
s_collectors.end()) {
DNBLogThreadedIf(LOG_DARWIN_LOG,
"attempted to add same collector multiple times");
return;
}
s_collectors.push_back(collector_sp);
}
static void StopTrackingCollector(const DarwinLogCollectorSP &collector_sp) {
std::lock_guard<Mutex> locker(s_collector_mutex);
s_collectors.erase(
std::remove(s_collectors.begin(), s_collectors.end(), collector_sp),
s_collectors.end());
}
static DarwinLogCollectorSP FindCollectorForProcess(pid_t pid) {
std::lock_guard<Mutex> locker(s_collector_mutex);
for (const auto &collector_sp : s_collectors) {
if (collector_sp && (collector_sp->GetProcessID() == pid))
return collector_sp;
}
return DarwinLogCollectorSP();
}
static FilterTarget TargetStringToEnum(const std::string &filter_target_name) {
if (filter_target_name == "activity")
return eFilterTargetActivity;
else if (filter_target_name == "activity-chain")
return eFilterTargetActivityChain;
else if (filter_target_name == "category")
return eFilterTargetCategory;
else if (filter_target_name == "message")
return eFilterTargetMessage;
else if (filter_target_name == "subsystem")
return eFilterTargetSubsystem;
else
return eFilterTargetInvalid;
}
class Configuration {
public:
Configuration(const JSONObject &config)
: m_is_valid(false),
m_activity_stream_flags(OS_ACTIVITY_STREAM_PROCESS_ONLY),
m_filter_chain_sp(nullptr) {
// Parse out activity stream flags
if (!ParseSourceFlags(config)) {
m_is_valid = false;
return;
}
// Parse filter rules
if (!ParseFilterRules(config)) {
m_is_valid = false;
return;
}
// Everything worked.
m_is_valid = true;
}
bool ParseSourceFlags(const JSONObject &config) {
// Get the source-flags dictionary.
auto source_flags_sp = config.GetObject("source-flags");
if (!source_flags_sp)
return false;
if (!JSONObject::classof(source_flags_sp.get()))
return false;
const JSONObject &source_flags =
*static_cast<JSONObject *>(source_flags_sp.get());
// Parse out the flags.
bool include_any_process = false;
bool include_callstacks = false;
bool include_info_level = false;
bool include_debug_level = false;
bool live_stream = false;
if (!source_flags.GetObjectAsBool("any-process", include_any_process)) {
DNBLogThreadedIf(LOG_DARWIN_LOG, "Source-flag 'any-process' missing from "
"configuration.");
return false;
}
if (!source_flags.GetObjectAsBool("callstacks", include_callstacks)) {
// We currently suppress the availability of this on the lldb
// side. We include here for devices when we enable in the
// future.
// DNBLogThreadedIf(LOG_DARWIN_LOG,
// "Source-flag 'callstacks' missing from "
// "configuration.");
// OK. We just skip callstacks.
// return false;
}
if (!source_flags.GetObjectAsBool("info-level", include_info_level)) {
DNBLogThreadedIf(LOG_DARWIN_LOG, "Source-flag 'info-level' missing from "
"configuration.");
return false;
}
if (!source_flags.GetObjectAsBool("debug-level", include_debug_level)) {
DNBLogThreadedIf(LOG_DARWIN_LOG, "Source-flag 'debug-level' missing from "
"configuration.");
return false;
}
if (!source_flags.GetObjectAsBool("live-stream", live_stream)) {
DNBLogThreadedIf(LOG_DARWIN_LOG, "Source-flag 'live-stream' missing from "
"configuration.");
return false;
}
// Setup the SPI flags based on this.
m_activity_stream_flags = 0;
if (!include_any_process)
m_activity_stream_flags |= OS_ACTIVITY_STREAM_PROCESS_ONLY;
if (include_callstacks)
m_activity_stream_flags |= OS_ACTIVITY_STREAM_CALLSTACK;
if (include_info_level)
m_activity_stream_flags |= OS_ACTIVITY_STREAM_INFO;
if (include_debug_level)
m_activity_stream_flags |= OS_ACTIVITY_STREAM_DEBUG;
if (!live_stream)
m_activity_stream_flags |= OS_ACTIVITY_STREAM_BUFFERED;
DNBLogThreadedIf(LOG_DARWIN_LOG, "m_activity_stream_flags = 0x%03x",
m_activity_stream_flags);
return true;
}
bool ParseFilterRules(const JSONObject &config) {
// Retrieve the default rule.
bool filter_default_accept = true;
if (!config.GetObjectAsBool("filter-fall-through-accepts",
filter_default_accept)) {
DNBLogThreadedIf(LOG_DARWIN_LOG, "Setting 'filter-fall-through-accepts' "
"missing from configuration.");
return false;
}
m_filter_chain_sp.reset(new LogFilterChain(filter_default_accept));
DNBLogThreadedIf(LOG_DARWIN_LOG, "DarwinLog no-match rule: %s.",
filter_default_accept ? "accept" : "reject");
// If we don't have the filter-rules array, we're done.
auto filter_rules_sp = config.GetObject("filter-rules");
if (!filter_rules_sp) {
DNBLogThreadedIf(LOG_DARWIN_LOG,
"No 'filter-rules' config element, all log "
"entries will use the no-match action (%s).",
filter_default_accept ? "accept" : "reject");
return true;
}
if (!JSONArray::classof(filter_rules_sp.get()))
return false;
const JSONArray &rules_config =
*static_cast<JSONArray *>(filter_rules_sp.get());
// Create the filters.
for (auto &rule_sp : rules_config.m_elements) {
if (!JSONObject::classof(rule_sp.get()))
return false;
const JSONObject &rule_config = *static_cast<JSONObject *>(rule_sp.get());
// Get whether this filter accepts or rejects.
bool filter_accepts = true;
if (!rule_config.GetObjectAsBool("accept", filter_accepts)) {
DNBLogThreadedIf(LOG_DARWIN_LOG, "Filter 'accept' element missing.");
return false;
}
// Grab the target log field attribute for the match.
std::string target_attribute;
if (!rule_config.GetObjectAsString("attribute", target_attribute)) {
DNBLogThreadedIf(LOG_DARWIN_LOG, "Filter 'attribute' element missing.");
return false;
}
auto target_enum = TargetStringToEnum(target_attribute);
if (target_enum == eFilterTargetInvalid) {
DNBLogThreadedIf(LOG_DARWIN_LOG, "Filter attribute '%s' unsupported.",
target_attribute.c_str());
return false;
}
// Handle operation-specific fields and filter creation.
std::string filter_type;
if (!rule_config.GetObjectAsString("type", filter_type)) {
DNBLogThreadedIf(LOG_DARWIN_LOG, "Filter 'type' element missing.");
return false;
}
DNBLogThreadedIf(LOG_DARWIN_LOG, "Reading filter of type '%s'",
filter_type.c_str());
LogFilterSP filter_sp;
if (filter_type == "regex") {
// Grab the regex for the match.
std::string regex;
if (!rule_config.GetObjectAsString("regex", regex)) {
DNBLogError("Regex filter missing 'regex' element.");
return false;
}
DNBLogThreadedIf(LOG_DARWIN_LOG, "regex for filter: \"%s\"",
regex.c_str());
// Create the regex filter.
auto regex_filter =
new LogFilterRegex(filter_accepts, target_enum, regex);
filter_sp.reset(regex_filter);
// Validate that the filter is okay.
if (!regex_filter->IsValid()) {
DNBLogError("Invalid regex in filter: "
"regex=\"%s\", error=%s",
regex.c_str(), regex_filter->GetErrorAsCString());
return false;
}
} else if (filter_type == "match") {
// Grab the regex for the match.
std::string exact_text;
if (!rule_config.GetObjectAsString("exact_text", exact_text)) {
DNBLogError("Exact match filter missing "
"'exact_text' element.");
return false;
}
// Create the filter.
filter_sp.reset(
new LogFilterExactMatch(filter_accepts, target_enum, exact_text));
}
// Add the filter to the chain.
m_filter_chain_sp->AppendFilter(filter_sp);
}
return true;
}
bool IsValid() const { return m_is_valid; }
os_activity_stream_flag_t GetActivityStreamFlags() const {
return m_activity_stream_flags;
}
const LogFilterChainSP &GetLogFilterChain() const {
return m_filter_chain_sp;
}
private:
bool m_is_valid;
os_activity_stream_flag_t m_activity_stream_flags;
LogFilterChainSP m_filter_chain_sp;
};
}
bool DarwinLogCollector::IsSupported() {
// We're supported if we have successfully looked up the SPI entry points.
return LookupSPICalls();
}
bool DarwinLogCollector::StartCollectingForProcess(nub_process_t pid,
const JSONObject &config) {
// If we're currently collecting for this process, kill the existing
// collector.
if (CancelStreamForProcess(pid)) {
DNBLogThreadedIf(LOG_DARWIN_LOG,
"%s() killed existing DarwinLog collector for pid %d.",
__FUNCTION__, pid);
}
// If the process isn't alive, we're done.
if (!DNBProcessIsAlive(pid)) {
DNBLogThreadedIf(LOG_DARWIN_LOG,
"%s() cannot collect for pid %d: process not alive.",
__FUNCTION__, pid);
return false;
}
// Validate the configuration.
auto spi_config = Configuration(config);
if (!spi_config.IsValid()) {
DNBLogThreadedIf(LOG_DARWIN_LOG,
"%s() invalid configuration, will not enable log "
"collection",
__FUNCTION__);
return false;
}
// Create the stream collector that will manage collected data
// for this pid.
DarwinLogCollectorSP collector_sp(
new DarwinLogCollector(pid, spi_config.GetLogFilterChain()));
std::weak_ptr<DarwinLogCollector> collector_wp(collector_sp);
// Setup the stream handling block.
os_activity_stream_block_t block =
^bool(os_activity_stream_entry_t entry, int error) {
// Check if our collector is still alive.
DarwinLogCollectorSP inner_collector_sp = collector_wp.lock();
if (!inner_collector_sp)
return false;
return inner_collector_sp->HandleStreamEntry(entry, error);
};
os_activity_stream_event_block_t stream_event_block = ^void(
os_activity_stream_t stream, os_activity_stream_event_t event) {
switch (event) {
case OS_ACTIVITY_STREAM_EVENT_STARTED:
DNBLogThreadedIf(LOG_DARWIN_LOG,
"received stream event: "
"OS_ACTIVITY_STREAM_EVENT_STARTED, stream %p.",
(void *)stream);
break;
case OS_ACTIVITY_STREAM_EVENT_STOPPED:
DNBLogThreadedIf(LOG_DARWIN_LOG,
"received stream event: "
"OS_ACTIVITY_STREAM_EVENT_STOPPED, stream %p.",
(void *)stream);
break;
case OS_ACTIVITY_STREAM_EVENT_FAILED:
DNBLogThreadedIf(LOG_DARWIN_LOG,
"received stream event: "
"OS_ACTIVITY_STREAM_EVENT_FAILED, stream %p.",
(void *)stream);
break;
case OS_ACTIVITY_STREAM_EVENT_CHUNK_STARTED:
DNBLogThreadedIf(LOG_DARWIN_LOG,
"received stream event: "
"OS_ACTIVITY_STREAM_EVENT_CHUNK_STARTED, stream %p.",
(void *)stream);
break;
case OS_ACTIVITY_STREAM_EVENT_CHUNK_FINISHED:
DNBLogThreadedIf(LOG_DARWIN_LOG,
"received stream event: "
"OS_ACTIVITY_STREAM_EVENT_CHUNK_FINISHED, stream %p.",
(void *)stream);
break;
}
};
// Create the stream.
os_activity_stream_t activity_stream = (*s_os_activity_stream_for_pid)(
pid, spi_config.GetActivityStreamFlags(), block);
collector_sp->SetActivityStream(activity_stream);
// Specify the stream-related event handler.
(*s_os_activity_stream_set_event_handler)(activity_stream,
stream_event_block);
// Start the stream.
(*s_os_activity_stream_resume)(activity_stream);
TrackCollector(collector_sp);
return true;
}
DarwinLogEventVector
DarwinLogCollector::GetEventsForProcess(nub_process_t pid) {
auto collector_sp = FindCollectorForProcess(pid);
if (!collector_sp) {
// We're not tracking a stream for this process.
return DarwinLogEventVector();
}
return collector_sp->RemoveEvents();
}
bool DarwinLogCollector::CancelStreamForProcess(nub_process_t pid) {
auto collector_sp = FindCollectorForProcess(pid);
if (!collector_sp) {
// We're not tracking a stream for this process.
return false;
}
collector_sp->CancelActivityStream();
StopTrackingCollector(collector_sp);
return true;
}
const char *
DarwinLogCollector::GetActivityForID(os_activity_id_t activity_id) const {
auto find_it = m_activity_map.find(activity_id);
return (find_it != m_activity_map.end()) ? find_it->second.m_name.c_str()
: nullptr;
}
/// Retrieve the full parent-child chain for activity names. These
/// can be arbitrarily deep. This method assumes the caller has already
/// locked the activity mutex.
void DarwinLogCollector::GetActivityChainForID_internal(
os_activity_id_t activity_id, std::string &result, size_t depth) const {
if (depth > MAX_ACTIVITY_CHAIN_DEPTH) {
// Terminating condition - too deeply nested.
return;
} else if (activity_id == 0) {
// Terminating condition - no activity.
return;
}
auto find_it = m_activity_map.find(activity_id);
if (find_it == m_activity_map.end()) {
// Terminating condition - no data for activity_id.
return;
}
// Activity name becomes parent activity name chain + ':' + our activity
// name.
GetActivityChainForID_internal(find_it->second.m_parent_id, result,
depth + 1);
if (!result.empty())
result += ':';
result += find_it->second.m_name;
}
std::string
DarwinLogCollector::GetActivityChainForID(os_activity_id_t activity_id) const {
std::string result;
{
std::lock_guard<std::mutex> locker(m_activity_info_mutex);
GetActivityChainForID_internal(activity_id, result, 1);
}
return result;
}
DarwinLogCollector::DarwinLogCollector(nub_process_t pid,
const LogFilterChainSP &filter_chain_sp)
: ActivityStore(), m_pid(pid), m_activity_stream(0), m_events(),
m_events_mutex(), m_filter_chain_sp(filter_chain_sp),
m_activity_info_mutex(), m_activity_map() {}
DarwinLogCollector::~DarwinLogCollector() {
// Cancel the stream.
if (m_activity_stream) {
DNBLogThreadedIf(LOG_DARWIN_LOG, "tearing down activity stream "
"collector for %d",
m_pid);
(*s_os_activity_stream_cancel)(m_activity_stream);
m_activity_stream = 0;
} else {
DNBLogThreadedIf(LOG_DARWIN_LOG, "no stream to tear down for %d", m_pid);
}
}
void DarwinLogCollector::SignalDataAvailable() {
RNBRemoteSP remoteSP(g_remoteSP);
if (!remoteSP) {
// We're done. This is unexpected.
StopTrackingCollector(shared_from_this());
return;
}
RNBContext &ctx = remoteSP->Context();
ctx.Events().SetEvents(RNBContext::event_darwin_log_data_available);
// Wait for the main thread to consume this notification if it requested
// we wait for it.
ctx.Events().WaitForResetAck(RNBContext::event_darwin_log_data_available);
}
void DarwinLogCollector::SetActivityStream(
os_activity_stream_t activity_stream) {
m_activity_stream = activity_stream;
}
bool DarwinLogCollector::HandleStreamEntry(os_activity_stream_entry_t entry,
int error) {
if ((error == 0) && (entry != nullptr)) {
if (entry->pid != m_pid) {
// For now, skip messages not originating from our process.
// Later we might want to keep all messages related to an event
// that we're tracking, even when it came from another process,
// possibly doing work on our behalf.
return true;
}
switch (entry->type) {
case OS_ACTIVITY_STREAM_TYPE_ACTIVITY_CREATE:
DNBLogThreadedIf(
LOG_DARWIN_LOG, "received activity create: "
"%s, creator aid %" PRIu64 ", unique_pid %" PRIu64
"(activity id=%" PRIu64 ", parent id=%" PRIu64 ")",
entry->activity_create.name, entry->activity_create.creator_aid,
entry->activity_create.unique_pid, entry->activity_id,
entry->parent_id);
{
std::lock_guard<std::mutex> locker(m_activity_info_mutex);
m_activity_map.insert(
std::make_pair(entry->activity_id,
ActivityInfo(entry->activity_create.name,
entry->activity_id, entry->parent_id)));
}
break;
case OS_ACTIVITY_STREAM_TYPE_ACTIVITY_TRANSITION:
DNBLogThreadedIf(
LOG_DARWIN_LOG, "received activity transition:"
"new aid: %" PRIu64 "(activity id=%" PRIu64
", parent id=%" PRIu64 ", tid %" PRIu64 ")",
entry->activity_transition.transition_id, entry->activity_id,
entry->parent_id, entry->activity_transition.thread);
break;
case OS_ACTIVITY_STREAM_TYPE_LOG_MESSAGE: {
DNBLogThreadedIf(
LOG_DARWIN_LOG, "received log message: "
"(activity id=%" PRIu64 ", parent id=%" PRIu64 ", "
"tid %" PRIu64 "): format %s",
entry->activity_id, entry->parent_id, entry->log_message.thread,
entry->log_message.format ? entry->log_message.format
: "<invalid-format>");
// Do the real work here.
{
// Ensure our process is still alive. If not, we can
// cancel the collection.
if (!DNBProcessIsAlive(m_pid)) {
// We're outta here. This is the manner in which we
// stop collecting for a process.
StopTrackingCollector(shared_from_this());
return false;
}
LogMessageOsLog os_log_message(*this, *entry);
if (!m_filter_chain_sp ||
!m_filter_chain_sp->GetAcceptMessage(os_log_message)) {
// This log message was rejected by the filter,
// so stop processing it now.
return true;
}
// Copy over the relevant bits from the message.
const struct os_log_message_s &log_message = entry->log_message;
DarwinLogEventSP message_sp(new DarwinLogEvent());
// Indicate this event is a log message event.
message_sp->AddStringItem("type", "log");
// Add the message contents (fully expanded).
// Consider expanding on the remote side.
// Then we don't pay for expansion until when it is
// used.
const char *message_text = os_log_message.GetMessage();
if (message_text)
message_sp->AddStringItem("message", message_text);
// Add some useful data fields.
message_sp->AddIntegerItem("timestamp", log_message.timestamp);
// Do we want to do all activity name resolution on this
// side? Maybe. For now, send IDs and ID->name mappings
// and fix this up on that side. Later, when we add
// debugserver-side filtering, we'll want to get the
// activity names over here, so we should probably
// just send them as resolved strings.
message_sp->AddIntegerItem("activity_id", entry->activity_id);
message_sp->AddIntegerItem("parent_id", entry->parent_id);
message_sp->AddIntegerItem("thread_id", log_message.thread);
if (log_message.subsystem && strlen(log_message.subsystem) > 0)
message_sp->AddStringItem("subsystem", log_message.subsystem);
if (log_message.category && strlen(log_message.category) > 0)
message_sp->AddStringItem("category", log_message.category);
if (entry->activity_id != 0) {
std::string activity_chain =
GetActivityChainForID(entry->activity_id);
if (!activity_chain.empty())
message_sp->AddStringItem("activity-chain", activity_chain);
}
// Add it to the list for later collection.
{
std::lock_guard<std::mutex> locker(m_events_mutex);
m_events.push_back(message_sp);
}
SignalDataAvailable();
}
break;
}
}
} else {
DNBLogThreadedIf(LOG_DARWIN_LOG, "HandleStreamEntry: final call, "
"error %d",
error);
}
return true;
}
DarwinLogEventVector DarwinLogCollector::RemoveEvents() {
DarwinLogEventVector returned_events;
{
std::lock_guard<std::mutex> locker(m_events_mutex);
returned_events.swap(m_events);
}
DNBLogThreadedIf(LOG_DARWIN_LOG, "DarwinLogCollector::%s(): removing %lu "
"queued log entries",
__FUNCTION__, returned_events.size());
return returned_events;
}
void DarwinLogCollector::CancelActivityStream() {
if (!m_activity_stream)
return;
DNBLogThreadedIf(LOG_DARWIN_LOG, "DarwinLogCollector::%s(): canceling "
"activity stream %p",
__FUNCTION__, reinterpret_cast<void *>(m_activity_stream));
(*s_os_activity_stream_cancel)(m_activity_stream);
m_activity_stream = nullptr;
}