blob: a83a72e4125489c7d233cea7b089bb3c94048f47 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.telemetry;
import android.annotation.Nullable;
import android.net.LocalSocket;
import android.net.LocalSocketAddress;
import android.os.Bundle;
import android.os.Process;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.ObjIntConsumer;
import java.util.function.Supplier;
/**
* Reads Android logs while there are log listeners registered to it and sends the event through the
* mLogEventConsumer.
*/
public class LogcatReader {
private static final String TAG = LogcatReader.class.getSimpleName();
// TODO(b/180515554). Find a proper place for LOG_* constants.
// They will be used in ScriptExecutor as well.
/** The value of key to retrieve log seconds since epoch time from bundle. */
private static final String LOG_SEC_KEY = "log.sec";
/** The value of key to retrieve log nanoseconds from bundle. */
private static final String LOG_NSEC_KEY = "log.nsec";
/** The value of key to retrieve log tag from bundle. */
private static final String LOG_TAG_KEY = "log.tag";
/** The value of key to retrieve log message from bundle. */
private static final String LOG_MESSAGE_KEY = "log.message";
// Defined in system/core/liblog/include/log/log_read.h
private static final int LOGGER_ENTRY_MAX_LEN = 5 * 1024;
// The entry sizes differ in Android 10. See
// https://cs.android.com/android/platform/superproject/+/android-10.0.0_r30:system/core/liblog/include/log/log_read.h
public static final int ENTRY_V1_SIZE = 20;
public static final int ENTRY_V2_V3_SIZE = 24;
public static final int ENTRY_V4_SIZE = 28;
private final AtomicBoolean mRunning = new AtomicBoolean(false);
private final ObjIntConsumer<Bundle> mLogEventConsumer;
private final Supplier<LocalSocket> mLocalSocketSupplier;
private final Executor mExecutor;
private final HashSet<LogFilter> mLogFilters = new HashSet<>();
private synchronized boolean logFiltersEmpty() {
return mLogFilters.isEmpty();
}
/**
* Replicates {@code struct log_msg} from system/core/liblog/include/log/log_read.h and {@code
* struct AndroidLogEntry} from system/core/liblog/include/log/logprint.h.
*/
static class LogEntry {
long mTvSec; // seconds since Epoch
long mTvNSec; // nanoseconds
int mPid; // generating process's pid
long mTid; // generating process's tid
long mUid; // generating process's uid
int mPriority; // log priority, e.g. {@link Log#INFO}.
String mTag;
String mMessage;
/**
* Parses raw bytes received from {@code logd}.
*
* @param data raw bytes
* @param readSize number of bytes received from logd.
*/
@Nullable
static LogEntry parse(byte[] data, int readSize) {
// Parsing log_msg struct defined in system/core/liblog/include/log/log_read.h.
// Only first headerSize is used to create LogEntry. Following message messageSize bytes
// define log message.
ByteBuffer dataBytes = ByteBuffer.wrap(data);
dataBytes.order(ByteOrder.LITTLE_ENDIAN);
int messageSize = dataBytes.getShort();
int headerSize = dataBytes.getShort();
if (readSize < messageSize + headerSize) {
Log.w(
TAG, "Invalid log message size "
+ (messageSize + headerSize)
+ ", received only "
+ readSize);
return null;
}
LogEntry entry = new LogEntry();
entry.mPid = dataBytes.getInt();
entry.mTid = dataBytes.getInt();
entry.mTvSec = dataBytes.getInt();
entry.mTvNSec = dataBytes.getInt();
if (headerSize >= ENTRY_V2_V3_SIZE) {
dataBytes.position(dataBytes.position() + 4); // lid is not used here.
}
if (headerSize >= ENTRY_V4_SIZE) {
entry.mUid = dataBytes.getInt();
}
// Parsing log message.
// See android_log_processLogBuffer() in system/core/liblog/logprint.cpp for details.
// message format: <priority:1><tag:N>\0<message:N>\0
// TODO(b/180516393): improve message parsing that were not transferred
// from the cpp above. Also verify this mechanism is ok from selinux perspective.
if (messageSize < 3) {
Log.w(TAG, "Log message is too small, size=" + messageSize);
return null;
}
if (headerSize != dataBytes.position()) {
Log.w(TAG, "Invalid header size " + headerSize + ", expected "
+ dataBytes.position());
return null;
}
int msgStart = -1;
int msgEnd = -1;
for (int i = 1; i < messageSize; i++) {
if (data[headerSize + i] == 0) {
if (msgStart == -1) {
msgStart = i + 1 + headerSize;
} else {
msgEnd = i + headerSize;
break;
}
}
}
if (msgStart == -1) {
Log.w(TAG, "Invalid log message");
return null;
}
if (msgEnd == -1) {
msgEnd = Math.max(msgStart, messageSize - 1);
}
entry.mPriority = data[headerSize];
entry.mTag =
new String(data, headerSize + 1, msgStart - headerSize - 2,
StandardCharsets.US_ASCII);
entry.mMessage =
new String(data, msgStart, msgEnd - msgStart, StandardCharsets.US_ASCII);
return entry;
}
}
/**
* Constructs {@link LogcatReader}.
*
* @param logEventConsumer a consumer that's called when a filter matches a log.
* @param localSocketSupplier a supplier for LocalSocket to connect logd.
* @param executor an {@link Executor} to run the LogcatReader instance.
*/
public LogcatReader(
ObjIntConsumer<Bundle> logEventConsumer,
Supplier<LocalSocket> localSocketSupplier,
Executor executor) {
this.mLogEventConsumer = logEventConsumer;
this.mLocalSocketSupplier = localSocketSupplier;
this.mExecutor = executor;
}
/** Runs {@link LogcatReader}. */
private void run() {
Log.d(TAG, "Running LogcatReader");
// Under the hood, logcat receives logs from logd, to remove the middleman, LogcatReader
// doesn't run logcat, but directly connects to logd socket and parses raw logs.
try (LocalSocket socket = mLocalSocketSupplier.get()) {
// Connect to /dev/socket/logdr
socket.connect(new LocalSocketAddress("logdr", LocalSocketAddress.Namespace.RESERVED));
try (OutputStreamWriter writer = new OutputStreamWriter(socket.getOutputStream());
InputStream reader = socket.getInputStream()) {
// Ask for streaming log and set tail=1 to get only new logs.
// See system/core/liblog/logd_reader.cpp for example on how to interact with logdr.
writer.write("stream tail=1");
writer.flush();
Log.d(TAG, "Sent request to logd and awaiting for logs");
byte[] data = new byte[LOGGER_ENTRY_MAX_LEN + 1];
while (!logFiltersEmpty()) {
int n = reader.read(data, 0, LOGGER_ENTRY_MAX_LEN);
if (n == -1) {
Log.e(TAG, "Disconnected from logd");
return;
}
LogEntry entry = LogEntry.parse(data, n);
if (entry == null) {
continue;
}
// Ignore the logs from the telemetry service. This
// makes sure a recursive log storm does not happen - i.e. app produces a log
// which in turn executes a code path that produces another log.
if (entry.mUid == Process.myUid()) {
continue;
}
// Check if it's running before processing the logs, because by the time we get
// here ScriptExecutor might get disconnected.
if (!mRunning.get()) {
Log.d(TAG, "Not running anymore, exiting.");
return;
}
// Keep track of which logListener an event for this entry has been sent,
// so that the same entry isn't sent multiple times for the same logListener
// if its multiple filters match.
HashSet<Integer> sentLogListenerIndices = new HashSet<>();
for (LogFilter filter : mLogFilters) {
if (!sentLogListenerIndices.contains(filter.getLogListenerIndex())
&& filter.matches(entry)) {
sentLogListenerIndices.add(filter.getLogListenerIndex());
sendLogEvent(filter.getLogListenerIndex(), entry);
}
}
}
Log.d(TAG, "Log filters are empty, exiting.");
}
} catch (IOException e) {
Log.e(TAG, "Failed to connect to logd", e);
} finally {
mRunning.set(false);
}
}
/**
* Sends the log event to the through the mLogEventConsumer.
*
* @param logListenerIndex the index of the logListener, whose function will receive the log
* event.
* @param entry the LogEntry instance to be bundled up and sent as event.
*/
private void sendLogEvent(int logListenerIndex, LogEntry entry) {
Bundle event = new Bundle();
event.putLong(LOG_SEC_KEY, entry.mTvSec);
event.putLong(LOG_NSEC_KEY, entry.mTvNSec);
event.putString(LOG_TAG_KEY, entry.mTag);
event.putString(LOG_MESSAGE_KEY, entry.mMessage);
mLogEventConsumer.accept(event, logListenerIndex);
}
/**
* Subscribes the list of {@link LogFilter} instances.
*
* @param newLogFilters the list of new log filters to be added to mLogFilters.
*/
synchronized void subscribeLogFilters(List<LogFilter> newLogFilters) {
mLogFilters.addAll(newLogFilters);
}
/**
* Unsubscribes all {@link LogFilter} associated with the logListenerIndex
*
* @param logListenerIndex the index of the logListener to unregister.
*/
synchronized void unsubscribeLogListener(int logListenerIndex) {
mLogFilters.removeIf(lf -> lf.getLogListenerIndex() == logListenerIndex);
}
/** Starts the run method in a new thread if logcatReader isn't running already. */
void startAsyncIfNotStarted() {
if (mRunning.compareAndSet(false, true)) {
mExecutor.execute(this::run);
}
}
/** Gracefully stops {@link LogcatReader}. */
synchronized void stop() {
mRunning.set(false);
mLogFilters.clear();
}
/** Builds a {@link LocalSocket} to read from {@code logdr}. */
static LocalSocket buildLocalSocket() {
return new LocalSocket(LocalSocket.SOCKET_SEQPACKET);
}
}