Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 75aea85a authored by Pablo Gamito's avatar Pablo Gamito
Browse files

Implement a command handler for the ProtoLog service

Use to toggle logcat and get information about available protolog groups.

Test: com.android.internal.protolog.ProtoLogServiceTest
Flag: android.tracing.client_side_proto_logging
Bug: 352538294
Change-Id: I23c44db3ddbcb2a9072df8bb4212b5dcbcfd951f
parent 7a0be13b
Loading
Loading
Loading
Loading
+172 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.internal.protolog;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.ShellCommand;

import com.android.internal.annotations.VisibleForTesting;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

public class ProtoLogCommandHandler extends ShellCommand {
    @NonNull
    private final ProtoLogService mProtoLogService;
    @Nullable
    private final PrintWriter mPrintWriter;

    public ProtoLogCommandHandler(@NonNull ProtoLogService protoLogService) {
        this(protoLogService, null);
    }

    @VisibleForTesting
    public ProtoLogCommandHandler(
            @NonNull ProtoLogService protoLogService, @Nullable PrintWriter printWriter) {
        this.mProtoLogService = protoLogService;
        this.mPrintWriter = printWriter;
    }

    @Override
    public int onCommand(String cmd) {
        if (cmd == null) {
            onHelp();
            return 0;
        }

        return switch (cmd) {
            case "groups" -> handleGroupsCommands(getNextArg());
            case "logcat" -> handleLogcatCommands(getNextArg());
            default -> handleDefaultCommands(cmd);
        };
    }

    @Override
    public void onHelp() {
        PrintWriter pw = getOutPrintWriter();
        pw.println("ProtoLog commands:");
        pw.println("  help");
        pw.println("    Print this help text.");
        pw.println();
        pw.println("  groups (list | status)");
        pw.println("    list - lists all ProtoLog groups registered with ProtoLog service");
        pw.println("    status <group> - print the status of a ProtoLog group");
        pw.println();
        pw.println("  logcat (enable | disable) <group>");
        pw.println("    enable or disable ProtoLog to logcat");
        pw.println();
    }

    @NonNull
    @Override
    public PrintWriter getOutPrintWriter() {
        if (mPrintWriter != null) {
            return mPrintWriter;
        }

        return super.getOutPrintWriter();
    }

    private int handleGroupsCommands(@Nullable String cmd) {
        PrintWriter pw = getOutPrintWriter();

        if (cmd == null) {
            pw.println("Incomplete command. Use 'cmd protolog help' for guidance.");
            return 0;
        }

        switch (cmd) {
            case "list": {
                final String[] availableGroups = mProtoLogService.getGroups();
                if (availableGroups.length == 0) {
                    pw.println("No ProtoLog groups registered with ProtoLog service.");
                    return 0;
                }

                pw.println("ProtoLog groups registered with service:");
                for (String group : availableGroups) {
                    pw.println("- " + group);
                }

                return 0;
            }
            case "status": {
                final String group = getNextArg();

                if (group == null) {
                    pw.println("Incomplete command. Use 'cmd protolog help' for guidance.");
                    return 0;
                }

                pw.println("ProtoLog group " + group + "'s status:");

                if (!Set.of(mProtoLogService.getGroups()).contains(group)) {
                    pw.println("UNREGISTERED");
                    return 0;
                }

                pw.println("LOG_TO_LOGCAT = " + mProtoLogService.isLoggingToLogcat(group));
                return 0;
            }
            default: {
                pw.println("Unknown command: " + cmd);
                return -1;
            }
        }
    }

    private int handleLogcatCommands(@Nullable String cmd) {
        PrintWriter pw = getOutPrintWriter();

        if (cmd == null || peekNextArg() == null) {
            pw.println("Incomplete command. Use 'cmd protolog help' for guidance.");
            return 0;
        }

        switch (cmd) {
            case "enable" -> {
                mProtoLogService.enableProtoLogToLogcat(processGroups());
                return 0;
            }
            case "disable" -> {
                mProtoLogService.disableProtoLogToLogcat(processGroups());
                return 0;
            }
            default -> {
                pw.println("Unknown command: " + cmd);
                return -1;
            }
        }
    }

    @NonNull
    private String[] processGroups() {
        if (getRemainingArgsCount() == 0) {
            return mProtoLogService.getGroups();
        }

        final List<String> groups = new ArrayList<>();
        while (getRemainingArgsCount() > 0) {
            groups.add(getNextArg());
        }

        return groups.toArray(new String[0]);
    }
}
+11 −0
Original line number Diff line number Diff line
@@ -33,6 +33,8 @@ import android.annotation.Nullable;
import android.annotation.SystemService;
import android.content.Context;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.os.SystemClock;
import android.tracing.perfetto.DataSourceParams;
import android.tracing.perfetto.InitArguments;
@@ -43,6 +45,7 @@ import android.util.proto.ProtoOutputStream;

import com.android.internal.annotations.VisibleForTesting;

import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -224,6 +227,14 @@ public final class ProtoLogService extends IProtoLogService.Stub {
        registerGroups(client, args.getGroups(), args.getGroupsDefaultLogcatStatus());
    }

    @Override
    public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out,
            @Nullable FileDescriptor err, @NonNull String[] args, @Nullable ShellCallback callback,
            @NonNull ResultReceiver resultReceiver) throws RemoteException {
        new ProtoLogCommandHandler(this)
                .exec(this, in, out, err, args, callback, resultReceiver);
    }

    /**
     * Get the list of groups clients have registered to the protolog service.
     * @return The list of ProtoLog groups registered with this service.
+207 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.internal.protolog;

import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.ArgumentMatchers.endsWith;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.times;

import android.platform.test.annotations.Presubmit;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;

import java.io.FileDescriptor;
import java.io.PrintWriter;

/**
 * Test class for {@link ProtoLogImpl}.
 */
@Presubmit
@RunWith(MockitoJUnitRunner.class)
public class ProtoLogCommandHandlerTest {

    @Mock
    ProtoLogService mProtoLogService;
    @Mock
    PrintWriter mPrintWriter;

    @Test
    public void printsHelpForAllAvailableCommands() {
        final ProtoLogCommandHandler cmdHandler =
                new ProtoLogCommandHandler(mProtoLogService, mPrintWriter);

        cmdHandler.onHelp();
        validateOnHelpPrinted();
    }

    @Test
    public void printsHelpIfCommandIsNull() {
        final ProtoLogCommandHandler cmdHandler =
                new ProtoLogCommandHandler(mProtoLogService, mPrintWriter);

        cmdHandler.onCommand(null);
        validateOnHelpPrinted();
    }

    @Test
    public void handlesGroupListCommand() {
        Mockito.when(mProtoLogService.getGroups())
                .thenReturn(new String[] {"MY_TEST_GROUP", "MY_OTHER_GROUP"});
        final ProtoLogCommandHandler cmdHandler =
                new ProtoLogCommandHandler(mProtoLogService, mPrintWriter);

        cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err,
                new String[] { "groups", "list" });

        Mockito.verify(mPrintWriter, times(1))
                .println(contains("MY_TEST_GROUP"));
        Mockito.verify(mPrintWriter, times(1))
                .println(contains("MY_OTHER_GROUP"));
    }

    @Test
    public void handlesIncompleteGroupsCommand() {
        final ProtoLogCommandHandler cmdHandler =
                new ProtoLogCommandHandler(mProtoLogService, mPrintWriter);

        cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err,
                new String[] { "groups" });

        Mockito.verify(mPrintWriter, times(1))
                .println(contains("Incomplete command"));
    }

    @Test
    public void handlesGroupStatusCommand() {
        Mockito.when(mProtoLogService.getGroups()).thenReturn(new String[] {"MY_GROUP"});
        Mockito.when(mProtoLogService.isLoggingToLogcat("MY_GROUP")).thenReturn(true);
        final ProtoLogCommandHandler cmdHandler =
                new ProtoLogCommandHandler(mProtoLogService, mPrintWriter);

        cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err,
                new String[] { "groups", "status", "MY_GROUP" });

        Mockito.verify(mPrintWriter, times(1))
                .println(contains("MY_GROUP"));
        Mockito.verify(mPrintWriter, times(1))
                .println(contains("LOG_TO_LOGCAT = true"));
    }

    @Test
    public void handlesGroupStatusCommandOfUnregisteredGroups() {
        Mockito.when(mProtoLogService.getGroups()).thenReturn(new String[] {});
        final ProtoLogCommandHandler cmdHandler =
                new ProtoLogCommandHandler(mProtoLogService, mPrintWriter);

        cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err,
                new String[] { "groups", "status", "MY_GROUP" });

        Mockito.verify(mPrintWriter, times(1))
                .println(contains("MY_GROUP"));
        Mockito.verify(mPrintWriter, times(1))
                .println(contains("UNREGISTERED"));
    }

    @Test
    public void handlesGroupStatusCommandWithNoGroups() {
        final ProtoLogCommandHandler cmdHandler =
                new ProtoLogCommandHandler(mProtoLogService, mPrintWriter);

        cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err,
                new String[] { "groups", "status" });

        Mockito.verify(mPrintWriter, times(1))
                .println(contains("Incomplete command"));
    }

    @Test
    public void handlesIncompleteLogcatCommand() {
        final ProtoLogCommandHandler cmdHandler =
                new ProtoLogCommandHandler(mProtoLogService, mPrintWriter);

        cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err,
                new String[] { "logcat" });

        Mockito.verify(mPrintWriter, times(1))
                .println(contains("Incomplete command"));
    }

    @Test
    public void handlesLogcatEnableCommand() {
        final ProtoLogCommandHandler cmdHandler =
                new ProtoLogCommandHandler(mProtoLogService, mPrintWriter);

        cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err,
                new String[] { "logcat", "enable", "MY_GROUP" });
        Mockito.verify(mProtoLogService).enableProtoLogToLogcat("MY_GROUP");

        cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err,
                new String[] { "logcat", "enable", "MY_GROUP", "MY_OTHER_GROUP" });
        Mockito.verify(mProtoLogService)
                .enableProtoLogToLogcat("MY_GROUP", "MY_OTHER_GROUP");
    }

    @Test
    public void handlesLogcatDisableCommand() {
        final ProtoLogCommandHandler cmdHandler =
                new ProtoLogCommandHandler(mProtoLogService, mPrintWriter);

        cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err,
                new String[] { "logcat", "disable", "MY_GROUP" });
        Mockito.verify(mProtoLogService).disableProtoLogToLogcat("MY_GROUP");

        cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err,
                new String[] { "logcat", "disable", "MY_GROUP", "MY_OTHER_GROUP" });
        Mockito.verify(mProtoLogService)
                .disableProtoLogToLogcat("MY_GROUP", "MY_OTHER_GROUP");
    }

    @Test
    public void handlesLogcatEnableCommandWithNoGroups() {
        final ProtoLogCommandHandler cmdHandler =
                new ProtoLogCommandHandler(mProtoLogService, mPrintWriter);

        cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err,
                new String[] { "logcat", "enable" });
        Mockito.verify(mPrintWriter).println(contains("Incomplete command"));
    }

    @Test
    public void handlesLogcatDisableCommandWithNoGroups() {
        final ProtoLogCommandHandler cmdHandler =
                new ProtoLogCommandHandler(mProtoLogService, mPrintWriter);

        cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err,
                new String[] { "logcat", "disable" });
        Mockito.verify(mPrintWriter).println(contains("Incomplete command"));
    }

    private void validateOnHelpPrinted() {
        Mockito.verify(mPrintWriter, times(1)).println(endsWith("help"));
        Mockito.verify(mPrintWriter, times(1))
                .println(endsWith("groups (list | status)"));
        Mockito.verify(mPrintWriter, times(1))
                .println(endsWith("logcat (enable | disable) <group>"));
        Mockito.verify(mPrintWriter, atLeast(0)).println(anyString());
    }
}